fix(daycycle): stage-sized plan layers without a frozen raster; revert #685 hatch

With the day/night background the plan went blurry after zooming from 100 %
(sharp when the page was opened at 800 %) and navigating a strongly zoomed
plan flashed the page white. Both came from #582's composition, not from the
wall hatch that #685 replaced:

- `.stage.daycycle.hp-safe-daycycle-outline .plan-svg` promoted the scene
  with `will-change: transform`; Chromium freezes the raster scale of such a
  layer, so the 100 % raster was shown stretched. The explicit layer #582
  needs is now `will-change: opacity` (re-rasters at the current scale).
- The filtered outline had `overflow: visible` and a gesture exposed every
  scene (#544) without bound, so the promoted layers grew with zoom squared
  (CDP LayerTree, ~460 %: plan-svg 15.9x, outline 13.2x the stage; 39x after
  navigating at 800 %). The full card clips its outline to its box and marks
  it data-hp-live-overflow="clip" (never exposed); the live viewport bounds
  every other exposure with an inline clip-path: inset(-25%) that leaves
  with it, so idle DOM stays byte-identical (#531).

Owner-verified in Chrome 152 (built-in browser, DPR 2): sharp after 100 ->
800 %, no white flashes after reloading at 800 %.

Owner decision: #685's analytic gradient is reverted (13af1d5e), the single
<pattern> is back at every zoom; its close-up golden scenes stay and check
the pattern, its terminal-frame smoke checks the pattern.

Witnesses: demo/smoke_daycycle_zoom_layers.mjs (800 % x DPR 2: layers vs
stage, the hint, reload path, button/wheel/pinch); #582/#532 smokes now pin
the opacity hint; test/live-viewport.test.mjs (bounded exposure, clipped
scene); test/daycycle-layers.test.mjs (cascade). Four Node-guarded mutants.

Issue: #689
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
Claude
2026-09-28 17:51:06 +03:00
parent c1e05e465b
commit 600330e187
18 changed files with 538 additions and 121 deletions
+65 -12
View File
@@ -4004,6 +4004,59 @@ const MUTANT_DEFINITIONS = [
replace: '',
}],
},
{
id: 'daycycle-scene-transform-hint-back',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node --test test/daycycle-layers.test.mjs',
because: '#689 K1: a will-change: transform hint freezes the scene raster scale; after '
+ 'zooming from 100 % the plan shows the stretched 100 % raster (owner, Chrome 152). '
+ 'Browser proof: demo/smoke_daycycle_zoom_layers.mjs',
patches: [{
file: 'src/styles/plan.styles.ts',
find: ' .stage.daycycle.hp-safe-daycycle-outline .plan-svg {\n'
+ ' will-change: opacity;\n',
replace: ' .stage.daycycle.hp-safe-daycycle-outline .plan-svg {\n'
+ ' will-change: transform;\n',
}],
},
{
id: 'daycycle-outline-overflow-visible',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node --test test/daycycle-layers.test.mjs',
because: '#689 K3: an unclipped filtered outline covers the whole paper at the current '
+ 'zoom (12.8x the stage at ~460 %, far more at 800 %) and navigation flashes white',
patches: [{
file: 'src/styles/plan.styles.ts',
find: ' .stage .hp-paper-outline-svg { overflow: hidden; }\n',
replace: ' .stage .hp-paper-outline-svg { overflow: visible; }\n',
}],
},
{
id: 'live-exposure-unbounded',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node --test test/live-viewport.test.mjs',
because: '#689 K3: without the clip a projected scene exposes its whole content and the '
+ 'promoted layer grows with zoom squared (plan-svg 15.9x the stage at ~460 %)',
patches: [{
file: 'src/live-viewport.ts',
find: ' if (expose && style.clipPath !== LIVE_SCENE_EXPOSURE_CLIP) {\n'
+ ' style.clipPath = LIVE_SCENE_EXPOSURE_CLIP;\n'
+ ' }\n',
replace: '',
}],
},
{
id: 'live-exposure-opens-clipped-scene',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node --test test/live-viewport.test.mjs',
because: '#689 K3: the filtered day/night outline must never be opened by a gesture: its '
+ 'filter layer precedes any clip-path and would again span the whole paper',
patches: [{
file: 'src/live-viewport.ts',
find: " && (layer as unknown as Element).getAttribute?.(SCENE_OVERFLOW_ATTRIBUTE) !== 'clip';",
replace: ' && true;',
}],
},
{
id: 'opening-light-quantum-identity',
guard: 'node --test --test-name-pattern="#366" test/logic.test.mjs',
@@ -7824,24 +7877,24 @@ const MUTANT_DEFINITIONS = [
{
id: 'hatch-stroke-not-scaled',
guard: 'node demo/smoke_wall_hatch_density.mjs',
because: 'штриховка обязана следовать за шагом: иначе на мелкой клетке '
+ 'полосы слипаются в сплошное пятно, а на крупной расходятся',
because: 'штрих обязан следовать за шагом: иначе на мелкой клетке полосы '
+ 'слипаются в сплошное пятно, а на крупной становятся волосяными',
patches: [{
file: 'src/houseplan-card.ts',
find: 'x2=${hatchStep} gradientTransform=rotate(45)',
replace: 'x2=${hatchStep * 7 / 8} gradientTransform=rotate(45)',
find: ' const stripe = 2 * (hatchStep / HATCH_BASE_STEP_UNITS);',
replace: ' const stripe = 2;',
}],
},
{
id: 'hatch-static-gradient-repeat-disabled',
guard: 'node demo/smoke_static_zoom_sharpness.mjs',
because: 'без repeat аналитический paint-server оставляет одну полосу и '
+ 'возвращает масштабозависимую пустую/мягкую штриховку #685; терминальный '
+ 'smoke обязан доказывать стабилизацию, а не только конечный viewBox',
id: 'hatch-zoom-compensation-back',
guard: 'node demo/smoke_wall_hatch_density.mjs',
because: 'компенсация 1/zoom возвращает ровно то, ради устранения чего '
+ 'задача и делалась: стена меняет вид при зуме (решение владельца §4.2)',
patches: [{
file: 'src/houseplan-card.ts',
find: 'gradientTransform=rotate(45) spreadMethod=repeat',
replace: 'gradientTransform=rotate(45) spreadMethod=pad',
find: ' width="${hatchStep}" height="${hatchStep}" patternTransform="rotate(45)">',
replace: ' width="${hatchStep}" height="${hatchStep}"\n'
+ ' patternTransform="rotate(45) scale(${Math.max(0.4, 1 / Math.max(this._zoom, 0.4)).toFixed(3)})">',
}],
},
{
@@ -9728,7 +9781,7 @@ const MUTANT_DEFINITIONS = [
+ 'until pointerup or a budgeted viewBox refresh; terminal screenshots miss the defect',
patches: [{
file: 'src/live-viewport.ts',
find: " if (options.exposeSceneOverflow && style.overflow !== 'visible') {\n"
find: " if (expose && style.overflow !== 'visible') {\n"
+ " style.overflow = 'visible';\n"
+ ' }',
replace: '',