@pascal-app/editor 1.0.0-beta.5 → 1.0.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/LICENSE +21 -0
- package/bunfig.toml +4 -0
- package/package.json +13 -7
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/bake-exporter.tsx +1 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
- package/src/components/editor/capture-camera-rig.tsx +59 -0
- package/src/components/editor/custom-camera-controls.tsx +20 -11
- package/src/components/editor/editor-layout-mobile.tsx +6 -6
- package/src/components/editor/editor-layout-v2.tsx +9 -2
- package/src/components/editor/export-manager.tsx +175 -33
- package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
- package/src/components/editor/first-person-controls.tsx +180 -12
- package/src/components/editor/floating-action-menu.tsx +42 -51
- package/src/components/editor/floorplan-panel.tsx +76 -12
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- package/src/components/editor/group-move-3d.ts +51 -21
- package/src/components/editor/group-transform-shared.test.ts +78 -0
- package/src/components/editor/group-transform-shared.ts +74 -7
- package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
- package/src/components/editor/handles/handle-arrow.tsx +109 -48
- package/src/components/editor/handles/linear-resize-drag.ts +67 -0
- package/src/components/editor/handles/resize-snap.test.ts +46 -0
- package/src/components/editor/handles/resize-snap.ts +10 -1
- package/src/components/editor/handles/use-handle-drag.ts +18 -1
- package/src/components/editor/index.tsx +140 -21
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +47 -58
- package/src/components/editor/selection-manager.tsx +104 -42
- package/src/components/editor/snapshot-capture-overlay.tsx +298 -39
- package/src/components/editor/snapshot-capture.test.ts +422 -0
- package/src/components/editor/snapshot-capture.ts +145 -0
- package/src/components/editor/thumbnail-generator.tsx +284 -237
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +180 -32
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
- package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
- package/src/components/systems/roof/roof-edit-system.tsx +10 -19
- package/src/components/systems/selection-affordance-manager.tsx +59 -9
- package/src/components/systems/selection-affordance-services.ts +19 -0
- package/src/components/systems/stair/stair-edit-system.tsx +5 -2
- package/src/components/tools/fence/fence-drafting.test.ts +100 -0
- package/src/components/tools/fence/fence-drafting.ts +6 -25
- package/src/components/tools/item/draft-creation.test.ts +12 -0
- package/src/components/tools/item/draft-creation.ts +10 -0
- package/src/components/tools/item/face-host-commit.test.ts +65 -0
- package/src/components/tools/item/face-host-commit.ts +56 -0
- package/src/components/tools/item/face-host-preview.test.ts +77 -0
- package/src/components/tools/item/face-host-preview.ts +82 -0
- package/src/components/tools/item/move-tool.tsx +4 -3
- package/src/components/tools/item/placement-math.test.ts +11 -1
- package/src/components/tools/item/placement-math.ts +0 -20
- package/src/components/tools/item/placement-strategies.test.ts +331 -4
- package/src/components/tools/item/placement-strategies.ts +135 -2
- package/src/components/tools/item/placement-surface.test.ts +61 -0
- package/src/components/tools/item/placement-surface.ts +24 -0
- package/src/components/tools/item/placement-types.ts +6 -0
- package/src/components/tools/item/test-face-host.ts +121 -0
- package/src/components/tools/item/use-draft-node.test.tsx +336 -0
- package/src/components/tools/item/use-draft-node.ts +26 -4
- package/src/components/tools/item/use-placement-coordinator.tsx +428 -153
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
- package/src/components/tools/registry-tool-context.tsx +30 -0
- package/src/components/tools/select/box-select-tool.tsx +3 -1
- package/src/components/tools/select/marquee-footprint.test.ts +53 -0
- package/src/components/tools/select/marquee-footprint.ts +29 -0
- package/src/components/tools/shared/placement-box.tsx +57 -7
- package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
- package/src/components/tools/shared/pointer-support-cap.ts +45 -8
- package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
- package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
- package/src/components/tools/stair/stair-tool.tsx +127 -74
- package/src/components/tools/tool-manager.tsx +25 -8
- package/src/components/tools/wall/wall-drafting.test.ts +320 -2
- package/src/components/tools/wall/wall-drafting.ts +23 -114
- package/src/components/ui/action-menu/control-modes.tsx +9 -6
- package/src/components/ui/action-menu/index.tsx +11 -1
- package/src/components/ui/command-palette/editor-commands.tsx +9 -12
- package/src/components/ui/command-palette/index.tsx +7 -2
- package/src/components/ui/controls/material-paint-panel.tsx +6 -4
- package/src/components/ui/controls/material-picker.tsx +4 -4
- package/src/components/ui/controls/scene-material-list.tsx +6 -5
- package/src/components/ui/controls/segmented-control.tsx +8 -1
- package/src/components/ui/controls/slider-control.tsx +19 -3
- package/src/components/ui/controls/toggle-control.tsx +18 -5
- package/src/components/ui/controls/tool-options-panel.tsx +126 -0
- package/src/components/ui/floating-level-selector.tsx +22 -5
- package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
- package/src/components/ui/helpers/helper-manager.tsx +47 -11
- package/src/components/ui/helpers/item-helper.tsx +1 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
- package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
- package/src/components/ui/panels/homogeneous-selection.ts +47 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
- package/src/components/ui/panels/multi-field-value.test.ts +209 -0
- package/src/components/ui/panels/multi-field-value.ts +163 -0
- package/src/components/ui/panels/multi-height-mode.tsx +185 -0
- package/src/components/ui/panels/multi-parametric-inspector.tsx +263 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +48 -37
- package/src/components/ui/panels/node-display.ts +4 -0
- package/src/components/ui/panels/panel-manager.tsx +77 -3
- package/src/components/ui/panels/parametric-field-control.tsx +173 -0
- package/src/components/ui/panels/parametric-field-utils.ts +18 -0
- package/src/components/ui/panels/parametric-inspector.tsx +29 -162
- package/src/components/ui/panels/reference-panel.tsx +31 -1
- package/src/components/ui/primitives/shortcut-token.tsx +23 -1
- package/src/components/ui/scene-loader.tsx +35 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +20 -23
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +140 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +116 -46
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
- package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +14 -6
- package/src/components/ui/sidebar/tab-bar.tsx +2 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/viewer/viewer-scene-header.tsx +2 -1
- package/src/components/viewer/viewer-stage.tsx +4 -1
- package/src/components/viewer-overlay.tsx +1 -22
- package/src/components/viewer-zone-system.tsx +6 -2
- package/src/components/walkthrough-hud.tsx +19 -10
- package/src/hooks/use-auto-save.test.ts +73 -1
- package/src/hooks/use-auto-save.ts +76 -5
- package/src/hooks/use-ceiling-events.test.ts +122 -0
- package/src/hooks/use-drag-action.ts +17 -3
- package/src/hooks/use-grid-events.test.ts +240 -0
- package/src/hooks/use-grid-events.ts +190 -11
- package/src/hooks/use-keyboard.test.ts +244 -0
- package/src/hooks/use-keyboard.ts +129 -62
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +82 -4
- package/src/lib/active-placement-surface.test.ts +16 -0
- package/src/lib/active-placement-surface.ts +14 -0
- package/src/lib/ceiling-surface-raycast.test.ts +64 -0
- package/src/lib/ceiling-surface-raycast.ts +12 -0
- package/src/lib/contextual-help-extension.ts +17 -0
- package/src/lib/continuation.test.ts +11 -0
- package/src/lib/continuation.ts +8 -1
- package/src/lib/direct-manipulation.test.ts +132 -0
- package/src/lib/direct-manipulation.ts +38 -1
- package/src/lib/editor-api.ts +6 -3
- package/src/lib/elevation-guides.test.ts +28 -1
- package/src/lib/elevation-guides.ts +22 -7
- package/src/lib/floating-menu-scale.test.ts +29 -0
- package/src/lib/floating-menu-scale.ts +14 -0
- package/src/lib/floorplan/apply-alignment.test.ts +47 -2
- package/src/lib/floorplan/apply-alignment.ts +10 -1
- package/src/lib/floorplan/floorplan-export.test.ts +397 -1
- package/src/lib/floorplan/floorplan-export.tsx +111 -16
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
- package/src/lib/floorplan/floorplan-readonly.ts +1 -0
- package/src/lib/floorplan-grid-event-point.test.ts +29 -0
- package/src/lib/floorplan-grid-event-point.ts +13 -0
- package/src/lib/glb-export.test.ts +1169 -21
- package/src/lib/glb-export.ts +629 -101
- package/src/lib/grid-event-presentation.ts +16 -0
- package/src/lib/history.test.ts +528 -147
- package/src/lib/history.ts +81 -7
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -0
- package/src/lib/interaction/hot-set.test.ts +10 -1
- package/src/lib/interaction/hot-set.ts +3 -1
- package/src/lib/interaction/overlay-policy.test.ts +33 -1
- package/src/lib/interaction/overlay-policy.ts +16 -0
- package/src/lib/interaction/registered-drafting.ts +36 -0
- package/src/lib/interaction/scope.ts +31 -0
- package/src/lib/level-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- package/src/lib/local-project-presentation-persistence.test.ts +302 -0
- package/src/lib/local-project-presentation-persistence.ts +335 -0
- package/src/lib/material-paint.ts +2 -0
- package/src/lib/model-export.ts +29 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- package/src/lib/planar-cursor-placement.test.ts +178 -1
- package/src/lib/planar-cursor-placement.ts +61 -5
- package/src/lib/plugin-panels.test.ts +92 -1
- package/src/lib/plugin-panels.ts +48 -0
- package/src/lib/portable-export.test.ts +366 -0
- package/src/lib/portable-export.ts +969 -0
- package/src/lib/print-3mf.test.ts +86 -0
- package/src/lib/print-3mf.ts +161 -0
- package/src/lib/print-content-scope.test.ts +107 -0
- package/src/lib/print-content-scope.ts +90 -0
- package/src/lib/print-export.test.ts +294 -0
- package/src/lib/print-export.ts +735 -0
- package/src/lib/print-feature-thickness.test.ts +147 -0
- package/src/lib/print-feature-thickness.ts +180 -0
- package/src/lib/print-golden-house.test-fixture.ts +352 -0
- package/src/lib/print-golden-house.test.ts +249 -0
- package/src/lib/print-roof-solids.test.ts +133 -0
- package/src/lib/print-roof-solids.ts +630 -0
- package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
- package/src/lib/print-shell-compiler-baseline.ts +225 -0
- package/src/lib/print-shell-compiler-manifold-core.ts +330 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +187 -0
- package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
- package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
- package/src/lib/print-shell-compiler-protocol.ts +30 -0
- package/src/lib/print-shell-compiler.ts +273 -0
- package/src/lib/print-wall-solids.test.ts +194 -0
- package/src/lib/print-wall-solids.ts +328 -0
- package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
- package/src/lib/rigid-plan-svg-transform.ts +24 -0
- package/src/lib/scene.ts +22 -8
- package/src/lib/selection-routing.test.ts +105 -1
- package/src/lib/selection-routing.ts +19 -0
- package/src/lib/sfx-bus.ts +4 -0
- package/src/lib/sfx-player.ts +8 -0
- package/src/lib/snapping-mode.test.ts +32 -4
- package/src/lib/snapping-mode.ts +11 -7
- package/src/lib/usdz-export.ts +38 -0
- package/src/lib/walkthrough-pointer-lock.ts +47 -0
- package/src/store/live-draft-preview-stores.test.ts +3 -0
- package/src/store/terrain-sculpt-mode.test.ts +3 -6
- package/src/store/tool-mode.test.ts +87 -0
- package/src/store/use-camera-hint-focus.ts +48 -0
- package/src/store/use-editor.tsx +299 -188
- package/src/store/use-floorplan-draft-preview.ts +9 -0
- package/src/store/use-interaction-scope.test.ts +23 -1
- package/src/store/use-interaction-scope.ts +63 -2
- package/src/store/use-placement-preview.ts +56 -3
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { expect, spyOn, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
CeilingNode,
|
|
4
|
+
emitter,
|
|
5
|
+
type GridEvent,
|
|
6
|
+
nodeRegistry,
|
|
7
|
+
registerNode,
|
|
8
|
+
SlabNode,
|
|
9
|
+
useRegistry,
|
|
10
|
+
WallNode,
|
|
11
|
+
} from '@pascal-app/core'
|
|
12
|
+
import { hideFromScene, showInScene, useViewer } from '@pascal-app/viewer'
|
|
13
|
+
import { _roots, act, createRoot } from '@react-three/fiber'
|
|
14
|
+
import { createElement } from 'react'
|
|
15
|
+
import {
|
|
16
|
+
DoubleSide,
|
|
17
|
+
Mesh,
|
|
18
|
+
MeshBasicMaterial,
|
|
19
|
+
PerspectiveCamera,
|
|
20
|
+
PlaneGeometry,
|
|
21
|
+
type WebGLRenderer,
|
|
22
|
+
} from 'three'
|
|
23
|
+
import { DRAFTING_SURFACE_EXTENSION_KEY } from '../lib/interaction/registered-drafting'
|
|
24
|
+
import useInteractionScope from '../store/use-interaction-scope'
|
|
25
|
+
import { useGridEvents } from './use-grid-events'
|
|
26
|
+
|
|
27
|
+
test('grid moves throttle camera drags at 100 ms and resume immediately during tool drags', async () => {
|
|
28
|
+
const previousViewer = useViewer.getState()
|
|
29
|
+
const actGlobal = globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
30
|
+
const previousAct = actGlobal.IS_REACT_ACT_ENVIRONMENT
|
|
31
|
+
actGlobal.IS_REACT_ACT_ENVIRONMENT = true
|
|
32
|
+
let now = 0
|
|
33
|
+
const clock = spyOn(performance, 'now').mockImplementation(() => now)
|
|
34
|
+
let rectReads = 0
|
|
35
|
+
const canvas = Object.assign(new EventTarget(), {
|
|
36
|
+
getBoundingClientRect() {
|
|
37
|
+
rectReads++
|
|
38
|
+
return { left: 0, top: 0, width: 100, height: 100 }
|
|
39
|
+
},
|
|
40
|
+
}) as unknown as HTMLCanvasElement
|
|
41
|
+
const root = createRoot(canvas)
|
|
42
|
+
const camera = new PerspectiveCamera(60, 1, 0.1, 100)
|
|
43
|
+
camera.position.set(0, 10, 10)
|
|
44
|
+
camera.lookAt(0, 0, 0)
|
|
45
|
+
camera.updateMatrixWorld()
|
|
46
|
+
const delivered: GridEvent[] = []
|
|
47
|
+
const onMove = (event: GridEvent) => delivered.push(event)
|
|
48
|
+
emitter.on('grid:move', onMove)
|
|
49
|
+
|
|
50
|
+
function Grid() {
|
|
51
|
+
useGridEvents(5)
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const send = (clientX = 50) => {
|
|
56
|
+
canvas.dispatchEvent(
|
|
57
|
+
Object.assign(new Event('pointermove'), { clientX, clientY: 50, button: 0 }),
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
useViewer.setState({
|
|
63
|
+
cameraDragging: false,
|
|
64
|
+
inputDragging: false,
|
|
65
|
+
selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [] },
|
|
66
|
+
})
|
|
67
|
+
await root.configure({
|
|
68
|
+
gl: {
|
|
69
|
+
domElement: canvas,
|
|
70
|
+
render() {},
|
|
71
|
+
setSize() {},
|
|
72
|
+
setPixelRatio() {},
|
|
73
|
+
} as unknown as WebGLRenderer,
|
|
74
|
+
camera,
|
|
75
|
+
frameloop: 'never',
|
|
76
|
+
dpr: 1,
|
|
77
|
+
size: { width: 100, height: 100, top: 0, left: 0 },
|
|
78
|
+
})
|
|
79
|
+
await act(async () => {
|
|
80
|
+
root.render(createElement(Grid))
|
|
81
|
+
})
|
|
82
|
+
send()
|
|
83
|
+
expect(rectReads).toBe(1)
|
|
84
|
+
expect(delivered).toHaveLength(1)
|
|
85
|
+
const initial = delivered[0]!
|
|
86
|
+
expect(initial.position[1]).toBeCloseTo(5)
|
|
87
|
+
expect(initial.localPosition).toEqual(initial.position)
|
|
88
|
+
|
|
89
|
+
for (const inputDragging of [false, true]) {
|
|
90
|
+
const before = delivered.length
|
|
91
|
+
useViewer.setState({ cameraDragging: true, inputDragging })
|
|
92
|
+
send(55)
|
|
93
|
+
expect(delivered).toHaveLength(before + 1)
|
|
94
|
+
for (now = 1; now < 100; now++) send(60)
|
|
95
|
+
expect(rectReads).toBe(before + 1)
|
|
96
|
+
expect(delivered).toHaveLength(before + 1)
|
|
97
|
+
|
|
98
|
+
now = 100
|
|
99
|
+
send(60)
|
|
100
|
+
expect(delivered).toHaveLength(before + 2)
|
|
101
|
+
expect(delivered.at(-1)!.position).not.toEqual(delivered.at(-2)!.position)
|
|
102
|
+
now = 101
|
|
103
|
+
useViewer.setState({ cameraDragging: false })
|
|
104
|
+
send()
|
|
105
|
+
expect(delivered).toHaveLength(before + 3)
|
|
106
|
+
expect(rectReads).toBe(delivered.length)
|
|
107
|
+
expect(delivered.at(-1)!.position).toEqual(initial.position)
|
|
108
|
+
expect(delivered.at(-1)!.localPosition).toEqual(initial.localPosition)
|
|
109
|
+
now = 0
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await act(async () => {
|
|
113
|
+
root.render(null)
|
|
114
|
+
})
|
|
115
|
+
const before = delivered.length
|
|
116
|
+
send()
|
|
117
|
+
expect(delivered).toHaveLength(before)
|
|
118
|
+
expect(rectReads).toBe(before)
|
|
119
|
+
} finally {
|
|
120
|
+
await act(async () => {
|
|
121
|
+
root.render(null)
|
|
122
|
+
})
|
|
123
|
+
clock.mockRestore()
|
|
124
|
+
emitter.off('grid:move', onMove)
|
|
125
|
+
_roots.delete(canvas)
|
|
126
|
+
useViewer.setState(previousViewer)
|
|
127
|
+
actGlobal.IS_REACT_ACT_ENVIRONMENT = previousAct
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test.each([
|
|
132
|
+
'slab',
|
|
133
|
+
'ceiling',
|
|
134
|
+
] as const)('grid moves keep the same %s hit when its source joins or leaves a batch', async (kind) => {
|
|
135
|
+
const previousViewer = useViewer.getState()
|
|
136
|
+
const previousScope = useInteractionScope.getState().scope
|
|
137
|
+
const restoreRegistry = nodeRegistry._snapshot()
|
|
138
|
+
const actGlobal = globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
139
|
+
const previousAct = actGlobal.IS_REACT_ACT_ENVIRONMENT
|
|
140
|
+
actGlobal.IS_REACT_ACT_ENVIRONMENT = true
|
|
141
|
+
const canvas = Object.assign(new EventTarget(), {
|
|
142
|
+
getBoundingClientRect: () => ({ left: 0, top: 0, width: 100, height: 100 }),
|
|
143
|
+
}) as unknown as HTMLCanvasElement
|
|
144
|
+
const root = createRoot(canvas)
|
|
145
|
+
const camera = new PerspectiveCamera(60, 1, 0.1, 100)
|
|
146
|
+
camera.position.set(0, kind === 'ceiling' ? 1 : 10, 10)
|
|
147
|
+
camera.lookAt(0, 5, 0)
|
|
148
|
+
camera.updateMatrixWorld()
|
|
149
|
+
const surface = new Mesh(
|
|
150
|
+
new PlaneGeometry(20, 20).rotateX(-Math.PI / 2),
|
|
151
|
+
new MeshBasicMaterial({ side: DoubleSide }),
|
|
152
|
+
)
|
|
153
|
+
surface.position.y = 5
|
|
154
|
+
surface.updateMatrixWorld(true)
|
|
155
|
+
const surfaceId = `${kind}_grid_batch_test` as const
|
|
156
|
+
const wall = WallNode.parse({ start: [0, 0], end: [0, 2] })
|
|
157
|
+
const delivered: GridEvent[] = []
|
|
158
|
+
const onMove = (event: GridEvent) => delivered.push(event)
|
|
159
|
+
emitter.on('grid:move', onMove)
|
|
160
|
+
|
|
161
|
+
function Grid() {
|
|
162
|
+
useRegistry(surfaceId, kind, { current: surface })
|
|
163
|
+
useGridEvents(0)
|
|
164
|
+
return null
|
|
165
|
+
}
|
|
166
|
+
const send = () => {
|
|
167
|
+
const before = delivered.length
|
|
168
|
+
canvas.dispatchEvent(
|
|
169
|
+
Object.assign(new Event('pointermove'), { clientX: 55, clientY: 50, button: 0 }),
|
|
170
|
+
)
|
|
171
|
+
expect(delivered).toHaveLength(before + 1)
|
|
172
|
+
return delivered.at(-1)!
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
nodeRegistry._reset()
|
|
177
|
+
registerNode({
|
|
178
|
+
kind,
|
|
179
|
+
schemaVersion: 1,
|
|
180
|
+
category: 'structure',
|
|
181
|
+
capabilities: {},
|
|
182
|
+
schema: kind === 'ceiling' ? CeilingNode : SlabNode,
|
|
183
|
+
defaults: () => ({}),
|
|
184
|
+
drafting: { surfaceQuery: true },
|
|
185
|
+
extensions: {
|
|
186
|
+
[DRAFTING_SURFACE_EXTENSION_KEY]: {
|
|
187
|
+
kind,
|
|
188
|
+
...(kind === 'ceiling' ? { raycast: 'underside' } : {}),
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
})
|
|
192
|
+
useViewer.setState({
|
|
193
|
+
cameraDragging: false,
|
|
194
|
+
selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [] },
|
|
195
|
+
})
|
|
196
|
+
await root.configure({
|
|
197
|
+
gl: {
|
|
198
|
+
domElement: canvas,
|
|
199
|
+
render() {},
|
|
200
|
+
setSize() {},
|
|
201
|
+
setPixelRatio() {},
|
|
202
|
+
} as unknown as WebGLRenderer,
|
|
203
|
+
camera,
|
|
204
|
+
frameloop: 'never',
|
|
205
|
+
dpr: 1,
|
|
206
|
+
size: { width: 100, height: 100, top: 0, left: 0 },
|
|
207
|
+
})
|
|
208
|
+
for (const scope of [
|
|
209
|
+
{ kind: 'moving', node: wall, nodeId: wall.id, nodeType: 'wall', view: '3d' },
|
|
210
|
+
{ kind: 'drafting', tool: kind },
|
|
211
|
+
] as const) {
|
|
212
|
+
await act(async () => {
|
|
213
|
+
useInteractionScope.setState({ scope })
|
|
214
|
+
root.render(createElement(Grid))
|
|
215
|
+
})
|
|
216
|
+
const before = send()
|
|
217
|
+
expect(before.surfaceHit?.hostId).toBe(surfaceId)
|
|
218
|
+
expect(before.position[1]).toBeCloseTo(5)
|
|
219
|
+
hideFromScene(surface, 'batched')
|
|
220
|
+
const batched = send()
|
|
221
|
+
expect(batched.surfaceHit).toEqual(before.surfaceHit)
|
|
222
|
+
expect(batched.position).toEqual(before.position)
|
|
223
|
+
expect(batched.localPosition).toEqual(before.localPosition)
|
|
224
|
+
showInScene(surface, 'batched')
|
|
225
|
+
expect(send().position).toEqual(before.position)
|
|
226
|
+
}
|
|
227
|
+
} finally {
|
|
228
|
+
await act(async () => {
|
|
229
|
+
root.render(null)
|
|
230
|
+
})
|
|
231
|
+
emitter.off('grid:move', onMove)
|
|
232
|
+
_roots.delete(canvas)
|
|
233
|
+
surface.geometry.dispose()
|
|
234
|
+
surface.material.dispose()
|
|
235
|
+
restoreRegistry()
|
|
236
|
+
useInteractionScope.setState({ scope: previousScope })
|
|
237
|
+
useViewer.setState(previousViewer)
|
|
238
|
+
actGlobal.IS_REACT_ACT_ENVIRONMENT = previousAct
|
|
239
|
+
}
|
|
240
|
+
})
|
|
@@ -3,14 +3,26 @@ import {
|
|
|
3
3
|
type EventSuffix,
|
|
4
4
|
emitter,
|
|
5
5
|
type GridEvent,
|
|
6
|
+
nodeRegistry,
|
|
6
7
|
sceneRegistry,
|
|
8
|
+
useScene,
|
|
7
9
|
} from '@pascal-app/core'
|
|
8
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
10
|
+
import { setSurfaceRaycastLayers, timeSpan, useViewer } from '@pascal-app/viewer'
|
|
9
11
|
import { useThree } from '@react-three/fiber'
|
|
10
12
|
import { useEffect, useRef } from 'react'
|
|
11
|
-
import { Plane, Raycaster, Vector2, Vector3 } from 'three'
|
|
13
|
+
import { Matrix3, type Object3D, Plane, Raycaster, Vector2, Vector3 } from 'three'
|
|
12
14
|
import { getPlacementSurface } from '../lib/active-placement-surface'
|
|
15
|
+
import { raycastCeilingUnderside } from '../lib/ceiling-surface-raycast'
|
|
13
16
|
import { resolveTerrainGroundHit } from '../lib/ground-surface'
|
|
17
|
+
import {
|
|
18
|
+
type DraftingSurfaceExtension,
|
|
19
|
+
registeredDraftingConfig,
|
|
20
|
+
registeredDraftingSurface,
|
|
21
|
+
} from '../lib/interaction/registered-drafting'
|
|
22
|
+
import useInteractionScope from '../store/use-interaction-scope'
|
|
23
|
+
|
|
24
|
+
// Keep tool previews tracking camera navigation at 10 Hz without querying every move.
|
|
25
|
+
const CAMERA_DRAG_MOVE_INTERVAL_MS = 100
|
|
14
26
|
|
|
15
27
|
/**
|
|
16
28
|
* Custom grid events hook that uses manual raycasting instead of mesh events.
|
|
@@ -18,11 +30,28 @@ import { resolveTerrainGroundHit } from '../lib/ground-surface'
|
|
|
18
30
|
*/
|
|
19
31
|
export function useGridEvents(gridY: number) {
|
|
20
32
|
const { camera, gl } = useThree()
|
|
33
|
+
const interactionScope = useInteractionScope((state) => state.scope)
|
|
34
|
+
const semanticSurfaceQueryRef = useRef(false)
|
|
35
|
+
semanticSurfaceQueryRef.current =
|
|
36
|
+
interactionScope.kind === 'placing' ||
|
|
37
|
+
interactionScope.kind === 'moving' ||
|
|
38
|
+
registeredDraftingConfig(interactionScope)?.surfaceQuery === true
|
|
21
39
|
const raycaster = useRef(new Raycaster())
|
|
22
40
|
const pointer = useRef(new Vector2())
|
|
23
41
|
const groundPlane = useRef(new Plane(new Vector3(0, 1, 0), 0))
|
|
24
42
|
const intersectionPoint = useRef(new Vector3())
|
|
25
43
|
|
|
44
|
+
type GridIntersection = {
|
|
45
|
+
point: Vector3
|
|
46
|
+
surface?: {
|
|
47
|
+
point: Vector3
|
|
48
|
+
object: Object3D
|
|
49
|
+
hostId: AnyNodeId
|
|
50
|
+
descriptor: DraftingSurfaceExtension
|
|
51
|
+
worldNormal?: Vector3
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
26
55
|
// Update ground plane when grid Y changes
|
|
27
56
|
useEffect(() => {
|
|
28
57
|
groundPlane.current.constant = -gridY
|
|
@@ -30,8 +59,64 @@ export function useGridEvents(gridY: number) {
|
|
|
30
59
|
|
|
31
60
|
useEffect(() => {
|
|
32
61
|
const canvas = gl.domElement
|
|
62
|
+
setSurfaceRaycastLayers(raycaster.current.layers)
|
|
63
|
+
|
|
64
|
+
const getSurfaceIntersection = (): GridIntersection | null => {
|
|
65
|
+
let closest: GridIntersection | null = null
|
|
66
|
+
let closestDistance = Number.POSITIVE_INFINITY
|
|
67
|
+
|
|
68
|
+
for (const [type, definition] of nodeRegistry.entries()) {
|
|
69
|
+
const descriptor = registeredDraftingSurface(definition)
|
|
70
|
+
if (!descriptor) continue
|
|
71
|
+
for (const id of sceneRegistry.byType[type] ?? []) {
|
|
72
|
+
const root = sceneRegistry.nodes.get(id)
|
|
73
|
+
if (!root) continue
|
|
74
|
+
const scope = useInteractionScope.getState().scope
|
|
75
|
+
const surfaceDraft = registeredDraftingConfig(scope)?.surfaceQuery === true
|
|
76
|
+
if (surfaceDraft && useScene.getState().nodes[id as AnyNodeId]?.visible === false)
|
|
77
|
+
continue
|
|
78
|
+
if (surfaceDraft && !root.visible) continue
|
|
79
|
+
const intersections =
|
|
80
|
+
surfaceDraft && descriptor.raycast === 'underside'
|
|
81
|
+
? raycastCeilingUnderside(raycaster.current, root)
|
|
82
|
+
: raycaster.current.intersectObject(root, true)
|
|
83
|
+
const hit = intersections.find((candidate) => {
|
|
84
|
+
if (!surfaceDraft) return true
|
|
85
|
+
if (useScene.getState().nodes[id as AnyNodeId]?.visible === false) return false
|
|
86
|
+
let object: Object3D | null = candidate.object
|
|
87
|
+
while (object) {
|
|
88
|
+
if (!object.visible || object.userData.wallHidden === true) return false
|
|
89
|
+
object = object.parent
|
|
90
|
+
}
|
|
91
|
+
return true
|
|
92
|
+
})
|
|
93
|
+
if (!hit || hit.distance >= closestDistance) continue
|
|
94
|
+
closestDistance = hit.distance
|
|
95
|
+
const worldNormal = hit.face
|
|
96
|
+
? hit.face.normal
|
|
97
|
+
.clone()
|
|
98
|
+
.applyNormalMatrix(new Matrix3().getNormalMatrix(hit.object.matrixWorld))
|
|
99
|
+
.normalize()
|
|
100
|
+
: undefined
|
|
101
|
+
if (surfaceDraft && worldNormal && worldNormal.dot(raycaster.current.ray.direction) > 0)
|
|
102
|
+
worldNormal.negate()
|
|
103
|
+
closest = {
|
|
104
|
+
point: hit.point.clone(),
|
|
105
|
+
surface: {
|
|
106
|
+
point: hit.point.clone(),
|
|
107
|
+
object: hit.object,
|
|
108
|
+
hostId: id as AnyNodeId,
|
|
109
|
+
descriptor,
|
|
110
|
+
worldNormal,
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return closest
|
|
117
|
+
}
|
|
33
118
|
|
|
34
|
-
const getIntersection = (nativeEvent: MouseEvent | PointerEvent):
|
|
119
|
+
const getIntersection = (nativeEvent: MouseEvent | PointerEvent): GridIntersection | null => {
|
|
35
120
|
// Convert mouse position to normalized device coordinates (-1 to +1)
|
|
36
121
|
const rect = canvas.getBoundingClientRect()
|
|
37
122
|
pointer.current.x = ((nativeEvent.clientX - rect.left) / rect.width) * 2 - 1
|
|
@@ -40,6 +125,33 @@ export function useGridEvents(gridY: number) {
|
|
|
40
125
|
// Update raycaster
|
|
41
126
|
raycaster.current.setFromCamera(pointer.current, camera)
|
|
42
127
|
|
|
128
|
+
// R3F node events can be stopped by another mesh, so this canvas-level
|
|
129
|
+
// raycast is the reliable architectural-surface source for placement and
|
|
130
|
+
// drawing tools. Keep it separate from the ordinary grid point so tools
|
|
131
|
+
// that intentionally place on the floor retain their existing behavior.
|
|
132
|
+
// Architectural meshes are expensive to raycast and are meaningful only
|
|
133
|
+
// to an active placement/drafting interaction. Floor tools retain the
|
|
134
|
+
// ordinary terrain/grid intersection without scanning every wall.
|
|
135
|
+
const surfaceHit = semanticSurfaceQueryRef.current ? getSurfaceIntersection() : null
|
|
136
|
+
|
|
137
|
+
// A semantic architectural hit is the authoritative cursor position.
|
|
138
|
+
// Do not replace it with the terrain/grid intersection below: that would
|
|
139
|
+
// make a wall hit carry wall metadata while still placing at the ground
|
|
140
|
+
// floor, especially in perspective views.
|
|
141
|
+
if (surfaceHit) return surfaceHit
|
|
142
|
+
|
|
143
|
+
const scope = useInteractionScope.getState().scope
|
|
144
|
+
const surfaceDraft = registeredDraftingConfig(scope)?.surfaceQuery === true
|
|
145
|
+
const workingSurface = getPlacementSurface()
|
|
146
|
+
if (surfaceDraft && workingSurface) {
|
|
147
|
+
const plane = new Plane().setFromNormalAndCoplanarPoint(
|
|
148
|
+
workingSurface.normal,
|
|
149
|
+
workingSurface.point,
|
|
150
|
+
)
|
|
151
|
+
const projected = raycaster.current.ray.intersectPlane(plane, intersectionPoint.current)
|
|
152
|
+
return { point: (projected ?? workingSurface.point).clone() }
|
|
153
|
+
}
|
|
154
|
+
|
|
43
155
|
// Sculpted ground wins over the plane, but only while the plane IS the
|
|
44
156
|
// ground (see `isSiteGroundPlane`): a plane riding a storey base or a slab
|
|
45
157
|
// top is a real flat surface and must stay planar. The march is what removes
|
|
@@ -60,14 +172,18 @@ export function useGridEvents(gridY: number) {
|
|
|
60
172
|
[direction.x, direction.y, direction.z],
|
|
61
173
|
-groundPlane.current.constant,
|
|
62
174
|
)
|
|
63
|
-
if (hit)
|
|
175
|
+
if (hit) {
|
|
176
|
+
return {
|
|
177
|
+
point: intersectionPoint.current.set(hit.x, hit.y, hit.z).clone(),
|
|
178
|
+
}
|
|
179
|
+
}
|
|
64
180
|
|
|
65
181
|
// Intersect with ground plane
|
|
66
182
|
if (raycaster.current.ray.intersectPlane(groundPlane.current, intersectionPoint.current)) {
|
|
67
|
-
return intersectionPoint.current.clone()
|
|
183
|
+
return { point: intersectionPoint.current.clone() }
|
|
68
184
|
}
|
|
69
185
|
|
|
70
|
-
return
|
|
186
|
+
return surfaceHit
|
|
71
187
|
}
|
|
72
188
|
|
|
73
189
|
const emit = (suffix: EventSuffix, nativeEvent: MouseEvent | PointerEvent) => {
|
|
@@ -75,14 +191,69 @@ export function useGridEvents(gridY: number) {
|
|
|
75
191
|
if (!point) return
|
|
76
192
|
|
|
77
193
|
// Convert world-space point to building-local for tools that live inside a building.
|
|
78
|
-
const
|
|
194
|
+
const scope = useInteractionScope.getState().scope
|
|
195
|
+
const surfaceDraft = registeredDraftingConfig(scope)?.surfaceQuery === true
|
|
196
|
+
const buildingId = surfaceDraft
|
|
197
|
+
? useViewer.getState().selection.levelId
|
|
198
|
+
: useViewer.getState().selection.buildingId
|
|
79
199
|
const buildingMesh = buildingId ? sceneRegistry.nodes.get(buildingId as AnyNodeId) : null
|
|
80
|
-
const localPoint = buildingMesh ? buildingMesh.worldToLocal(point.clone()) : point
|
|
200
|
+
const localPoint = buildingMesh ? buildingMesh.worldToLocal(point.point.clone()) : point.point
|
|
201
|
+
const surfaceLocalPoint = point.surface
|
|
202
|
+
? buildingMesh
|
|
203
|
+
? buildingMesh.worldToLocal(point.surface.point.clone())
|
|
204
|
+
: point.surface.point.clone()
|
|
205
|
+
: undefined
|
|
206
|
+
const localNormal = point.surface?.worldNormal
|
|
207
|
+
? buildingMesh
|
|
208
|
+
? buildingMesh
|
|
209
|
+
.worldToLocal(point.surface.point.clone().add(point.surface.worldNormal))
|
|
210
|
+
.sub(surfaceLocalPoint ?? localPoint)
|
|
211
|
+
.normalize()
|
|
212
|
+
: point.surface.worldNormal
|
|
213
|
+
: undefined
|
|
214
|
+
const surfaceNode = point.surface
|
|
215
|
+
? useScene.getState().nodes[point.surface.hostId]
|
|
216
|
+
: undefined
|
|
217
|
+
const classifiedFace =
|
|
218
|
+
point.surface && localNormal
|
|
219
|
+
? point.surface.descriptor.classifyFace?.(surfaceNode, [
|
|
220
|
+
localNormal.x,
|
|
221
|
+
localNormal.y,
|
|
222
|
+
localNormal.z,
|
|
223
|
+
])
|
|
224
|
+
: null
|
|
225
|
+
const { origin, direction } = raycaster.current.ray
|
|
226
|
+
const localRayOrigin = buildingMesh
|
|
227
|
+
? buildingMesh.worldToLocal(origin.clone())
|
|
228
|
+
: origin.clone()
|
|
229
|
+
const localRayDirection = buildingMesh
|
|
230
|
+
? buildingMesh.worldToLocal(origin.clone().add(direction)).sub(localRayOrigin).normalize()
|
|
231
|
+
: direction.clone()
|
|
81
232
|
|
|
82
233
|
const eventKey = `grid:${suffix}` as `grid:${EventSuffix}`
|
|
83
234
|
const payload: GridEvent = {
|
|
84
|
-
|
|
235
|
+
localFrameId: buildingId ?? undefined,
|
|
236
|
+
position: [point.point.x, point.point.y, point.point.z],
|
|
85
237
|
localPosition: [localPoint.x, localPoint.y, localPoint.z],
|
|
238
|
+
localRay: {
|
|
239
|
+
origin: [localRayOrigin.x, localRayOrigin.y, localRayOrigin.z],
|
|
240
|
+
direction: [localRayDirection.x, localRayDirection.y, localRayDirection.z],
|
|
241
|
+
},
|
|
242
|
+
surfaceLocalPosition: surfaceLocalPoint
|
|
243
|
+
? [surfaceLocalPoint.x, surfaceLocalPoint.y, surfaceLocalPoint.z]
|
|
244
|
+
: undefined,
|
|
245
|
+
surfaceNormal: localNormal ? [localNormal.x, localNormal.y, localNormal.z] : undefined,
|
|
246
|
+
surfaceObject: point.surface?.object,
|
|
247
|
+
surfaceHit:
|
|
248
|
+
semanticSurfaceQueryRef.current && point.surface
|
|
249
|
+
? {
|
|
250
|
+
kind: point.surface.descriptor.kind,
|
|
251
|
+
hostId: point.surface.hostId,
|
|
252
|
+
face: classifiedFace?.face ?? 'unknown',
|
|
253
|
+
levelId: useViewer.getState().selection.levelId ?? undefined,
|
|
254
|
+
side: classifiedFace?.side,
|
|
255
|
+
}
|
|
256
|
+
: undefined,
|
|
86
257
|
nativeEvent: nativeEvent as any, // Type compatibility with ThreeEvent
|
|
87
258
|
}
|
|
88
259
|
|
|
@@ -107,9 +278,17 @@ export function useGridEvents(gridY: number) {
|
|
|
107
278
|
emit('click', e)
|
|
108
279
|
}
|
|
109
280
|
|
|
281
|
+
let lastCameraDragMove = Number.NEGATIVE_INFINITY
|
|
110
282
|
const handlePointerMove = (e: PointerEvent) => {
|
|
111
|
-
//
|
|
112
|
-
|
|
283
|
+
// Moves keep tool cursor snapshots current, including wheel zoom during a tool gesture.
|
|
284
|
+
if (useViewer.getState().cameraDragging) {
|
|
285
|
+
const now = performance.now()
|
|
286
|
+
if (now - lastCameraDragMove < CAMERA_DRAG_MOVE_INTERVAL_MS) return
|
|
287
|
+
lastCameraDragMove = now
|
|
288
|
+
} else {
|
|
289
|
+
lastCameraDragMove = Number.NEGATIVE_INFINITY
|
|
290
|
+
}
|
|
291
|
+
timeSpan('pointer', () => emit('move', e))
|
|
113
292
|
}
|
|
114
293
|
|
|
115
294
|
const handleDoubleClick = (e: MouseEvent) => {
|