@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
|
@@ -38,7 +38,6 @@ test('roof trim drag keeps its plane hit over a surface joining and leaving a ba
|
|
|
38
38
|
id: 'rseg_batch_drag',
|
|
39
39
|
width: 10,
|
|
40
40
|
depth: 10,
|
|
41
|
-
metadata: { showTrimPlanes: true },
|
|
42
41
|
})
|
|
43
42
|
const source = new THREE.Group()
|
|
44
43
|
const surface = new THREE.Mesh(new THREE.BoxGeometry(30, 0.25, 30), new THREE.MeshBasicMaterial())
|
|
@@ -52,6 +51,9 @@ test('roof trim drag keeps its plane hit over a surface joining and leaving a ba
|
|
|
52
51
|
hoveredId: null,
|
|
53
52
|
selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [segment.id] },
|
|
54
53
|
})
|
|
54
|
+
useInteractionScope
|
|
55
|
+
.getState()
|
|
56
|
+
.begin({ kind: 'reshaping', reshape: 'trim', nodeId: segment.id, driver: 'tool' })
|
|
55
57
|
await root.configure({
|
|
56
58
|
gl: {
|
|
57
59
|
domElement: canvas,
|
|
@@ -99,6 +101,18 @@ test('roof trim drag keeps its plane hit over a surface joining and leaving a ba
|
|
|
99
101
|
events.dispatchEvent(new Event('pointercancel'))
|
|
100
102
|
})
|
|
101
103
|
expect(useLiveNodeOverrides.getState().overrides.has(segment.id)).toBe(false)
|
|
104
|
+
const fieldEscape = Object.assign(new Event('keydown'), { key: 'Escape' })
|
|
105
|
+
Object.defineProperty(fieldEscape, 'target', {
|
|
106
|
+
value: { closest: () => ({ tagName: 'INPUT' }) },
|
|
107
|
+
})
|
|
108
|
+
await act(async () => {
|
|
109
|
+
events.dispatchEvent(fieldEscape)
|
|
110
|
+
})
|
|
111
|
+
expect(useInteractionScope.getState().scope.kind).toBe('reshaping')
|
|
112
|
+
await act(async () => {
|
|
113
|
+
events.dispatchEvent(Object.assign(new Event('keydown'), { key: 'Escape' }))
|
|
114
|
+
})
|
|
115
|
+
expect(useInteractionScope.getState().scope.kind).toBe('idle')
|
|
102
116
|
} finally {
|
|
103
117
|
await act(async () => {
|
|
104
118
|
root.render(null)
|
|
@@ -34,6 +34,7 @@ import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
|
|
|
34
34
|
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
35
35
|
import { isHistoryShortcut } from '../../../lib/history'
|
|
36
36
|
import { getHoveredRoofSegmentOutlineProxyName } from '../../../lib/roof-hover-outline-proxy'
|
|
37
|
+
import { isTypingTarget } from '../../../lib/typing-target'
|
|
37
38
|
import useInteractionScope, { useMovingNode } from '../../../store/use-interaction-scope'
|
|
38
39
|
import { swallowNextClick } from '../../editor/handles/use-handle-drag'
|
|
39
40
|
|
|
@@ -1006,15 +1007,6 @@ function getTrimCursor(side: RoofTrimSide): string {
|
|
|
1006
1007
|
}
|
|
1007
1008
|
}
|
|
1008
1009
|
|
|
1009
|
-
function shouldShowTrimPlanes(metadata: unknown): boolean {
|
|
1010
|
-
return (
|
|
1011
|
-
typeof metadata === 'object' &&
|
|
1012
|
-
metadata !== null &&
|
|
1013
|
-
!Array.isArray(metadata) &&
|
|
1014
|
-
(metadata as Record<string, unknown>).showTrimPlanes === true
|
|
1015
|
-
)
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
1010
|
function commitSegmentTrim(segment: RoofSegmentNode, trim: RoofSegmentTrim) {
|
|
1019
1011
|
const scene = useScene.getState()
|
|
1020
1012
|
scene.applyNodeChanges({
|
|
@@ -1031,6 +1023,28 @@ function RoofTrimHandles() {
|
|
|
1031
1023
|
return node?.type === 'roof-segment' ? (node as RoofSegmentNode) : null
|
|
1032
1024
|
})
|
|
1033
1025
|
const readOnly = useScene((s) => s.readOnly)
|
|
1026
|
+
const showTrimPlanes = useInteractionScope(
|
|
1027
|
+
(s) =>
|
|
1028
|
+
s.scope.kind === 'reshaping' && s.scope.reshape === 'trim' && s.scope.nodeId === selectedId,
|
|
1029
|
+
)
|
|
1030
|
+
useEffect(() => {
|
|
1031
|
+
const end = () =>
|
|
1032
|
+
useInteractionScope
|
|
1033
|
+
.getState()
|
|
1034
|
+
.endIf(
|
|
1035
|
+
(scope) =>
|
|
1036
|
+
scope.kind === 'reshaping' && scope.reshape === 'trim' && scope.nodeId === selectedId,
|
|
1037
|
+
)
|
|
1038
|
+
const onKey = (event: KeyboardEvent) => {
|
|
1039
|
+
if (isTypingTarget(event.target)) return
|
|
1040
|
+
if (event.key === 'Escape') end()
|
|
1041
|
+
}
|
|
1042
|
+
window.addEventListener('keydown', onKey)
|
|
1043
|
+
return () => {
|
|
1044
|
+
window.removeEventListener('keydown', onKey)
|
|
1045
|
+
end()
|
|
1046
|
+
}
|
|
1047
|
+
}, [selectedId])
|
|
1034
1048
|
const liveOverrideKey = useLiveNodeOverrides((s) =>
|
|
1035
1049
|
segment ? JSON.stringify(s.overrides.get(segment.id) ?? null) : null,
|
|
1036
1050
|
)
|
|
@@ -1042,7 +1056,10 @@ function RoofTrimHandles() {
|
|
|
1042
1056
|
const zoom = camera instanceof THREE.OrthographicCamera ? 1 / camera.zoom : 1
|
|
1043
1057
|
const handleBaseScale = zoom * TRIM_HANDLE_BASE_SCALE
|
|
1044
1058
|
|
|
1045
|
-
useEffect(() =>
|
|
1059
|
+
useEffect(() => {
|
|
1060
|
+
if (!selectedId || !showTrimPlanes || readOnly) dragCleanupRef.current?.()
|
|
1061
|
+
return () => dragCleanupRef.current?.()
|
|
1062
|
+
}, [selectedId, showTrimPlanes, readOnly])
|
|
1046
1063
|
|
|
1047
1064
|
const liveSegment = useMemo(() => {
|
|
1048
1065
|
if (!segment) return null
|
|
@@ -1059,8 +1076,6 @@ function RoofTrimHandles() {
|
|
|
1059
1076
|
groupRef.current.matrixAutoUpdate = false
|
|
1060
1077
|
})
|
|
1061
1078
|
|
|
1062
|
-
const showTrimPlanes = shouldShowTrimPlanes(liveSegment?.metadata)
|
|
1063
|
-
|
|
1064
1079
|
if (readOnly || !segment || !liveSegment || !showTrimPlanes) return null
|
|
1065
1080
|
|
|
1066
1081
|
const trim = normalizeRoofSegmentTrim(liveSegment)
|
|
@@ -1314,9 +1329,7 @@ function RoofTrimHandles() {
|
|
|
1314
1329
|
|
|
1315
1330
|
document.body.style.cursor = getTrimCursor(side)
|
|
1316
1331
|
setDraggingSide(side)
|
|
1317
|
-
|
|
1318
|
-
.getState()
|
|
1319
|
-
.begin({ kind: 'handle-drag', nodeId: segmentId, handle: getTrimLabel(side) })
|
|
1332
|
+
|
|
1320
1333
|
useViewer.getState().setInputDragging(true)
|
|
1321
1334
|
useScene.temporal.getState().pause()
|
|
1322
1335
|
|
|
@@ -1355,9 +1368,7 @@ function RoofTrimHandles() {
|
|
|
1355
1368
|
document.body.style.cursor = ''
|
|
1356
1369
|
}
|
|
1357
1370
|
useScene.temporal.getState().resume()
|
|
1358
|
-
|
|
1359
|
-
.getState()
|
|
1360
|
-
.endIf((scope) => scope.kind === 'handle-drag' && scope.nodeId === segmentId)
|
|
1371
|
+
|
|
1361
1372
|
useViewer.getState().setInputDragging(false)
|
|
1362
1373
|
setDraggingSide(null)
|
|
1363
1374
|
dragCleanupRef.current = null
|
|
@@ -1393,7 +1404,7 @@ function RoofTrimHandles() {
|
|
|
1393
1404
|
onCancel()
|
|
1394
1405
|
}
|
|
1395
1406
|
|
|
1396
|
-
dragCleanupRef.current =
|
|
1407
|
+
dragCleanupRef.current = onCancel
|
|
1397
1408
|
window.addEventListener('pointermove', onMove)
|
|
1398
1409
|
window.addEventListener('pointerup', onUp)
|
|
1399
1410
|
window.addEventListener('pointercancel', onCancel)
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import { expect, test } from 'bun:test'
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
type AnyNodeId,
|
|
5
|
+
emitter,
|
|
6
|
+
type GridEvent,
|
|
7
|
+
nodeRegistry,
|
|
8
|
+
registerNode,
|
|
9
|
+
useScene,
|
|
10
|
+
WallNode,
|
|
11
|
+
} from '@pascal-app/core'
|
|
12
|
+
import { type EditorGridEvent, useEditor, useInteractionScope } from '@pascal-app/editor'
|
|
4
13
|
import { useThree } from '@react-three/fiber'
|
|
5
14
|
import { act, create } from '@react-three/test-renderer'
|
|
6
|
-
import { useDistributionRunTool } from './distribution-run-tool'
|
|
15
|
+
import { type RunConnection, type RunPoint, useDistributionRunTool } from './distribution-run-tool'
|
|
16
|
+
import { findNearestRunBody3D, type ScenePort } from './ports'
|
|
7
17
|
|
|
8
18
|
test.each(['duct-segment', 'pipe-segment'] as const)(
|
|
9
19
|
'%s releases its wall plane after an outward commit and retains it along the wall',
|
|
@@ -123,3 +133,361 @@ test.each(['duct-segment', 'pipe-segment'] as const)(
|
|
|
123
133
|
}
|
|
124
134
|
},
|
|
125
135
|
)
|
|
136
|
+
|
|
137
|
+
test.each(['duct-segment', 'pipe-segment'] as const)(
|
|
138
|
+
'%s routes at an exact elevation and updates a vertical target without pointer movement',
|
|
139
|
+
async (toolName) => {
|
|
140
|
+
const previousEditor = useEditor.getState()
|
|
141
|
+
const previousScope = useInteractionScope.getState()
|
|
142
|
+
const previousWindow = Object.getOwnPropertyDescriptor(globalThis, 'window')
|
|
143
|
+
const previousAct = Object.getOwnPropertyDescriptor(globalThis, 'IS_REACT_ACT_ENVIRONMENT')
|
|
144
|
+
Object.defineProperty(globalThis, 'window', {
|
|
145
|
+
value: new EventTarget(),
|
|
146
|
+
configurable: true,
|
|
147
|
+
writable: true,
|
|
148
|
+
})
|
|
149
|
+
Object.defineProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT', {
|
|
150
|
+
value: true,
|
|
151
|
+
configurable: true,
|
|
152
|
+
writable: true,
|
|
153
|
+
})
|
|
154
|
+
let tool: ReturnType<typeof useDistributionRunTool> | undefined
|
|
155
|
+
const commits: Array<[number, number, number]> = []
|
|
156
|
+
let routingMode: 'horizontal' | 'vertical' = 'horizontal'
|
|
157
|
+
let targetElevation = 4
|
|
158
|
+
function Host() {
|
|
159
|
+
const { gl } = useThree()
|
|
160
|
+
gl.domElement.getBoundingClientRect = () =>
|
|
161
|
+
({ left: 0, top: 0, width: 100, height: 100 }) as DOMRect
|
|
162
|
+
tool = useDistributionRunTool({
|
|
163
|
+
active: true,
|
|
164
|
+
levelId: 'level_test',
|
|
165
|
+
toolName,
|
|
166
|
+
routingMode,
|
|
167
|
+
elevation: 2,
|
|
168
|
+
targetElevation,
|
|
169
|
+
getPorts: () => [],
|
|
170
|
+
findBody: () => null,
|
|
171
|
+
commit: ({ end }) => {
|
|
172
|
+
commits.push(end)
|
|
173
|
+
return { nextStart: end, nextConnection: { port: null, body: null } }
|
|
174
|
+
},
|
|
175
|
+
})
|
|
176
|
+
return null
|
|
177
|
+
}
|
|
178
|
+
let renderer: Awaited<ReturnType<typeof create>> | undefined
|
|
179
|
+
const event = (x: number): GridEvent => ({
|
|
180
|
+
position: [x, 0, 0],
|
|
181
|
+
localPosition: [x, 0, 0],
|
|
182
|
+
nativeEvent: {} as GridEvent['nativeEvent'],
|
|
183
|
+
})
|
|
184
|
+
try {
|
|
185
|
+
useEditor.setState({ snappingModeByContext: { wall: 'off', polygon: 'off', item: 'off' } })
|
|
186
|
+
useEditor.getState().showView('2d')
|
|
187
|
+
renderer = await create(<Host />)
|
|
188
|
+
await act(async () => {
|
|
189
|
+
emitter.emit('grid:click', event(1))
|
|
190
|
+
})
|
|
191
|
+
expect(tool?.start).toEqual([1, 2, 0])
|
|
192
|
+
await act(async () => {
|
|
193
|
+
emitter.emit('grid:click', event(3))
|
|
194
|
+
})
|
|
195
|
+
expect(commits).toEqual([[3, 2, 0]])
|
|
196
|
+
await act(async () => {
|
|
197
|
+
const planEvent: EditorGridEvent = {
|
|
198
|
+
...event(7),
|
|
199
|
+
screenProjection: { pointer: [120, 180], localToScreen: [1, 0, 0, 1, 0, 0] },
|
|
200
|
+
}
|
|
201
|
+
emitter.emit('grid:move', planEvent)
|
|
202
|
+
})
|
|
203
|
+
expect(tool?.cursorScreen).toEqual({ x: 120, y: 180, depth: 0 })
|
|
204
|
+
await act(async () => useEditor.getState().showView('3d'))
|
|
205
|
+
expect(tool?.cursorScreen).toBeNull()
|
|
206
|
+
routingMode = 'vertical'
|
|
207
|
+
await renderer.update(<Host />)
|
|
208
|
+
expect(tool?.cursor).toEqual([3, 4, 0])
|
|
209
|
+
targetElevation = 5
|
|
210
|
+
await renderer.update(<Host />)
|
|
211
|
+
expect(tool?.cursor).toEqual([3, 5, 0])
|
|
212
|
+
await act(async () => {
|
|
213
|
+
emitter.emit('grid:click', event(7))
|
|
214
|
+
})
|
|
215
|
+
expect(commits.at(-1)).toEqual([3, 5, 0])
|
|
216
|
+
} finally {
|
|
217
|
+
await renderer?.unmount()
|
|
218
|
+
useEditor.setState(previousEditor)
|
|
219
|
+
useInteractionScope.setState(previousScope)
|
|
220
|
+
if (previousWindow) Object.defineProperty(globalThis, 'window', previousWindow)
|
|
221
|
+
else Reflect.deleteProperty(globalThis, 'window')
|
|
222
|
+
if (previousAct) Object.defineProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT', previousAct)
|
|
223
|
+
else Reflect.deleteProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT')
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
test.each(['duct-segment', 'pipe-segment'] as const)(
|
|
229
|
+
'%s previews the vertical riser as soon as its start is placed',
|
|
230
|
+
async (toolName) => {
|
|
231
|
+
const previousEditor = useEditor.getState()
|
|
232
|
+
const previousScope = useInteractionScope.getState()
|
|
233
|
+
const previousWindow = Object.getOwnPropertyDescriptor(globalThis, 'window')
|
|
234
|
+
const previousAct = Object.getOwnPropertyDescriptor(globalThis, 'IS_REACT_ACT_ENVIRONMENT')
|
|
235
|
+
const keyTarget = Object.assign(new EventTarget(), { closest: () => null })
|
|
236
|
+
Object.defineProperty(globalThis, 'window', {
|
|
237
|
+
value: keyTarget,
|
|
238
|
+
configurable: true,
|
|
239
|
+
writable: true,
|
|
240
|
+
})
|
|
241
|
+
Object.defineProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT', {
|
|
242
|
+
value: true,
|
|
243
|
+
configurable: true,
|
|
244
|
+
writable: true,
|
|
245
|
+
})
|
|
246
|
+
let tool: ReturnType<typeof useDistributionRunTool> | undefined
|
|
247
|
+
const commits: Array<[number, number, number]> = []
|
|
248
|
+
function Host() {
|
|
249
|
+
const { gl } = useThree()
|
|
250
|
+
gl.domElement.getBoundingClientRect = () =>
|
|
251
|
+
({ left: 0, top: 0, width: 100, height: 100 }) as DOMRect
|
|
252
|
+
tool = useDistributionRunTool({
|
|
253
|
+
active: true,
|
|
254
|
+
levelId: 'level_test',
|
|
255
|
+
toolName,
|
|
256
|
+
routingMode: 'vertical',
|
|
257
|
+
elevation: 2,
|
|
258
|
+
targetElevation: 4,
|
|
259
|
+
getPorts: () => [],
|
|
260
|
+
findBody: () => null,
|
|
261
|
+
commit: ({ end }) => {
|
|
262
|
+
commits.push(end)
|
|
263
|
+
return { nextStart: end, nextConnection: { port: null, body: null } }
|
|
264
|
+
},
|
|
265
|
+
})
|
|
266
|
+
return null
|
|
267
|
+
}
|
|
268
|
+
let renderer: Awaited<ReturnType<typeof create>> | undefined
|
|
269
|
+
const event = (x: number): GridEvent => ({
|
|
270
|
+
position: [x, 0, 0],
|
|
271
|
+
localPosition: [x, 0, 0],
|
|
272
|
+
nativeEvent: {} as GridEvent['nativeEvent'],
|
|
273
|
+
})
|
|
274
|
+
try {
|
|
275
|
+
useEditor.setState({ snappingModeByContext: { wall: 'off', polygon: 'off', item: 'off' } })
|
|
276
|
+
renderer = await create(<Host />)
|
|
277
|
+
await act(async () => {
|
|
278
|
+
emitter.emit('grid:click', event(1))
|
|
279
|
+
})
|
|
280
|
+
expect(tool?.start).toEqual([1, 2, 0])
|
|
281
|
+
expect(tool?.cursor).toEqual([1, 4, 0])
|
|
282
|
+
|
|
283
|
+
await act(async () => {
|
|
284
|
+
keyTarget.dispatchEvent(Object.assign(new Event('keydown'), { key: 'Enter' }))
|
|
285
|
+
})
|
|
286
|
+
expect(commits).toEqual([[1, 4, 0]])
|
|
287
|
+
expect(tool?.start).toEqual([1, 4, 0])
|
|
288
|
+
} finally {
|
|
289
|
+
await renderer?.unmount()
|
|
290
|
+
useEditor.setState(previousEditor)
|
|
291
|
+
useInteractionScope.setState(previousScope)
|
|
292
|
+
if (previousWindow) Object.defineProperty(globalThis, 'window', previousWindow)
|
|
293
|
+
else Reflect.deleteProperty(globalThis, 'window')
|
|
294
|
+
if (previousAct) Object.defineProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT', previousAct)
|
|
295
|
+
else Reflect.deleteProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT')
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
test.each(['duct-segment', 'pipe-segment'] as const)(
|
|
301
|
+
'%s drawn from a collar rises to each panel target without a pointer event',
|
|
302
|
+
async (toolName) => {
|
|
303
|
+
const previousEditor = useEditor.getState()
|
|
304
|
+
const previousScope = useInteractionScope.getState()
|
|
305
|
+
const previousWindow = Object.getOwnPropertyDescriptor(globalThis, 'window')
|
|
306
|
+
const previousAct = Object.getOwnPropertyDescriptor(globalThis, 'IS_REACT_ACT_ENVIRONMENT')
|
|
307
|
+
const keyTarget = Object.assign(new EventTarget(), { closest: () => null })
|
|
308
|
+
Object.defineProperty(globalThis, 'window', {
|
|
309
|
+
value: keyTarget,
|
|
310
|
+
configurable: true,
|
|
311
|
+
writable: true,
|
|
312
|
+
})
|
|
313
|
+
Object.defineProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT', {
|
|
314
|
+
value: true,
|
|
315
|
+
configurable: true,
|
|
316
|
+
writable: true,
|
|
317
|
+
})
|
|
318
|
+
let tool: ReturnType<typeof useDistributionRunTool> | undefined
|
|
319
|
+
const commits: Array<[number, number, number]> = []
|
|
320
|
+
let targetElevation = 4
|
|
321
|
+
function Host() {
|
|
322
|
+
const { gl } = useThree()
|
|
323
|
+
gl.domElement.getBoundingClientRect = () =>
|
|
324
|
+
({ left: 0, top: 0, width: 100, height: 100 }) as DOMRect
|
|
325
|
+
tool = useDistributionRunTool({
|
|
326
|
+
active: true,
|
|
327
|
+
levelId: 'level_test',
|
|
328
|
+
toolName,
|
|
329
|
+
routingMode: 'vertical',
|
|
330
|
+
elevation: 2,
|
|
331
|
+
targetElevation,
|
|
332
|
+
initialStart: [1, 2, 0],
|
|
333
|
+
getPorts: () => [],
|
|
334
|
+
findBody: () => null,
|
|
335
|
+
commit: ({ end }) => {
|
|
336
|
+
commits.push(end)
|
|
337
|
+
return { nextStart: end, nextConnection: { port: null, body: null } }
|
|
338
|
+
},
|
|
339
|
+
})
|
|
340
|
+
return null
|
|
341
|
+
}
|
|
342
|
+
const pressEnter = () =>
|
|
343
|
+
act(async () => {
|
|
344
|
+
keyTarget.dispatchEvent(Object.assign(new Event('keydown'), { key: 'Enter' }))
|
|
345
|
+
})
|
|
346
|
+
let renderer: Awaited<ReturnType<typeof create>> | undefined
|
|
347
|
+
try {
|
|
348
|
+
useEditor.setState({ snappingModeByContext: { wall: 'off', polygon: 'off', item: 'off' } })
|
|
349
|
+
renderer = await create(<Host />)
|
|
350
|
+
expect(tool?.cursor).toEqual([1, 4, 0])
|
|
351
|
+
targetElevation = 5
|
|
352
|
+
await renderer.update(<Host />)
|
|
353
|
+
expect(tool?.cursor).toEqual([1, 5, 0])
|
|
354
|
+
await pressEnter()
|
|
355
|
+
expect(commits).toEqual([[1, 5, 0]])
|
|
356
|
+
|
|
357
|
+
targetElevation = 3
|
|
358
|
+
await renderer.update(<Host />)
|
|
359
|
+
expect(tool?.cursor).toEqual([1, 3, 0])
|
|
360
|
+
await pressEnter()
|
|
361
|
+
expect(commits).toEqual([
|
|
362
|
+
[1, 5, 0],
|
|
363
|
+
[1, 3, 0],
|
|
364
|
+
])
|
|
365
|
+
} finally {
|
|
366
|
+
await renderer?.unmount()
|
|
367
|
+
useEditor.setState(previousEditor)
|
|
368
|
+
useInteractionScope.setState(previousScope)
|
|
369
|
+
if (previousWindow) Object.defineProperty(globalThis, 'window', previousWindow)
|
|
370
|
+
else Reflect.deleteProperty(globalThis, 'window')
|
|
371
|
+
if (previousAct) Object.defineProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT', previousAct)
|
|
372
|
+
else Reflect.deleteProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT')
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
test('a sloped horizontal route connects at the height its fall reaches', async () => {
|
|
378
|
+
const previousScene = useScene.getState()
|
|
379
|
+
const previousEditor = useEditor.getState()
|
|
380
|
+
const previousScope = useInteractionScope.getState()
|
|
381
|
+
const previousWindow = Object.getOwnPropertyDescriptor(globalThis, 'window')
|
|
382
|
+
const previousAct = Object.getOwnPropertyDescriptor(globalThis, 'IS_REACT_ACT_ENVIRONMENT')
|
|
383
|
+
const restoreRegistry = nodeRegistry._snapshot()
|
|
384
|
+
Object.defineProperty(globalThis, 'window', {
|
|
385
|
+
value: new EventTarget(),
|
|
386
|
+
configurable: true,
|
|
387
|
+
writable: true,
|
|
388
|
+
})
|
|
389
|
+
Object.defineProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT', {
|
|
390
|
+
value: true,
|
|
391
|
+
configurable: true,
|
|
392
|
+
writable: true,
|
|
393
|
+
})
|
|
394
|
+
const toolName = 'test:sloped-run'
|
|
395
|
+
registerNode({
|
|
396
|
+
kind: toolName,
|
|
397
|
+
schemaVersion: 1,
|
|
398
|
+
category: 'structure',
|
|
399
|
+
defaults: () => ({}),
|
|
400
|
+
snapProfile: 'structural',
|
|
401
|
+
} as never)
|
|
402
|
+
const collar = (nodeId: string, position: [number, number, number]): ScenePort => ({
|
|
403
|
+
nodeId: nodeId as AnyNodeId,
|
|
404
|
+
id: 'outlet',
|
|
405
|
+
position,
|
|
406
|
+
direction: [-1, 0, 0],
|
|
407
|
+
diameter: 2,
|
|
408
|
+
})
|
|
409
|
+
const run = (id: string, x: number, y: number) =>
|
|
410
|
+
({
|
|
411
|
+
object: 'node',
|
|
412
|
+
id,
|
|
413
|
+
type: 'pipe-segment',
|
|
414
|
+
parentId: 'level_test',
|
|
415
|
+
path: [
|
|
416
|
+
[x, y, -2],
|
|
417
|
+
[x, y, 2],
|
|
418
|
+
],
|
|
419
|
+
}) as unknown as AnyNode
|
|
420
|
+
// A 2% fall from [0, 2, 0] reaches 1.9 m at x = 5 and 1.84 m at x = 8.
|
|
421
|
+
const ports = [collar('item_start_height', [5, 2, 0.05]), collar('item_graded', [5, 1.9, 0])]
|
|
422
|
+
let tool: ReturnType<typeof useDistributionRunTool> | undefined
|
|
423
|
+
const commits: Array<{ end: RunPoint; endConnection: RunConnection }> = []
|
|
424
|
+
function Host() {
|
|
425
|
+
const { gl } = useThree()
|
|
426
|
+
gl.domElement.getBoundingClientRect = () =>
|
|
427
|
+
({ left: 0, top: 0, width: 100, height: 100 }) as DOMRect
|
|
428
|
+
tool = useDistributionRunTool({
|
|
429
|
+
active: true,
|
|
430
|
+
levelId: 'level_test',
|
|
431
|
+
toolName,
|
|
432
|
+
routingMode: 'horizontal',
|
|
433
|
+
elevation: 2,
|
|
434
|
+
getPorts: () => ports,
|
|
435
|
+
findBody: (point) => findNearestRunBody3D(point, 0.3, { kinds: ['pipe-segment'] }),
|
|
436
|
+
resolveFreeEnd: (start, end) => [
|
|
437
|
+
end[0],
|
|
438
|
+
start[1] - Math.hypot(end[0] - start[0], end[2] - start[2]) * 0.02,
|
|
439
|
+
end[2],
|
|
440
|
+
],
|
|
441
|
+
commit: ({ end, endConnection }) => {
|
|
442
|
+
commits.push({ end, endConnection })
|
|
443
|
+
return { nextStart: end, nextConnection: endConnection }
|
|
444
|
+
},
|
|
445
|
+
})
|
|
446
|
+
return null
|
|
447
|
+
}
|
|
448
|
+
let renderer: Awaited<ReturnType<typeof create>> | undefined
|
|
449
|
+
const planEvent = (x: number, z: number): EditorGridEvent => ({
|
|
450
|
+
position: [x, 0, z],
|
|
451
|
+
localPosition: [x, 0, z],
|
|
452
|
+
screenProjection: { pointer: [x * 100, z * 100], localToScreen: [100, 0, 0, 100, 0, 0] },
|
|
453
|
+
nativeEvent: {} as GridEvent['nativeEvent'],
|
|
454
|
+
})
|
|
455
|
+
try {
|
|
456
|
+
useScene.setState({
|
|
457
|
+
nodes: Object.fromEntries(
|
|
458
|
+
[run('pipe_start_height', 8.05, 1.9), run('pipe_graded', 8, 1.84)].map((node) => [
|
|
459
|
+
node.id,
|
|
460
|
+
node,
|
|
461
|
+
]),
|
|
462
|
+
),
|
|
463
|
+
})
|
|
464
|
+
useEditor.setState({
|
|
465
|
+
snappingModeByContext: { ...previousEditor.snappingModeByContext, wall: 'lines' },
|
|
466
|
+
})
|
|
467
|
+
renderer = await create(<Host />)
|
|
468
|
+
await act(async () => {
|
|
469
|
+
emitter.emit('grid:click', planEvent(0, 0))
|
|
470
|
+
})
|
|
471
|
+
expect(tool?.start).toEqual([0, 2, 0])
|
|
472
|
+
await act(async () => {
|
|
473
|
+
emitter.emit('grid:click', planEvent(5, 0))
|
|
474
|
+
})
|
|
475
|
+
expect(commits[0]?.end).toEqual([5, 1.9, 0])
|
|
476
|
+
expect(commits[0]?.endConnection.port?.nodeId).toBe('item_graded' as AnyNodeId)
|
|
477
|
+
await act(async () => {
|
|
478
|
+
emitter.emit('grid:click', planEvent(8, 0.04))
|
|
479
|
+
})
|
|
480
|
+
expect(commits[1]?.endConnection.body?.nodeId).toBe('pipe_graded' as AnyNodeId)
|
|
481
|
+
expect(commits[1]?.end[1]).toBeCloseTo(1.84, 6)
|
|
482
|
+
} finally {
|
|
483
|
+
await renderer?.unmount()
|
|
484
|
+
restoreRegistry()
|
|
485
|
+
useScene.setState(previousScene, true)
|
|
486
|
+
useEditor.setState(previousEditor, true)
|
|
487
|
+
useInteractionScope.setState(previousScope, true)
|
|
488
|
+
if (previousWindow) Object.defineProperty(globalThis, 'window', previousWindow)
|
|
489
|
+
else Reflect.deleteProperty(globalThis, 'window')
|
|
490
|
+
if (previousAct) Object.defineProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT', previousAct)
|
|
491
|
+
else Reflect.deleteProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT')
|
|
492
|
+
}
|
|
493
|
+
})
|