Skip to content

feat(cabinet): AutoCAD-style typed coordinate entry for placement - #887

Open
dkbbdev wants to merge 10 commits into
pascalorg:mainfrom
dkbbdev:feat/885-coordinate-entry
Open

dkbbdev wants to merge 10 commits into
pascalorg:mainfrom
dkbbdev:feat/885-coordinate-entry

Conversation

@dkbbdev

@dkbbdev dkbbdev commented Sep 17, 2026

Copy link
Copy Markdown

Closes #885

Interaction design

  • Two independent fields: distance (wall start → near edge) and signed perpendicular offset, matching AutoCAD muscle memory.
  • Tab toggles the active field; live preview updates while either field changes; Enter places; first Escape clears the typed fields, second cancels the tool.
  • Metric + imperial input via the existing parseMeasurement.
  • Preview starts from a wall-snapped position when typing begins; projection preserves wall orientation, handles curved-wall frames, and rotates cabinet yaw to the projected wall tangent.

Parity (2D ↔ 3D)

  • 3D: coordinate entry rendered next to the preview via Html.
  • 2D floorplan: same UI in the preview layer via SVG foreignObject.
  • Both consume one generic usePlacementTyping Zustand store (buffers, active-field, projection, commit revisions, clear/reset) — reusable for future placement tools.

Files

  • packages/editor/src/store/use-placement-typing.ts (+ test)
  • packages/editor/src/components/tools/shared/placement-coordinate-input.tsx
  • packages/editor/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx
  • packages/editor/src/index.tsx
  • packages/nodes/src/cabinet/placement-dimensions.ts (+ test)
  • packages/nodes/src/cabinet/tool.tsx

Verification

  • Targeted store/projection tests: 12 passed
  • tsgo --noEmit: clean in packages/editor and packages/nodes
  • biome: clean
  • Full bun test: 7,988 passed; sole failure is the pre-existing unrelated safeFetch test on main

Risks / follow-ups

  • Manual browser check of 2D foreignObject sizing/focus across zoom levels recommended.
  • If 2D and 3D are mounted simultaneously, both instances share the store and may compete for focus (editor never mounts both today).

Note

Medium Risk
Substantial changes to cabinet placement commit paths, keyboard handling, and wall projection math; incorrect edge cases could mis-place cabinets or fight focus between 2D/3D HUDs.

Overview
Adds AutoCAD-style typed coordinate entry for wall-snapped cabinet placement: users type distance (wall start → near edge) and offset (perpendicular from the wall face), with live preview and Enter / click to commit.

A shared usePlacementTyping store and PlacementCoordinateInput HUD back both views—3D via Html, 2D floorplan via SVG foreignObject (gated by viewMode so only one surface owns focus). Pointer moves are ignored while typing; cancel restores the last pointer pose unless a commit succeeds.

Cabinet math gains getCabinetPlacementCoordinates / resolveCabinetTypedPlacementPosition (curved walls, miter face offset at destination, oversized cabinets stay centered). findClosestCabinetWallInPlan accepts optional maxDistance: Infinity when the wall is already known. The cabinet tool wires keyboard/HUD input through parseMeasurement, strips run-insertion preview on typed commits, and seeds continuous stretch at the typed pose.

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

@pascal

pascal Bot commented Sep 17, 2026

Copy link
Copy Markdown

I hit an error while handling your request (Model unavailable on AI Gateway free tier: Free tier users do not have access to this model. Upgrade to paid credits at https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%3Fmodal%3Dtop-up for unrestricted…).

Please try again, rephrase, or reach out if it keeps failing.

Error id: 5d855979-f1f0-4e3f-8558-79abd46c6a3f

@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.

Stale Bugbot comment from a previous run.

Comment thread packages/nodes/src/cabinet/tool.tsx
Comment thread packages/nodes/src/cabinet/tool.tsx Outdated
- Strip insertionPreview/insertionFailure in applyTypedPlacement so Enter
  commits the typed wall position instead of re-entering the pointer
  path's run-insertion branch (Bugbot: stale insertion, high).
- Drop the trailing setProjectedPosition(snap) in beginTypedPlacement that
  pinned the HUD to the pre-typing snap instead of the first digit's
  projection.
- Clear typedWallHitRef/typedCoordinateDefaultsRef on every typing exit
  (becameInactive), covering Escape typed inside the HUD input whose
  window handler skips INPUT targets.
- Render the coordinate HUD from exactly one surface: 2D pane in 2d/split,
  3D Html in 3d-only — split view mounts both panes, so both instances
  fought over caret/focus.
@dkbbdev

