@pascal-app/editor 1.0.7 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -7
- package/src/components/editor/custom-camera-controls.tsx +90 -34
- package/src/components/editor/duplicate-node.ts +2 -1
- package/src/components/editor/first-person-controls.tsx +5 -7
- package/src/components/editor/fit-scene-framing.test.ts +318 -0
- package/src/components/editor/fit-scene-framing.ts +142 -0
- package/src/components/editor/floating-action-menu.tsx +4 -2
- package/src/components/editor/floor-openings-overlay.tsx +1 -1
- package/src/components/editor/floor-region-controls.tsx +2 -2
- package/src/components/editor/floorplan-panel.tsx +173 -8
- package/src/components/editor/floorplan-wall-typing-keys.test.tsx +152 -0
- package/src/components/editor/floorplan-wall-typing-keys.ts +79 -0
- package/src/components/editor/footprint-height-handle.tsx +1 -1
- package/src/components/editor/group-transform-shared.ts +3 -1
- package/src/components/editor/index.tsx +20 -0
- package/src/components/editor/measurement-pill.tsx +1 -1
- package/src/components/editor/node-arrow-handles.tsx +1 -1
- package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -2
- package/src/components/editor/open-wall-ends-hint.tsx +1 -1
- package/src/components/editor/quick-measurement-card.tsx +1 -1
- package/src/components/editor/room-controls.tsx +3 -2
- package/src/components/editor/room-handles-2d.tsx +2 -1
- package/src/components/editor/room-handles.tsx +3 -3
- package/src/components/editor/room-labels.tsx +4 -2
- package/src/components/editor/site-edge-labels.tsx +6 -2
- package/src/components/editor/wall-measurement-label.tsx +30 -14
- package/src/components/editor/wall-move-side-handles.tsx +10 -6
- package/src/components/editor/wall-region-handles.tsx +2 -2
- package/src/components/editor/wall-snap-beacon-layer.tsx +53 -32
- package/src/components/editor-2d/floorplan-group-move.tsx +1 -1
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +12 -12
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +1 -2
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +2 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +22 -4
- package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +7 -1
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +13 -1
- package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.test.tsx +35 -0
- package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.tsx +65 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +22 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +14 -2
- package/src/components/systems/roof/roof-edit-system.test.tsx +15 -1
- package/src/components/systems/roof/roof-edit-system.tsx +30 -19
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +371 -3
- package/src/components/tools/distribution/distribution-run-tool.tsx +150 -32
- package/src/components/tools/item/move-tool.tsx +1 -6
- package/src/components/tools/item/use-placement-coordinator.tsx +1 -1
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +1 -1
- package/src/components/tools/registry/move-registry-node-tool.test.ts +42 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +38 -14
- package/src/components/tools/shared/placement-box.tsx +1 -5
- package/src/components/tools/shared/placement-coordinate-input.tsx +94 -0
- package/src/components/tools/shared/segment-angle.ts +0 -4
- package/src/components/tools/tool-manager.tsx +1 -24
- package/src/components/tools/wall/wall-typed-entry.test.ts +99 -0
- package/src/components/tools/wall/wall-typed-entry.ts +70 -0
- package/src/components/ui/controls/slider-control.tsx +18 -6
- package/src/components/ui/controls/tool-options-panel.tsx +74 -44
- package/src/components/ui/helpers/helper-manager.test.tsx +53 -0
- package/src/components/ui/helpers/helper-manager.tsx +54 -0
- package/src/components/ui/panels/floor-foundation-panel.tsx +1 -1
- package/src/components/ui/panels/floor-opening-panel.tsx +1 -1
- package/src/components/ui/panels/mobile-selection-bar.tsx +28 -24
- package/src/components/ui/panels/multi-height-mode.tsx +3 -3
- package/src/components/ui/panels/multi-selection-panel.tsx +1 -1
- package/src/components/ui/panels/panel-manager.tsx +4 -2
- package/src/components/ui/panels/panel-wrapper.tsx +41 -18
- package/src/components/ui/panels/room-construction-rows.tsx +1 -1
- package/src/components/ui/panels/room-openings-field.tsx +1 -1
- package/src/components/ui/panels/room-panel.tsx +1 -1
- package/src/components/ui/right-stack.test.ts +13 -6
- package/src/components/ui/right-stack.tsx +21 -10
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +1 -8
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +1 -1
- package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +1 -1
- package/src/components/viewer/viewer-rooms.tsx +1 -1
- package/src/i18n/locales/en/messages.po +856 -124
- package/src/i18n/locales/en/messages.ts +1 -1
- package/src/i18n/locales/en-XA/messages.ts +1 -1
- package/src/i18n/locales/es/messages.po +2087 -257
- package/src/i18n/locales/fr/messages.po +2087 -257
- package/src/i18n/locales/ja/messages.po +2087 -257
- package/src/i18n/locales/zh-CN/messages.po +2087 -257
- package/src/index.tsx +74 -47
- package/src/lib/contextual-help.ts +6 -0
- package/src/lib/elevation-guides.ts +21 -6
- package/src/lib/elevator-support.ts +1 -72
- package/src/lib/floor-footprints.test.ts +1 -1
- package/src/lib/floor-footprints.ts +1 -9
- package/src/lib/floor-foundation-support.test.ts +8 -3
- package/src/lib/floor-opening-draft.ts +1 -1
- package/src/lib/floor-region-session.ts +1 -1
- package/src/lib/floorplan/open-wall-ends.ts +2 -1
- package/src/lib/gesture-lifecycle.ts +3 -1
- package/src/lib/interaction/scope.ts +1 -0
- package/src/lib/layer-thickness.ts +1 -1
- package/src/lib/level-selection.test.ts +18 -2
- package/src/lib/level-selection.ts +31 -8
- package/src/lib/linear-display.ts +1 -1
- package/src/lib/live-gesture-round-trip.test.ts +1 -1
- package/src/lib/measurement-label.test.ts +1 -1
- package/src/lib/measurements.test.ts +8 -7
- package/src/lib/measurements.ts +1 -218
- package/src/lib/node-action-movement.test.tsx +115 -0
- package/src/lib/node-action-movement.ts +2 -0
- package/src/lib/placement-metadata.ts +4 -0
- package/src/lib/point-ask/describe-target.ts +6 -8
- package/src/lib/point-ask/region.ts +1 -1
- package/src/lib/print-shell-compiler.ts +12 -1
- package/src/lib/print-wall-solids.test.ts +11 -0
- package/src/lib/print-wall-solids.ts +9 -0
- package/src/lib/room-assembly-overlay.ts +1 -1
- package/src/lib/room-built-on.test.ts +2 -5
- package/src/lib/room-built-on.ts +1 -15
- package/src/lib/room-construction-commands.ts +3 -2
- package/src/lib/room-controls-phase6.test.ts +1 -1
- package/src/lib/room-handle-drag.ts +6 -25
- package/src/lib/room-push-dimensions.test.ts +1 -1
- package/src/lib/scene-bounds.test.ts +144 -0
- package/src/lib/scene-bounds.ts +79 -15
- package/src/lib/scene-clipboard.ts +2 -1
- package/src/lib/snapping-mode.test.ts +23 -1
- package/src/lib/viewer-selection.ts +1 -1
- package/src/lib/wall-region-list.ts +3 -1
- package/src/lib/window-interaction.test.ts +4 -2
- package/src/store/use-editor.tsx +5 -0
- package/src/store/use-placement-typing.test.ts +165 -0
- package/src/store/use-placement-typing.ts +186 -0
- package/src/components/tools/elevator/elevator-defaults.ts +0 -8
- package/src/components/tools/elevator/elevator-tool.tsx +0 -372
- package/src/components/tools/elevator/move-elevator-tool.tsx +0 -262
- package/src/lib/door-interaction.ts +0 -102
- package/src/lib/measurement-label.ts +0 -68
- package/src/lib/window-interaction.ts +0 -142
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { beforeEach, expect, test } from 'bun:test'
|
|
2
|
+
import { emitter } from '@pascal-app/core'
|
|
3
|
+
import { cancelActiveTool, useKeyboard } from '../../hooks/use-keyboard'
|
|
4
|
+
import useEditor from '../../store/use-editor'
|
|
5
|
+
import {
|
|
6
|
+
PLACEMENT_TYPING_LENGTH_ANGLE,
|
|
7
|
+
placementTypingLengthOptions,
|
|
8
|
+
usePlacementTyping,
|
|
9
|
+
} from '../../store/use-placement-typing'
|
|
10
|
+
import { sceneLayout } from '../../store/view-layout'
|
|
11
|
+
import { withSelectionHarness } from '../../test-utils/selection-harness'
|
|
12
|
+
import type { WallPlanPoint } from '../tools/wall/wall-drafting'
|
|
13
|
+
import {
|
|
14
|
+
endFloorplanWallTypingOnCancel,
|
|
15
|
+
type FloorplanWallTypingDraft,
|
|
16
|
+
installFloorplanWallTypingKeys,
|
|
17
|
+
} from './floorplan-wall-typing-keys'
|
|
18
|
+
|
|
19
|
+
function GlobalShortcuts() {
|
|
20
|
+
useKeyboard()
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const press = (key: string) => {
|
|
25
|
+
const event = Object.assign(new Event('keydown', { cancelable: true }), {
|
|
26
|
+
key,
|
|
27
|
+
code: '',
|
|
28
|
+
metaKey: false,
|
|
29
|
+
ctrlKey: false,
|
|
30
|
+
altKey: false,
|
|
31
|
+
shiftKey: false,
|
|
32
|
+
repeat: false,
|
|
33
|
+
})
|
|
34
|
+
window.dispatchEvent(event)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
usePlacementTyping.getState().clear()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('2D-only typed wall keys run ahead of the global shortcuts', async () => {
|
|
42
|
+
await withSelectionHarness(async ({ render }) => {
|
|
43
|
+
useEditor.getState().setViewLayout(sceneLayout('2d'))
|
|
44
|
+
useEditor.getState().setPhase('building')
|
|
45
|
+
useEditor.getState().armToolMode({ mode: 'build', tool: 'wall' })
|
|
46
|
+
// The global handler is registered first, as the app mounts it.
|
|
47
|
+
await render(<GlobalShortcuts />)
|
|
48
|
+
|
|
49
|
+
let draft: FloorplanWallTypingDraft | null = {
|
|
50
|
+
start: [0, 0],
|
|
51
|
+
pointer: [2, 0],
|
|
52
|
+
lengthOptions: placementTypingLengthOptions('metric', 'meters'),
|
|
53
|
+
}
|
|
54
|
+
const committed: WallPlanPoint[] = []
|
|
55
|
+
const dropDraft = () => {
|
|
56
|
+
draft = null
|
|
57
|
+
}
|
|
58
|
+
emitter.on('tool:cancel', dropDraft)
|
|
59
|
+
const uninstall = installFloorplanWallTypingKeys({
|
|
60
|
+
getDraft: () => draft,
|
|
61
|
+
commit: (pointer) => committed.push(pointer),
|
|
62
|
+
})
|
|
63
|
+
try {
|
|
64
|
+
press('1')
|
|
65
|
+
press('m')
|
|
66
|
+
expect(usePlacementTyping.getState().fields[0]).toBe('1m')
|
|
67
|
+
expect(useEditor.getState().phase).toBe('building')
|
|
68
|
+
expect(useEditor.getState().mode).toBe('build')
|
|
69
|
+
expect(useEditor.getState().tool).toBe('wall')
|
|
70
|
+
|
|
71
|
+
press('Escape')
|
|
72
|
+
expect(usePlacementTyping.getState().isActive).toBe(false)
|
|
73
|
+
expect(draft).not.toBeNull()
|
|
74
|
+
expect(useEditor.getState().tool).toBe('wall')
|
|
75
|
+
|
|
76
|
+
press('Escape')
|
|
77
|
+
expect(draft).toBeNull()
|
|
78
|
+
expect(committed).toEqual([])
|
|
79
|
+
} finally {
|
|
80
|
+
uninstall()
|
|
81
|
+
emitter.off('tool:cancel', dropDraft)
|
|
82
|
+
usePlacementTyping.getState().clear()
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('Enter commits the pointer the typed fields re-project, only in 2D-only view', async () => {
|
|
88
|
+
await withSelectionHarness(async () => {
|
|
89
|
+
const committed: WallPlanPoint[] = []
|
|
90
|
+
const uninstall = installFloorplanWallTypingKeys({
|
|
91
|
+
getDraft: () => ({
|
|
92
|
+
start: [0, 0],
|
|
93
|
+
pointer: [2, 0],
|
|
94
|
+
lengthOptions: placementTypingLengthOptions('metric', 'meters'),
|
|
95
|
+
}),
|
|
96
|
+
commit: (pointer) => committed.push(pointer),
|
|
97
|
+
})
|
|
98
|
+
try {
|
|
99
|
+
useEditor.getState().setViewLayout(sceneLayout('split'))
|
|
100
|
+
press('3')
|
|
101
|
+
expect(usePlacementTyping.getState().isActive).toBe(false)
|
|
102
|
+
|
|
103
|
+
useEditor.getState().setViewLayout(sceneLayout('2d'))
|
|
104
|
+
press('3')
|
|
105
|
+
press('Enter')
|
|
106
|
+
expect(committed).toEqual([[2, 0]])
|
|
107
|
+
} finally {
|
|
108
|
+
uninstall()
|
|
109
|
+
usePlacementTyping.getState().clear()
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('in 2D-only view a cancel from any source first ends the typed session, then the draft', async () => {
|
|
115
|
+
await withSelectionHarness(async () => {
|
|
116
|
+
useEditor.getState().setViewLayout(sceneLayout('2d'))
|
|
117
|
+
useEditor.getState().armToolMode({ mode: 'build', tool: 'wall' })
|
|
118
|
+
let draftKept = true
|
|
119
|
+
// The plan's own `tool:cancel` handler.
|
|
120
|
+
const onCancel = () => {
|
|
121
|
+
if (endFloorplanWallTypingOnCancel()) return
|
|
122
|
+
draftKept = false
|
|
123
|
+
}
|
|
124
|
+
emitter.on('tool:cancel', onCancel)
|
|
125
|
+
try {
|
|
126
|
+
usePlacementTyping.getState().begin({ labels: PLACEMENT_TYPING_LENGTH_ANGLE })
|
|
127
|
+
usePlacementTyping.getState().append('3')
|
|
128
|
+
|
|
129
|
+
// The path ⌘Z takes mid-draft, not a keydown the plan sees itself.
|
|
130
|
+
expect(cancelActiveTool()).toBe(true)
|
|
131
|
+
expect(usePlacementTyping.getState().isActive).toBe(false)
|
|
132
|
+
expect(draftKept).toBe(true)
|
|
133
|
+
expect(useEditor.getState().mode).toBe('build')
|
|
134
|
+
expect(useEditor.getState().tool).toBe('wall')
|
|
135
|
+
|
|
136
|
+
cancelActiveTool()
|
|
137
|
+
expect(draftKept).toBe(false)
|
|
138
|
+
} finally {
|
|
139
|
+
emitter.off('tool:cancel', onCancel)
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('in split view the typed session and its cancel stay with the 3D wall tool', async () => {
|
|
145
|
+
await withSelectionHarness(async () => {
|
|
146
|
+
useEditor.getState().setViewLayout(sceneLayout('split'))
|
|
147
|
+
useEditor.getState().armToolMode({ mode: 'build', tool: 'wall' })
|
|
148
|
+
usePlacementTyping.getState().begin({ labels: PLACEMENT_TYPING_LENGTH_ANGLE })
|
|
149
|
+
expect(endFloorplanWallTypingOnCancel()).toBe(true)
|
|
150
|
+
expect(usePlacementTyping.getState().isActive).toBe(true)
|
|
151
|
+
})
|
|
152
|
+
})
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { markToolCancelConsumed } from '../../hooks/use-keyboard'
|
|
2
|
+
import type { ParseMeasurementOptions } from '../../lib/measurement-parser'
|
|
3
|
+
import { isTypingTarget } from '../../lib/typing-target'
|
|
4
|
+
import useEditor from '../../store/use-editor'
|
|
5
|
+
import {
|
|
6
|
+
PLACEMENT_TYPING_LENGTH_ANGLE,
|
|
7
|
+
routePlacementTypingKey,
|
|
8
|
+
usePlacementTyping,
|
|
9
|
+
} from '../../store/use-placement-typing'
|
|
10
|
+
import { visibleScene } from '../../store/view-layout'
|
|
11
|
+
import { getSegmentGridStep, type WallPlanPoint } from '../tools/wall/wall-drafting'
|
|
12
|
+
import { resolveTypedWallEnd } from '../tools/wall/wall-typed-entry'
|
|
13
|
+
|
|
14
|
+
export type FloorplanWallTypingDraft = {
|
|
15
|
+
start: WallPlanPoint
|
|
16
|
+
/** The plan-snapped pointer the typed fields re-project. */
|
|
17
|
+
pointer: WallPlanPoint
|
|
18
|
+
lengthOptions: ParseMeasurementOptions
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Typed entry for the plan's own wall draft (2D-only view; in split view the
|
|
23
|
+
* 3D wall tool drafts and owns these keys). A window capture listener, so it
|
|
24
|
+
* runs before the global shortcuts and a typed `1`, `m` or the session's
|
|
25
|
+
* Escape never reaches them.
|
|
26
|
+
*/
|
|
27
|
+
export function installFloorplanWallTypingKeys({
|
|
28
|
+
getDraft,
|
|
29
|
+
commit,
|
|
30
|
+
}: {
|
|
31
|
+
getDraft: () => FloorplanWallTypingDraft | null
|
|
32
|
+
commit: (pointer: WallPlanPoint) => void
|
|
33
|
+
}): () => void {
|
|
34
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
35
|
+
if (isTypingTarget(event.target)) return
|
|
36
|
+
if (visibleScene(useEditor.getState()) !== '2d') return
|
|
37
|
+
const draft = getDraft()
|
|
38
|
+
if (!draft) return
|
|
39
|
+
const outcome = routePlacementTypingKey(event, {
|
|
40
|
+
begin: (activeField) => {
|
|
41
|
+
usePlacementTyping.getState().begin({ labels: PLACEMENT_TYPING_LENGTH_ANGLE, activeField })
|
|
42
|
+
return true
|
|
43
|
+
},
|
|
44
|
+
tabBegins: true,
|
|
45
|
+
})
|
|
46
|
+
if (!outcome) return
|
|
47
|
+
event.preventDefault()
|
|
48
|
+
event.stopPropagation()
|
|
49
|
+
if (outcome !== 'commit') return
|
|
50
|
+
const typedEnd = resolveTypedWallEnd(
|
|
51
|
+
draft.start,
|
|
52
|
+
draft.pointer,
|
|
53
|
+
usePlacementTyping.getState().fields,
|
|
54
|
+
draft.lengthOptions,
|
|
55
|
+
getSegmentGridStep(),
|
|
56
|
+
)
|
|
57
|
+
if (typedEnd) commit(draft.pointer)
|
|
58
|
+
}
|
|
59
|
+
window.addEventListener('keydown', onKeyDown, true)
|
|
60
|
+
return () => window.removeEventListener('keydown', onKeyDown, true)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The plan's `tool:cancel` first stage. A cancel from any source (Escape, ⌘Z,
|
|
65
|
+
* a toolbar button) while a typed wall session is open only ends that
|
|
66
|
+
* session: in 2D-only view the plan clears it and consumes the cancel. In
|
|
67
|
+
* split view the 3D wall tool owns the session and its cancel. Returns true
|
|
68
|
+
* when the plan's wall draft must be kept.
|
|
69
|
+
*/
|
|
70
|
+
export function endFloorplanWallTypingOnCancel(): boolean {
|
|
71
|
+
const editor = useEditor.getState()
|
|
72
|
+
if (!(editor.mode === 'build' && editor.tool === 'wall')) return false
|
|
73
|
+
if (!usePlacementTyping.getState().isActive) return false
|
|
74
|
+
if (visibleScene(editor) === '2d') {
|
|
75
|
+
markToolCancelConsumed()
|
|
76
|
+
usePlacementTyping.getState().clear()
|
|
77
|
+
}
|
|
78
|
+
return true
|
|
79
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
type AnyNodeId,
|
|
5
|
+
formatLinearMeasurement,
|
|
5
6
|
getStoredLevelHeight,
|
|
6
7
|
type Point,
|
|
7
8
|
type SlabNode,
|
|
@@ -20,7 +21,6 @@ import {
|
|
|
20
21
|
THICK_FLOOR_HINT,
|
|
21
22
|
thickFloorAdvice,
|
|
22
23
|
} from '../../lib/floor-footprints'
|
|
23
|
-
import { formatLinearMeasurement } from '../../lib/measurements'
|
|
24
24
|
import { runRoomHandleDrag } from '../../lib/room-handle-drag'
|
|
25
25
|
import useEditor, { isGridSnapActive } from '../../store/use-editor'
|
|
26
26
|
import useInteractionScope from '../../store/use-interaction-scope'
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type AnyNodeId,
|
|
4
4
|
calculateLevelMiters,
|
|
5
5
|
getWallPlanFootprint,
|
|
6
|
+
isMoveLocked,
|
|
6
7
|
nodeRegistry,
|
|
7
8
|
resolveBuildingForLevel,
|
|
8
9
|
sceneRegistry,
|
|
@@ -99,7 +100,8 @@ export function classifyParticipant(
|
|
|
99
100
|
levelId: string | null,
|
|
100
101
|
sceneNodes: Record<string, AnyNode | undefined>,
|
|
101
102
|
): ParticipantKind | null {
|
|
102
|
-
if (!node || !isInGroupTransformScope(node, levelId, sceneNodes)
|
|
103
|
+
if (!node || !isInGroupTransformScope(node, levelId, sceneNodes) || isMoveLocked(node))
|
|
104
|
+
return null
|
|
103
105
|
const shape = classifyPlacementShape(node)
|
|
104
106
|
if (shape) return shape
|
|
105
107
|
const start = (node as { start?: unknown }).start
|
|
@@ -4,6 +4,7 @@ import { Icon } from '@iconify/react'
|
|
|
4
4
|
import {
|
|
5
5
|
acquireSceneReadOnlyLease,
|
|
6
6
|
createI18n,
|
|
7
|
+
emitter,
|
|
7
8
|
getCatalogMaterialById,
|
|
8
9
|
getLibraryMaterialIdFromRef,
|
|
9
10
|
getMaterialSwatch,
|
|
@@ -43,6 +44,7 @@ import {
|
|
|
43
44
|
} from 'react'
|
|
44
45
|
import { ViewerOverlay } from '../../components/viewer-overlay'
|
|
45
46
|
import { ViewerZoneSystem } from '../../components/viewer-zone-system'
|
|
47
|
+
import { useAutoFrame } from '../../hooks/use-auto-frame'
|
|
46
48
|
import { type SaveStatus, useAutoSave } from '../../hooks/use-auto-save'
|
|
47
49
|
import { useKeyboard } from '../../hooks/use-keyboard'
|
|
48
50
|
import { useIsMobile } from '../../hooks/use-mobile'
|
|
@@ -67,6 +69,7 @@ import {
|
|
|
67
69
|
type SceneGraph,
|
|
68
70
|
writePersistedSelection,
|
|
69
71
|
} from '../../lib/scene'
|
|
72
|
+
import { computeSceneBoundsXZ } from '../../lib/scene-bounds'
|
|
70
73
|
import { disposeSFXBus, initSFXBus } from '../../lib/sfx-bus'
|
|
71
74
|
import { useUnitFocusRules } from '../../lib/units'
|
|
72
75
|
import { cn } from '../../lib/utils'
|
|
@@ -1570,6 +1573,8 @@ function EditorContent({
|
|
|
1570
1573
|
useKeyboard({ isVersionPreviewMode, disabled: isFirstPersonMode || isStudioMode })
|
|
1571
1574
|
useGestureLifecycleOwner()
|
|
1572
1575
|
|
|
1576
|
+
useAutoFrame()
|
|
1577
|
+
|
|
1573
1578
|
const { beginSceneLoad, completeSceneLoad, saveNow } = useAutoSave({
|
|
1574
1579
|
guardAgainstSceneWipe,
|
|
1575
1580
|
onSave,
|
|
@@ -1741,6 +1746,21 @@ function EditorContent({
|
|
|
1741
1746
|
return () => window.clearTimeout(timer)
|
|
1742
1747
|
}, [hasLoadedInitialScene, isLoading, isSceneLoading, isViewerSceneReady, sceneReadyKey])
|
|
1743
1748
|
|
|
1749
|
+
// The useAutoFrame emit can be clobbered by the level-follow effect's
|
|
1750
|
+
// first-run pivot rest on fast (client-side navigation) loads. Re-emitting
|
|
1751
|
+
// here is the last word after every load-driven camera effect has run.
|
|
1752
|
+
// Once per load: a remounted viewer reports ready again, but only a new
|
|
1753
|
+
// load reframes; remounted controls rest their own pivot on the level.
|
|
1754
|
+
const framedSceneReadyKeyRef = useRef<number | null>(null)
|
|
1755
|
+
useEffect(() => {
|
|
1756
|
+
if (!isViewerSceneReady || framedSceneReadyKeyRef.current === sceneReadyKey) return
|
|
1757
|
+
framedSceneReadyKeyRef.current = sceneReadyKey
|
|
1758
|
+
const nodes = useScene.getState().nodes
|
|
1759
|
+
if (Object.keys(nodes).length === 0) return
|
|
1760
|
+
const bounds = computeSceneBoundsXZ(nodes)
|
|
1761
|
+
emitter.emit('camera-controls:fit-scene', bounds ? { bounds } : {})
|
|
1762
|
+
}, [isViewerSceneReady, sceneReadyKey])
|
|
1763
|
+
|
|
1744
1764
|
const isSceneDataLoading = isLoading || isSceneLoading || !hasLoadedInitialScene
|
|
1745
1765
|
const showLoader = isSceneDataLoading || !isViewerSceneReady
|
|
1746
1766
|
const visibleLoader =
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { formatLinearMeasurement, type MetricNotation } from '@pascal-app/core'
|
|
3
4
|
import { useViewer } from '@pascal-app/viewer'
|
|
4
5
|
import { type ForwardedRef, Fragment, forwardRef } from 'react'
|
|
5
|
-
import { formatLinearMeasurement, type MetricNotation } from '../../lib/measurements'
|
|
6
6
|
|
|
7
7
|
// Canonical in-world dimension formatter — metric metres or imperial
|
|
8
8
|
// feet/inches. Shared by every measurement readout so they read the same.
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
createAngleAccumulator,
|
|
10
10
|
createSceneApi,
|
|
11
11
|
DEFAULT_ANGLE_STEP,
|
|
12
|
+
formatAngleRadians,
|
|
12
13
|
type HandleDescriptor,
|
|
13
14
|
type HandlePortal,
|
|
14
15
|
type LatchHandle,
|
|
@@ -59,7 +60,6 @@ import useInteractionScope, {
|
|
|
59
60
|
useMovingNode,
|
|
60
61
|
} from '../../store/use-interaction-scope'
|
|
61
62
|
import useOpeningGuides from '../../store/use-opening-guides'
|
|
62
|
-
import { formatAngleRadians } from '../tools/shared/segment-angle'
|
|
63
63
|
import {
|
|
64
64
|
ARROW_COLOR,
|
|
65
65
|
ARROW_HOVER_COLOR,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { type OpenWallEnd, sceneRegistry } from '@pascal-app/core'
|
|
4
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
4
|
+
import { MEASUREMENT_DANGLING_COLOR, useViewer } from '@pascal-app/viewer'
|
|
5
5
|
import { useFrame } from '@react-three/fiber'
|
|
6
6
|
import { memo, useEffect, useMemo, useRef } from 'react'
|
|
7
7
|
import { BoxGeometry, CircleGeometry, type Group, RingGeometry } from 'three'
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
} from '../../hooks/use-open-wall-ends'
|
|
14
14
|
import { EDITOR_LAYER } from '../../lib/constants'
|
|
15
15
|
import { openWallEndKey, openWallEndLabel } from '../../lib/floorplan/open-wall-ends'
|
|
16
|
-
import { MEASUREMENT_DANGLING_COLOR } from '../../lib/measurements'
|
|
17
16
|
import { cn } from '../../lib/utils'
|
|
18
17
|
import useEditor from '../../store/use-editor'
|
|
19
18
|
import { isViewVisible, VIEW_3D, visibleScene } from '../../store/view-layout'
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { type AnyNodeId, emitter, type OpenWallEnd } from '@pascal-app/core'
|
|
4
|
+
import { MEASUREMENT_DANGLING_COLOR } from '@pascal-app/viewer'
|
|
4
5
|
import { useEffect, useState } from 'react'
|
|
5
6
|
import { createPortal } from 'react-dom'
|
|
6
7
|
import { openWallEndsSummary } from '../../lib/floorplan/open-wall-ends'
|
|
7
|
-
import { MEASUREMENT_DANGLING_COLOR } from '../../lib/measurements'
|
|
8
8
|
|
|
9
9
|
// The viewer area holds the floor plan and the 3D view side by side, so its
|
|
10
10
|
// top centre clears both views' corner controls in 2D, 3D and split alike.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import type { QuickMeasurementMetric, QuickMeasurementReport } from '@pascal-app/core'
|
|
4
|
+
import { formatAreaLabel, formatLinearMeasurement, formatVolumeLabel } from '@pascal-app/core'
|
|
4
5
|
import { Crosshair, MapPin } from 'lucide-react'
|
|
5
6
|
import { memo } from 'react'
|
|
6
|
-
import { formatAreaLabel, formatLinearMeasurement, formatVolumeLabel } from '../../lib/measurements'
|
|
7
7
|
|
|
8
8
|
function formatMetric(
|
|
9
9
|
metric: QuickMeasurementMetric,
|
|
@@ -6,7 +6,9 @@ import {
|
|
|
6
6
|
DEFAULT_LEVEL_HEIGHT,
|
|
7
7
|
deleteZone,
|
|
8
8
|
emitter,
|
|
9
|
+
formatLinearMeasurement,
|
|
9
10
|
type Point,
|
|
11
|
+
roomFloorElevation,
|
|
10
12
|
type StructureNodes,
|
|
11
13
|
sceneRegistry,
|
|
12
14
|
useScene,
|
|
@@ -35,7 +37,6 @@ import { getRoomSelectionIndex } from '../../hooks/use-selected-room'
|
|
|
35
37
|
import { EDITOR_LAYER } from '../../lib/constants'
|
|
36
38
|
import { getFloatingMenuScale } from '../../lib/floating-menu-scale'
|
|
37
39
|
import { clientToPlan } from '../../lib/floorplan/plan-coords'
|
|
38
|
-
import { formatLinearMeasurement } from '../../lib/measurements'
|
|
39
40
|
import { addMezzanineStairs } from '../../lib/mezzanine-stairs'
|
|
40
41
|
import {
|
|
41
42
|
cancelRoomDivide,
|
|
@@ -46,7 +47,7 @@ import {
|
|
|
46
47
|
roomDivideContains,
|
|
47
48
|
startRoomDivide,
|
|
48
49
|
} from '../../lib/room-divide-session'
|
|
49
|
-
import {
|
|
50
|
+
import { useRoomHandleDrag } from '../../lib/room-handle-drag'
|
|
50
51
|
import type { RoomSelectionRecord } from '../../lib/room-selection'
|
|
51
52
|
import { requestRoomDeletion } from '../../lib/room-structure-commands'
|
|
52
53
|
import {
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type AnyNode,
|
|
5
5
|
type AnyNodeId,
|
|
6
6
|
type FloorplanGeometry,
|
|
7
|
+
formatLinearMeasurement,
|
|
7
8
|
nodeRegistry,
|
|
8
9
|
type Point,
|
|
9
10
|
roomDrawnFloor,
|
|
@@ -15,7 +16,7 @@ import { type PointerEvent as ReactPointerEvent, useMemo, useState } from 'react
|
|
|
15
16
|
import { useShallow } from 'zustand/react/shallow'
|
|
16
17
|
import { useRoomRecords, useSelectedRoom } from '../../hooks/use-selected-room'
|
|
17
18
|
import { getFloorplanNodeExtension } from '../../lib/floorplan/floorplan-extension'
|
|
18
|
-
import {
|
|
19
|
+
import { MEASUREMENT_FLOORPLAN_COLOR } from '../../lib/measurements'
|
|
19
20
|
import {
|
|
20
21
|
commitRoomFloorElevation,
|
|
21
22
|
roomRelativeFloorHeight,
|
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
import {
|
|
4
4
|
type AnyNodeId,
|
|
5
5
|
clampRoomFloorHandle,
|
|
6
|
+
formatLinearMeasurement,
|
|
6
7
|
type Point,
|
|
7
8
|
roomDrawnFloor,
|
|
9
|
+
roomFloorElevation,
|
|
8
10
|
sceneRegistry,
|
|
9
11
|
usePascalI18n,
|
|
10
12
|
useScene,
|
|
11
13
|
} from '@pascal-app/core'
|
|
12
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
14
|
+
import { MEASUREMENT_ACTIVE_COLOR, useViewer } from '@pascal-app/viewer'
|
|
13
15
|
import { type ThreeEvent, useFrame, useThree } from '@react-three/fiber'
|
|
14
16
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
15
17
|
import {
|
|
@@ -35,7 +37,6 @@ import {
|
|
|
35
37
|
resolveStructuralElevationSnap,
|
|
36
38
|
} from '../../lib/elevation-guides'
|
|
37
39
|
import { parseDimensionInput } from '../../lib/floorplan/dimension-drive'
|
|
38
|
-
import { formatLinearMeasurement, MEASUREMENT_ACTIVE_COLOR } from '../../lib/measurements'
|
|
39
40
|
import { commitRoomFloorElevation, roomFloorBase } from '../../lib/room-construction-commands'
|
|
40
41
|
import {
|
|
41
42
|
type MezzanineEdgeHandle,
|
|
@@ -44,7 +45,6 @@ import {
|
|
|
44
45
|
ownFloorElevationBounds,
|
|
45
46
|
ROOM_ELEVATION_DRAG_LABEL,
|
|
46
47
|
type RoomHandleDrag,
|
|
47
|
-
roomFloorElevation,
|
|
48
48
|
roomPushHandles,
|
|
49
49
|
runMezzanineEdgeDrag,
|
|
50
50
|
runRoomHandleDrag,
|
|
@@ -8,11 +8,15 @@ import {
|
|
|
8
8
|
DEFAULT_LEVEL_HEIGHT,
|
|
9
9
|
emitter,
|
|
10
10
|
formatNumber,
|
|
11
|
+
getAreaUnitLabel,
|
|
12
|
+
type LinearUnit,
|
|
11
13
|
type PascalI18n,
|
|
12
14
|
type Polygon,
|
|
13
15
|
polygonInteriorPoint,
|
|
14
16
|
type Ring,
|
|
17
|
+
roomFloorElevation,
|
|
15
18
|
sceneRegistry,
|
|
19
|
+
squareMetersToAreaUnit,
|
|
16
20
|
useLocale,
|
|
17
21
|
usePascalI18n,
|
|
18
22
|
useScene,
|
|
@@ -48,8 +52,6 @@ import {
|
|
|
48
52
|
} from 'three'
|
|
49
53
|
import { useShallow } from 'zustand/react/shallow'
|
|
50
54
|
import { useRoomRecords } from '../../hooks/use-selected-room'
|
|
51
|
-
import { getAreaUnitLabel, type LinearUnit, squareMetersToAreaUnit } from '../../lib/measurements'
|
|
52
|
-
import { roomFloorElevation } from '../../lib/room-handle-drag'
|
|
53
55
|
import { displayRoomName } from '../../lib/room-name-messages'
|
|
54
56
|
import type { RoomSelectionRecord } from '../../lib/room-selection'
|
|
55
57
|
import { clickZoneArea, hoverZoneArea, leaveZoneArea } from '../../lib/room-zone-routing'
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import type { SiteNode } from '@pascal-app/core'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
formatLinearMeasurement,
|
|
6
|
+
sceneRegistry,
|
|
7
|
+
useLiveNodeOverrides,
|
|
8
|
+
useScene,
|
|
9
|
+
} from '@pascal-app/core'
|
|
5
10
|
import { getSceneTheme, useViewer } from '@pascal-app/viewer'
|
|
6
11
|
import { createPortal, useFrame, useThree } from '@react-three/fiber'
|
|
7
12
|
import { useCallback, useMemo, useRef, useState } from 'react'
|
|
8
13
|
import { type Camera, type Object3D, Vector3 } from 'three'
|
|
9
14
|
import { groundHeightAt } from '../../lib/ground-surface'
|
|
10
|
-
import { formatLinearMeasurement } from '../../lib/measurements'
|
|
11
15
|
import { SITE_BOUNDARY_DRAG_LABEL } from '../../lib/site-boundary'
|
|
12
16
|
import { useActiveHandleDrag } from '../../store/use-interaction-scope'
|
|
13
17
|
import { Html } from './localized-html'
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type AnyNode,
|
|
5
5
|
type AnyNodeId,
|
|
6
6
|
calculateLevelMiters,
|
|
7
|
+
formatLinearMeasurement,
|
|
7
8
|
getWallCurveLength,
|
|
8
9
|
getWallEffectiveHeightForNodes,
|
|
9
10
|
getWallMiterBoundaryPoints,
|
|
@@ -23,7 +24,6 @@ import { getSceneTheme, useViewer } from '@pascal-app/viewer'
|
|
|
23
24
|
import { createPortal, useFrame } from '@react-three/fiber'
|
|
24
25
|
import { useMemo, useState } from 'react'
|
|
25
26
|
import * as THREE from 'three'
|
|
26
|
-
import { formatLinearMeasurement } from '../../lib/measurements'
|
|
27
27
|
import { Html } from './localized-html'
|
|
28
28
|
|
|
29
29
|
const GUIDE_Y_OFFSET = 0.08
|
|
@@ -315,16 +315,25 @@ function buildMeasurementGuide(
|
|
|
315
315
|
const measurementPoints = measurementLine ?? fallbackMiddlePoints
|
|
316
316
|
if (!measurementPoints) return null
|
|
317
317
|
|
|
318
|
-
const
|
|
318
|
+
const heightStart = getWallEffectiveHeightForNodes(wall, nodes, 0)
|
|
319
|
+
const heightEnd = getWallEffectiveHeightForNodes(wall, nodes, 1)
|
|
319
320
|
const startLocal = worldPointToWallLocal(wall, measurementPoints.start)
|
|
320
321
|
const endLocal = worldPointToWallLocal(wall, measurementPoints.end)
|
|
321
322
|
const curvedMeasurementPath = isCurvedWall(wall)
|
|
322
323
|
? getCurvedWallMeasurementPath(wall, miterData, levelWalls)
|
|
323
324
|
: null
|
|
325
|
+
const dx = wall.end[0] - wall.start[0]
|
|
326
|
+
const dz = wall.end[1] - wall.start[1]
|
|
327
|
+
const wallChordLength = Math.hypot(dx, dz)
|
|
328
|
+
const getChordT = (localX: number) =>
|
|
329
|
+
wallChordLength > 1e-6 ? Math.max(0, Math.min(1, localX / wallChordLength)) : 0
|
|
330
|
+
|
|
324
331
|
const guidePath: Vec3[] = curvedMeasurementPath
|
|
325
332
|
? curvedMeasurementPath.map((point) => {
|
|
326
333
|
const localPoint = worldPointToWallLocal(wall, point)
|
|
327
|
-
|
|
334
|
+
const t = getChordT(localPoint[0])
|
|
335
|
+
const h = getWallEffectiveHeightForNodes(wall, nodes, t)
|
|
336
|
+
return [localPoint[0], h + GUIDE_Y_OFFSET, localPoint[2]]
|
|
328
337
|
})
|
|
329
338
|
: isCurvedWall(wall)
|
|
330
339
|
? sampleWallCenterline(wall, 24).map((point, index, points) => {
|
|
@@ -334,12 +343,14 @@ function buildMeasurementGuide(
|
|
|
334
343
|
: index === points.length - 1
|
|
335
344
|
? endLocal
|
|
336
345
|
: worldPointToWallLocal(wall, point)
|
|
346
|
+
const t = getChordT(localPoint[0])
|
|
347
|
+
const h = getWallEffectiveHeightForNodes(wall, nodes, t)
|
|
337
348
|
|
|
338
|
-
return [localPoint[0],
|
|
349
|
+
return [localPoint[0], h + GUIDE_Y_OFFSET, localPoint[2]]
|
|
339
350
|
})
|
|
340
351
|
: [
|
|
341
|
-
[startLocal[0],
|
|
342
|
-
[endLocal[0],
|
|
352
|
+
[startLocal[0], heightStart + GUIDE_Y_OFFSET, startLocal[2]],
|
|
353
|
+
[endLocal[0], heightEnd + GUIDE_Y_OFFSET, endLocal[2]],
|
|
343
354
|
]
|
|
344
355
|
|
|
345
356
|
if (guidePath.length < 2) return null
|
|
@@ -397,26 +408,30 @@ function buildMeasurementGuide(
|
|
|
397
408
|
],
|
|
398
409
|
})
|
|
399
410
|
const bottomHeightTick = getHorizontalHeightTick(0)
|
|
400
|
-
const topHeightTick = getHorizontalHeightTick(
|
|
411
|
+
const topHeightTick = getHorizontalHeightTick(heightEnd)
|
|
401
412
|
|
|
402
413
|
return {
|
|
403
414
|
guidePath,
|
|
404
|
-
extStartStart: [extensionStartBase[0],
|
|
415
|
+
extStartStart: [extensionStartBase[0], heightStart, extensionStartBase[2]],
|
|
405
416
|
extStartEnd: [
|
|
406
417
|
extensionStartBase[0],
|
|
407
|
-
|
|
418
|
+
heightStart + GUIDE_Y_OFFSET + extOvershoot,
|
|
408
419
|
extensionStartBase[2],
|
|
409
420
|
],
|
|
410
|
-
extEndStart: [extensionEndBase[0],
|
|
411
|
-
extEndEnd: [
|
|
421
|
+
extEndStart: [extensionEndBase[0], heightEnd, extensionEndBase[2]],
|
|
422
|
+
extEndEnd: [
|
|
423
|
+
extensionEndBase[0],
|
|
424
|
+
heightEnd + GUIDE_Y_OFFSET + extOvershoot,
|
|
425
|
+
extensionEndBase[2],
|
|
426
|
+
],
|
|
412
427
|
labelPosition: [midpoint[0], midpoint[1] + LABEL_LIFT, midpoint[2]],
|
|
413
428
|
heightStart: [heightGuidePosition[0], 0, heightGuidePosition[2]],
|
|
414
|
-
heightEnd: [heightGuidePosition[0],
|
|
429
|
+
heightEnd: [heightGuidePosition[0], heightEnd, heightGuidePosition[2]],
|
|
415
430
|
heightBottomTickStart: bottomHeightTick.start,
|
|
416
431
|
heightBottomTickEnd: bottomHeightTick.end,
|
|
417
432
|
heightTopTickStart: topHeightTick.start,
|
|
418
433
|
heightTopTickEnd: topHeightTick.end,
|
|
419
|
-
heightLabelPosition: [heightGuidePosition[0],
|
|
434
|
+
heightLabelPosition: [heightGuidePosition[0], heightEnd / 2, heightGuidePosition[2]],
|
|
420
435
|
}
|
|
421
436
|
}
|
|
422
437
|
|
|
@@ -530,7 +545,8 @@ function WallMeasurementAnnotation({ wall }: { wall: WallNode }) {
|
|
|
530
545
|
return total
|
|
531
546
|
}, [guide, wall])
|
|
532
547
|
const label = formatLinearMeasurement(length, unit, metricNotation)
|
|
533
|
-
|
|
548
|
+
// Height annotation uses t=1 (wall end) because the vertical guide is drawn at the endpoint
|
|
549
|
+
const height = useMemo(() => getWallEffectiveHeightForNodes(wall, nodes, 1), [nodes, wall])
|
|
534
550
|
const heightLabel = `H ${formatLinearMeasurement(height, unit, metricNotation)}`
|
|
535
551
|
|
|
536
552
|
if (!(guide && Number.isFinite(length) && length >= 0.01)) return null
|