Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 174 additions & 10 deletions packages/editor/src/components/editor/floorplan-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 &&
Expand Down Expand Up @@ -4810,15 +4818,23 @@ function FloorplanLinearDraftLayer({
}

return {
lengthLabel: formatMeasurement(length, unit, null, metricNotation),
lengthLabel: wallTypingInput || formatMeasurement(length, unit, null, metricNotation),
Comment thread
cursor[bot] marked this conversation as resolved.
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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -5143,7 +5162,11 @@ export function FloorplanPanel({
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)
},
[],
)
Expand Down Expand Up @@ -7839,6 +7862,7 @@ export function FloorplanPanel({
wallConstructionOptionsRef.current = undefined
wallChainWallIdsRef.current = []
setDraftEnd(null)
useWallDraftTyping.getState().clearInput()
useSegmentDraftChain.getState().clear('wall')
}, [setDraftEnd])
const clearFencePlacementDraft = useCallback(() => {
Expand Down Expand Up @@ -8238,6 +8262,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
}
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
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
}
}
}
Comment thread
cursor[bot] marked this conversation as resolved.
}

if (
isStairBuildActive &&
useEditor.getState().viewMode === '2d' &&
Expand Down Expand Up @@ -8293,7 +8384,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) => {
Expand Down Expand Up @@ -9521,8 +9621,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)
setCursorPoint(draftEndPoint)
Comment thread
cursor[bot] marked this conversation as resolved.

if (!draftStart) {
return
Expand All @@ -9531,13 +9657,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
})
},
[
Expand Down Expand Up @@ -9572,6 +9698,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,
Expand Down Expand Up @@ -9758,7 +9889,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(
Expand Down Expand Up @@ -9857,16 +10010,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,
})
Expand Down
5 changes: 5 additions & 0 deletions packages/editor/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,11 @@ export {
useStairBuildPreview,
} from './store/use-stair-build-preview'
export { useUploadStore } from './store/use-upload'
export {
isWallTypingKey,
resolveTypedCommitEnd,
useWallDraftTyping,
} from './store/use-wall-draft-typing'
export { useWallMoveGhosts, type WallMoveGhostBridge } from './store/use-wall-move-ghosts'
export {
default as useWallSnapIndicator,
Expand Down
Loading