Skip to content

feat: interactive doors in walkthrough view + swing-direction consistency#253

Closed
b9llach wants to merge 1 commit intopascalorg:mainfrom
b9llach:feat/interactive-doors
Closed

feat: interactive doors in walkthrough view + swing-direction consistency#253
b9llach wants to merge 1 commit intopascalorg:mainfrom
b9llach:feat/interactive-doors

Conversation

@b9llach
Copy link
Copy Markdown
Contributor

@b9llach b9llach commented Apr 16, 2026

What does this PR do?

Hover a door, press F, it opens. Press F again, it closes. Smooth cubic ease-in-out on a 450 ms swing around the hinge edge, with mid-animation toggles reversing from wherever the leaf is rather than snapping. Works in both walkthrough and editor modes — pointer-hover drives the target in the editor; a camera-forward raycast drives it in walkthrough (with a Press F to open / close hint that appears under the crosshair when the target is within ~2.5 m).

Rolls in two consistency fixes that I found while wiring this up — both are surface-level mismatches between what the door panel lets you set and what actually renders.

1. Interactive open / close

Core — new hinge-pivot group in DoorSystem. The leaf, handle, door closer, and panic bar now live inside a named leafPivot group positioned at the hinge edge (with a zero-offset child leafOffset so existing door-center-relative coordinates still work). Rotating leafPivot.rotation.y swings the free edge around the correct axis. The frame posts, head, threshold, hinges, and wall cutout stay on the root mesh where they belong. On dirty-rebuild the group is preserved, so whatever rotation was applied survives geometry regeneration (so e.g. flipping hingesSide mid-swing repositions the pivot without resetting the rotation to 0).

Viewer — new DoorInteractiveSystem and DoorInteractionHint. The system owns:

  1. An F-key handler (guards on inputs/textareas the same way use-keyboard.ts does).
  2. A per-frame crosshair raycast in walkthrough mode that finds the nearest door within range and sets useViewer.crosshairHoveredDoorId.
  3. The per-frame animation tick that reads time-based entries out of useViewer.doorAnim and pushes an eased rotation onto each door's leafPivot. Store writes only happen on toggle, so subscribers don't re-render during the swing.
  4. A periodic (~1s) prune of anim entries for doors that have since been deleted from the scene.

DoorInteractionHint is a plain DOM overlay that portals into whatever layout already sits over the canvas (the editor's FirstPersonOverlay in this PR). The viewer package exports it so the read-only /viewer/[id] route could use it too.

Store — useViewer.doorAnim, toggleDoor, pruneDoorAnim, crosshairHoveredDoorId. Door animation state is { from, target, startedAt } per door id — intentionally not persisted across full reloads (excluded from partialize) so opening a bunch of doors while reviewing a scene doesn't leak into the next session.

2. Swing direction matches the 2D floorplan arc

The floorplan panel already draws a swing arc from hingesSide + swingDirection. The 3D renderer didn't rotate the leaf in a way that matched the arc — the initial sign convention I tried for the swing animation swung the free edge opposite the floorplan's curve. Fixed in DoorInteractiveSystem so the rotation sign agrees with the floorplan convention: "inward" swings the free edge toward the wall's interior-side perpendicular, "outward" the other way, hingesSide picks the pivot edge. Editing either field in the door panel now updates both views the same way.

3. Handle auto-derived from hingesSide (on both faces)

Two small bugs in DoorSystem that I hit while testing:

  • Handle side was independent from hinges side. The leaf had a separate handleSide field with its own segmented control in the panel. Flipping hingesSide (e.g. to correct the swing) left the handle stranded on the hinge edge, which is never what a real door looks like. The renderer now always places the handle on the side opposite the hinges, derived from hingesSide directly. handleSide is still in the schema (backward-compat for existing scenes) but no longer affects rendering, and the redundant panel control is removed.
  • Handle only existed on one face of the leaf. Only the +Z face had a backplate + lever — from the other room the door looked like it had no handle. The renderer now stamps a mirrored pair on the -Z face too. The asymmetric hardware (door closer, panic bar) stays single-sided on purpose, since those really are one-sided in practice (push-side vs pull-side).

How to test

  1. bun dev, open any scene with a door.
  2. In editor mode: hover a door with the mouse → press F. The door should swing open around its hinge edge on a ~450 ms ease. Press F again to close. Open a door, change its hingesSide or swingDirection in the panel → the leaf repositions around the new pivot and continues with the correct rotation direction. Confirm there's no longer a Handle Side control in the panel.
  3. In walkthrough mode: click the walkthrough toggle → look at a door within ~2.5 m. A Press F to open hint should appear under the crosshair. Press F. Walk to the other side of the door — there should be a handle on this face too. Glance away and back mid-swing; the ratio should pick up from where it is.
  4. Floorplan consistency: open the 2D floorplan. Compare the arc direction with which way the 3D leaf swings in walkthrough. They should agree across all four hingesSide × swingDirection combinations.
  5. Editor F still opens the furnish panel out of walkthrough. The walkthrough guard in use-keyboard.ts only bails when walkthroughMode === true; flat 3D / floorplan F keeps its existing behaviour.
  6. bun check, bun check-types, bun run build all clean on the touched files.

Scope

  • 9 files:
    • packages/core/src/systems/door/door-system.tsx — leaf-pivot group, handle-side auto-derive, handle on both faces
    • packages/viewer/src/store/use-viewer.tsdoorAnim, toggleDoor, pruneDoorAnim, crosshairHoveredDoorId, shared easing helper + duration/angle constants
    • packages/viewer/src/systems/door/door-interactive-system.tsxnew
    • packages/viewer/src/systems/door/door-interaction-hint.tsxnew
    • packages/viewer/src/index.ts — export DoorInteractionHint
    • packages/viewer/src/components/viewer/index.tsx — mount <DoorInteractiveSystem />
    • packages/editor/src/hooks/use-keyboard.ts — let DoorInteractiveSystem claim F in walkthrough
    • packages/editor/src/components/editor/first-person-controls.tsxOpen door [F] in the controls hint; mount <DoorInteractionHint />
    • packages/editor/src/components/ui/panels/door-panel.tsx — remove the redundant Handle Side segmented control
  • Schema-compatible: no new fields on DoorNode; the animation state is runtime-only in the viewer store. handleSide is preserved in the schema for existing scenes, just no longer load-bearing.
  • Deprecation note: if nothing else ends up needing handleSide, it's a safe follow-up to drop from the schema in a later PR.

Checklist

  • I've tested this locally with bun dev
  • My code follows the existing code style (bun check passes on the touched files — verified via biome check at @biomejs/biome@^2.4.6)
  • I've updated relevant documentation (N/A — no docs affected)
  • This PR targets the main branch

…ency

Hover a door and press F to open or close it. The leaf animates around
its hinge edge with a cubic ease-in-out; mid-swing toggles reverse
smoothly from wherever the leaf is. Works in both walkthrough and
editor modes (pointer-hover source in editor, crosshair raycast in
walkthrough). Bundles two consistency fixes that surface the same
hingesSide/swingDirection fields the floorplan already exposed.
@b9llach b9llach closed this Apr 16, 2026
nnhhoang pushed a commit to nnhhoang/editor that referenced this pull request Apr 16, 2026
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