Repository navigation
Conversation
… fences in 3D and 2D
…d dimension input
…anchors in React 19
Reject malformed degree suffixes before locking wall geometry while retaining normalization and unchanged dimension-unit behavior. Cover public core exports. Follow-up to pascalorg#1207, original feature by saadmooh.
…es (#1249) * feat(wall): typed exact length during two-click wall drafting (#308) While drafting a wall, printable keys open a typing buffer; the draft endpoint is projected onto the typed length along the current draft direction, so the pointer keeps steering direction only. Enter commits at the typed length, two-stage Escape clears the buffer first and then cancels the draft, Backspace/Delete edit the buffer. Shared zustand store keeps the 2D floor plan and the 3D view in lockstep (parity rule in wiki/architecture/tools.md); angle/junction snapping is preserved on untyped drafts and bypassed only for the typed commit. * fix(wall): address review — commit typed length exactly, no buffer leaks - click/Enter commit reuses the projected endpoint the previews show instead of re-snapping the raw pointer (2D + 3D parity) - HUD subscribes to the typing store so the label tracks keystrokes - typing buffer cleared on every commit/cancel path; never carries into the next chain segment - 2D typing keys stopPropagation so tool shortcuts don't fire mid-type - drop unused begin() action * fix(wall): honor typed length on Enter/click exits, latest-ref for 2D Enter commit - 3D Enter keeps the buffer across the synthetic grid:click so the commit is recognized as typed (no re-snap), clears defensively after - stopDrafting clears the typing store: double-click finish and chain auto-close no longer leak the old length into the next draft - 2D Enter commits through a latest-ref instead of the first-render closure, which used to restart the draft at the typed point - 2D move preview publishes projectedEnd so a click mid-type commits the projected endpoint, matching the 3D tool * feat(cabinet): add typed coordinate placement * fix(cabinet): address typed-placement review findings - 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. * fix(cabinet): typed-commit hardening + screen-space 2D HUD - 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. * fix(cabinet): typed-commit stretch seeding + click ends typing session - 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). * fix(cabinet): keep typed entry reachable after large offsets 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. * fix(cabinet): typed-entry cancel/island/short-wall hardening - 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). * fix(cabinet): typed-entry WYSIWYG click, click-swallow, miter station - 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). * fix(review): HIGH-2, HIGH-3, QUICKWIN-2 - 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. * fix(cabinet): keep typing session on invalid click commit; comma toggles 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). * fix(cabinet): center a cabinet wider than its wall on typed entry 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. * fix(wall): re-derive typed endpoint at click commit time Bugbot 8497d792 (round 3, high): append/backspace only change the buffer, while projectedEnd is published by pointer move — so digits typed after the last move left the click commit using a stale endpoint (typed "1" then "2" without moving committed 1, not 12). Enter already re-parsed and re-projected; click now does too, via a shared resolveTypedCommitEnd(start, currentEnd, input, parseOpts) helper used by both the 3D grid-click path and the 2D floor-plan placement callback, falling back to the last projected end only when there is no usable direction. The 2D move preview also re-binds on unit/metricNotation changes so the parse options stay in sync. check-types + biome + editor (1072) + nodes (3392) suites pass. * fix(wall): let the 2D-only view own the typed Enter commit Bugbot cb434c3d (round 4, high): the 3D tool's capture-phase Enter handler synthesized grid:click and stopped the event, so the 2D Enter path never ran handleWallPlacementPoint. In 2D-only that click pipeline commits nothing (the canvas is display:none), so a typed Enter silently did nothing; in split view the 3D commit left the panel's draftStart on the previous segment, so the next 2D preview and typed commit projected from a stale origin. The tool's key handler now yields in 2D-only view — the floor-plan panel is the committer there (view-gated createWallOnCurrentLevel already exists). For split view, the panel subscribes to the chain start the 3D tool publishes on useSegmentDraftChain and re-bases its draft onto it, so both views keep drafting from the same point with a single commit owner. The subscription re-arms reactively on mode/tool instead of reading the store once at mount. check-types + biome + editor (1072) + nodes (3393) suites pass. * fix(wall): round-4 bugbot fixes for the typed 2D draft - Escape during a typed 2D draft now only clears the buffer: the panel's cancel handler recognizes the stage-1 tool:cancel via wallTypingEscapeClearedRef and consumes it, so the draft and the tool survive (Bugbot e9c12894). - A null publish from the 3D chain store (auto-close, T-junction, single-segment finish, cancel) now tears the 2D draft down too — clearWallPlacementDraft resets draftStart and wallChainFirstVertex (Bugbot a172c934). - 2D Enter guards before clearing: a valid buffer with a degenerate preview no longer wipes the input without committing; clearInput moves after the guards and after setDraftEnd, matching the 3D path (Bugbot 622bea04). - Split view: a grid:move emitted by the floor-plan panel carries a screenProjection; while a typing buffer is active the 3D move path now trusts that point verbatim instead of re-running magnetic/grid snap, which could rotate the draft ray away from the 2D preview (Bugbot 3cd53dd1). - acceptsWallTypingKey gates buffer START on a digit or leading dot; letters only continue an active buffer, so c/m/t/f drafting shortcuts stay live before typing begins (Bugbot 7dcff331). * refactor(typed-entry): share one store, key convention and HUD between walls and cabinets #883 (typed wall length) and #887 (typed cabinet distance/offset) each brought their own typed-entry store, key handling and HUD. Replace `useWallDraftTyping` with the cabinet's `usePlacementTyping`, now carrying its session's field labels, so both tools share: - `routePlacementTypingKey`: a digit or `.` opens a session, Tab or comma switches field, Enter commits, Escape clears the session (the next Escape cancels the draft), Backspace edits, Delete empties the field; Cmd/Ctrl/Alt chords are never consumed. - A display-only `PlacementCoordinateInput` (no focused inputs), rendered in 3D through `LocalizedHtml` and in the plan through `FloorplanPlacementTypingHud`, so split view shows it in both panes. - `resolveTypedWallEnd` for the wall projection, used by the 3D tool and the plan alike. The plan forwards the pointer itself on `grid:move` and each side re-projects it, and a typed Enter that resolves to nothing keeps the session instead of dropping it. Also drops the floorplan preview layer's cabinet kind check (E-002), clears the session after each Polygon corner, and removes the frozen-copy typed-length test in favour of tests through the exported resolver. * feat(wall): type the drafted wall's angle as well as its length Adapted from the typed-angle idea in #1207: the wall draft's typed entry gets a second field, the heading. Tab (or comma) switches between length and angle, and Tab with no session open starts on the angle. - Length only: the pointer steers the direction, the length is exact. - Angle only: the direction is locked and the pointer's projection onto it sets the length, on the grid step when grid snapping is on. - Both: the endpoint is fully determined. Degrees are measured the way the 15° angle snap builds its rays, `atan2(dz, dx)` from the building's +X axis towards +Z; the field's placeholder shows the live heading in that convention. The same resolver drives the 3D tool and the plan, so 2D, 3D and split view agree. Only the angle field is taken from #1207; its chain, guide and parser changes are not. Co-authored-by: saadmooh <saadmoooooha2000@gmail.com> * docs(tools): describe the shared typed-entry convention Co-authored-by: dkbbdev <dkbbdev@users.noreply.github.com> Co-authored-by: saadmooh <saadmoooooha2000@gmail.com> * fix(floorplan): take 2D-only typed wall keys ahead of the global shortcuts The plan's typed entry sat in its bubble-phase keydown listener, which is re-registered (after the global `useKeyboard` one) on every draft's first click. In 2D-only view a typed `1`/`2` switched phase, `m` armed Measure and the first Escape cancelled the tool instead of clearing what was typed. Move it into `installFloorplanWallTypingKeys`, a window capture listener registered once that reads the draft through refs, consumes its keys before any shortcut sees them, and stands down outside 2D-only view, where the 3D wall tool's capture listener owns them. The stage-1 Escape marker and its `tool:cancel` workaround are gone: that Escape no longer reaches the global handler. Also tidies two comments left half-edited. * fix(wall): let a 3D typed Enter commit before the pointer moves Right after the start click the tool has no pointer yet, so Enter with both length and angle typed did nothing and typing showed no preview. Fall back to the start point, as the plan does. * fix(floorplan): keep the typed-entry HUD off the ghost it labels The plan's HUD was centred on the cabinet with a pointer-active box around it, so clicks on the ghost hit the HUD. Lift it just above its anchor and let only the HUD panel take the pointer. * fix(wall): wrap the angle placeholder's rounded heading to 0° A heading just under 360° rounded to a 360° placeholder; show 0° as the typed-angle convention reads it. * docs(tools): spell out the typed wall angle convention and pointer forwarding The typed angle is an absolute heading in the plan frame, as the angle snap measures it, and its placeholder shows the live heading the same way, while the draft's angle labels stay relative. The plan forwards its snapped pointer, never the typed end, and takes 2D-only keys in a capture listener. * test(floorplan): start the typed wall key test from an empty session The typing store is shared across test files, so a session another file left open turned the typed `1m` into `51m` in the full suite. * fix(floorplan): size the 2D wall typing HUD from the plan's own scale The wall draft renders outside FloorplanRenderProvider, so the shared HUD read the context defaults (unitsPerPixel 1, rotation 0) and drew a plan-metre-sized box far off screen in the 2D-only view. The HUD now takes unitsPerPixel and sceneRotationDeg from its caller. Co-authored-by: dkbbdev <dkbbdev@users.noreply.github.com> * docs(floorplan): reflow the typing HUD comment --------- Co-authored-by: dkbbdev <dkbbdev@users.noreply.github.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: saadmooh <saadmoooooha2000@gmail.com>
| const geo = new BoxGeometry(length, DEFAULT_LEVEL_HEIGHT, DRAFT_WALL_THICKNESS) | ||
| const matrix = new Vector3(midX, y + DEFAULT_LEVEL_HEIGHT / 2, midZ) | ||
| return { geo, position: matrix, angle } | ||
| }, [start, end, y]) |
There was a problem hiding this comment.
Ghost wall geometries are leaked
Low Severity
GhostWallSegment allocates a new BoxGeometry in useMemo whenever start, end, or y change and never disposes the previous geometry. Long chain drafts and live previews will accumulate GPU buffers until the tool unmounts.
Reviewed by Cursor Bugbot for commit 199443e. Configure here.
| if (factor === undefined) return null | ||
|
|
||
| return value * factor | ||
| } |
There was a problem hiding this comment.
Dimension parser ignores project units
High Severity
parseDimension and the copy inside setValues treat a bare number as meters and reject architectural feet-inch strings. The editor already has parseDimensionInput, which honors metric vs imperial. In an imperial project, typing 10 or 12'6" during wall/fence/slab/ceiling drafting produces the wrong length or fails to parse.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 199443e. Configure here.
| } catch { | ||
| // Read-only or unsupported environment | ||
| } | ||
| } |
There was a problem hiding this comment.
Global root unmount is deferred
High Severity
Patching ReactDOMClient.createRoot so every root’s unmount runs on a timeout applies to the whole app, not just drei Html. A remount on the same container can run createRoot while the previous root is still mounted; the delayed unmount can then tear down the new tree. Html-heavy tools (dimension HUD, opening pills, snap beacons) remount often enough to hit this.
Reviewed by Cursor Bugbot for commit 199443e. Configure here.
…art shadowing, 2D vertex measurement, and fence confirm
| handleCeilingPlacementPoint(pt) | ||
| } else { | ||
| handleSlabPlacementPoint(pt) | ||
| } |
There was a problem hiding this comment.
2D dimension Enter crashes on commit
High Severity
Confirming a typed length in the 2D overlay passes a constructed MouseEvent into emitFloorplanGridEvent, which reads nativeEvent.currentTarget.querySelector. A constructed event has a null currentTarget, so Enter during 2D wall, slab, or ceiling drafting throws and never places the segment.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 1a9ed75. Configure here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
There are 6 total unresolved issues (including 4 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 39b11ff. Configure here.
| else clearSlabPlacementDraft() | ||
| useDimensionDraftStore.getState().reset() | ||
| }} | ||
| /> |
There was a problem hiding this comment.
Split view shows duplicate overlays
Medium Severity
In split view, fence, slab, and ceiling each mount a 3D DimensionInput while FloorplanDimensionInputOverlay also mounts in the floorplan. Two inputs bind to the same store, so users see duplicate boxes and both fight for focus on dimension:focus.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 39b11ff. Configure here.
| else if (isZoneBuildActive) clearZonePlacementDraft() | ||
| else clearSlabPlacementDraft() | ||
| useDimensionDraftStore.getState().reset() | ||
| }} |
There was a problem hiding this comment.
2D cancel leaves 3D draft live
Medium Severity
Escape on the 2D dimension overlay only clears local floorplan draft state and useDimensionDraftStore. It never emits tool:cancel, so in split view the 3D wall/fence/slab/ceiling tool keeps drafting after 2D has already cancelled.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 39b11ff. Configure here.


What does this PR do?
Fixes #1206.
Adds interactive numerical dimension input and continuous multi-point chain drafting for walls in both 3D canvas and 2D floorplan views:
Interactive Dimension Input Overlay:
Continuous Multi-Point Chain Drafting:
2D Floorplan Drafting Preview:
How to test
�un test packages/core/src/lib/units.test.ts
5mor4.2) and press Enter to commit segment.Screenshots / screen recording
Not added yet.
Checklist
bun devbun checkto verify)mainbranchNote
Medium Risk
Broad changes to drafting state machines and floorplan/3D parity across several build tools, plus scene node updates from typed opening dimensions.
Overview
Adds typed length/angle drafting across wall, fence, slab, and ceiling tools in both 3D and the 2D floorplan: a shared
DimensionInput+useDimensionDraftStore, coreparseDimension/parseAnglehelpers, keyboard shortcuts (D/A, digits), and ghost-segment previews.Wall-specific behavior includes continuous multi-point chaining, collinear transit along existing walls via exported
wallSegmentsCoverSegment(no duplicate segment when tracking an existing wall), and 2D overlay positioning.3D opening workflow extends opening guide pills with click-to-edit dimensions (gaps, sill, head) that commit position changes on the selected door/window; dimension-drive logic is tightened for openings measured toward a wall end. Fixes: React 19-safe deferred
root.unmountfor localized DreiHtml, and room labels hidden viadisplay: noneinstead of unmounting.Reviewed by Cursor Bugbot for commit 39b11ff. Bugbot is set up for automated code reviews on this repo. Configure here.