@pascal-app/editor 1.0.0-beta.4 → 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 +15 -8
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/alignment-3d-guide-layer.tsx +12 -3
- 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 +17 -5
- package/src/components/editor/elevation-3d-guide-layer.tsx +3 -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 +182 -57
- package/src/components/editor/floating-action-menu.tsx +55 -55
- package/src/components/editor/floorplan-background-selection.test.ts +31 -4
- package/src/components/editor/floorplan-background-selection.ts +27 -7
- package/src/components/editor/floorplan-panel.tsx +81 -57
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- package/src/components/editor/group-floating-action-menu.tsx +30 -15
- 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 +252 -46
- package/src/components/editor/measurement-pill.tsx +12 -12
- package/src/components/editor/node-action-menu.tsx +42 -1
- package/src/components/editor/node-arrow-handles.tsx +47 -58
- package/src/components/editor/opening-guides-3d-layer.tsx +13 -4
- package/src/components/editor/selection-manager.tsx +132 -47
- package/src/components/editor/site-edge-labels.tsx +2 -1
- package/src/components/editor/snapshot-capture-overlay.tsx +309 -41
- 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 +295 -232
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-measurement-label.tsx +3 -2
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +23 -16
- 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 +204 -114
- 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 +494 -0
- package/src/components/tools/item/placement-strategies.ts +192 -24
- 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 +447 -180
- 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 +63 -8
- 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-brush-cursor.tsx +2 -1
- 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 +527 -4
- package/src/components/tools/wall/wall-drafting.ts +55 -462
- 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 +12 -13
- 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 +23 -6
- 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 +78 -26
- 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/panel-wrapper.tsx +228 -12
- 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/items-panel/function-tree-panel.tsx +7 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +31 -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 +128 -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 +22 -7
- package/src/components/ui/sidebar/tab-bar.tsx +9 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/viewer/floorplan-compass-button.tsx +50 -0
- package/src/components/viewer/floorplan-preview-geometry.test.ts +107 -0
- package/src/components/viewer/floorplan-preview-geometry.ts +281 -0
- package/src/components/viewer/floorplan-preview-navigation.test.ts +43 -0
- package/src/components/viewer/floorplan-preview-navigation.ts +62 -0
- package/src/components/viewer/floorplan-preview.tsx +1069 -0
- package/src/components/viewer/use-viewer-camera-navigation-sync.ts +140 -0
- package/src/components/viewer/viewer-scene-header.tsx +2 -1
- package/src/components/viewer/viewer-stage-modes.test.ts +38 -0
- package/src/components/viewer/viewer-stage-modes.ts +49 -0
- package/src/components/viewer/viewer-stage-switcher.tsx +95 -0
- package/src/components/viewer/viewer-stage.test.tsx +47 -0
- package/src/components/viewer/viewer-stage.tsx +221 -0
- package/src/components/viewer-overlay.tsx +13 -30
- 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 +122 -1
- package/src/hooks/use-auto-save.ts +119 -13
- 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 +164 -63
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +109 -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/contextual-help.test.ts +8 -0
- package/src/lib/contextual-help.ts +8 -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.test.ts +47 -0
- package/src/lib/floorplan/floorplan-readonly.ts +85 -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 +523 -77
- package/src/lib/history.ts +133 -14
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -0
- package/src/lib/inspector-card-mode.test.ts +92 -0
- package/src/lib/inspector-card-mode.ts +60 -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/measurement-parser.ts +3 -0
- package/src/lib/measurements.test.ts +4 -0
- package/src/lib/model-export.ts +29 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- package/src/lib/paint-scope.test.ts +58 -2
- package/src/lib/paint-scope.ts +8 -2
- 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 +125 -6
- package/src/lib/selection-routing.ts +34 -1
- package/src/lib/session-groups.test.ts +88 -0
- package/src/lib/session-groups.ts +201 -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/structured-clone-fallback.test.ts +45 -0
- package/src/lib/structured-clone-fallback.ts +24 -0
- 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/store/use-session-groups.ts +103 -0
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -9,9 +9,12 @@ import {
|
|
|
9
9
|
import { z } from 'zod'
|
|
10
10
|
import {
|
|
11
11
|
canDirectMoveNode,
|
|
12
|
+
EDITOR_HANDLE_HIT_AREA_USER_DATA_KEY,
|
|
13
|
+
pointerEventHitsEditorHandle,
|
|
12
14
|
resolveDirectManipulationNode,
|
|
13
15
|
resolveDirectRotationDragDelta,
|
|
14
16
|
resolveMoveActionNode,
|
|
17
|
+
shouldStartDirectMoveDrag,
|
|
15
18
|
snapDirectRotationDelta,
|
|
16
19
|
} from './direct-manipulation'
|
|
17
20
|
|
|
@@ -116,6 +119,91 @@ describe('canDirectMoveNode', () => {
|
|
|
116
119
|
})
|
|
117
120
|
})
|
|
118
121
|
|
|
122
|
+
describe('shouldStartDirectMoveDrag', () => {
|
|
123
|
+
test('arms a plain drag for a kind that opts into direct dragging', () => {
|
|
124
|
+
expect(
|
|
125
|
+
shouldStartDirectMoveDrag({
|
|
126
|
+
allowPlainDrag: true,
|
|
127
|
+
commandModifier: false,
|
|
128
|
+
handleOwnsPointer: false,
|
|
129
|
+
nodeId: 'cabinet_existing',
|
|
130
|
+
selectedIds: [],
|
|
131
|
+
}),
|
|
132
|
+
).toBe(true)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('keeps modifier dragging limited to the sole selected node', () => {
|
|
136
|
+
expect(
|
|
137
|
+
shouldStartDirectMoveDrag({
|
|
138
|
+
allowPlainDrag: false,
|
|
139
|
+
commandModifier: true,
|
|
140
|
+
handleOwnsPointer: false,
|
|
141
|
+
nodeId: 'item_selected',
|
|
142
|
+
selectedIds: ['item_selected'],
|
|
143
|
+
}),
|
|
144
|
+
).toBe(true)
|
|
145
|
+
expect(
|
|
146
|
+
shouldStartDirectMoveDrag({
|
|
147
|
+
allowPlainDrag: false,
|
|
148
|
+
commandModifier: true,
|
|
149
|
+
handleOwnsPointer: false,
|
|
150
|
+
nodeId: 'item_other',
|
|
151
|
+
selectedIds: ['item_selected'],
|
|
152
|
+
}),
|
|
153
|
+
).toBe(false)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('does not arm body dragging when a resize handle owns the pointer', () => {
|
|
157
|
+
expect(
|
|
158
|
+
shouldStartDirectMoveDrag({
|
|
159
|
+
allowPlainDrag: true,
|
|
160
|
+
commandModifier: false,
|
|
161
|
+
handleOwnsPointer: true,
|
|
162
|
+
nodeId: 'cabinet_selected',
|
|
163
|
+
selectedIds: ['cabinet_selected'],
|
|
164
|
+
}),
|
|
165
|
+
).toBe(false)
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
describe('pointerEventHitsEditorHandle', () => {
|
|
170
|
+
test('keeps a visible resize handle from falling through to a nearer cabinet body', () => {
|
|
171
|
+
expect(
|
|
172
|
+
pointerEventHitsEditorHandle({
|
|
173
|
+
intersections: [
|
|
174
|
+
{ object: { userData: {} } },
|
|
175
|
+
{
|
|
176
|
+
object: {
|
|
177
|
+
userData: { [EDITOR_HANDLE_HIT_AREA_USER_DATA_KEY]: true },
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
}),
|
|
182
|
+
).toBe(true)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('recognises a handle when it is the nearest R3F intersection', () => {
|
|
186
|
+
expect(
|
|
187
|
+
pointerEventHitsEditorHandle({
|
|
188
|
+
intersections: [
|
|
189
|
+
{
|
|
190
|
+
object: {
|
|
191
|
+
userData: { [EDITOR_HANDLE_HIT_AREA_USER_DATA_KEY]: true },
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
{ object: { userData: {} } },
|
|
195
|
+
],
|
|
196
|
+
}),
|
|
197
|
+
).toBe(true)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
test('does not claim ordinary scene intersections', () => {
|
|
201
|
+
expect(pointerEventHitsEditorHandle({ intersections: [{ object: { userData: {} } }] })).toBe(
|
|
202
|
+
false,
|
|
203
|
+
)
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
|
|
119
207
|
describe('resolveDirectManipulationNode', () => {
|
|
120
208
|
test('routes proxied members to their assembly for direct transforms', () => {
|
|
121
209
|
const group = {
|
|
@@ -272,4 +360,48 @@ describe('resolveMoveActionNode', () => {
|
|
|
272
360
|
}),
|
|
273
361
|
).toBe(child)
|
|
274
362
|
})
|
|
363
|
+
|
|
364
|
+
test('routes a parent-frame child move to a rotatable assembly parent', () => {
|
|
365
|
+
const parentKind = 'move-action-rotatable-parent-kind-test'
|
|
366
|
+
const childKind = 'move-action-rotatable-child-kind-test'
|
|
367
|
+
registerTestDefinition(parentKind, {
|
|
368
|
+
capabilities: { rotatable: { axes: ['y'], snapAngles: [Math.PI / 4] } },
|
|
369
|
+
})
|
|
370
|
+
registerTestDefinition(childKind, {
|
|
371
|
+
capabilities: {
|
|
372
|
+
movable: {
|
|
373
|
+
axes: ['x', 'z'],
|
|
374
|
+
gridSnap: true,
|
|
375
|
+
parentFrame: {
|
|
376
|
+
resolveParent: (node: AnyNode, nodes: Readonly<Record<string, AnyNode>>) =>
|
|
377
|
+
(node.parentId ? nodes[node.parentId] : null) ?? null,
|
|
378
|
+
parentRotationY: () => 0,
|
|
379
|
+
localToPlan: (_parent: AnyNode, local: readonly [number, number, number]) => [
|
|
380
|
+
local[0],
|
|
381
|
+
local[1],
|
|
382
|
+
local[2],
|
|
383
|
+
],
|
|
384
|
+
planToLocal: (_parent: AnyNode, planX: number, localY: number, planZ: number) => [
|
|
385
|
+
planX,
|
|
386
|
+
localY,
|
|
387
|
+
planZ,
|
|
388
|
+
],
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
})
|
|
393
|
+
const parent = { id: 'move_action_rotatable_run', type: parentKind } as unknown as AnyNode
|
|
394
|
+
const child = {
|
|
395
|
+
id: 'move_action_rotatable_module',
|
|
396
|
+
type: childKind,
|
|
397
|
+
parentId: parent.id,
|
|
398
|
+
} as unknown as AnyNode
|
|
399
|
+
|
|
400
|
+
expect(
|
|
401
|
+
resolveMoveActionNode(child, {
|
|
402
|
+
[parent.id]: parent,
|
|
403
|
+
[child.id]: child,
|
|
404
|
+
}),
|
|
405
|
+
).toBe(parent)
|
|
406
|
+
})
|
|
275
407
|
})
|
|
@@ -45,6 +45,25 @@ const BESPOKE_SELECTION_MOVE_KINDS = new Set([
|
|
|
45
45
|
'liquid-line',
|
|
46
46
|
])
|
|
47
47
|
|
|
48
|
+
export const EDITOR_HANDLE_HIT_AREA_USER_DATA_KEY = 'editorHandleHitArea'
|
|
49
|
+
|
|
50
|
+
export function pointerEventHitsEditorHandle(event: unknown): boolean {
|
|
51
|
+
if (!event || typeof event !== 'object') return false
|
|
52
|
+
const intersections = (
|
|
53
|
+
event as {
|
|
54
|
+
intersections?: readonly {
|
|
55
|
+
object?: { userData?: Record<string, unknown> }
|
|
56
|
+
}[]
|
|
57
|
+
}
|
|
58
|
+
).intersections
|
|
59
|
+
return (
|
|
60
|
+
intersections?.some(
|
|
61
|
+
(intersection) =>
|
|
62
|
+
intersection.object?.userData?.[EDITOR_HANDLE_HIT_AREA_USER_DATA_KEY] === true,
|
|
63
|
+
) ?? false
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
48
67
|
export function canDirectMoveNode(node: AnyNode): boolean {
|
|
49
68
|
// These MEP kinds own move through bespoke selection rigs (latch cubes,
|
|
50
69
|
// directional arrows, grid-driven previews). Sending body drags/clicks
|
|
@@ -64,6 +83,24 @@ export function canDirectMoveNode(node: AnyNode): boolean {
|
|
|
64
83
|
return isMovable(node)
|
|
65
84
|
}
|
|
66
85
|
|
|
86
|
+
export function shouldStartDirectMoveDrag({
|
|
87
|
+
allowPlainDrag,
|
|
88
|
+
commandModifier,
|
|
89
|
+
handleOwnsPointer,
|
|
90
|
+
nodeId,
|
|
91
|
+
selectedIds,
|
|
92
|
+
}: {
|
|
93
|
+
allowPlainDrag: boolean
|
|
94
|
+
commandModifier: boolean
|
|
95
|
+
handleOwnsPointer: boolean
|
|
96
|
+
nodeId: string
|
|
97
|
+
selectedIds: readonly string[]
|
|
98
|
+
}): boolean {
|
|
99
|
+
if (handleOwnsPointer) return false
|
|
100
|
+
if (commandModifier) return selectedIds.length === 1 && selectedIds[0] === nodeId
|
|
101
|
+
return allowPlainDrag && selectedIds.length < 2
|
|
102
|
+
}
|
|
103
|
+
|
|
67
104
|
export function resolveDirectManipulationNode(
|
|
68
105
|
node: AnyNode,
|
|
69
106
|
nodes: Readonly<Record<string, AnyNode | undefined>>,
|
|
@@ -80,7 +117,7 @@ export function resolveMoveActionNode(
|
|
|
80
117
|
): AnyNode {
|
|
81
118
|
const parentFrame = nodeRegistry.get(node.type)?.capabilities?.movable?.parentFrame
|
|
82
119
|
const parent = parentFrame?.resolveParent(node, nodes as Readonly<Record<string, AnyNode>>)
|
|
83
|
-
return parent
|
|
120
|
+
return parent && (parent.type === node.type || canDirectRotateNode(parent)) ? parent : node
|
|
84
121
|
}
|
|
85
122
|
|
|
86
123
|
export function snapDirectRotationDelta(delta: number, free: boolean): number {
|
package/src/lib/editor-api.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AnyNode, type EditorApi, useScene } from '@pascal-app/core'
|
|
2
2
|
import useEditor from '../store/use-editor'
|
|
3
3
|
import useInteractionScope from '../store/use-interaction-scope'
|
|
4
|
+
import { resolveDirectManipulationNode, resolveMoveActionNode } from './direct-manipulation'
|
|
4
5
|
import {
|
|
5
6
|
controlPointReshapeScope,
|
|
6
7
|
endpointReshapeScope,
|
|
@@ -25,14 +26,16 @@ export function createEditorApi(): EditorApi {
|
|
|
25
26
|
// (every concrete kind enumerated). Descriptors pass any node; the
|
|
26
27
|
// cast lets registry-driven move kinds through without forcing a
|
|
27
28
|
// schema-level type widening.
|
|
28
|
-
|
|
29
|
+
const target = resolveMoveActionNode(node, useScene.getState().nodes)
|
|
30
|
+
editor.setMovingNode(target as Parameters<typeof editor.setMovingNode>[0])
|
|
29
31
|
},
|
|
30
32
|
engageMoveDrag(node: AnyNode) {
|
|
31
33
|
const editor = useEditor.getState()
|
|
32
34
|
// Flag drag mode BEFORE mounting the move tool so the coordinator reads
|
|
33
35
|
// it at setup and wires its commit-on-release listener.
|
|
34
36
|
editor.setPlacementDragMode(true)
|
|
35
|
-
|
|
37
|
+
const target = resolveDirectManipulationNode(node, useScene.getState().nodes)
|
|
38
|
+
editor.setMovingNode(target as Parameters<typeof editor.setMovingNode>[0])
|
|
36
39
|
},
|
|
37
40
|
engageEndpointMove(node: AnyNode, endpoint: 'start' | 'end') {
|
|
38
41
|
// Endpoint reshape is kind-agnostic: the scope carries the node id + which
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test'
|
|
2
2
|
import { type AnyNode, CeilingNode, LevelNode, SlabNode } from '@pascal-app/core'
|
|
3
3
|
import useElevationGuides from '../store/use-elevation-guides'
|
|
4
4
|
import {
|
|
5
5
|
clearStructuralElevationGuide,
|
|
6
6
|
collectElevationSnapTargets,
|
|
7
|
+
publishResolvedElevationGuide,
|
|
7
8
|
publishStructuralElevationGuide,
|
|
8
9
|
resolveElevationSnapMatch,
|
|
9
10
|
resolveStructuralElevationSnap,
|
|
@@ -42,6 +43,10 @@ function structuralScene() {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
describe('elevation guides', () => {
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
useElevationGuides.setState({ guide: null })
|
|
48
|
+
})
|
|
49
|
+
|
|
45
50
|
test('collects the level, slab faces, and ceiling plane on the source level', () => {
|
|
46
51
|
const { level, nodes, slab } = structuralScene()
|
|
47
52
|
const targets = collectElevationSnapTargets(
|
|
@@ -104,4 +109,26 @@ describe('elevation guides', () => {
|
|
|
104
109
|
publishStructuralElevationGuide(source, 0.7, nodes)
|
|
105
110
|
expect(useElevationGuides.getState().guide).toBeNull()
|
|
106
111
|
})
|
|
112
|
+
|
|
113
|
+
test('publishes an explicitly resolved neighboring datum', () => {
|
|
114
|
+
const { level } = structuralScene()
|
|
115
|
+
useElevationGuides.setState({ guide: null })
|
|
116
|
+
|
|
117
|
+
publishResolvedElevationGuide(
|
|
118
|
+
{ nodeId: 'leanto_moving', levelId: level.id, anchor: [2, 1] },
|
|
119
|
+
{
|
|
120
|
+
id: 'leanto_neighbor:high-edge',
|
|
121
|
+
elevation: 3.4,
|
|
122
|
+
anchor: [5, 1],
|
|
123
|
+
label: 'Neighbor shed edge',
|
|
124
|
+
},
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
expect(useElevationGuides.getState().guide).toMatchObject({
|
|
128
|
+
ownerId: 'leanto_moving',
|
|
129
|
+
elevation: 3.4,
|
|
130
|
+
direction: [1, 0],
|
|
131
|
+
label: 'Neighbor shed edge',
|
|
132
|
+
})
|
|
133
|
+
})
|
|
107
134
|
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type AnyNode,
|
|
3
3
|
type AnyNodeId,
|
|
4
|
+
type FenceNode,
|
|
4
5
|
findLevelAncestorId,
|
|
5
6
|
getWallBaseElevationForNodes,
|
|
6
7
|
getWallEffectiveHeightForNodes,
|
|
@@ -53,8 +54,10 @@ function segmentCenter(
|
|
|
53
54
|
// A stale host resolves to the level base — the sculpted ground under the
|
|
54
55
|
// fence's start point, sampled where the builder samples it, so the guide line
|
|
55
56
|
// lands on the rail it claims to describe.
|
|
56
|
-
function
|
|
57
|
-
|
|
57
|
+
export function getFenceBaseElevationForNodes(
|
|
58
|
+
node: FenceNode,
|
|
59
|
+
nodes: Record<string, AnyNode>,
|
|
60
|
+
): number {
|
|
58
61
|
const host = node.supportSlabId ? nodes[node.supportSlabId as AnyNodeId] : undefined
|
|
59
62
|
const hosted = host?.type === 'slab' && (host.parentId ?? null) === (node.parentId ?? null)
|
|
60
63
|
const levelId = findLevelAncestorId(node.id as AnyNodeId, nodes)
|
|
@@ -161,7 +164,7 @@ export function collectElevationSnapTargets(
|
|
|
161
164
|
}
|
|
162
165
|
|
|
163
166
|
if (node.type === 'fence') {
|
|
164
|
-
const base =
|
|
167
|
+
const base = getFenceBaseElevationForNodes(node, nodes)
|
|
165
168
|
const center = segmentCenter(node.start, node.end)
|
|
166
169
|
targets.push({
|
|
167
170
|
id: `${node.id}:base`,
|
|
@@ -245,8 +248,20 @@ export function publishStructuralElevationGuide(
|
|
|
245
248
|
return
|
|
246
249
|
}
|
|
247
250
|
|
|
248
|
-
|
|
249
|
-
|
|
251
|
+
publishResolvedElevationGuide(source, match.target)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function publishResolvedElevationGuide(
|
|
255
|
+
source: ElevationGuideSource,
|
|
256
|
+
target: ElevationSnapTarget,
|
|
257
|
+
): void {
|
|
258
|
+
if (!source.levelId) {
|
|
259
|
+
clearStructuralElevationGuide(source.nodeId)
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const dx = target.anchor[0] - source.anchor[0]
|
|
264
|
+
const dz = target.anchor[1] - source.anchor[1]
|
|
250
265
|
const length = Math.hypot(dx, dz)
|
|
251
266
|
const direction: [number, number] = length > 1e-6 ? [dx / length, dz / length] : [1, 0]
|
|
252
267
|
|
|
@@ -255,8 +270,8 @@ export function publishStructuralElevationGuide(
|
|
|
255
270
|
levelId: source.levelId,
|
|
256
271
|
center: source.anchor,
|
|
257
272
|
direction,
|
|
258
|
-
elevation:
|
|
259
|
-
label:
|
|
273
|
+
elevation: target.elevation,
|
|
274
|
+
label: target.label,
|
|
260
275
|
})
|
|
261
276
|
}
|
|
262
277
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { OrthographicCamera, PerspectiveCamera, Vector3 } from 'three'
|
|
3
|
+
import { getFloatingMenuScale } from './floating-menu-scale'
|
|
4
|
+
|
|
5
|
+
describe('getFloatingMenuScale', () => {
|
|
6
|
+
test('uses the standard orthographic zoom scale and clamps its range', () => {
|
|
7
|
+
const camera = new OrthographicCamera()
|
|
8
|
+
const anchor = new Vector3()
|
|
9
|
+
|
|
10
|
+
camera.zoom = 15
|
|
11
|
+
expect(getFloatingMenuScale(camera, anchor)).toBe(0.75)
|
|
12
|
+
camera.zoom = 2
|
|
13
|
+
expect(getFloatingMenuScale(camera, anchor)).toBe(0.5)
|
|
14
|
+
camera.zoom = 40
|
|
15
|
+
expect(getFloatingMenuScale(camera, anchor)).toBe(1)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('uses inverse perspective-camera distance and clamps its range', () => {
|
|
19
|
+
const camera = new PerspectiveCamera()
|
|
20
|
+
const anchor = new Vector3()
|
|
21
|
+
|
|
22
|
+
camera.position.set(0, 0, 16)
|
|
23
|
+
expect(getFloatingMenuScale(camera, anchor)).toBe(0.75)
|
|
24
|
+
camera.position.set(0, 0, 48)
|
|
25
|
+
expect(getFloatingMenuScale(camera, anchor)).toBe(0.5)
|
|
26
|
+
camera.position.set(0, 0, 6)
|
|
27
|
+
expect(getFloatingMenuScale(camera, anchor)).toBe(1)
|
|
28
|
+
})
|
|
29
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Camera, OrthographicCamera, type Vector3 } from 'three'
|
|
2
|
+
|
|
3
|
+
const MIN_MENU_SCALE = 0.5
|
|
4
|
+
const MAX_MENU_SCALE = 1
|
|
5
|
+
const REF_ORTHO_ZOOM = 20
|
|
6
|
+
const REF_CAMERA_DISTANCE = 12
|
|
7
|
+
|
|
8
|
+
export function getFloatingMenuScale(camera: Camera, anchor: Vector3): number {
|
|
9
|
+
const raw =
|
|
10
|
+
camera instanceof OrthographicCamera
|
|
11
|
+
? camera.zoom / REF_ORTHO_ZOOM
|
|
12
|
+
: REF_CAMERA_DISTANCE / Math.max(camera.position.distanceTo(anchor), 0.001)
|
|
13
|
+
return Math.min(MAX_MENU_SCALE, Math.max(MIN_MENU_SCALE, raw))
|
|
14
|
+
}
|
|
@@ -1,8 +1,23 @@
|
|
|
1
|
-
import { afterEach, describe, expect, test } from 'bun:test'
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { LevelNode, useScene, WallNode } from '@pascal-app/core'
|
|
3
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
2
4
|
import useAlignmentGuides from '../../store/use-alignment-guides'
|
|
3
|
-
import { applyFloorplanAlignment } from './apply-alignment'
|
|
5
|
+
import { alignFloorplanDraftPoint, applyFloorplanAlignment } from './apply-alignment'
|
|
4
6
|
|
|
5
7
|
describe('applyFloorplanAlignment', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
// These tests assume no active building (alignment runs on world axes).
|
|
10
|
+
// The scene/viewer stores are process-wide singletons, so an earlier test
|
|
11
|
+
// FILE can leak a selected building fixture into them — under bun's
|
|
12
|
+
// platform-dependent file order that turned into an order-dependent
|
|
13
|
+
// failure (getActiveBuildingPose reading a fixture building without a
|
|
14
|
+
// rotation array). Pin the empty-scene context explicitly.
|
|
15
|
+
useScene.setState({ nodes: {} } as never)
|
|
16
|
+
useViewer.setState({
|
|
17
|
+
selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [] },
|
|
18
|
+
} as never)
|
|
19
|
+
})
|
|
20
|
+
|
|
6
21
|
afterEach(() => {
|
|
7
22
|
useAlignmentGuides.getState().clear()
|
|
8
23
|
})
|
|
@@ -22,4 +37,34 @@ describe('applyFloorplanAlignment', () => {
|
|
|
22
37
|
expect(result.guides).toHaveLength(1)
|
|
23
38
|
expect(useAlignmentGuides.getState().guides).toHaveLength(1)
|
|
24
39
|
})
|
|
40
|
+
|
|
41
|
+
test('can restrict candidates to the active level', () => {
|
|
42
|
+
const lowerLevel = LevelNode.parse({ id: 'level_lower', level: 0 })
|
|
43
|
+
const upperLevel = LevelNode.parse({ id: 'level_upper', level: 1 })
|
|
44
|
+
const lowerWall = WallNode.parse({
|
|
45
|
+
id: 'wall_lower',
|
|
46
|
+
parentId: lowerLevel.id,
|
|
47
|
+
start: [10, 10],
|
|
48
|
+
end: [14, 10],
|
|
49
|
+
})
|
|
50
|
+
const upperWall = WallNode.parse({
|
|
51
|
+
id: 'wall_upper',
|
|
52
|
+
parentId: upperLevel.id,
|
|
53
|
+
start: [0, 0],
|
|
54
|
+
end: [4, 0],
|
|
55
|
+
})
|
|
56
|
+
useScene.setState({
|
|
57
|
+
nodes: Object.fromEntries(
|
|
58
|
+
[lowerLevel, upperLevel, lowerWall, upperWall].map((node) => [node.id, node]),
|
|
59
|
+
),
|
|
60
|
+
} as never)
|
|
61
|
+
|
|
62
|
+
expect(
|
|
63
|
+
alignFloorplanDraftPoint([0.04, 0], {
|
|
64
|
+
applySnap: true,
|
|
65
|
+
levelId: lowerLevel.id,
|
|
66
|
+
}),
|
|
67
|
+
).toEqual([0.04, 0])
|
|
68
|
+
expect(useAlignmentGuides.getState().guides).toHaveLength(0)
|
|
69
|
+
})
|
|
25
70
|
})
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
type AlignmentAnchor,
|
|
3
3
|
type AlignmentGuide,
|
|
4
4
|
collectAlignmentAnchors,
|
|
5
|
+
resolveLevelId,
|
|
5
6
|
useScene,
|
|
6
7
|
} from '@pascal-app/core'
|
|
7
8
|
import useAlignmentGuides from '../../store/use-alignment-guides'
|
|
@@ -96,13 +97,21 @@ export function alignFloorplanDraftPoint(
|
|
|
96
97
|
bypass?: boolean
|
|
97
98
|
threshold?: number
|
|
98
99
|
excludeIds?: readonly string[]
|
|
100
|
+
levelId?: string | null
|
|
99
101
|
},
|
|
100
102
|
): [number, number] {
|
|
101
103
|
if (opts?.bypass) {
|
|
102
104
|
useAlignmentGuides.getState().clear()
|
|
103
105
|
return [point[0], point[1]]
|
|
104
106
|
}
|
|
105
|
-
|
|
107
|
+
const nodes = useScene.getState().nodes
|
|
108
|
+
let candidates = collectAlignmentAnchors(nodes, FLOORPLAN_DRAFT_ALIGN_ID)
|
|
109
|
+
if (opts && 'levelId' in opts) {
|
|
110
|
+
candidates = candidates.filter((anchor) => {
|
|
111
|
+
const candidate = nodes[anchor.nodeId as keyof typeof nodes]
|
|
112
|
+
return candidate ? resolveLevelId(candidate, nodes) === opts.levelId : false
|
|
113
|
+
})
|
|
114
|
+
}
|
|
106
115
|
if (opts?.excludeIds?.length) {
|
|
107
116
|
const excluded = new Set(opts.excludeIds)
|
|
108
117
|
candidates = candidates.filter((anchor) => !excluded.has(anchor.nodeId))
|