@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
|
@@ -40,6 +40,7 @@ type FloorplanDraftPreviewState = {
|
|
|
40
40
|
wallDraftStart: WallPlanPoint | null
|
|
41
41
|
fenceDraftStart: WallPlanPoint | null
|
|
42
42
|
roofDraftStart: WallPlanPoint | null
|
|
43
|
+
roofDraftQuarterTurn: boolean
|
|
43
44
|
polygonDraftType: FloorplanPolygonDraftType | null
|
|
44
45
|
polygonDraftPoints: WallPlanPoint[]
|
|
45
46
|
/** Set the snapped cursor point. No-ops (skips the store update, so
|
|
@@ -54,6 +55,7 @@ type FloorplanDraftPreviewState = {
|
|
|
54
55
|
setWallDraftStart(point: WallPlanPoint | null): void
|
|
55
56
|
setFenceDraftStart(point: WallPlanPoint | null): void
|
|
56
57
|
setRoofDraftStart(point: WallPlanPoint | null): void
|
|
58
|
+
setRoofDraftQuarterTurn(quarterTurn: boolean): void
|
|
57
59
|
setPolygonDraft(type: FloorplanPolygonDraftType | null, points: readonly WallPlanPoint[]): void
|
|
58
60
|
reset(): void
|
|
59
61
|
}
|
|
@@ -94,6 +96,7 @@ export const useFloorplanDraftPreview = create<FloorplanDraftPreviewState>((set)
|
|
|
94
96
|
wallDraftStart: null,
|
|
95
97
|
fenceDraftStart: null,
|
|
96
98
|
roofDraftStart: null,
|
|
99
|
+
roofDraftQuarterTurn: false,
|
|
97
100
|
polygonDraftType: null,
|
|
98
101
|
polygonDraftPoints: [],
|
|
99
102
|
setCursorPoint: (point) =>
|
|
@@ -116,6 +119,10 @@ export const useFloorplanDraftPreview = create<FloorplanDraftPreviewState>((set)
|
|
|
116
119
|
setWallDraftStart: (point) => set(setPlanPointField('wallDraftStart', point)),
|
|
117
120
|
setFenceDraftStart: (point) => set(setPlanPointField('fenceDraftStart', point)),
|
|
118
121
|
setRoofDraftStart: (point) => set(setPlanPointField('roofDraftStart', point)),
|
|
122
|
+
setRoofDraftQuarterTurn: (quarterTurn) =>
|
|
123
|
+
set((state) =>
|
|
124
|
+
state.roofDraftQuarterTurn === quarterTurn ? state : { roofDraftQuarterTurn: quarterTurn },
|
|
125
|
+
),
|
|
119
126
|
setPolygonDraft: (type, points) =>
|
|
120
127
|
set((state) =>
|
|
121
128
|
state.polygonDraftType === type && planPointsEqual(state.polygonDraftPoints, points)
|
|
@@ -132,6 +139,7 @@ export const useFloorplanDraftPreview = create<FloorplanDraftPreviewState>((set)
|
|
|
132
139
|
state.wallDraftStart === null &&
|
|
133
140
|
state.fenceDraftStart === null &&
|
|
134
141
|
state.roofDraftStart === null &&
|
|
142
|
+
state.roofDraftQuarterTurn === false &&
|
|
135
143
|
state.polygonDraftType === null &&
|
|
136
144
|
state.polygonDraftPoints.length === 0
|
|
137
145
|
? state
|
|
@@ -144,6 +152,7 @@ export const useFloorplanDraftPreview = create<FloorplanDraftPreviewState>((set)
|
|
|
144
152
|
wallDraftStart: null,
|
|
145
153
|
fenceDraftStart: null,
|
|
146
154
|
roofDraftStart: null,
|
|
155
|
+
roofDraftQuarterTurn: false,
|
|
147
156
|
polygonDraftType: null,
|
|
148
157
|
polygonDraftPoints: [],
|
|
149
158
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { afterEach, describe, expect, test } from 'bun:test'
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
2
|
import type { AnyNode } from '@pascal-app/core'
|
|
3
3
|
import {
|
|
4
4
|
type ActiveInteractionScope,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
isActive,
|
|
9
9
|
isIdle,
|
|
10
10
|
isToolDrivenReshape,
|
|
11
|
+
meshEditScope,
|
|
11
12
|
scopeNodeId,
|
|
12
13
|
selectionEnabled,
|
|
13
14
|
} from '../lib/interaction/scope'
|
|
@@ -20,6 +21,7 @@ const mockNode = (id: string, type: string): AnyNode => ({ id, type }) as unknow
|
|
|
20
21
|
function reset() {
|
|
21
22
|
useInteractionScope.getState().end()
|
|
22
23
|
}
|
|
24
|
+
beforeEach(reset)
|
|
23
25
|
afterEach(reset)
|
|
24
26
|
|
|
25
27
|
describe('use-interaction-scope state machine', () => {
|
|
@@ -71,6 +73,7 @@ describe('use-interaction-scope state machine', () => {
|
|
|
71
73
|
nodeType: 'item',
|
|
72
74
|
view: '3d',
|
|
73
75
|
pressDrag: false,
|
|
76
|
+
driver: 'move-tool',
|
|
74
77
|
})
|
|
75
78
|
s.update({ pressDrag: true })
|
|
76
79
|
const scope = useInteractionScope.getState().scope
|
|
@@ -104,6 +107,7 @@ describe('use-interaction-scope state machine', () => {
|
|
|
104
107
|
nodeType: 'item',
|
|
105
108
|
view: '3d',
|
|
106
109
|
pressDrag: true,
|
|
110
|
+
driver: 'move-tool',
|
|
107
111
|
})
|
|
108
112
|
expect(useInteractionScope.getState().scope.kind).toBe('moving')
|
|
109
113
|
})
|
|
@@ -116,6 +120,22 @@ describe('use-interaction-scope state machine', () => {
|
|
|
116
120
|
expect(isActive(useInteractionScope.getState().scope)).toBe(true)
|
|
117
121
|
})
|
|
118
122
|
|
|
123
|
+
test('mesh edit mode owns its node and disables scene selection for the full session', () => {
|
|
124
|
+
const s = useInteractionScope.getState()
|
|
125
|
+
s.begin(meshEditScope('block_1'))
|
|
126
|
+
expect(scopeNodeId(useInteractionScope.getState().scope)).toBe('block_1')
|
|
127
|
+
expect(selectionEnabled(useInteractionScope.getState().scope)).toBe(false)
|
|
128
|
+
|
|
129
|
+
s.begin(meshEditScope('block_1', 'operating', 'translate'))
|
|
130
|
+
expect(useInteractionScope.getState().scope).toEqual({
|
|
131
|
+
kind: 'mesh-editing',
|
|
132
|
+
nodeId: 'block_1',
|
|
133
|
+
phase: 'operating',
|
|
134
|
+
operator: 'translate',
|
|
135
|
+
})
|
|
136
|
+
expect(selectionEnabled(useInteractionScope.getState().scope)).toBe(false)
|
|
137
|
+
})
|
|
138
|
+
|
|
119
139
|
test('end is idempotent', () => {
|
|
120
140
|
const s = useInteractionScope.getState()
|
|
121
141
|
s.end()
|
|
@@ -188,9 +208,11 @@ describe('derived flag views are leak-free (no parallel flags)', () => {
|
|
|
188
208
|
nodeType: 'item',
|
|
189
209
|
view: '3d',
|
|
190
210
|
pressDrag: false,
|
|
211
|
+
driver: 'move-tool',
|
|
191
212
|
},
|
|
192
213
|
{ kind: 'moving', node: mockNode('i', 'item'), nodeId: 'i', nodeType: 'item', view: '3d' },
|
|
193
214
|
{ kind: 'drafting', tool: 'wall' },
|
|
215
|
+
{ kind: 'mesh-editing', nodeId: 'mesh_1', phase: 'selecting' },
|
|
194
216
|
{ kind: 'box-select' },
|
|
195
217
|
{ kind: 'painting' },
|
|
196
218
|
{ kind: 'sculpting' },
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { type AnyNode, type AnyNodeId, useScene } from '@pascal-app/core'
|
|
4
|
+
import {
|
|
5
|
+
beginPerfAction,
|
|
6
|
+
commitPerfAction,
|
|
7
|
+
getActivePerfActionId,
|
|
8
|
+
hasUncommittedPerfAction,
|
|
9
|
+
} from '@pascal-app/viewer'
|
|
4
10
|
import { useRef } from 'react'
|
|
5
11
|
import { create } from 'zustand'
|
|
6
12
|
import { useShallow } from 'zustand/react/shallow'
|
|
@@ -45,9 +51,60 @@ export type InteractionScopeState = {
|
|
|
45
51
|
endIf: (match: (scope: ActiveInteractionScope) => boolean) => void
|
|
46
52
|
}
|
|
47
53
|
|
|
54
|
+
// Perf action ledger (`?perf`): every 3D gesture funnels through this store, so
|
|
55
|
+
// begin/end are the one generic bracket for action-cost receipts. A more
|
|
56
|
+
// specific call site (use-drag-action, the 2D floorplan layer) may have begun
|
|
57
|
+
// its own action first — yield to it while ITS gesture is in flight, but a
|
|
58
|
+
// merely-settling previous action must not swallow a new gesture. The scope
|
|
59
|
+
// remembers the id it began and commits only that action at `end`, so a
|
|
60
|
+
// specific site's receipt (or a cancelled one finalized via
|
|
61
|
+
// markToolCancelConsumed) is never committed by the generic bracket. Known
|
|
62
|
+
// limit: a scope-begun gesture cancelled through a path that skips
|
|
63
|
+
// markToolCancelConsumed still commits at end and bills its revert as settle.
|
|
64
|
+
let scopePerfActionId: number | null = null
|
|
65
|
+
|
|
66
|
+
function beginScopePerfAction(scope: ActiveInteractionScope): void {
|
|
67
|
+
if (hasUncommittedPerfAction()) return
|
|
68
|
+
switch (scope.kind) {
|
|
69
|
+
case 'moving':
|
|
70
|
+
scopePerfActionId = beginPerfAction('drag:move', scope.nodeId)
|
|
71
|
+
break
|
|
72
|
+
case 'placing':
|
|
73
|
+
scopePerfActionId = beginPerfAction(
|
|
74
|
+
`place:${scope.node?.type ?? 'node'}`,
|
|
75
|
+
scope.node?.id ?? '',
|
|
76
|
+
)
|
|
77
|
+
break
|
|
78
|
+
case 'reshaping': {
|
|
79
|
+
const nodeType = useScene.getState().nodes[scope.nodeId as AnyNodeId]?.type
|
|
80
|
+
scopePerfActionId = beginPerfAction(
|
|
81
|
+
`drag:${nodeType ? `${nodeType}-` : ''}${scope.reshape}`,
|
|
82
|
+
scope.nodeId,
|
|
83
|
+
)
|
|
84
|
+
break
|
|
85
|
+
}
|
|
86
|
+
case 'handle-drag':
|
|
87
|
+
scopePerfActionId = beginPerfAction(`drag:${scope.handle}`, scope.nodeId)
|
|
88
|
+
break
|
|
89
|
+
default:
|
|
90
|
+
// drafting / mesh-editing are long-lived modes, not gestures
|
|
91
|
+
break
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function commitScopePerfAction(): void {
|
|
96
|
+
if (scopePerfActionId !== null && getActivePerfActionId() === scopePerfActionId) {
|
|
97
|
+
commitPerfAction()
|
|
98
|
+
}
|
|
99
|
+
scopePerfActionId = null
|
|
100
|
+
}
|
|
101
|
+
|
|
48
102
|
const useInteractionScope = create<InteractionScopeState>((set, get) => ({
|
|
49
103
|
scope: IDLE_SCOPE,
|
|
50
|
-
begin: (scope) =>
|
|
104
|
+
begin: (scope) => {
|
|
105
|
+
beginScopePerfAction(scope)
|
|
106
|
+
set({ scope })
|
|
107
|
+
},
|
|
51
108
|
update: (patch) =>
|
|
52
109
|
set((state) => {
|
|
53
110
|
if (state.scope.kind === 'idle') return state
|
|
@@ -56,12 +113,16 @@ const useInteractionScope = create<InteractionScopeState>((set, get) => ({
|
|
|
56
113
|
}),
|
|
57
114
|
end: () => {
|
|
58
115
|
if (get().scope.kind === 'idle') return
|
|
116
|
+
commitScopePerfAction()
|
|
59
117
|
set({ scope: IDLE_SCOPE })
|
|
60
118
|
},
|
|
61
119
|
endIf: (match) => {
|
|
62
120
|
const scope = get().scope
|
|
63
121
|
if (scope.kind === 'idle') return
|
|
64
|
-
if (match(scope))
|
|
122
|
+
if (match(scope)) {
|
|
123
|
+
commitScopePerfAction()
|
|
124
|
+
set({ scope: IDLE_SCOPE })
|
|
125
|
+
}
|
|
65
126
|
},
|
|
66
127
|
}))
|
|
67
128
|
|
|
@@ -14,10 +14,22 @@
|
|
|
14
14
|
import type { AnyNode } from '@pascal-app/core'
|
|
15
15
|
import { create } from 'zustand'
|
|
16
16
|
|
|
17
|
+
export type PlacementPreviewDimension = {
|
|
18
|
+
id: string
|
|
19
|
+
start: [number, number, number]
|
|
20
|
+
end: [number, number, number]
|
|
21
|
+
offsetNormal: [number, number]
|
|
22
|
+
offsetDistance: number
|
|
23
|
+
value: number
|
|
24
|
+
renderIn3d?: boolean
|
|
25
|
+
renderInFloorplan?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
17
28
|
type PlacementPreviewState = {
|
|
18
29
|
/** Transient preview node, already positioned + rotated at the (snapped,
|
|
19
30
|
* aligned) cursor. `null` when no placement is active. */
|
|
20
31
|
node: AnyNode | null
|
|
32
|
+
contextNodes: AnyNode[]
|
|
21
33
|
/** Optional synthetic parent for the preview's `def.floorplan` context.
|
|
22
34
|
* Door / window glyph builders need `ctx.parent` to be a wall to draw their
|
|
23
35
|
* real symbol (swing arc / panes); off any real wall we hand them a
|
|
@@ -25,15 +37,56 @@ type PlacementPreviewState = {
|
|
|
25
37
|
* the faithful blueprint symbol instead of a bare rectangle. `null` for
|
|
26
38
|
* self-contained kinds (column / elevator). */
|
|
27
39
|
parentNode: AnyNode | null
|
|
28
|
-
|
|
40
|
+
dimensions: PlacementPreviewDimension[]
|
|
41
|
+
activeDimensionId: string | null
|
|
42
|
+
dimensionInput: string
|
|
43
|
+
set(
|
|
44
|
+
node: AnyNode | null,
|
|
45
|
+
parentNode?: AnyNode | null,
|
|
46
|
+
dimensions?: PlacementPreviewDimension[],
|
|
47
|
+
contextNodes?: AnyNode[],
|
|
48
|
+
): void
|
|
49
|
+
selectDimension(id: string | null): void
|
|
50
|
+
setDimensionInput(value: string): void
|
|
51
|
+
clearDimensionEditor(): void
|
|
29
52
|
clear(): void
|
|
30
53
|
}
|
|
31
54
|
|
|
32
55
|
const usePlacementPreview = create<PlacementPreviewState>((set) => ({
|
|
33
56
|
node: null,
|
|
57
|
+
contextNodes: [],
|
|
34
58
|
parentNode: null,
|
|
35
|
-
|
|
36
|
-
|
|
59
|
+
dimensions: [],
|
|
60
|
+
activeDimensionId: null,
|
|
61
|
+
dimensionInput: '',
|
|
62
|
+
set: (node, parentNode = null, dimensions = [], contextNodes = []) =>
|
|
63
|
+
set((state) => {
|
|
64
|
+
const activeDimensionId = dimensions.some(
|
|
65
|
+
(dimension) => dimension.id === state.activeDimensionId,
|
|
66
|
+
)
|
|
67
|
+
? state.activeDimensionId
|
|
68
|
+
: null
|
|
69
|
+
return {
|
|
70
|
+
node,
|
|
71
|
+
contextNodes,
|
|
72
|
+
parentNode,
|
|
73
|
+
dimensions,
|
|
74
|
+
activeDimensionId,
|
|
75
|
+
dimensionInput: activeDimensionId ? state.dimensionInput : '',
|
|
76
|
+
}
|
|
77
|
+
}),
|
|
78
|
+
selectDimension: (id) => set({ activeDimensionId: id, dimensionInput: '' }),
|
|
79
|
+
setDimensionInput: (dimensionInput) => set({ dimensionInput }),
|
|
80
|
+
clearDimensionEditor: () => set({ activeDimensionId: null, dimensionInput: '' }),
|
|
81
|
+
clear: () =>
|
|
82
|
+
set({
|
|
83
|
+
node: null,
|
|
84
|
+
contextNodes: [],
|
|
85
|
+
parentNode: null,
|
|
86
|
+
dimensions: [],
|
|
87
|
+
activeDimensionId: null,
|
|
88
|
+
dimensionInput: '',
|
|
89
|
+
}),
|
|
37
90
|
}))
|
|
38
91
|
|
|
39
92
|
export default usePlacementPreview
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { type AnyNodeId, useScene } from '@pascal-app/core'
|
|
2
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
3
|
+
import { create } from 'zustand'
|
|
4
|
+
import {
|
|
5
|
+
canCreateSessionGroup,
|
|
6
|
+
createSessionGroup,
|
|
7
|
+
expandSessionGroupMembers,
|
|
8
|
+
type SessionSelectionGroup,
|
|
9
|
+
selectionIntersectsSessionGroup,
|
|
10
|
+
selectionMatchesSessionGroup,
|
|
11
|
+
ungroupSessionSelection,
|
|
12
|
+
} from '../lib/session-groups'
|
|
13
|
+
import { sfxEmitter } from '../lib/sfx-bus'
|
|
14
|
+
|
|
15
|
+
type SessionGroupsState = {
|
|
16
|
+
groups: SessionSelectionGroup[]
|
|
17
|
+
setGroups: (groups: SessionSelectionGroup[]) => void
|
|
18
|
+
clearGroups: () => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const useSessionGroups = create<SessionGroupsState>((set) => ({
|
|
22
|
+
groups: [],
|
|
23
|
+
setGroups: (groups) => set({ groups }),
|
|
24
|
+
clearGroups: () => set({ groups: [] }),
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
function liveNodeIdSet(): Set<string> {
|
|
28
|
+
return new Set(Object.keys(useScene.getState().nodes))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Membership is never rewritten when a member is deleted — every read filters
|
|
33
|
+
* against the live scene instead. Deleting a member and undoing must restore it
|
|
34
|
+
* to its group, and a destructive prune would have already dropped it (or the
|
|
35
|
+
* whole group, once it fell under the two-member floor).
|
|
36
|
+
*/
|
|
37
|
+
export function expandSessionSelectionForNode(nodeId: string): string[] | null {
|
|
38
|
+
const groups = useSessionGroups.getState().groups
|
|
39
|
+
if (groups.length === 0) return null
|
|
40
|
+
return expandSessionGroupMembers(groups, nodeId, liveNodeIdSet())
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Ctrl/Cmd+G — create session group from multi-selection. */
|
|
44
|
+
export function groupCurrentSelection(): boolean {
|
|
45
|
+
const selectedIds = useViewer.getState().selection.selectedIds as string[]
|
|
46
|
+
if (selectedIds.length < 2) return false
|
|
47
|
+
|
|
48
|
+
const liveIds = liveNodeIdSet()
|
|
49
|
+
const { groups, created } = createSessionGroup(useSessionGroups.getState().groups, selectedIds, {
|
|
50
|
+
liveIds,
|
|
51
|
+
})
|
|
52
|
+
useSessionGroups.getState().setGroups(groups)
|
|
53
|
+
if (!created) return false
|
|
54
|
+
|
|
55
|
+
useViewer.getState().setSelection({
|
|
56
|
+
selectedIds: created.memberIds as AnyNodeId[],
|
|
57
|
+
})
|
|
58
|
+
sfxEmitter.emit('sfx:menu-click')
|
|
59
|
+
return true
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Ctrl/Cmd+Shift+G — dissolve session groups intersecting selection. */
|
|
63
|
+
export function ungroupCurrentSelection(): boolean {
|
|
64
|
+
const selectedIds = useViewer.getState().selection.selectedIds as string[]
|
|
65
|
+
if (selectedIds.length === 0) return false
|
|
66
|
+
|
|
67
|
+
const liveIds = liveNodeIdSet()
|
|
68
|
+
const { groups, dissolved } = ungroupSessionSelection(
|
|
69
|
+
useSessionGroups.getState().groups,
|
|
70
|
+
selectedIds,
|
|
71
|
+
liveIds,
|
|
72
|
+
)
|
|
73
|
+
if (dissolved.length === 0) return false
|
|
74
|
+
useSessionGroups.getState().setGroups(groups)
|
|
75
|
+
sfxEmitter.emit('sfx:menu-click')
|
|
76
|
+
return true
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function currentSelectionMatchesSessionGroup(): SessionSelectionGroup | null {
|
|
80
|
+
return selectionMatchesSessionGroup(
|
|
81
|
+
useSessionGroups.getState().groups,
|
|
82
|
+
useViewer.getState().selection.selectedIds as string[],
|
|
83
|
+
liveNodeIdSet(),
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function currentSelectionIntersectsSessionGroup(): boolean {
|
|
88
|
+
return selectionIntersectsSessionGroup(
|
|
89
|
+
useSessionGroups.getState().groups,
|
|
90
|
+
useViewer.getState().selection.selectedIds as string[],
|
|
91
|
+
liveNodeIdSet(),
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function currentSelectionCanCreateSessionGroup(): boolean {
|
|
96
|
+
return canCreateSessionGroup(
|
|
97
|
+
useSessionGroups.getState().groups,
|
|
98
|
+
useViewer.getState().selection.selectedIds as string[],
|
|
99
|
+
liveNodeIdSet(),
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default useSessionGroups
|