Skip to content

Adjustments pass: three 0.185, undo/cancel semantics, guide & panel fixes#496

Merged
wass08 merged 16 commits into
mainfrom
fix/adjustments-pass
Jul 14, 2026
Merged

Adjustments pass: three 0.185, undo/cancel semantics, guide & panel fixes#496
wass08 merged 16 commits into
mainfrom
fix/adjustments-pass

Conversation

@wass08

@wass08 wass08 commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Bug-fix and adjustments pass across viewer + editor, plus the three.js migration:

three.js 184 → 185 (acb9c55a)

  • Runtime bumped to 0.185.1 (TSL renames directionToColor/colorToDirectionpackNormalToRGB/unpackRGBToNormal, SSGI's packed rgba output split into separate AO/GI textures). Note: wind-node keeps positionLocal — it was never removed in r185, and swapping it for positionGeometry discards instance transforms (edc4e2f4).
  • @types/three stays pinned at 0.184.1: the 0.185 typings send tsgo's inference into unbounded allocation (Unbounded memory consumption with recursive conditional types causes compilation OOM microsoft/typescript-go#2125 class — ~70GB/90s, OOM-kills the machine). viewer/lib/tsl-compat.ts bridges the two renamed TSL exports; drop it and the pin together once tsgo copes.

Undo vs. gestures

  • ⌘Z pressed mid-interaction (moving, drawing, placing, dragging a handle) now cancels the gesture exactly like Escape instead of history-jumping under a live pointer. Covers tool:cancel consumers, scope-tracked drags, and draft placement (detected via paused temporal history).
  • Handle drags that only knew pointercancel (resize/height/rotate handles, wall side handles, roof trim, group rotate) gain capture-phase Escape/⌘Z cancel — fixing Escape for them too.
  • Undo history floored at scene load (92e7b4ef) — undo can no longer wipe a freshly-loaded project.

Viewer fixes

  • Shadows toggle flips renderer.shadowMap.enabled instead of per-light castShadow (r184 WebGPU cache-eviction crash) (b3283366).
  • .ktx2 preset-texture loads gate on detectSupport via whenKtx2Ready() — fabric/leather slots no longer capture white in standalone-canvas thumbnails (c9aef1fc).
  • Measurement-unit default derived from timezone/locale until the user picks one (f51cd858), plus applyCountryUnitDefault for hosts with an authoritative IP-derived country (3158ec7a).

Editor / 2D floorplan

  • Right-click no longer starts handle drags (117c59db).
  • Box select arms over any guide image that won't itself drag (locked or unselected) (cd830279, 3b07955b).
  • Live rotation readout (wedge + degree chip) while rotating a guide image (daf01b96).
  • Guide set-scale dialog accepts lingo free-text (5'11", 180cm, 1m80) with a = 1.80 m hint (84a1c4f5).
  • Selecting a scene node clears a lingering reference selection (the floorplan panel no longer sticks); the inspector's expanded state resets once everything is deselected (5488cae5).

How to test

  1. bun dev, open a scene — verify it renders on WebGPU with no LightsNode/GPUValidationError console errors (three 0.185).
  2. Rotate an item with a handle, press ⌘Z mid-drag → drag reverts, no undo. Same for Escape. Arm a preset placement, ⌘Z → ghost cancels, back to select; ⌘Z again → normal undo.
  3. Draw a wall, ⌘Z mid-draw → draft clears, tool stays armed.
  4. Display settings → shadows off, then on → no WebGPU crash.
  5. 2D floorplan with a guide image: marquee-drag starting on the guide → box select arms; click → selects; rotate with corner handles → live degree readout; set scale → type 5'11" → hint shows = 1.80 m.
  6. Select a guide (floorplan), then click a wall → wall panel replaces the reference panel immediately.
  7. Fresh browser profile: unit default follows timezone (US → imperial); picking a unit manually sticks.
  8. Item thumbnail capture with a fabric/leather slot material → material renders (not white).

Screenshots / screen recording

N/A — verified live in the hosting app (Wawa House renders clean on WebGPU post-migration); happy to attach clips on request.

Checklist

  • I've tested this locally with bun dev
  • My code follows the existing code style (run bun check to verify)
  • I've updated relevant documentation (if applicable)
  • This PR targets the main branch

Note

Medium Risk
Touches core scene undo (zundo), WebGPU rendering (three 0.185 + shadows/SSGI), and widespread gesture keyboard handling; changes are targeted but affect critical editor and viewer paths.

Overview
Bumps three.js to 0.185 across apps and packages (lockfile + root override), with viewer/editor post-processing and thumbnails updated for r185 TSL normal packing (packNormalToRGB / unpackRGBToNormal via tsl-compat) and SSGI’s split AO/GI outputs instead of a single packed texture.

Undo and in-flight gestures are reworked: ⌘Z/⌘⇧Z during moves, handle drags, and paused-history placement cancels like Escape (capture-phase handlers + shared isHistoryShortcut), and global keyboard routing skips history when an interaction is active. Scene load/import/reset now calls clearSceneHistory() after setScene/clearScene, with setScene doing one tracked write after normalization and clearSceneHistory resuming temporal tracking so undo is floored at the loaded baseline.

Viewer: shadows toggle drives renderer.shadowMap.enabled (not per-light castShadow) to avoid r184 WebGPU validation failures; .ktx2 loads wait on whenKtx2Ready() after detectSupport; default metric/imperial follows timezone/locale until the user sets a unit (applyCountryUnitDefault for hosts).

Editor / floorplan: primary-button-only handle activation; live guide rotation wedge; reference-scale lingo length input; guide pointer-down only consumes events when selected+unlocked (box select on guide images); selecting scene nodes clears stale reference selection and resets inspector collapse; settings import/reset clear undo history.

Reviewed by Cursor Bugbot for commit edc4e2f. Bugbot is set up for automated code reviews on this repo. Configure here.

wass08 and others added 16 commits July 14, 2026 09:19
Right-click over a rotation/move/resize handle started the gesture and
stopPropagation()'d, fighting the camera orbit. Guard every gesture
starter (shared useHandleDrag, group rotate gizmo, wall endpoint/height/
move, fence move, roof trim) with event.button !== 0 before it swallows
the event.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Derive the metric/imperial default from the IANA timezone (US, Liberia,
Myanmar zones -> imperial; anything else -> metric), falling back to an
explicit locale region subtag only when no timezone resolves. Timezone
tracks actual location, unlike navigator.language where en-US is a common
default far outside the US. The unit is only persisted once the user
explicitly sets it, so an untouched preference keeps tracking location;
existing persisted values are treated as explicit and left alone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hadow

Flipping a light's castShadow at runtime crashes three r184's WebGPU
renderer: toggling off disposes the shadow map's GPU texture, but the
node builder cache evicts with the post-toggle key, so the shadows-on
entry survives still referencing the destroyed texture. Re-enabling
reuses that stale state and every frame submit fails with
GPUValidationError ("Invalid CommandBuffer from CommandEncoder").

Keep castShadow static and drive the user-facing toggle through the
Canvas shadows prop (renderer.shadowMap.enabled), which rebuilds
materials without disposing shadow resources.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Undo could step back past the scene load into the pre-load (empty)
state, wiping the whole project — which autosave would then persist.
Two defects: clearSceneHistory() had zero call sites, so every load left
the empty pre-load state in zundo's pastStates; and setScene wrote the
store twice, recording a half-normalized intermediate as a second undo
target.

- applySceneGraphToEditor, JSON import, and reset-to-default now clear
  history so the loaded scene is the undo floor
- setScene collapses to a single tracked write (final state identical)
- clearSceneHistory also resumes tracking so a load landing inside a
  pause window can't strand undo recording off

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
r185 renames directionToColor/colorToDirection to packNormalToRGB/
unpackRGBToNormal and splits SSGI's packed rgba output into separate AO
(getAONode, single channel) and GI (getGINode) textures; wind-node's
positionLocal reads become positionGeometry.

@types/three stays at 0.184.1: the 0.185 typings send tsgo's inference
into unbounded allocation (microsoft/typescript-go#2125 class — it ate
~70GB/90s and OOM-killed the machine). viewer/lib/tsl-compat.ts bridges
the two renamed TSL exports with 0.184-typed signatures; drop it and the
pin together once tsgo copes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
KTX2Loader.load throws before detectSupport has run, and materials
created while a standalone capture canvas's renderer was still
initializing cached themselves permanently texture-less — fabric slots
rendered white in item thumbnails. .ktx2 loads now await whenKtx2Ready()
(resolved by the first successful ensureKtx2Support), which is exported
so hosts with standalone canvases can arm it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
applyCountryUnitDefault lets the host app feed an authoritative
IP-derived country (e.g. Vercel's x-vercel-ip-country) into the unit
default. Stronger signal than the timezone heuristic applied at store
creation, still never overrides an explicit user choice.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A locked guide's hit-rect swallowed pointer-down via stopPropagation, so
marquee selection couldn't start on top of it. Locked guides now let the
event bubble to the svg root; click-to-select and the unlock affordance
still work because a non-drag release fires onClick as before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rotating a guide with the 2D handles gave no angle feedback. Reuse the
registry layer's RotationAngleOverlay (wedge + degree chip) for guide
rotate drags: sweeps from the grabbed corner's bearing at grab to its
current snapped bearing, suppressed under ~0.5deg so a fresh grab doesn't
flash a sliver.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The real-length field accepts natural measurement text via
@pascal-app/lingo — 5'11", 180cm, 1m80, 12ft — parsed in the dropdown's
unit (a bare number still means that unit, a typed unit wins). A faint
'= 1.80 m' hint previews non-trivial input, unparseable text gets a
clear error, and the odd onBlur force-reset to 0.0001 is gone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…doing

Undo pressed while the mouse is mid-action (moving, drawing, dragging a
handle) used to history-jump under the live pointer — stale carry, half
gestures committing against a rewound scene. Now it reads as 'abort this
action', exactly like Escape:

- the global undo/redo arms first route through the tool:cancel path
  (covers build drafts, placement ghosts, move tools) and skip the
  history jump when anything was in flight (consumed, scope-active, or
  inputDragging);
- pointer drags that only knew pointercancel (generic handle drags,
  group rotate, wall side/height handles, roof trim) gain the same
  capture-phase Escape/cmd+z keydown the group-move drags already had —
  fixing Escape for them too;
- the existing capture-phase handlers (3D/2D group move, 2D registry
  move overlay) additionally accept cmd+z as cancel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-up to cd83027, which only let LOCKED guides bubble pointer-down.
An unlocked, unselected guide also swallowed the event for nothing (no
translate drag starts), so marquee selection could never start on top of
it. Now only the one case that uses the event consumes it — selected +
unlocked → translate drag — and everything else bubbles to the svg root.
Click-to-select still works: a non-drag release never crosses the
box-select threshold, so the trailing click fires the guide's onClick.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The preset/item placement flow (useDraftNode + placement coordinator)
registers no interaction scope and holds no pointer, so the cmd+z cancel
guard from c699d74 saw it as idle and history-jumped mid-placement.
Paused scene history is the universal tell — the draft cycle (and every
adopted-move session) keeps temporal paused for the whole gesture, and
an undo against a paused store lands on a stale baseline anyway. Treat
!isTracking as in-flight.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… undo

4ffda72 stopped the history jump during preset/item placement but left
the draft alive: the item tool passes no coordinator onCancel — it is
Escape's fall-through (switch to select, unmount the tool) that actually
destroys the draft. Extract that fall-through and run it from the cmd+z
path too whenever a gesture is live and nothing consumed tool:cancel,
so cmd+z now behaves exactly like Escape end to end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Selecting a guide clears the node selection (handleGuideSelect), but the
reverse was never wired: clicking a wall with a floorplan reference
selected left selectedReferenceId set, and the panel manager's
reference-first priority kept showing the floorplan panel until it was
closed by hand. PanelManager now drops the stale reference the moment a
scene selection (nodes or zone) appears.

Also: the inspector's expanded state is shared across panel swaps by
design, but it survived close/reopen too — deselecting everything now
resets it, so a fresh selection opens the panel collapsed again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nGeometry

The three-0.185 migration renamed positionLocal to positionGeometry in
the wind nodes, but positionLocal was never removed in r185 — and the
two are not interchangeable here. NodeMaterial.setupPosition applies the
instance transform by mutating positionLocal, then overwrites it with
positionNode's output; reading raw positionGeometry therefore discarded
every instance matrix — leaf cards rendered unscaled at tree-local
coordinates (a giant canopy filling the sky) and grass/flower instances
collapsed invisibly. Reading positionLocal (instance transform included)
restores r184 behavior exactly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@wass08 wass08 merged commit a590747 into main Jul 14, 2026
3 checks passed

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 4 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit edc4e2f. Configure here.

try {
;(ktx2Loader as unknown as { detectSupport: (r: unknown) => void }).detectSupport(renderer)
configuredRenderers.add(key)
resolveKtx2Ready()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KTX2 readiness never resolves

Medium Severity

The whenKtx2Ready promise only resolves on successful KTX2 support detection. If detectSupport consistently fails (e.g., renderer not ready), the promise never settles. This leaves .ktx2 texture loads indefinitely pending, resulting in white materials and no retry.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit edc4e2f. Configure here.

// Escape does — the mode switch is what actually cancels tools that hook
// their teardown to unmount (preset/item placement).
exitToSelectAfterUnconsumedCancel()
return true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Paused history blocks undo

Medium Severity

cancelInteractionForHistoryShortcut treats !useScene.temporal.getState().isTracking as an active gesture and calls exitToSelectAfterUnconsumedCancel() instead of running undo. If temporal history stays paused after a leaked or mismatched pause (direct temporal.pause() without a matching resume), ⌘Z clears selection and exits tools rather than stepping history.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit edc4e2f. Configure here.

} else if (e.key === 'Z' && e.shiftKey && (e.metaKey || e.ctrlKey)) {
if (isVersionPreviewMode) return
e.preventDefault()
if (cancelInteractionForHistoryShortcut()) return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Palette undo skips cancel

Low Severity

Keyboard undo/redo now calls cancelInteractionForHistoryShortcut() before runUndo / runRedo, but command-palette undo/redo still invokes runUndo / runRedo directly. Mid-drag or mid-placement, palette undo can history-jump under a live pointer—the behavior this PR explicitly removes from ⌘Z.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit edc4e2f. Configure here.

resetDesktopInspectorCollapsed()
}
}, [hasAnySelection])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reference panel wins over nodes

Medium Severity

Node selection clears selectedReferenceId in a useEffect, but desktop and mobile still render ReferencePanel whenever that id is set, before checking scene selection. After picking a wall or item from paths that only call setSelection, both ids can be set for a render, so the reference panel stays visible until the effect clears it.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit edc4e2f. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant