@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
package/src/index.tsx
CHANGED
|
@@ -56,6 +56,7 @@ export {
|
|
|
56
56
|
formatMeasurement,
|
|
57
57
|
MeasurementPill,
|
|
58
58
|
} from './components/editor/measurement-pill'
|
|
59
|
+
export { NodeActionMenu } from './components/editor/node-action-menu'
|
|
59
60
|
// In-world arrow handle primitives (chevron geometry, invisible hit area,
|
|
60
61
|
// shared material, palette + scale constants). Re-exported so kind-owned
|
|
61
62
|
// 3D selection affordances in `@pascal-app/nodes` (duct side-move / height /
|
|
@@ -98,6 +99,11 @@ export {
|
|
|
98
99
|
buildSvgArrowHeadPoints,
|
|
99
100
|
getArcPlanPoint,
|
|
100
101
|
} from './components/editor-2d/svg-paths'
|
|
102
|
+
export type {
|
|
103
|
+
SelectionAffordanceHistoryApi,
|
|
104
|
+
SelectionAffordanceInteractionApi,
|
|
105
|
+
SelectionAffordanceProps,
|
|
106
|
+
} from './components/systems/selection-affordance-services'
|
|
101
107
|
// Phase 5 Stage D transitional exports — pure drafting / angle helpers
|
|
102
108
|
// consumed by kind-owned drag actions in @pascal-app/nodes. Stage F
|
|
103
109
|
// cleanup moves these into @pascal-app/nodes (fence/drafting.ts +
|
|
@@ -113,7 +119,6 @@ export { MoveTool } from './components/tools/item/move-tool'
|
|
|
113
119
|
// `@pascal-app/nodes` (wall curve sagitta snap, door / window placement,
|
|
114
120
|
// item drop) so kinds don't reach into editor internals.
|
|
115
121
|
export {
|
|
116
|
-
calculateCursorRotation,
|
|
117
122
|
calculateItemRotation,
|
|
118
123
|
getSideFromNormal,
|
|
119
124
|
isValidWallSideFace,
|
|
@@ -132,6 +137,7 @@ export {
|
|
|
132
137
|
type PlacementCoordinatorConfig,
|
|
133
138
|
usePlacementCoordinator,
|
|
134
139
|
} from './components/tools/item/use-placement-coordinator'
|
|
140
|
+
export { useRegistryToolContext } from './components/tools/registry-tool-context'
|
|
135
141
|
export { CursorSphere } from './components/tools/shared/cursor-sphere'
|
|
136
142
|
export { DragBoundingBox } from './components/tools/shared/drag-bounding-box'
|
|
137
143
|
export { getFloorStackPreviewPosition } from './components/tools/shared/floor-stack-preview'
|
|
@@ -144,6 +150,7 @@ export {
|
|
|
144
150
|
resolveLevelConstructionPlane,
|
|
145
151
|
} from './components/tools/shared/horizontal-construction-plane'
|
|
146
152
|
export { PlacementBox } from './components/tools/shared/placement-box'
|
|
153
|
+
export { PlacementDimensionGuides } from './components/tools/shared/placement-dimension-guides'
|
|
147
154
|
// Pointer-decided support surface (deck top vs floor underneath) — the
|
|
148
155
|
// draw tools (wall / fence) ride their grid plane and commit cap on it.
|
|
149
156
|
export {
|
|
@@ -234,6 +241,7 @@ export { SegmentedControl } from './components/ui/controls/segmented-control'
|
|
|
234
241
|
export { SliderControl } from './components/ui/controls/slider-control'
|
|
235
242
|
export { TerrainSculptPanel } from './components/ui/controls/terrain-sculpt-panel'
|
|
236
243
|
export { ToggleControl } from './components/ui/controls/toggle-control'
|
|
244
|
+
export { ToolOptionsPanel } from './components/ui/controls/tool-options-panel'
|
|
237
245
|
export { FloatingLevelSelector } from './components/ui/floating-level-selector'
|
|
238
246
|
export { CATALOG_ITEMS } from './components/ui/item-catalog/catalog-items'
|
|
239
247
|
// Item collections UI — used by the kind-owned ItemPanel in nodes/.
|
|
@@ -254,6 +262,10 @@ export {
|
|
|
254
262
|
DropdownMenuSubTrigger,
|
|
255
263
|
DropdownMenuTrigger,
|
|
256
264
|
} from './components/ui/primitives/dropdown-menu'
|
|
265
|
+
export {
|
|
266
|
+
ShortcutToken,
|
|
267
|
+
shortcutDisplayValue,
|
|
268
|
+
} from './components/ui/primitives/shortcut-token'
|
|
257
269
|
export { useSidebarStore } from './components/ui/primitives/sidebar'
|
|
258
270
|
export { Slider } from './components/ui/primitives/slider'
|
|
259
271
|
export { SceneLoader } from './components/ui/scene-loader'
|
|
@@ -333,6 +345,12 @@ export {
|
|
|
333
345
|
resolveCeilingPlanPointSnap,
|
|
334
346
|
} from './lib/ceiling-plan-snap'
|
|
335
347
|
export { EDITOR_LAYER } from './lib/constants'
|
|
348
|
+
export type { ContextualShortcutHint } from './lib/contextual-help'
|
|
349
|
+
export {
|
|
350
|
+
CONTEXTUAL_HELP_NODE_EXTENSION_KEY,
|
|
351
|
+
type ContextualHelpNodeExtension,
|
|
352
|
+
getContextualHelpNodeExtension,
|
|
353
|
+
} from './lib/contextual-help-extension'
|
|
336
354
|
// Helper libs used by the kind-owned roof / stair / elevator panels.
|
|
337
355
|
export {
|
|
338
356
|
CONTINUATION_PROFILES,
|
|
@@ -341,6 +359,7 @@ export {
|
|
|
341
359
|
continuationContextOf,
|
|
342
360
|
nextContinuation,
|
|
343
361
|
} from './lib/continuation'
|
|
362
|
+
export { createEditorApi } from './lib/editor-api'
|
|
344
363
|
export {
|
|
345
364
|
clearStructuralElevationGuide,
|
|
346
365
|
collectElevationSnapTargets,
|
|
@@ -348,6 +367,7 @@ export {
|
|
|
348
367
|
type ElevationGuideSource,
|
|
349
368
|
type ElevationSnapMatch,
|
|
350
369
|
type ElevationSnapTarget,
|
|
370
|
+
publishResolvedElevationGuide,
|
|
351
371
|
publishStructuralElevationGuide,
|
|
352
372
|
resolveElevationSnapMatch,
|
|
353
373
|
resolveStructuralElevationSnap,
|
|
@@ -358,6 +378,7 @@ export {
|
|
|
358
378
|
resolveElevatorSupportLevelId,
|
|
359
379
|
resolveElevatorSupportY,
|
|
360
380
|
} from './lib/elevator-support'
|
|
381
|
+
export { getFloatingMenuScale } from './lib/floating-menu-scale'
|
|
361
382
|
// Floor-plan stair helpers — the cumulative-transform walk
|
|
362
383
|
// (`computeFloorplanStairSegmentTransforms`) and the rich segment-entry
|
|
363
384
|
// builder (`buildFloorplanStairEntry`) used by the kind-owned stair
|
|
@@ -382,6 +403,10 @@ export type {
|
|
|
382
403
|
FloorplanAnnotationCategory,
|
|
383
404
|
FloorplanAnnotationVisibility,
|
|
384
405
|
} from './lib/floorplan/annotation-visibility'
|
|
406
|
+
export {
|
|
407
|
+
exportFloorplanPdf,
|
|
408
|
+
type FloorplanExportScope,
|
|
409
|
+
} from './lib/floorplan/floorplan-export'
|
|
385
410
|
export {
|
|
386
411
|
createFloorplanContextExtensions,
|
|
387
412
|
FLOORPLAN_CONTEXT_EXTENSION_KEY,
|
|
@@ -407,8 +432,16 @@ export {
|
|
|
407
432
|
type FloorplanMode,
|
|
408
433
|
isFloorplanToolAvailableInMode,
|
|
409
434
|
} from './lib/floorplan/floorplan-mode'
|
|
410
|
-
export {
|
|
411
|
-
|
|
435
|
+
export {
|
|
436
|
+
commitFreshPlacementSubtree,
|
|
437
|
+
createFreshPlacementSubtree,
|
|
438
|
+
} from './lib/fresh-planar-placement'
|
|
439
|
+
export { exportSceneToGlb, type GlbExportOptions } from './lib/glb-export'
|
|
440
|
+
export {
|
|
441
|
+
type EditorGridEvent,
|
|
442
|
+
type GridEventScreenProjection,
|
|
443
|
+
getGridEventScreenProjection,
|
|
444
|
+
} from './lib/grid-event-presentation'
|
|
412
445
|
export {
|
|
413
446
|
getHistoryCommandState,
|
|
414
447
|
type HistoryCommandDelegate,
|
|
@@ -419,15 +452,27 @@ export {
|
|
|
419
452
|
runUndo,
|
|
420
453
|
subscribeHistoryCommandState,
|
|
421
454
|
} from './lib/history'
|
|
455
|
+
export {
|
|
456
|
+
type EditorHostTreeChildren,
|
|
457
|
+
type EditorHostTreeChildrenProps,
|
|
458
|
+
editorHostTreeChildrenRegistry,
|
|
459
|
+
registerEditorHostTreeChildren,
|
|
460
|
+
} from './lib/host-tree-children'
|
|
461
|
+
export {
|
|
462
|
+
DRAFTING_SURFACE_EXTENSION_KEY,
|
|
463
|
+
type DraftingSurfaceExtension,
|
|
464
|
+
} from './lib/interaction/registered-drafting'
|
|
422
465
|
export {
|
|
423
466
|
boundaryReshapeScope,
|
|
424
467
|
curveReshapeScope,
|
|
425
468
|
endpointReshapeScope,
|
|
426
469
|
holeEditScope,
|
|
470
|
+
meshEditScope,
|
|
427
471
|
movingNodeOf,
|
|
428
472
|
scopeNodeId,
|
|
429
473
|
} from './lib/interaction/scope'
|
|
430
474
|
export {
|
|
475
|
+
type ActivePaintMaterial,
|
|
431
476
|
buildResetSurfaceMaterialUpdates,
|
|
432
477
|
buildRoofSurfaceMaterialPatch,
|
|
433
478
|
buildSingleSurfaceMaterialPatch,
|
|
@@ -446,6 +491,14 @@ export {
|
|
|
446
491
|
measurementPolygonLabelAnchor,
|
|
447
492
|
triangulateMeasurementPolygon,
|
|
448
493
|
} from './lib/measurement-label'
|
|
494
|
+
export {
|
|
495
|
+
type LingoUnitSpec,
|
|
496
|
+
lingoUnitSpec,
|
|
497
|
+
type MeasurementHintOptions,
|
|
498
|
+
measurementHint,
|
|
499
|
+
type ParseMeasurementOptions,
|
|
500
|
+
parseMeasurement,
|
|
501
|
+
} from './lib/measurement-parser'
|
|
449
502
|
export {
|
|
450
503
|
buildMeasurementAngleArcPoints,
|
|
451
504
|
cubicMetersToVolumeUnit,
|
|
@@ -467,6 +520,12 @@ export {
|
|
|
467
520
|
metersToLinearUnit,
|
|
468
521
|
squareMetersToAreaUnit,
|
|
469
522
|
} from './lib/measurements'
|
|
523
|
+
export type {
|
|
524
|
+
ModelExport,
|
|
525
|
+
ModelExportArtifact,
|
|
526
|
+
ModelExportFormat,
|
|
527
|
+
ModelExportOptions,
|
|
528
|
+
} from './lib/model-export'
|
|
470
529
|
export { consumePlacementDragRelease } from './lib/placement-drag-release'
|
|
471
530
|
export {
|
|
472
531
|
addFreshPlacementMetadata,
|
|
@@ -510,6 +569,13 @@ export {
|
|
|
510
569
|
type SlabPlanSnapInput,
|
|
511
570
|
type SlabPlanSnapResult,
|
|
512
571
|
} from './lib/slab-plan-snap'
|
|
572
|
+
export {
|
|
573
|
+
getSnappingModeLabel,
|
|
574
|
+
resolveSnapFlags,
|
|
575
|
+
type SnapContext,
|
|
576
|
+
type SnapFlags,
|
|
577
|
+
type SnappingMode,
|
|
578
|
+
} from './lib/snapping-mode'
|
|
513
579
|
export { duplicateStairSubtree } from './lib/stair-duplication'
|
|
514
580
|
export {
|
|
515
581
|
getBuildingLevelsForLevel,
|
|
@@ -533,6 +599,7 @@ export {
|
|
|
533
599
|
resolveFlattenTarget,
|
|
534
600
|
sculptFieldForSite,
|
|
535
601
|
} from './lib/terrain-sculpt'
|
|
602
|
+
export { exportSceneToUsdz, type UsdzExportOptions } from './lib/usdz-export'
|
|
536
603
|
// `cn` (twMerge + clsx) — used by kind-owned panels in `@pascal-app/
|
|
537
604
|
// nodes` so they don't need their own copy / their own tailwind-merge
|
|
538
605
|
// dependency.
|
|
@@ -548,6 +615,7 @@ export {
|
|
|
548
615
|
export { subscribeCameraPose } from './store/camera-pose-store'
|
|
549
616
|
export { default as useAlignmentGuides } from './store/use-alignment-guides'
|
|
550
617
|
export { default as useAudio } from './store/use-audio'
|
|
618
|
+
export { type CameraHintAction, useCameraHintFocus } from './store/use-camera-hint-focus'
|
|
551
619
|
export { type CommandAction, useCommandRegistry } from './store/use-command-registry'
|
|
552
620
|
export {
|
|
553
621
|
DRAWING_TYPE_OPTIONS,
|
|
@@ -556,17 +624,24 @@ export {
|
|
|
556
624
|
export type {
|
|
557
625
|
CaptureMode,
|
|
558
626
|
FloorplanSelectionTool,
|
|
627
|
+
Mode,
|
|
559
628
|
SnapshotCropMode,
|
|
560
629
|
SnapshotStandardAspect,
|
|
561
630
|
SplitOrientation,
|
|
631
|
+
StructureTool,
|
|
562
632
|
Tool,
|
|
563
633
|
ToolDefaults,
|
|
634
|
+
ToolMode,
|
|
564
635
|
ViewMode,
|
|
565
636
|
WorkspaceMode,
|
|
566
637
|
} from './store/use-editor'
|
|
567
638
|
export {
|
|
639
|
+
armMaterialPaint,
|
|
640
|
+
armToolMode,
|
|
568
641
|
default as useEditor,
|
|
569
642
|
getActiveContinuationContext,
|
|
643
|
+
getActiveSnapContext,
|
|
644
|
+
getActiveSnappingMode,
|
|
570
645
|
getContinuation,
|
|
571
646
|
isAlignmentGuideActive,
|
|
572
647
|
isAngleSnapActive,
|
|
@@ -623,7 +698,10 @@ export {
|
|
|
623
698
|
type PathDraftPoint,
|
|
624
699
|
usePathDraftPreview,
|
|
625
700
|
} from './store/use-path-draft-preview'
|
|
626
|
-
export {
|
|
701
|
+
export {
|
|
702
|
+
default as usePlacementPreview,
|
|
703
|
+
type PlacementPreviewDimension,
|
|
704
|
+
} from './store/use-placement-preview'
|
|
627
705
|
export {
|
|
628
706
|
activateQuickMeasurementHudSource,
|
|
629
707
|
clearQuickMeasurementHudSource,
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
clearPlacementSurface,
|
|
5
5
|
getPlacementSurface,
|
|
6
6
|
publishPlacementSurface,
|
|
7
|
+
usesOrientedPlacementPlane,
|
|
7
8
|
} from './active-placement-surface'
|
|
8
9
|
|
|
9
10
|
describe('active placement surface', () => {
|
|
@@ -21,4 +22,19 @@ describe('active placement surface', () => {
|
|
|
21
22
|
|
|
22
23
|
expect(getPlacementSurface()?.projection).toBe('surface')
|
|
23
24
|
})
|
|
25
|
+
|
|
26
|
+
test('copies a stable lattice anchor when one is provided', () => {
|
|
27
|
+
publishPlacementSurface(
|
|
28
|
+
new Vector3(4, 2, 3),
|
|
29
|
+
new Vector3(0, 0, 1),
|
|
30
|
+
'surface',
|
|
31
|
+
new Vector3(1, 2, 3),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
expect(getPlacementSurface()?.anchor?.toArray()).toEqual([1, 2, 3])
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('orients the grid to a sloped placement surface', () => {
|
|
38
|
+
expect(usesOrientedPlacementPlane(new Vector3(0, 0.6, 0.8))).toBe(true)
|
|
39
|
+
})
|
|
24
40
|
})
|
|
@@ -11,12 +11,15 @@ import { Vector3 } from 'three'
|
|
|
11
11
|
// readers must consume them within the same frame.
|
|
12
12
|
export type PlacementSurface = {
|
|
13
13
|
point: Vector3
|
|
14
|
+
/** Optional stable origin for the construction lattice. */
|
|
15
|
+
anchor?: Vector3
|
|
14
16
|
normal: Vector3
|
|
15
17
|
projection: 'surface' | 'fixed-plane'
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
const surface: PlacementSurface = {
|
|
19
21
|
point: new Vector3(),
|
|
22
|
+
anchor: new Vector3(),
|
|
20
23
|
normal: new Vector3(0, 1, 0),
|
|
21
24
|
projection: 'surface',
|
|
22
25
|
}
|
|
@@ -26,8 +29,15 @@ export function publishPlacementSurface(
|
|
|
26
29
|
point: Vector3,
|
|
27
30
|
normal: Vector3,
|
|
28
31
|
projection: PlacementSurface['projection'] = 'surface',
|
|
32
|
+
anchor?: Vector3,
|
|
29
33
|
): void {
|
|
30
34
|
surface.point.copy(point)
|
|
35
|
+
if (anchor) {
|
|
36
|
+
surface.anchor ??= new Vector3()
|
|
37
|
+
surface.anchor.copy(anchor)
|
|
38
|
+
} else {
|
|
39
|
+
surface.anchor = undefined
|
|
40
|
+
}
|
|
31
41
|
surface.normal.copy(normal)
|
|
32
42
|
surface.projection = projection
|
|
33
43
|
active = true
|
|
@@ -40,3 +50,7 @@ export function clearPlacementSurface(): void {
|
|
|
40
50
|
export function getPlacementSurface(): PlacementSurface | null {
|
|
41
51
|
return active ? surface : null
|
|
42
52
|
}
|
|
53
|
+
|
|
54
|
+
export function usesOrientedPlacementPlane(normal: Vector3): boolean {
|
|
55
|
+
return Math.abs(normal.y) < 0.95
|
|
56
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
BackSide,
|
|
4
|
+
Mesh,
|
|
5
|
+
MeshBasicMaterial,
|
|
6
|
+
Path,
|
|
7
|
+
Raycaster,
|
|
8
|
+
Shape,
|
|
9
|
+
ShapeGeometry,
|
|
10
|
+
Vector3,
|
|
11
|
+
} from 'three'
|
|
12
|
+
import { raycastCeilingUnderside } from './ceiling-surface-raycast'
|
|
13
|
+
|
|
14
|
+
function ceilingMesh() {
|
|
15
|
+
const shape = new Shape()
|
|
16
|
+
shape.moveTo(-2, -2)
|
|
17
|
+
shape.lineTo(2, -2)
|
|
18
|
+
shape.lineTo(2, 2)
|
|
19
|
+
shape.lineTo(-2, 2)
|
|
20
|
+
shape.closePath()
|
|
21
|
+
const hole = new Path()
|
|
22
|
+
hole.moveTo(-0.5, -0.5)
|
|
23
|
+
hole.lineTo(-0.5, 0.5)
|
|
24
|
+
hole.lineTo(0.5, 0.5)
|
|
25
|
+
hole.lineTo(0.5, -0.5)
|
|
26
|
+
hole.closePath()
|
|
27
|
+
shape.holes.push(hole)
|
|
28
|
+
const geometry = new ShapeGeometry(shape).rotateX(-Math.PI / 2)
|
|
29
|
+
const mesh = new Mesh(geometry, new MeshBasicMaterial({ side: BackSide }))
|
|
30
|
+
mesh.position.y = 3
|
|
31
|
+
mesh.updateMatrixWorld(true)
|
|
32
|
+
return mesh
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('ceiling drawing surfaces', () => {
|
|
36
|
+
test('lets drawing pass through the transparent top while picking the underside', () => {
|
|
37
|
+
const ceiling = ceilingMesh()
|
|
38
|
+
for (const side of [-1, 1]) {
|
|
39
|
+
const ray = new Raycaster(new Vector3(1, 3 + side * 2, 0), new Vector3(0, -side, 0))
|
|
40
|
+
const hit = raycastCeilingUnderside(ray, ceiling)[0]
|
|
41
|
+
if (side > 0) {
|
|
42
|
+
expect(hit).toBeUndefined()
|
|
43
|
+
} else {
|
|
44
|
+
expect(hit?.point.y).toBeCloseTo(3)
|
|
45
|
+
expect(hit?.object).toBe(ceiling)
|
|
46
|
+
}
|
|
47
|
+
expect(ceiling.material.side).toBe(BackSide)
|
|
48
|
+
}
|
|
49
|
+
ceiling.geometry.dispose()
|
|
50
|
+
ceiling.material.dispose()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('respects ceiling holes and the polygon boundary from either side', () => {
|
|
54
|
+
const ceiling = ceilingMesh()
|
|
55
|
+
for (const side of [-1, 1]) {
|
|
56
|
+
for (const x of [0, 3]) {
|
|
57
|
+
const ray = new Raycaster(new Vector3(x, 3 + side * 2, 0), new Vector3(0, -side, 0))
|
|
58
|
+
expect(raycastCeilingUnderside(ray, ceiling)).toHaveLength(0)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
ceiling.geometry.dispose()
|
|
62
|
+
ceiling.material.dispose()
|
|
63
|
+
})
|
|
64
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { BackSide, Mesh, MeshBasicMaterial, type Object3D, type Raycaster } from 'three'
|
|
2
|
+
|
|
3
|
+
const pickingMaterial = new MeshBasicMaterial({ side: BackSide })
|
|
4
|
+
|
|
5
|
+
export function raycastCeilingUnderside(raycaster: Raycaster, ceiling: Object3D) {
|
|
6
|
+
if (!(ceiling instanceof Mesh)) return []
|
|
7
|
+
// Ignore the transparent top and its grid overlay so drawing can reach surfaces below.
|
|
8
|
+
const proxy = new Mesh(ceiling.geometry, pickingMaterial)
|
|
9
|
+
proxy.matrixWorld.copy(ceiling.matrixWorld)
|
|
10
|
+
proxy.layers.mask = ceiling.layers.mask
|
|
11
|
+
return raycaster.intersectObject(proxy, false).map((hit) => ({ ...hit, object: ceiling }))
|
|
12
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { NodeDefinition } from '@pascal-app/core'
|
|
2
|
+
import type { ContextualShortcutHint } from './contextual-help'
|
|
3
|
+
|
|
4
|
+
export const CONTEXTUAL_HELP_NODE_EXTENSION_KEY = 'pascal:editor/contextual-help'
|
|
5
|
+
|
|
6
|
+
export type ContextualHelpNodeExtension = {
|
|
7
|
+
subscribe: (onChange: () => void) => () => void
|
|
8
|
+
getHints: (nodeId: string) => ContextualShortcutHint[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getContextualHelpNodeExtension(
|
|
12
|
+
definition: NodeDefinition<any> | undefined,
|
|
13
|
+
): ContextualHelpNodeExtension | undefined {
|
|
14
|
+
return definition?.extensions?.[CONTEXTUAL_HELP_NODE_EXTENSION_KEY] as
|
|
15
|
+
| ContextualHelpNodeExtension
|
|
16
|
+
| undefined
|
|
17
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { CONTINUATION_PROFILES, continuationContextOf, nextContinuation } from './continuation'
|
|
3
|
+
|
|
4
|
+
describe('canopy continuation', () => {
|
|
5
|
+
test('maps the canopy tool to its own single and continuous profile', () => {
|
|
6
|
+
expect(continuationContextOf('lean-to-extension')).toBe('canopy')
|
|
7
|
+
expect(CONTINUATION_PROFILES.canopy.default).toBe('single')
|
|
8
|
+
expect(nextContinuation('canopy', 'single')).toBe('continuous')
|
|
9
|
+
expect(nextContinuation('canopy', 'continuous')).toBe('single')
|
|
10
|
+
})
|
|
11
|
+
})
|
package/src/lib/continuation.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ContinuationContext = 'wall' | 'fence' | 'point' | 'cabinet'
|
|
1
|
+
export type ContinuationContext = 'wall' | 'fence' | 'point' | 'cabinet' | 'canopy'
|
|
2
2
|
export type ContinuationMode = string
|
|
3
3
|
|
|
4
4
|
export const CONTINUATION_PROFILES: Record<
|
|
@@ -42,6 +42,12 @@ export const CONTINUATION_PROFILES: Record<
|
|
|
42
42
|
labels: { single: 'Single cabinet', continuous: 'Continuous run' },
|
|
43
43
|
icons: { single: 'lucide:minus', continuous: 'lucide:waypoints' },
|
|
44
44
|
},
|
|
45
|
+
canopy: {
|
|
46
|
+
options: ['single', 'continuous'],
|
|
47
|
+
default: 'single',
|
|
48
|
+
labels: { single: 'Single canopy', continuous: 'Continuous canopy' },
|
|
49
|
+
icons: { single: 'lucide:minus', continuous: 'lucide:waypoints' },
|
|
50
|
+
},
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
const POINT_KINDS = new Set(['item', 'door', 'window', 'shelf', 'column'])
|
|
@@ -60,5 +66,6 @@ export function continuationContextOf(kind: string): ContinuationContext | null
|
|
|
60
66
|
if (kind === 'wall') return 'wall'
|
|
61
67
|
if (kind === 'fence') return 'fence'
|
|
62
68
|
if (kind === 'cabinet') return 'cabinet'
|
|
69
|
+
if (kind === 'lean-to-extension') return 'canopy'
|
|
63
70
|
return POINT_KINDS.has(kind) ? 'point' : null
|
|
64
71
|
}
|
|
@@ -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
|