diff --git a/README.md b/README.md index b811431..a9e89d2 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Every documentation page is one Markdown file under `content/` (frontmatter + bo | `/docs/parts/` … `/docs/adapters/` | `content/docs/.md` | One concept page per package: what it is, when to use it, what it solves, what you get, how it works, key concepts, and links into the repo for detail | | `/examples/` | `content/examples.md` | Reproducible examples (`rig-c/examples`, `rig-c/trailer`) | | `/demos/head-turn/` | `demos/head-turn/index.html` | Interactive preview of an upcoming head-turn feature (JavaScript/WebGL required) | +| `/demos/head-turn/curvature/` | `demos/head-turn/curvature/index.html` | Curvature check: the original painting displaced by its depth map in one fragment shader, no rig (WebGL required) | | `/exam/` | `content/exam.md` | [`rig-c/exam`](https://github.com/rig-c/exam) — the open-book exam for AI agents, Basic and Advanced editions | The first version's routes (`/parts/`, `/core/`, `/getting-started/`, …) are redirect stubs to their `/docs/` pages (`REDIRECTS` in `src/content.ts`); they are `noindex` and stay out of the sitemap. @@ -113,4 +114,6 @@ After building, `bun run serve` serves `/demos/head-turn/` from the isolated out `demos/head-turn/bundle/` preserves all ten approved producer files byte for byte: `index.html`, `app.js`, `aperture.js`, `runtime.js`, `look.png`, `depth.jpg`, `scene.json`, `scene.bin.gz`, `README.md`, `THIRD_PARTY_LICENSES.txt`. The adjacent `bundle-manifest.json` records their path-sorted lengths and SHA-256 values; the build checks its pinned digest before fetching registry data. Verification checks the whole copied `demos/` tree and the complete deployment manifest. The original depth logic, renderer, geometry and licences are unchanged. +`demos/head-turn/curvature/` is a separate static page linked from the preview's caption: one WebGL fragment shader samples `original.webp` at each pixel's position plus depth × camera offset (pointer on desktop, drag on touch), with depth gamma, contrast and bias and a Show depth view. It reads the depth map and is not the head-turn feature. `depth.webp` is the supplied 1359×2000 depth map resampled into the painting's 1368×2000 frame by `x_depth = 0.99908·x − 2.939` (pixel indices, y unchanged), fitted to the silhouette edges of both images; a uniform 1359/1368 stretch left a median edge residual of 2.77 px, this fit 0.17 px. `verifyCurvaturePage` (in `src/head-turn.ts`) checks its five files, caption, back link, that every reference is relative and resolves, and the preview's link to it. + Serve `scene.bin.gz` as raw gzip bytes with **no Content-Encoding header**; the application decompresses it itself. The local static server returns the file directly. The Examples page links it from a "Preview: depth-driven head turn" section; the landing page does not feature it. Its `assets/head-turn-yaw15.png` poster is a real canvas render at yaw +15 degrees, captured with a synthetic pointer event at the right canvas edge through the original handler, with default controls; separate motion evidence uses real mouse movement. It is not an image-model asset. Browser captures and measured viewport/renderer details are separate acceptance evidence; source tests alone do not establish visual or physical-device correctness. diff --git a/demos/head-turn/curvature/curvature.css b/demos/head-turn/curvature/curvature.css new file mode 100644 index 0000000..8618316 --- /dev/null +++ b/demos/head-turn/curvature/curvature.css @@ -0,0 +1,33 @@ +/* Curvature check: one canvas, one small panel. Same palette as the head-turn preview. */ +* { box-sizing: border-box; } +html, body { margin: 0; width: 100%; height: 100%; overflow: hidden; background: #20232a; color: #f0f3f8; font: 14px system-ui, sans-serif; } +canvas { position: fixed; inset: 0; display: block; width: 100vw; height: 100vh; touch-action: none; cursor: crosshair; } +header { position: fixed; z-index: 2; top: 20px; left: 20px; right: 20px; display: flex; justify-content: space-between; gap: 24px; pointer-events: none; } +.intro { max-width: 350px; } +h1 { margin: 0 0 10px; font-size: 24px; letter-spacing: -.5px; } +.caption { margin: 0 0 10px; color: #bcc8d6; line-height: 1.6; font-size: 13px; } +a { color: #a8cfdf; font-size: 12px; pointer-events: auto; } +.controls { pointer-events: auto; width: 300px; padding: 16px 18px; border: 1px solid #ffffff20; border-radius: 14px; background: #111923e8; box-shadow: 0 12px 36px #0003; } +.hint { margin: 0 0 12px; color: #c5d1df; font-size: 12px; } +.row { display: grid; grid-template-columns: 1fr 45px; gap: 7px; margin: 12px 0; } +.row label { font-size: 12px; } +.row output { text-align: right; font-size: 12px; font-variant-numeric: tabular-nums; } +.row input { grid-column: 1 / -1; width: 100%; margin: 0; accent-color: #61bddd; } +.toggle { display: flex; align-items: center; gap: 8px; font-size: 12px; margin: 14px 0 0; } +.toggle input { margin: 0; accent-color: #61bddd; } +.actions { display: flex; align-items: center; justify-content: space-between; margin-top: 14px; } +button { padding: 7px 12px; border-radius: 7px; border: 1px solid #ffffff30; color: #eef6fd; background: #2c4354; cursor: pointer; font: inherit; font-size: 13px; } +#controls-toggle { display: none; pointer-events: auto; } +#status { position: fixed; z-index: 3; top: 48%; left: 20px; right: 20px; text-align: center; } +#status:empty { display: none; } +@media (max-width: 799px) { + header { display: block; top: 12px; left: 14px; right: 14px; } + .intro { max-width: 100%; } + .intro h1 { padding-right: 80px; font-size: 19px; margin-bottom: 5px; } + .caption { font-size: 11px; max-width: 100%; margin-bottom: 4px; } + #controls-toggle { display: block; position: absolute; top: 0; right: 0; padding: 7px 10px; font-size: 12px; } + .controls { position: fixed; bottom: 14px; right: 14px; width: 276px; padding: 12px 15px; max-height: calc(100dvh - 130px); overflow-y: auto; } + .controls:not([data-open]) { display: none; } + .row { margin: 9px 0; } + .hint { margin-bottom: 6px; } +} diff --git a/demos/head-turn/curvature/curvature.js b/demos/head-turn/curvature/curvature.js new file mode 100644 index 0000000..b2c9d63 --- /dev/null +++ b/demos/head-turn/curvature/curvature.js @@ -0,0 +1,219 @@ +// Curvature check: one fragment shader over the original painting. Each pixel samples the +// painting at its own position plus depth × camera offset, so near (bright) pixels move more +// than far ones. No mesh, no rig, no rotation of the whole image. +// depth.webp is the supplied depth map resampled into the painting's 1368×2000 frame +// (x_depth = 0.99908·x − 2.939, y unchanged; see the site README). +'use strict'; + +const MAX_SHIFT = 0.03; // fraction of the image width at strength 1 and the pointer at the edge +const SETTLE = 8; // 1/s; exponential damping rate toward the pointer target +const BACKGROUND = [0x20 / 255, 0x23 / 255, 0x2a / 255]; + +const VERT = `attribute vec2 aPos; void main() { gl_Position = vec4(aPos, 0.0, 1.0); }`; +const FRAG = ` +precision mediump float; +uniform sampler2D uImage; +uniform sampler2D uDepth; +uniform vec4 uRect; // image rectangle in drawing-buffer pixels: x, y (bottom-left), w, h +uniform vec2 uOffset; // camera offset in image UV units +uniform float uGamma; +uniform float uContrast; +uniform float uBias; +uniform float uShowDepth; +uniform vec3 uBackground; +float depthAt(vec2 uv) { + float raw = texture2D(uDepth, uv).r; + return clamp((pow(raw, uGamma) - 0.5) * uContrast + 0.5 + uBias, 0.0, 1.0); +} +void main() { + vec2 p = (gl_FragCoord.xy - uRect.xy) / uRect.zw; + p.y = 1.0 - p.y; + if (p.x < 0.0 || p.x > 1.0 || p.y < 0.0 || p.y > 1.0) { gl_FragColor = vec4(uBackground, 1.0); return; } + float depth = depthAt(p); + if (uShowDepth > 0.5) { gl_FragColor = vec4(vec3(depth), 1.0); return; } + gl_FragColor = texture2D(uImage, clamp(p + depth * uOffset, 0.0, 1.0)); +}`; + +const $ = (id) => document.getElementById(id); +const main = $('curvature'); +const canvas = $('view'); +const status = $('status'); + +function fail(message) { + status.textContent = message; + main.setAttribute('aria-busy', 'false'); + main.dataset.state = 'error'; +} + +function loadImage(src) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = () => reject(new Error(`Could not load ${src}`)); + img.src = src; + }); +} + +function compile(gl, type, source) { + const shader = gl.createShader(type); + gl.shaderSource(shader, source); + gl.compileShader(shader); + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(shader) || 'shader compile failed'); + return shader; +} + +function texture(gl, unit, image) { + const tex = gl.createTexture(); + gl.activeTexture(gl.TEXTURE0 + unit); + gl.bindTexture(gl.TEXTURE_2D, tex); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); + return tex; +} + +// Controls panel: open on desktop, collapsed below 800 CSS pixels (as on the head-turn page). +const toggle = $('controls-toggle'); +const panel = $('curvature-controls'); +const phone = matchMedia('(max-width: 799px)'); +function setOpen(open) { + const expanded = !phone.matches || open; + panel.toggleAttribute('data-open', expanded); + panel.inert = !expanded; + toggle.setAttribute('aria-expanded', String(expanded)); +} +setOpen(false); +phone.addEventListener('change', () => setOpen(false)); +toggle.addEventListener('click', () => setOpen(toggle.getAttribute('aria-expanded') !== 'true')); +addEventListener('keydown', (event) => { + if (phone.matches && event.key === 'Escape' && toggle.getAttribute('aria-expanded') === 'true') { + setOpen(false); + toggle.focus(); + } +}); + +const params = { strength: 1, gamma: 1, contrast: 1, bias: 0, showDepth: false }; +const target = { x: 0, y: 0 }; +const camera = { x: 0, y: 0 }; +let requestDraw = () => {}; + +function bindSlider(name, format) { + const input = $(name); + const output = $(`${name}Value`); + const update = () => { + params[name] = Number(input.value); + output.textContent = format(params[name]); + requestDraw(); + }; + input.addEventListener('input', update); + update(); +} +bindSlider('strength', (v) => `${v.toFixed(2)}×`); +bindSlider('gamma', (v) => v.toFixed(2)); +bindSlider('contrast', (v) => v.toFixed(2)); +bindSlider('bias', (v) => v.toFixed(2)); +$('showDepth').addEventListener('change', (event) => { params.showDepth = event.target.checked; requestDraw(); }); +$('centre').addEventListener('click', () => { target.x = 0; target.y = 0; requestDraw(); }); + +const clamp1 = (v) => Math.max(-1, Math.min(1, v)); +// Mouse and pen: the pointer's position relative to the viewport centre is the camera target. +// Touch: dragging moves the target by the drag distance, so a finger never has to cover the face. +let drag = null; +canvas.addEventListener('pointerdown', (event) => { + if (event.pointerType !== 'touch') return; + drag = { id: event.pointerId, x: event.clientX, y: event.clientY }; + canvas.setPointerCapture(event.pointerId); +}); +canvas.addEventListener('pointerup', (event) => { if (drag && drag.id === event.pointerId) drag = null; }); +canvas.addEventListener('pointercancel', (event) => { if (drag && drag.id === event.pointerId) drag = null; }); +addEventListener('pointermove', (event) => { + const halfW = innerWidth / 2; + const halfH = innerHeight / 2; + if (event.pointerType === 'touch') { + if (!drag || drag.id !== event.pointerId) return; + target.x = clamp1(target.x + (event.clientX - drag.x) / halfW); + target.y = clamp1(target.y + (event.clientY - drag.y) / halfH); + drag.x = event.clientX; + drag.y = event.clientY; + } else { + target.x = clamp1((event.clientX - halfW) / halfW); + target.y = clamp1((event.clientY - halfH) / halfH); + } + requestDraw(); +}); + +async function start() { + const gl = canvas.getContext('webgl', { antialias: false, alpha: false, premultipliedAlpha: false }); + if (!gl) return fail('This check needs WebGL, which this browser did not provide.'); + const [image, depth] = await Promise.all([loadImage('./original.webp'), loadImage('./depth.webp')]); + const program = gl.createProgram(); + gl.attachShader(program, compile(gl, gl.VERTEX_SHADER, VERT)); + gl.attachShader(program, compile(gl, gl.FRAGMENT_SHADER, FRAG)); + gl.linkProgram(program); + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(program) || 'program link failed'); + gl.useProgram(program); + gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW); + const aPos = gl.getAttribLocation(program, 'aPos'); + gl.enableVertexAttribArray(aPos); + gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0); + texture(gl, 0, image); + texture(gl, 1, depth); + const u = (name) => gl.getUniformLocation(program, name); + const uni = { + image: u('uImage'), depth: u('uDepth'), rect: u('uRect'), offset: u('uOffset'), gamma: u('uGamma'), + contrast: u('uContrast'), bias: u('uBias'), showDepth: u('uShowDepth'), background: u('uBackground'), + }; + gl.uniform1i(uni.image, 0); + gl.uniform1i(uni.depth, 1); + gl.uniform3fv(uni.background, BACKGROUND); + const aspect = image.naturalWidth / image.naturalHeight; + + function resize() { + const dpr = Math.min(devicePixelRatio || 1, 2); + const w = Math.round(canvas.clientWidth * dpr); + const h = Math.round(canvas.clientHeight * dpr); + if (canvas.width !== w || canvas.height !== h) { canvas.width = w; canvas.height = h; } + // Contain-fit with a margin of 4% of the shorter side, leaving room for the camera shift. + const margin = Math.min(w, h) * 0.04; + let rw = w - 2 * margin; + let rh = rw / aspect; + if (rh > h - 2 * margin) { rh = h - 2 * margin; rw = rh * aspect; } + gl.viewport(0, 0, w, h); + gl.uniform4f(uni.rect, (w - rw) / 2, (h - rh) / 2, rw, rh); + } + + let last = 0; + let pending = false; + function frame(now) { + pending = false; + const dt = last ? Math.min((now - last) / 1000, 0.1) : 1 / 60; + last = now; + const k = 1 - Math.exp(-SETTLE * dt); + camera.x += (target.x - camera.x) * k; + camera.y += (target.y - camera.y) * k; + resize(); + const shift = MAX_SHIFT * params.strength; + // Same pixel distance on both axes: the y component is scaled by the image's aspect. + gl.uniform2f(uni.offset, camera.x * shift, camera.y * shift * aspect); + gl.uniform1f(uni.gamma, params.gamma); + gl.uniform1f(uni.contrast, params.contrast); + gl.uniform1f(uni.bias, params.bias); + gl.uniform1f(uni.showDepth, params.showDepth ? 1 : 0); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + const moving = Math.abs(target.x - camera.x) > 1e-4 || Math.abs(target.y - camera.y) > 1e-4; + main.dataset.settled = String(!moving); + if (moving) requestDraw(); else last = 0; + } + requestDraw = () => { if (!pending) { pending = true; requestAnimationFrame(frame); } }; + addEventListener('resize', requestDraw); + canvas.addEventListener('webglcontextlost', (event) => { event.preventDefault(); fail('The WebGL context was lost; reload the page.'); }); + requestDraw(); + status.textContent = ''; + main.setAttribute('aria-busy', 'false'); + main.dataset.state = 'ready'; +} + +start().catch((error) => fail(`The curvature check could not start: ${error.message}`)); diff --git a/demos/head-turn/curvature/depth.webp b/demos/head-turn/curvature/depth.webp new file mode 100644 index 0000000..879ca0f Binary files /dev/null and b/demos/head-turn/curvature/depth.webp differ diff --git a/demos/head-turn/curvature/index.html b/demos/head-turn/curvature/index.html new file mode 100644 index 0000000..0359e31 --- /dev/null +++ b/demos/head-turn/curvature/index.html @@ -0,0 +1,36 @@ + + + + + + +Curvature check — rig-c + + + + + + +
+ +
+
+

Curvature check

+

Curvature check: the original painting displaced by its depth map, with no rig. A tool for reading the depth, not the head-turn feature.

+← Back to the head-turn preview +
+ +
+

Move the pointer to move the camera; drag on a phone.

+
1.00×
+
1.00
+
1.00
+
0.00
+ +
Depth map
+
+
+
Loading the painting and its depth map…
+
+ + diff --git a/demos/head-turn/curvature/original.webp b/demos/head-turn/curvature/original.webp new file mode 100644 index 0000000..9e66165 Binary files /dev/null and b/demos/head-turn/curvature/original.webp differ diff --git a/demos/head-turn/index.html b/demos/head-turn/index.html index 06abc4a..7c4261c 100644 --- a/demos/head-turn/index.html +++ b/demos/head-turn/index.html @@ -1,4 +1,4 @@ Depth-driven head turn — rig-c

Depth-driven head turn

A preview of an upcoming @rig-c/parts feature; the producer is in progress.

Move the pointer across the character.

1.00×
55
10%
10%
Zero rear counter removes its follow-through lever; local depth motion stays active.
Loading the depth demo…
+

Depth-driven head turn

A preview of an upcoming @rig-c/parts feature; the producer is in progress.

Curvature check

Move the pointer across the character.

1.00×
55
10%
10%
Zero rear counter removes its follow-through lever; local depth motion stays active.
Loading the depth demo…
diff --git a/demos/head-turn/site-ui.css b/demos/head-turn/site-ui.css index b229509..e740e7c 100644 --- a/demos/head-turn/site-ui.css +++ b/demos/head-turn/site-ui.css @@ -1,5 +1,6 @@ /* Integration UI only; the producer canvas and depth runtime stay unchanged. */ #controls-toggle { display: none; pointer-events: auto; } +#curvature-link { display: inline-block; margin-top: 8px; pointer-events: auto; } @media (max-width: 799px) { header { top: 12px; left: 14px; right: 14px; } .intro h1 { padding-right: 80px; font-size: 19px; } diff --git a/src/head-turn.test.ts b/src/head-turn.test.ts index b4f875b..e27ed0e 100644 --- a/src/head-turn.test.ts +++ b/src/head-turn.test.ts @@ -1,8 +1,9 @@ import { expect, test } from 'bun:test'; -import { readFileSync } from 'node:fs'; +import { cpSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { ROOT_DIR } from './output.ts'; -import { HEAD_TURN_PATH, verifyHeadTurnBundle } from './head-turn.ts'; +import { CURVATURE_PATH, HEAD_TURN_PATH, verifyCurvaturePage, verifyHeadTurnBundle } from './head-turn.ts'; const read = (path: string) => readFileSync(join(ROOT_DIR, path), 'utf8'); test('approved producer bundle is byte exact, including original entry and raw gzip', () => { @@ -17,6 +18,7 @@ test('site entry changes only caption and integration UI; original resources sti const restored = entry .replace('', '') .replace('', '') + .replace('Curvature check', '') .replace('A preview of an upcoming @rig-c/parts feature; the producer is in progress.', 'Depth-driven head turn: a preview of an upcoming @rig-c/parts feature (the producer is in progress)') .replace('
{ + expect(verifyCurvaturePage(ROOT_DIR)).toEqual([]); + const entry = read(`${HEAD_TURN_PATH}/index.html`); + expect(entry.split('href="../curvature/"')).toHaveLength(2); + const page = read(`${CURVATURE_PATH}/index.html`); + expect(page).not.toMatch(/(?:src|href)="(?:https?:)?\/\/(?!rig-c\.org\/demos\/head-turn\/curvature\/")/); + expect(read(`${CURVATURE_PATH}/curvature.js`)).not.toMatch(/https?:\/\//); +}); + +test('curvature check verification names a missing file and a broken reference', () => { + const copy = mkdtempSync(join(tmpdir(), 'curvature-')); + try { + cpSync(join(ROOT_DIR, 'demos'), join(copy, 'demos'), { recursive: true }); + rmSync(join(copy, CURVATURE_PATH, 'depth.webp')); + expect(verifyCurvaturePage(copy).join('\n')).toContain('Curvature check files differ'); + writeFileSync(join(copy, CURVATURE_PATH, 'depth.webp'), ''); + const html = join(copy, CURVATURE_PATH, 'index.html'); + writeFileSync(html, readFileSync(html, 'utf8').replace('./curvature.css', './missing.css')); + expect(verifyCurvaturePage(copy)).toEqual(['Curvature check reference does not resolve: ./missing.css']); + } finally { + rmSync(copy, { recursive: true, force: true }); + } +}); diff --git a/src/head-turn.ts b/src/head-turn.ts index 7d1afe2..110b956 100644 --- a/src/head-turn.ts +++ b/src/head-turn.ts @@ -1,4 +1,4 @@ -import { readFileSync } from 'node:fs'; +import { existsSync, readdirSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { artifactManifest, parseManifest } from './deployment.ts'; @@ -13,3 +13,30 @@ export function verifyHeadTurnBundle(root: string): void { throw new Error('Head-turn producer bundle differs from the approved ten-file inventory'); } } + +// Curvature check: a static shader page over the original painting and its depth map, no rig. +export const CURVATURE_PATH = `${HEAD_TURN_PATH}/curvature`; +export const CURVATURE_FILES = ['curvature.css', 'curvature.js', 'depth.webp', 'index.html', 'original.webp']; +export const CURVATURE_CAPTION = 'Curvature check: the original painting displaced by its depth map, with no rig. A tool for reading the depth, not the head-turn feature.'; +export function verifyCurvaturePage(root: string): string[] { + const errors: string[] = []; + const page = join(root, CURVATURE_PATH); + const listed = existsSync(page) ? readdirSync(page).sort() : []; + if (JSON.stringify(listed) !== JSON.stringify(CURVATURE_FILES)) { + errors.push(`Curvature check files differ: expected ${CURVATURE_FILES.join(', ')}, found ${listed.join(', ') || 'none'}`); + return errors; + } + const html = readFileSync(join(page, 'index.html'), 'utf8'); + const script = readFileSync(join(page, 'curvature.js'), 'utf8'); + if (!html.includes(`

${CURVATURE_CAPTION}

`)) errors.push('Curvature check caption missing'); + if (!html.includes('')) errors.push('Curvature check back link missing'); + // Self-contained: every reference is relative and resolves inside the page folder. + for (const [, ref] of [...html.matchAll(/(?:src|href)="([^"]+)"/g), ...script.matchAll(/loadImage\('([^']+)'\)/g)]) { + if (ref === undefined || ref.startsWith('data:') || ref === 'https://rig-c.org/demos/head-turn/curvature/') continue; + if (/^[a-z]+:|^\/\//i.test(ref)) errors.push(`Curvature check references an external URL: ${ref}`); + else if (!existsSync(join(page, ref.endsWith('/') ? `${ref}index.html` : ref))) errors.push(`Curvature check reference does not resolve: ${ref}`); + } + const entry = readFileSync(join(root, HEAD_TURN_PATH, 'index.html'), 'utf8'); + if (!entry.includes('Curvature check')) errors.push('Head-turn page does not link the curvature check'); + return errors; +} diff --git a/src/verify.ts b/src/verify.ts index d4b77dd..ac15829 100644 --- a/src/verify.ts +++ b/src/verify.ts @@ -1,7 +1,7 @@ import { existsSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { artifactManifest, verifyManifest } from './deployment.ts'; -import { verifyHeadTurnBundle } from './head-turn.ts'; +import { verifyCurvaturePage, verifyHeadTurnBundle } from './head-turn.ts'; import { DEFAULT_OUTPUT, outputRoot, parseOutputArgs, ROOT_DIR as SOURCE_ROOT } from './output.ts'; import { htmlRel, mdRoute, PAGES, REDIRECTS, SITE_ORIGIN } from './content.ts'; @@ -34,6 +34,11 @@ export function verifySite(outDir = DEFAULT_OUTPUT): void { 'demos/head-turn/index.html', 'demos/head-turn/site-ui.css', 'demos/head-turn/site-ui.js', + 'demos/head-turn/curvature/index.html', + 'demos/head-turn/curvature/curvature.css', + 'demos/head-turn/curvature/curvature.js', + 'demos/head-turn/curvature/original.webp', + 'demos/head-turn/curvature/depth.webp', 'assets/head-turn-yaw15.png', ]) { if (!existsSync(join(ROOT_DIR, requiredFile))) { @@ -161,6 +166,7 @@ export function verifySite(outDir = DEFAULT_OUTPUT): void { if (JSON.stringify(sourceFiles) !== JSON.stringify(outputFiles)) errors.push(`Authored ${directory} file set differs`); } verifyHeadTurnBundle(ROOT_DIR); + errors.push(...verifyCurvaturePage(ROOT_DIR)); verifyManifest(ROOT_DIR); } catch (error) { errors.push(String(error));