dkbbdev commented Sep 17, 2026

Copy link
Copy Markdown
Author

Addressed all 4 Bugbot findings in 27101de:

  1. Stale insertion (high)applyTypedPlacement now strips insertionPreview/insertionFailure before recomputing, so Enter commits the typed wall position instead of re-entering the run-insertion branch from the pointer path.
  2. HUD pinned to pre-typing snap — dropped the trailing setProjectedPosition(current.position) in beginTypedPlacement; the projection written by the first keystroke's applyTypedPlacement now stands.
  3. Escape leaves stale wall hit — the placement-typing subscription now clears typedWallHitRef/typedCoordinateDefaultsRef on every becameInactive transition, covering Escape typed inside the HUD input (whose window handler skips INPUT targets).
  4. Split-view double HUD — the HUD now renders from exactly one surface: the 2D pane owns it in 2d/split view, the 3D Html mounts it only in 3d-only mode (the 2D pane stays CSS-mounted when hidden, so both gates are needed). Note: split view does mount both panes (show2d && show3d), so the original "never mounts both" assumption was wrong.

Verification: tsgo --noEmit clean in packages/nodes + packages/editor; biome clean; cabinet suite 937 pass; editor suite 1,041 pass; full bun test 7,988 pass with the sole failure being the pre-existing unrelated safeFetch test on main.

@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.

Stale Bugbot comment from a previous run.

Comment thread packages/nodes/src/cabinet/tool.tsx
Comment thread packages/nodes/src/cabinet/tool.tsx Outdated
- Neutralize altKey on the reused pointer event for typed commits so a
  stale force-place modifier cannot override the typed pose (Bugbot:
  stale Alt click).
- onClick now reports whether a cabinet was committed; a rejected typed
  commit (e.g. collision) keeps the typing session so values survive and
  can be adjusted instead of being silently wiped.
- 2D typed-entry HUD sizes its foreignObject from CSS px × unitsPerPixel
  and counter-rotates by -sceneRotationDeg (mirrors the measurement
  extrusion control), so inputs render screen-upright at a usable size
  at any zoom/rotation.
@dkbbdev

dkbbdev commented Sep 17, 2026

Copy link
Copy Markdown
Author

Round 2 in 4ec9ae8 — addressed the 3 new findings:

  1. Stale Alt click overrides typed commit — the reused pointer event now has altKey neutralized ({...nativeEvent, altKey: false}), so a stale force-place modifier can no longer divert the typed commit into resolvePlacement(event).
  2. Enter clears typing after failed placeonClick now returns a commit boolean; the typing subscription only ends the session when a cabinet was actually committed. A rejected (colliding) pose keeps the typed distance/offset for adjustment.
  3. 2D HUD ignores screen-pixel scale (high) — the foreignObject is now sized cssPx × unitsPerPixel with a compensating inner scale() transform, wrapped in a rotate(-sceneRotationDeg) group, mirroring the established measurement extrusion control pattern (floorplan-measurement-tool-layer.tsx). The HUD stays screen-upright at a usable pixel size at any zoom/rotation.

Verification: tsgo clean (nodes+editor), biome clean, cabinet suite 937 pass, editor suite 1,041 pass, full bun test 7,988 pass (sole failure = pre-existing safeFetch on main).

@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.

Stale Bugbot comment from a previous run.

Comment thread packages/nodes/src/cabinet/tool.tsx Outdated
Comment thread packages/nodes/src/cabinet/tool.tsx
- Continuous-mode typed Enter now seeds the stretch preview at the typed
  position (zero-length span) via a rawOverride on
  resolveStretchedPlacement, instead of growing toward the stale cursor
  position carried by the reused pointer event (Bugbot: high).
- A canvas click while typed entry is active ends the typing session
  before committing, so a newly started stretch can follow the pointer
  instead of being frozen until Escape (Bugbot: medium).
@dkbbdev

dkbbdev commented Sep 17, 2026

Copy link
Copy Markdown
Author

Round 3 in 2074553:

  1. Continuous Enter stretches to stale cursor (high) — typed commits in continuous mode now pass the typed position into resolveStretchedPlacement as a rawOverride, seeding a zero-length stretch at the typed pose instead of growing toward the pre-typing cursor.
  2. Click during typing freezes stretch — a canvas click while typed entry is active now ends the typing session first, so a stretch started by that click can follow the pointer. The synthetic typed-commit call is exempt (fromTypedCommit), keeping the rejected-commit behavior from round 2 (typed values survive a colliding pose).

(The third comment on 4ec9ae8 was a re-anchor of the already-fixed Enter-clears-typing finding.)

Verification: tsgo clean, biome clean, cabinet suite 937 pass / 0 fail.

@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.

Stale Bugbot comment from a previous run.

Comment thread packages/nodes/src/cabinet/tool.tsx
findClosestCabinetWallInPlan now accepts maxDistance; beginTypedPlacement
passes Infinity since the placement already knows its wallId and only
needs the projection onto it. A typed perpendicular offset (or deep
cabinet) can push the center beyond WALL_SNAP_DISTANCE_M, which made the
next session fail to start and let digits fall through to global phase
shortcuts.
@dkbbdev

dkbbdev commented Sep 17, 2026

Copy link
Copy Markdown
Author

Round 4 in 9eabd0a:

Typed entry blocked after offsetfindClosestCabinetWallInPlan now accepts maxDistance; beginTypedPlacement passes Infinity because the placement already knows its wallId and only needs the projection onto that wall (the closest-wall guard hit.wall.id !== current.wallId still protects against grabbing a different wall). A typed perpendicular offset or deep cabinet can push the center past WALL_SNAP_DISTANCE_M, which previously made re-entering coordinate entry fail and let digits fall through to global phase shortcuts.

Verification: tsgo clean, biome clean, cabinet suite 937 pass / 0 fail.

@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.

Stale Bugbot comment from a previous run.

Comment thread packages/nodes/src/cabinet/tool.tsx
Comment thread packages/nodes/src/cabinet/tool.tsx
Comment thread packages/nodes/src/cabinet/placement-dimensions.ts Outdated
- Restore the pre-typing pose when a typing session ends uncommitted
  (Escape/tool cancel): placementRef kept the typed pose, so a click right
  after Escape committed the cancelled coordinates (Bugbot: high).
- Ending the session on island toggle: island has no wall snapping, so the
  session would be orphaned (frozen pointer, vanished HUD) until Escape.
- resolveCabinetTypedPlacementPosition clamps distance instead of
  rejecting when the cabinet is wider than the wall, matching the
  coordinate getter's clamp so the pose stays committable (Bugbot: medium).
@dkbbdev

dkbbdev commented Sep 17, 2026

Copy link
Copy Markdown
Author

Round 5 in e709e8d:

  1. Escape click commits cancelled pose (high)beginTypedPlacement now snapshots the pre-typing placement; any uncommitted exit (Escape, tool cancel) restores it, so placementRef no longer holds the typed pose for the next click to commit. Successful typed commits and canvas-click commits suppress the restore (suppressTypedRestoreRef) since they own the placement afterwards.
  2. Island toggle freezes typed sessionapplyPlacementType now ends an active typed session when switching to island (no wall snapping → the session would be orphaned with a frozen pointer and no HUD until Escape).
  3. Short wall rejects typed placementresolveCabinetTypedPlacementPosition clamps the distance into the valid span instead of rejecting, matching getCabinetPlacementCoordinates' clamp so a cabinet wider than the wall remains committable (the span is empty → distance 0).

Verification: tsgo clean, biome clean, cabinet suite 937 pass / 0 fail.

@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.

Stale Bugbot comment from a previous run.

Comment thread packages/nodes/src/cabinet/placement-dimensions.ts
Comment thread packages/nodes/src/cabinet/tool.tsx
Comment thread packages/nodes/src/cabinet/tool.tsx
- A canvas click during typed entry now commits the typed preview pose
  (suppress the pre-typing restore; keep placementRef) instead of the
  restored original snap (Bugbot: high).
- Strip 'node' from the synthetic typed-commit event so
  stopPlacementCommitPropagation does not install its 300ms window click
  swallow, which could eat the user's next real click (Bugbot: medium).
- resolveCabinetTypedPlacementPosition samples the mitered face offset at
  the destination station (hit.localX = typed distance + width/2) instead
  of the frozen pre-typing snap (Bugbot: medium).
@dkbbdev

dkbbdev commented Sep 17, 2026

Copy link
Copy Markdown
Author

Round 6 in 7c7851a:

  1. Click while typing places original snap (high) — the click guard now suppresses the pre-typing restore and keeps placementRef at the typed pose, so a canvas click during typed entry commits what the user sees (the typed preview), not the restored original snap.
  2. Typed continuous Enter swallows next click — the synthetic commit event strips node, so stopPlacementCommitPropagation no longer installs its 300ms window click swallow for a keyboard commit that has no follow-up browser click.
  3. Typed distance ignores mitered face offsetresolveCabinetTypedPlacementPosition now samples the miter at the destination station (localX = clampedDistance + width/2) via {...hit, localX}, matching what the dimension path already did with providedWallHit.

Verification: tsgo clean, biome clean, cabinet suite 937 pass / 0 fail.

- HIGH-2: recompute wallSurfaceNormal from updated resolved.yaw in applyTypedPlacement

- HIGH-3: re-resolve and publish placement from lastPlacementEventRef when typing ends without commit

- QUICKWIN-2: allow '/' and ',' in isPlacementTypingKey and advance field on comma in PlacementCoordinateInput

Note: HIGH-1 and QUICKWIN-1 were already addressed in commit 7c7851a.
@dkbbdev

dkbbdev commented Sep 17, 2026

Copy link
Copy Markdown
Author

Post-review hardening in bb8ea5e (+7c7851af earlier in this push):

  • Recompute wallSurfaceNormal from the resolved yaw in applyTypedPlacement — fixes stale normals on curved walls
  • Re-resolve and re-publish the placement from the last pointer event when typed entry is cancelled (Escape/tool switch) — no more committing the cancelled pose on the next click
  • Accept / and , in the typing key filter: imperial fractions (24 1/2") now typeable, and comma advances to the offset field (AutoCAD dynamic-input convention)
  • Boundary checks on typed distance now use DIMENSION_EPSILON tolerance (was already in 7c7851a)

Verified: 57 targeted tests pass, full suite 7,990 tests — only pre-existing unrelated safeFetch mcp failure remains.

@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.

Stale Bugbot comment from a previous run.

Comment thread packages/nodes/src/cabinet/tool.tsx
Comment thread packages/nodes/src/cabinet/tool.tsx
…les field

- A canvas click during typed entry now ends the session only when the
  commit succeeds (onClick wrapper defers clear() past onClickInner). An
  invalid typed pose keeps the session alive so values stay editable
  (Bugbot: invalid click ends typed entry, high).
- The capture-phase window keydown handler now treats ',' as a field
  toggle like the focused HUD input does, so comma advances to the
  offset field even when the HUD is unfocused (Bugbot: medium).
@dkbbdev

dkbbdev commented Sep 17, 2026

Copy link
Copy Markdown
Author

Round 7 in 7eb477c (on top of bb8ea5e):

  1. Invalid click ends typed entry (high)onClick is now a wrapper that defers ending the typing session until after onClickInner resolves the commit. A canvas click on an invalid typed pose keeps the session (and the editable values) alive instead of clearing it up front.
  2. Comma does not advance offset field — the capture-phase window keydown handler now treats , as a field toggle (matching the focused HUD input), so comma advances to the offset field even when the HUD is unfocused (e.g. 2D foreignObject case).

Verification: tsgo clean (nodes+editor), biome clean, cabinet suite 937 pass, editor store suite 79 pass.

@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 1 potential issue.

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 7eb477c. Configure here.

Comment thread packages/nodes/src/cabinet/placement-dimensions.ts Outdated
Wall snap centers an oversized cabinet on the wall (empty span →
midpoint), but typed entry mapped default distance 0 to
localX = width / 2, sliding the cabinet off the wall center — possibly
past the wall end — the moment coordinate entry started or the default
was committed. resolveCabinetTypedPlacementPosition now resolves the
centered pose (wallLength / 2) when the cabinet is wider than the wall,
matching getCabinetPlacementCoordinates and resolveCabinetWallSnapPlacement.

Adds a regression test: wall 4m, cabinet 5m wide → distance 0 resolves
to wallLocalX 2 (wall center), not 2.5.
@dkbbdev

dkbbdev commented Sep 17, 2026

Copy link
Copy Markdown
Author

Round 8 in 51953c0:

Wide cabinet jumps off wall center (medium)resolveCabinetTypedPlacementPosition now centers the cabinet on the wall (wallLength / 2) when the cabinet is wider than the wall, mirroring resolveCabinetWallSnapPlacement's empty-span → midpoint rule. Previously typed distance 0 mapped to localX = width / 2, so merely starting coordinate entry (or committing the default) slid the cabinet off the wall center, possibly past the wall end. getCabinetPlacementCoordinates already read distance 0 for that centered pose, so the round-5 clamp fix is preserved — the two functions now agree in both directions.

Regression test added (wall 4 m, cabinet 5 m wide → typed distance 0 resolves to wallLocalX = 2, not 2.5); verified it fails without the fix.

Verification: tsgo clean (nodes+editor), biome clean, cabinet suite 938 pass / 0 fail.

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.

Typed coordinate entry for item placement (distance-from-wall + offset, AutoCAD-style)

1 participant