-
-
Notifications
You must be signed in to change notification settings - Fork 3k
feat(wall): typed exact length during two-click wall drafting #883
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
9a3002e
11e8fa5
596c7e7
b6da3b6
afe71e4
43d7a64
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -122,6 +122,11 @@ import useInteractionScope, { | |
| import usePlacementPreview from '../../store/use-placement-preview' | ||
| import { expandSessionSelectionForNode } from '../../store/use-session-groups' | ||
| import { useStairBuildPreview } from '../../store/use-stair-build-preview' | ||
| import { | ||
| isWallTypingKey, | ||
| resolveTypedCommitEnd, | ||
| useWallDraftTyping, | ||
| } from '../../store/use-wall-draft-typing' | ||
| import { FloorplanAlignmentGuideLayer } from '../editor-2d/floorplan-alignment-guide-layer' | ||
| import { FloorplanCursorIndicatorOverlay as Editor2dFloorplanCursorIndicatorOverlay } from '../editor-2d/floorplan-cursor-indicator-overlay' | ||
| import { FloorplanGroupActionMenu } from '../editor-2d/floorplan-group-action-menu' | ||
|
|
@@ -4677,6 +4682,7 @@ function FloorplanLinearDraftLayer({ | |
| }) { | ||
| const metricNotation = useViewer((state) => state.metricNotation) | ||
| const wallDraftEnd = useFloorplanDraftPreview((s) => s.wallDraftEnd) | ||
| const wallTypingInput = useWallDraftTyping((s) => s.input) | ||
| const fenceDraftEnd = useFloorplanDraftPreview((s) => s.fenceDraftEnd) | ||
| const roofDraftEnd = useFloorplanDraftPreview((s) => s.roofDraftEnd) | ||
| const roofDraftQuarterTurn = useFloorplanDraftPreview((s) => s.roofDraftQuarterTurn) | ||
|
|
@@ -4749,6 +4755,8 @@ function FloorplanLinearDraftLayer({ | |
| // Live length + angle feedback for the wall draft — parity with the 3D | ||
| // `WallTool`, ported to 2D plan space. | ||
| const draftWallMeasurement = useMemo(() => { | ||
| // Typed-length editing (#308): while a buffer is active the HUD shows the | ||
| // buffer instead of the live pointer length — parity with the 3D tool. | ||
| if ( | ||
| !( | ||
| isWallBuildActive && | ||
|
|
@@ -4810,15 +4818,23 @@ function FloorplanLinearDraftLayer({ | |
| } | ||
|
|
||
| return { | ||
| lengthLabel: formatMeasurement(length, unit, null, metricNotation), | ||
| lengthLabel: wallTypingInput || formatMeasurement(length, unit, null, metricNotation), | ||
| midpoint: [ | ||
| (wallDraftStart[0] + wallDraftEnd[0]) / 2, | ||
| (wallDraftStart[1] + wallDraftEnd[1]) / 2, | ||
| ] as WallPlanPoint, | ||
| direction: [dx / length, dy / length] as WallPlanPoint, | ||
| angleLabels, | ||
| } | ||
| }, [isWallBuildActive, metricNotation, unit, wallDraftEnd, wallDraftStart, walls]) | ||
| }, [ | ||
| isWallBuildActive, | ||
| metricNotation, | ||
| unit, | ||
| wallDraftEnd, | ||
| wallDraftStart, | ||
| wallTypingInput, | ||
| walls, | ||
| ]) | ||
|
|
||
| // Axis guides for wall and fence drafts — parity with the 3D tools' | ||
| // `DraftAxisGuides`: an X/Z cross through the draft start, and a single | ||
|
|
@@ -4965,6 +4981,9 @@ export function FloorplanPanel({ | |
| const floorplanSpacePanPressedRef = useRef(false) | ||
| const floorplanNavigationClickSuppressedRef = useRef(false) | ||
| const guideInteractionRef = useRef<GuideInteractionState | null>(null) | ||
| // Late-bound so the window keydown effect can commit the wall draft at the | ||
| // typed length without an ordering dependency on `handleWallPlacementPoint`. | ||
| const wallPlacementPointRef = useRef<((point: WallPlanPoint) => void) | null>(null) | ||
| const guideTransformDraftRef = useRef<GuideTransformDraft | null>(null) | ||
| const pendingFenceDragRef = useRef<PendingFenceDragState | null>(null) | ||
| const wallEndpointDragRef = useRef<WallEndpointDragState | null>(null) | ||
|
|
@@ -5140,10 +5159,33 @@ export function FloorplanPanel({ | |
| // Walls committed by the current 2D-only chain — exclusion set for the | ||
| // T-junction chain-termination test (mirrors the 3D tool's `chainWallIds`). | ||
| const wallChainWallIdsRef = useRef<string[]>([]) | ||
| // Split view: the 3D wall tool owns the commit and publishes each segment's | ||
| // resolved end as the chain start. Re-base this panel's draft onto it so a | ||
| // typed Enter in the plan projects from the segment the 3D chain is | ||
| // actually drafting — without this, `draftStart` stays on the last point | ||
| // the panel itself placed and the typed commit uses a stale origin | ||
| // (Bugbot cb434c3d). In 2D-only the panel is the committer and never | ||
| // publishes, so this subscription stays dormant there. | ||
| useEffect(() => { | ||
| if (!(mode === 'build' && tool === 'wall')) return | ||
| return useSegmentDraftChain.subscribe((state, previousState) => { | ||
| const next = state.wall | ||
| if (next === previousState.wall) return | ||
| if (!next) return | ||
| setDraftStart((current) => | ||
| current && current[0] === next[0] && current[1] === next[1] ? current : next, | ||
| ) | ||
| setWallChainFirstVertex((current) => current ?? next) | ||
| }) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Stale 2D draft after 3D stopHigh Severity The new Additional Locations (1)Reviewed by Cursor Bugbot for commit 43d7a64. Configure here. |
||
| }, [mode, tool]) | ||
| const setDraftEnd = useCallback( | ||
| (next: WallPlanPoint | null | ((prev: WallPlanPoint | null) => WallPlanPoint | null)) => { | ||
| const store = useFloorplanDraftPreview.getState() | ||
| store.setWallDraftEnd(typeof next === 'function' ? next(store.wallDraftEnd) : next) | ||
| const value = typeof next === 'function' ? next(store.wallDraftEnd) : next | ||
| store.setWallDraftEnd(value) | ||
| // Keep the typing store's projected endpoint in sync with the live | ||
| // preview so a click mid-type commits the projected point (#308). | ||
| if (value) useWallDraftTyping.getState().setProjectedEnd(value) | ||
| }, | ||
| [], | ||
| ) | ||
|
|
@@ -7839,6 +7881,7 @@ export function FloorplanPanel({ | |
| wallConstructionOptionsRef.current = undefined | ||
| wallChainWallIdsRef.current = [] | ||
| setDraftEnd(null) | ||
| useWallDraftTyping.getState().clearInput() | ||
| useSegmentDraftChain.getState().clear('wall') | ||
| }, [setDraftEnd]) | ||
| const clearFencePlacementDraft = useCallback(() => { | ||
|
|
@@ -8238,6 +8281,73 @@ export function FloorplanPanel({ | |
| setShiftPressed(true) | ||
| } | ||
|
|
||
| // Typed-length editing for the 2D wall draft (#308) — parity with the | ||
| // 3D wall tool's `onKeyDown`: printable keys extend the buffer, Enter | ||
| // commits at the typed length, Escape (stage 1) clears it. | ||
| if (isWallBuildActive && draftStart) { | ||
| const typing = useWallDraftTyping.getState() | ||
| const hasInput = typing.input.length > 0 | ||
| if (!event.metaKey && !event.ctrlKey && !event.altKey) { | ||
| if (isWallTypingKey(event.key)) { | ||
| typing.append(event.key) | ||
| event.preventDefault() | ||
| event.stopPropagation() | ||
| return | ||
| } | ||
| if (hasInput) { | ||
| if (event.key === 'Backspace') { | ||
| typing.backspace() | ||
| event.preventDefault() | ||
| event.stopPropagation() | ||
| return | ||
| } | ||
| if (event.key === 'Delete') { | ||
| typing.clearInput() | ||
| event.preventDefault() | ||
| event.stopPropagation() | ||
| return | ||
| } | ||
| if (event.key === 'Escape') { | ||
| typing.clearInput() | ||
| event.preventDefault() | ||
| event.stopPropagation() | ||
| return | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Escape cancels typed 2D draftHigh Severity First Escape during a typed 2D wall draft cancels the draft (and can exit the tool) instead of only clearing the buffer. The new handler calls Additional Locations (1)Reviewed by Cursor Bugbot for commit 43d7a64. Configure here. |
||
| } | ||
| if (event.key === 'Enter') { | ||
| const value = parseMeasurement( | ||
| typing.input, | ||
| { kind: 'length', unitId: 'm' }, | ||
| { | ||
| bareUnit: | ||
| unit === 'imperial' ? 'in' : metricNotation === 'millimeters' ? 'mm' : 'm', | ||
| system: unit === 'imperial' ? 'imperial' : 'metric', | ||
| }, | ||
| ) | ||
| typing.clearInput() | ||
| if (value === null || value <= 0 || !draftStart) return | ||
| const store = useFloorplanDraftPreview.getState() | ||
| const previousEnd = store.wallDraftEnd | ||
| if (!previousEnd) return | ||
| const dx = previousEnd[0] - draftStart[0] | ||
| const dz = previousEnd[1] - draftStart[1] | ||
| const length = Math.hypot(dx, dz) | ||
| if (length <= 1e-6) return | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Enter drops typed length earlyMedium Severity 2D Enter calls Additional Locations (1)Reviewed by Cursor Bugbot for commit 43d7a64. Configure here. |
||
| const typedEnd: WallPlanPoint = [ | ||
| draftStart[0] + (dx / length) * value, | ||
| draftStart[1] + (dz / length) * value, | ||
| ] | ||
| // typedEnd is passed as the raw point; handleWallPlacementPoint | ||
| // commits it verbatim (no snap) since the buffer is cleared. | ||
| setDraftEnd(typedEnd) | ||
| wallPlacementPointRef.current?.(typedEnd) | ||
| event.preventDefault() | ||
| event.stopPropagation() | ||
| return | ||
| } | ||
| } | ||
| } | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| if ( | ||
| isStairBuildActive && | ||
| useEditor.getState().viewMode === '2d' && | ||
|
|
@@ -8293,7 +8403,16 @@ export function FloorplanPanel({ | |
| window.removeEventListener('keyup', handleKeyUp) | ||
| window.removeEventListener('blur', handleBlur) | ||
| } | ||
| }, [isFloorplanOpen, isStairBuildActive, movingNode]) | ||
| }, [ | ||
| isFloorplanOpen, | ||
| isStairBuildActive, | ||
| movingNode, | ||
| isWallBuildActive, | ||
| draftStart, | ||
| unit, | ||
| metricNotation, | ||
| setDraftEnd, | ||
| ]) | ||
|
|
||
| useEffect(() => { | ||
| const handleWindowPointerMove = (event: PointerEvent) => { | ||
|
|
@@ -9521,8 +9640,34 @@ export function FloorplanPanel({ | |
| // Emit `grid:move` so the registry-driven wall tool's 3D preview | ||
| // tracks the cursor. The local draftEnd update below is what | ||
| // drives the 2D draft polygon — both views update in parallel. | ||
| emitFloorplanGridEvent('move', snappedPoint, event) | ||
| setCursorPoint(snappedPoint) | ||
| // Typed-length editing (#308): while a buffer is active the draft end | ||
| // is the start projected along the draft direction onto the typed | ||
| // length; the pointer keeps steering direction only. | ||
| let draftEndPoint = snappedPoint | ||
| const wallTyping = useWallDraftTyping.getState() | ||
| if (draftStart && wallTyping.input) { | ||
| const typedLength = parseMeasurement( | ||
| wallTyping.input, | ||
| { kind: 'length', unitId: 'm' }, | ||
| { | ||
| bareUnit: unit === 'imperial' ? 'in' : metricNotation === 'millimeters' ? 'mm' : 'm', | ||
| system: unit === 'imperial' ? 'imperial' : 'metric', | ||
| }, | ||
| ) | ||
| if (typedLength !== null && typedLength > 0) { | ||
| const dx = snappedPoint[0] - draftStart[0] | ||
| const dz = snappedPoint[1] - draftStart[1] | ||
| const pointerLength = Math.hypot(dx, dz) | ||
| if (pointerLength > 1e-6) { | ||
| draftEndPoint = [ | ||
| draftStart[0] + (dx / pointerLength) * typedLength, | ||
| draftStart[1] + (dz / pointerLength) * typedLength, | ||
| ] | ||
| } | ||
| } | ||
| } | ||
| emitFloorplanGridEvent('move', draftEndPoint, event) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Split view re-snaps typed endpointMedium Severity In split view the 2D move path emits the already-projected typed endpoint as Additional Locations (1)Reviewed by Cursor Bugbot for commit 43d7a64. Configure here. |
||
| setCursorPoint(draftEndPoint) | ||
|
cursor[bot] marked this conversation as resolved.
|
||
|
|
||
| if (!draftStart) { | ||
| return | ||
|
|
@@ -9531,13 +9676,13 @@ export function FloorplanPanel({ | |
| setDraftEnd((previousEnd) => { | ||
| if ( | ||
| !previousEnd || | ||
| previousEnd[0] !== snappedPoint[0] || | ||
| previousEnd[1] !== snappedPoint[1] | ||
| previousEnd[0] !== draftEndPoint[0] || | ||
| previousEnd[1] !== draftEndPoint[1] | ||
| ) { | ||
| sfxEmitter.emit('sfx:grid-snap') | ||
| } | ||
|
|
||
| return snappedPoint | ||
| return draftEndPoint | ||
| }) | ||
| }, | ||
| [ | ||
|
|
@@ -9572,6 +9717,11 @@ export function FloorplanPanel({ | |
| isRoofBuildActive, | ||
| isWallBuildActive, | ||
| levelId, | ||
| // `unit` / `metricNotation` feed the typed-length projection helpers | ||
| // shared with the commit path; listed so a unit change re-binds the | ||
| // move preview to the new bare-unit parse. | ||
| metricNotation, | ||
| unit, | ||
| publishFloorplanNavigationPose, | ||
| referenceScaleDraft, | ||
| roofDraftStart, | ||
|
|
@@ -9758,7 +9908,29 @@ export function FloorplanPanel({ | |
| ) | ||
|
|
||
| const handleWallPlacementPoint = useCallback( | ||
| (point: WallPlanPoint) => { | ||
| (rawPoint: WallPlanPoint) => { | ||
| // Typed-length editing (#308): while a buffer is active, commit the | ||
| // projected endpoint the preview shows instead of the raw pointer — | ||
| // keeps the 2D-only commit, the chain continuation, and the length | ||
| // label in agreement with what the user typed. Re-derived at commit | ||
| // time (Bugbot 8497d792): digits arriving after the last pointer move | ||
| // leave `projectedEnd` stale; the commit must reflect the full buffer. | ||
| const typing = useWallDraftTyping.getState() | ||
| const draftPreviewStore = useFloorplanDraftPreview.getState() | ||
| const typedEnd = | ||
| (draftStart && | ||
| typing.input && | ||
| resolveTypedCommitEnd( | ||
| draftStart, | ||
| draftPreviewStore.wallDraftEnd ?? rawPoint, | ||
| typing.input, | ||
| { | ||
| bareUnit: unit === 'imperial' ? 'in' : metricNotation === 'millimeters' ? 'mm' : 'm', | ||
| system: unit === 'imperial' ? 'imperial' : 'metric', | ||
| }, | ||
| )) || | ||
| (typing.input ? typing.projectedEnd : null) | ||
| const point: WallPlanPoint = typedEnd ?? rawPoint | ||
| if (!draftStart) { | ||
| wallConstructionOptionsRef.current = levelId | ||
| ? resolveTerrainWallConstructionOptions( | ||
|
|
@@ -9857,16 +10029,27 @@ export function FloorplanPanel({ | |
| setDraftStart(nextStart) | ||
| setDraftEnd(nextStart) | ||
| setCursorPoint(nextStart) | ||
| // Typed length applied to the committed segment only — never carry it | ||
| // into the next chain segment. | ||
| useWallDraftTyping.getState().clearInput() | ||
| }, | ||
| [ | ||
| clearWallPlacementDraft, | ||
| draftStart, | ||
| levelId, | ||
| metricNotation, | ||
| unit, | ||
| wallChainFirstVertex, | ||
| setDraftEnd, | ||
| setCursorPoint, | ||
| ], | ||
| ) | ||
| // Latest-ref for the Enter-commit path: assigning the ref inside the | ||
| // callback itself froze the first-render closure (draftStart === null), so | ||
| // a typed Enter re-started the draft instead of committing the wall. | ||
| useEffect(() => { | ||
| wallPlacementPointRef.current = handleWallPlacementPoint | ||
| }, [handleWallPlacementPoint]) | ||
| const { getFloorplanHitIdAtPoint, getFloorplanSelectionIdsInBounds } = useFloorplanHitTesting({ | ||
| sceneRef: floorplanSceneRef, | ||
| }) | ||
|
|
||


Uh oh!
There was an error while loading. Please reload this page.