From 78a0158956ba69bfeb7a1c6da9ebcfdbd6f01e85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 9 Oct 2026 01:45:00 -0400 Subject: [PATCH] Site: names use the SVG's screen transform; resize tests use CSS size (R2.2) --- site/e2e/touch.spec.ts | 27 +++++++++++++++------------ site/src/touch.ts | 5 +++-- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/site/e2e/touch.spec.ts b/site/e2e/touch.spec.ts index 0968fce..c25cf2d 100644 --- a/site/e2e/touch.spec.ts +++ b/site/e2e/touch.spec.ts @@ -145,16 +145,16 @@ test("holding Enter sinks the focused building until release", async ({ page }) await expect(page.locator(".block.pressed")).toHaveCount(0); }); -/** Pixels the name sits from its expected place. */ +/** Pixels from one cell above the drawn building. */ const drift = (page: Page, name: string) => page.evaluate(([n, unit]) => { const svg = document.querySelector(".tour .stage")!; - const scale = svg.getBoundingClientRect().width / svg.viewBox.baseVal.width; - const rects = [...document.querySelectorAll(`.hit-face[data-part="${n}"]`)].map((f) => f.getBoundingClientRect()); + const cell = unit * svg.getScreenCTM()!.d; + const rects = [...document.querySelectorAll(`.block[data-part="${n}"] .face`)].map((f) => f.getBoundingClientRect()); const centre = (Math.min(...rects.map((r) => r.left)) + Math.max(...rects.map((r) => r.right))) / 2; - const bottom = Math.min(...rects.map((r) => r.top)) - 2 * unit * scale; + const top = Math.min(...rects.map((r) => r.top)); const box = document.querySelector(".tour .caption.shown")!.getBoundingClientRect(); - return Math.max(Math.abs(box.left + box.width / 2 - centre), Math.abs(box.bottom - bottom)); + return Math.max(Math.abs(box.left + box.width / 2 - centre), Math.abs(box.bottom - (top - cell))); }, [name, CELL] as const); test("a focused name follows the model when the window narrows", async ({ page }) => { @@ -165,14 +165,17 @@ test("a focused name follows the model when the window narrows", async ({ page } await expect.poll(() => drift(page, "ElyPDF")).toBeLessThan(1); }); -test("a focused name follows the model when only the model resizes", async ({ page }) => { - await page.locator(".doors a").nth(3).focus(); - await page.evaluate(() => { - const svg = document.querySelector(".tour .stage")!; - svg.style.width = `${svg.getBoundingClientRect().width * 0.8}px`; +for (const side of ["width", "height"] as const) { + test(`a focused name follows the model when only its ${side} changes`, async ({ page }) => { + await page.locator(".doors a").nth(3).focus(); + await page.evaluate((side) => { + const svg = document.querySelector(".tour .stage")!; + // CSS size: the screen box includes the camera zoom. + svg.style[side] = `${parseFloat(getComputedStyle(svg)[side]) * 0.8}px`; + }, side); + await expect.poll(() => drift(page, "ElyPDF")).toBeLessThan(1); }); - await expect.poll(() => drift(page, "ElyPDF")).toBeLessThan(1); -}); +} test.describe("with motion", () => { test.use({ reducedMotion: "no-preference" }); diff --git a/site/src/touch.ts b/site/src/touch.ts index 25a6784..b0ccd40 100644 --- a/site/src/touch.ts +++ b/site/src/touch.ts @@ -91,12 +91,13 @@ function still(svg: SVGSVGElement, owner: Map): SVGPolygonE /** Puts the caption one cell above the lifted top. */ function place(caption: HTMLElement, host: HTMLElement, svg: SVGSVGElement, faces: SVGPolygonElement[]) { const origin = host.getBoundingClientRect(); - const scale = svg.getBoundingClientRect().width / svg.viewBox.baseVal.width; + const ctm = svg.getScreenCTM(); + if (!ctm) throw new Error("model is not rendered"); // Hit faces never lift, so they give the rest pose. const rects = faces.map((f) => f.getBoundingClientRect()); const left = Math.min(...rects.map((r) => r.left)); const right = Math.max(...rects.map((r) => r.right)); const top = Math.min(...rects.map((r) => r.top)); caption.style.left = `${(left + right) / 2 - origin.left}px`; - caption.style.top = `${top - origin.top - 2 * UNIT * scale}px`; + caption.style.top = `${top - origin.top - 2 * UNIT * ctm.d}px`; }