@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
|
@@ -7,7 +7,7 @@ const baseArgs = {
|
|
|
7
7
|
currentSelectedIds: ['wall_1'],
|
|
8
8
|
getFloorplanHitIdAtPoint: () => 'door_1',
|
|
9
9
|
isWallBuildActive: false,
|
|
10
|
-
modifierKeys: { meta: false, ctrl: false, shift: false },
|
|
10
|
+
modifierKeys: { meta: false, ctrl: false, shift: false, alt: false },
|
|
11
11
|
planPoint: [0, 0] as [number, number],
|
|
12
12
|
structureLayer: 'elements',
|
|
13
13
|
}
|
|
@@ -16,7 +16,7 @@ describe('resolveFloorplanBackgroundSelection', () => {
|
|
|
16
16
|
test('shift-click on a floorplan node toggles into the current selection', () => {
|
|
17
17
|
const result = resolveFloorplanBackgroundSelection({
|
|
18
18
|
...baseArgs,
|
|
19
|
-
modifierKeys: { meta: false, ctrl: false, shift: true },
|
|
19
|
+
modifierKeys: { meta: false, ctrl: false, shift: true, alt: false },
|
|
20
20
|
})
|
|
21
21
|
|
|
22
22
|
expect(result).toEqual({
|
|
@@ -30,7 +30,7 @@ describe('resolveFloorplanBackgroundSelection', () => {
|
|
|
30
30
|
const result = resolveFloorplanBackgroundSelection({
|
|
31
31
|
...baseArgs,
|
|
32
32
|
currentSelectedIds: ['wall_1', 'door_1'],
|
|
33
|
-
modifierKeys: { meta: false, ctrl: false, shift: true },
|
|
33
|
+
modifierKeys: { meta: false, ctrl: false, shift: true, alt: false },
|
|
34
34
|
})
|
|
35
35
|
|
|
36
36
|
expect(result).toEqual({
|
|
@@ -44,7 +44,7 @@ describe('resolveFloorplanBackgroundSelection', () => {
|
|
|
44
44
|
const result = resolveFloorplanBackgroundSelection({
|
|
45
45
|
...baseArgs,
|
|
46
46
|
getFloorplanHitIdAtPoint: () => null,
|
|
47
|
-
modifierKeys: { meta: false, ctrl: false, shift: true },
|
|
47
|
+
modifierKeys: { meta: false, ctrl: false, shift: true, alt: false },
|
|
48
48
|
})
|
|
49
49
|
|
|
50
50
|
expect(result).toEqual({
|
|
@@ -54,6 +54,33 @@ describe('resolveFloorplanBackgroundSelection', () => {
|
|
|
54
54
|
})
|
|
55
55
|
})
|
|
56
56
|
|
|
57
|
+
test('plain click expands a session group', () => {
|
|
58
|
+
const result = resolveFloorplanBackgroundSelection({
|
|
59
|
+
...baseArgs,
|
|
60
|
+
expandIdsForNode: (nodeId) => (nodeId === 'door_1' ? ['door_1', 'wall_2'] : null),
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
expect(result).toEqual({
|
|
64
|
+
handled: true,
|
|
65
|
+
kind: 'select-elements',
|
|
66
|
+
selectedIds: ['door_1', 'wall_2'],
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('alt-click selects one member without expanding', () => {
|
|
71
|
+
const result = resolveFloorplanBackgroundSelection({
|
|
72
|
+
...baseArgs,
|
|
73
|
+
expandIdsForNode: () => ['door_1', 'wall_2'],
|
|
74
|
+
modifierKeys: { meta: false, ctrl: false, shift: false, alt: true },
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
expect(result).toEqual({
|
|
78
|
+
handled: true,
|
|
79
|
+
kind: 'select-elements',
|
|
80
|
+
selectedIds: ['door_1'],
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
57
84
|
test('uses the registry hit result for zone selection', () => {
|
|
58
85
|
const result = resolveFloorplanBackgroundSelection({
|
|
59
86
|
...baseArgs,
|
|
@@ -7,12 +7,16 @@ type ModifierKeys = {
|
|
|
7
7
|
meta: boolean
|
|
8
8
|
ctrl: boolean
|
|
9
9
|
shift: boolean
|
|
10
|
+
/** Alt alone: select one session-group member without expanding. */
|
|
11
|
+
alt: boolean
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
type ResolveFloorplanBackgroundSelectionArgs = {
|
|
13
15
|
canSelectElementFloorplanGeometry: boolean
|
|
14
16
|
canSelectFloorplanZones: boolean
|
|
15
17
|
currentSelectedIds: string[]
|
|
18
|
+
/** Session-group expand on plain click (not on modifier/Alt). */
|
|
19
|
+
expandIdsForNode?: (nodeId: string) => string[] | null
|
|
16
20
|
getFloorplanHitIdAtPoint: (planPoint: WallPlanPoint) => string | null
|
|
17
21
|
isWallBuildActive: boolean
|
|
18
22
|
modifierKeys: ModifierKeys
|
|
@@ -20,6 +24,26 @@ type ResolveFloorplanBackgroundSelectionArgs = {
|
|
|
20
24
|
structureLayer: string
|
|
21
25
|
}
|
|
22
26
|
|
|
27
|
+
function hasToggleModifier(modifierKeys: ModifierKeys): boolean {
|
|
28
|
+
return modifierKeys.meta || modifierKeys.ctrl || modifierKeys.shift
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolveHitSelection(
|
|
32
|
+
hitId: string,
|
|
33
|
+
currentSelectedIds: string[],
|
|
34
|
+
modifierKeys: ModifierKeys,
|
|
35
|
+
expandIdsForNode?: (nodeId: string) => string[] | null,
|
|
36
|
+
): string[] {
|
|
37
|
+
if (hasToggleModifier(modifierKeys)) {
|
|
38
|
+
return currentSelectedIds.includes(hitId)
|
|
39
|
+
? currentSelectedIds.filter((selectedId) => selectedId !== hitId)
|
|
40
|
+
: [...currentSelectedIds, hitId]
|
|
41
|
+
}
|
|
42
|
+
if (modifierKeys.alt) return [hitId]
|
|
43
|
+
const expanded = expandIdsForNode?.(hitId)
|
|
44
|
+
return expanded && expanded.length > 1 ? expanded : [hitId]
|
|
45
|
+
}
|
|
46
|
+
|
|
23
47
|
export type FloorplanBackgroundSelectionResult =
|
|
24
48
|
| {
|
|
25
49
|
handled: true
|
|
@@ -48,6 +72,7 @@ export function resolveFloorplanBackgroundSelection({
|
|
|
48
72
|
canSelectElementFloorplanGeometry,
|
|
49
73
|
canSelectFloorplanZones,
|
|
50
74
|
currentSelectedIds,
|
|
75
|
+
expandIdsForNode,
|
|
51
76
|
getFloorplanHitIdAtPoint,
|
|
52
77
|
isWallBuildActive,
|
|
53
78
|
modifierKeys,
|
|
@@ -71,12 +96,7 @@ export function resolveFloorplanBackgroundSelection({
|
|
|
71
96
|
return {
|
|
72
97
|
handled: true,
|
|
73
98
|
kind: 'select-elements',
|
|
74
|
-
selectedIds:
|
|
75
|
-
modifierKeys.meta || modifierKeys.ctrl || modifierKeys.shift
|
|
76
|
-
? currentSelectedIds.includes(hitId)
|
|
77
|
-
? currentSelectedIds.filter((selectedId) => selectedId !== hitId)
|
|
78
|
-
: [...currentSelectedIds, hitId]
|
|
79
|
-
: [hitId],
|
|
99
|
+
selectedIds: resolveHitSelection(hitId, currentSelectedIds, modifierKeys, expandIdsForNode),
|
|
80
100
|
}
|
|
81
101
|
}
|
|
82
102
|
}
|
|
@@ -92,7 +112,7 @@ export function resolveFloorplanBackgroundSelection({
|
|
|
92
112
|
return {
|
|
93
113
|
handled: true,
|
|
94
114
|
kind: 'clear-elements',
|
|
95
|
-
preserveSelection: modifierKeys
|
|
115
|
+
preserveSelection: hasToggleModifier(modifierKeys),
|
|
96
116
|
}
|
|
97
117
|
}
|
|
98
118
|
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
type RoofNode,
|
|
34
34
|
type RoofSegmentNode,
|
|
35
35
|
resolveSlabPlacementElevation,
|
|
36
|
+
resolveTerrainWallConstructionOptions,
|
|
36
37
|
type SiteNode,
|
|
37
38
|
type SlabNode,
|
|
38
39
|
SlabNode as SlabNodeSchema,
|
|
@@ -88,6 +89,8 @@ import {
|
|
|
88
89
|
type FloorplanNodeTransform as SharedFloorplanNodeTransform,
|
|
89
90
|
worldToFloorplanLocalPoint,
|
|
90
91
|
} from '../../lib/floorplan'
|
|
92
|
+
import { resolveGenericFloorplanGridEventPoint } from '../../lib/floorplan-grid-event-point'
|
|
93
|
+
import type { EditorGridEvent } from '../../lib/grid-event-presentation'
|
|
91
94
|
import { groundHeightAt } from '../../lib/ground-surface'
|
|
92
95
|
import { guideEmitter } from '../../lib/guide-events'
|
|
93
96
|
import { measurementHint, parseMeasurement } from '../../lib/measurement-parser'
|
|
@@ -115,6 +118,7 @@ import useInteractionScope, {
|
|
|
115
118
|
useReshapingNode,
|
|
116
119
|
} from '../../store/use-interaction-scope'
|
|
117
120
|
import usePlacementPreview from '../../store/use-placement-preview'
|
|
121
|
+
import { expandSessionSelectionForNode } from '../../store/use-session-groups'
|
|
118
122
|
import { useStairBuildPreview } from '../../store/use-stair-build-preview'
|
|
119
123
|
import { FloorplanAlignmentGuideLayer } from '../editor-2d/floorplan-alignment-guide-layer'
|
|
120
124
|
import { FloorplanCursorIndicatorOverlay as Editor2dFloorplanCursorIndicatorOverlay } from '../editor-2d/floorplan-cursor-indicator-overlay'
|
|
@@ -147,6 +151,7 @@ import {
|
|
|
147
151
|
isBoxSelectPointerSuppressed,
|
|
148
152
|
markBoxSelectHandled,
|
|
149
153
|
} from '../tools/select/box-select-state'
|
|
154
|
+
import { marqueePolygon } from '../tools/select/marquee-footprint'
|
|
150
155
|
import {
|
|
151
156
|
type Point2 as MarqueePoint2,
|
|
152
157
|
polygonsIntersect as marqueePolygonsIntersect,
|
|
@@ -183,7 +188,6 @@ import {
|
|
|
183
188
|
chainEndJoinsExistingWall,
|
|
184
189
|
createWallOnCurrentLevel,
|
|
185
190
|
isSegmentLongEnough,
|
|
186
|
-
resolveTerrainWallConstructionOptions,
|
|
187
191
|
snapWallDraftPoint,
|
|
188
192
|
snapWallDraftPointDetailed,
|
|
189
193
|
snapPointToGrid as snapWallPointToGrid,
|
|
@@ -193,7 +197,7 @@ import {
|
|
|
193
197
|
} from '../tools/wall/wall-drafting'
|
|
194
198
|
|
|
195
199
|
import { PALETTE_COLORS } from '../ui/primitives/color-dot'
|
|
196
|
-
import {
|
|
200
|
+
import { FloorplanCompassButton } from '../viewer/floorplan-compass-button'
|
|
197
201
|
import { resolveFloorplanBackgroundSelection } from './floorplan-background-selection'
|
|
198
202
|
import {
|
|
199
203
|
subscribeFloorplanCameraNavigation,
|
|
@@ -505,50 +509,6 @@ type GuideHandleHintAnchor = {
|
|
|
505
509
|
directionY: number
|
|
506
510
|
}
|
|
507
511
|
|
|
508
|
-
function FloorplanCompassButton({
|
|
509
|
-
northRotationDeg,
|
|
510
|
-
onAlignNorth,
|
|
511
|
-
needleRef,
|
|
512
|
-
}: {
|
|
513
|
-
northRotationDeg: number
|
|
514
|
-
onAlignNorth: () => void
|
|
515
|
-
needleRef?: React.RefObject<SVGSVGElement | null>
|
|
516
|
-
}) {
|
|
517
|
-
return (
|
|
518
|
-
<Tooltip>
|
|
519
|
-
<TooltipTrigger asChild>
|
|
520
|
-
<button
|
|
521
|
-
aria-label="Align view to north"
|
|
522
|
-
className="group absolute bottom-3 left-3 z-30 flex h-8 w-8 items-center justify-center rounded-full border border-black/10 bg-white/85 shadow-sm backdrop-blur-md transition hover:bg-white hover:shadow-md dark:border-white/10 dark:bg-neutral-900/85 dark:hover:bg-neutral-900"
|
|
523
|
-
onClick={(event) => {
|
|
524
|
-
event.preventDefault()
|
|
525
|
-
event.stopPropagation()
|
|
526
|
-
onAlignNorth()
|
|
527
|
-
}}
|
|
528
|
-
onPointerDown={(event) => {
|
|
529
|
-
event.stopPropagation()
|
|
530
|
-
}}
|
|
531
|
-
type="button"
|
|
532
|
-
>
|
|
533
|
-
<span className="relative flex h-6 w-6 items-center justify-center rounded-full bg-[#b8b8b8] shadow-inner dark:bg-neutral-700">
|
|
534
|
-
<svg
|
|
535
|
-
aria-hidden="true"
|
|
536
|
-
className="h-6 w-6"
|
|
537
|
-
ref={needleRef}
|
|
538
|
-
style={{ transform: `rotate(${northRotationDeg}deg)` }}
|
|
539
|
-
viewBox="0 0 48 48"
|
|
540
|
-
>
|
|
541
|
-
<path d="M24 4.5 31.5 25 24 21.5 16.5 25Z" fill="#f15b5b" />
|
|
542
|
-
<path d="M24 43.5 16.5 23 24 26.5 31.5 23Z" fill="#ffffff" />
|
|
543
|
-
</svg>
|
|
544
|
-
</span>
|
|
545
|
-
</button>
|
|
546
|
-
</TooltipTrigger>
|
|
547
|
-
<TooltipContent side="right">Align view to north</TooltipContent>
|
|
548
|
-
</Tooltip>
|
|
549
|
-
)
|
|
550
|
-
}
|
|
551
|
-
|
|
552
512
|
type GuideInteractionState = {
|
|
553
513
|
pointerId: number
|
|
554
514
|
guideId: GuideNode['id']
|
|
@@ -843,11 +803,13 @@ function getSelectionModifierKeys(event?: {
|
|
|
843
803
|
metaKey?: boolean
|
|
844
804
|
ctrlKey?: boolean
|
|
845
805
|
shiftKey?: boolean
|
|
806
|
+
altKey?: boolean
|
|
846
807
|
}) {
|
|
847
808
|
return {
|
|
848
809
|
meta: Boolean(event?.metaKey),
|
|
849
810
|
ctrl: Boolean(event?.ctrlKey),
|
|
850
811
|
shift: Boolean(event?.shiftKey),
|
|
812
|
+
alt: Boolean(event?.altKey),
|
|
851
813
|
}
|
|
852
814
|
}
|
|
853
815
|
|
|
@@ -905,7 +867,8 @@ function collectFloorplanScreenSelectionIds(rect: ScreenRect, svg: SVGSVGElement
|
|
|
905
867
|
| { start?: unknown; end?: unknown; polygon?: unknown }
|
|
906
868
|
| undefined
|
|
907
869
|
if (!node) continue
|
|
908
|
-
const { start, end
|
|
870
|
+
const { start, end } = node
|
|
871
|
+
const polygon = marqueePolygon(node)
|
|
909
872
|
if (isMarqueeVec2(start) && isMarqueeVec2(end)) {
|
|
910
873
|
dataTested.add(id)
|
|
911
874
|
if (marqueeSegmentIntersectsPolygon(start, end, planQuad)) hitIdsFromData.add(id)
|
|
@@ -4714,6 +4677,7 @@ function FloorplanLinearDraftLayer({
|
|
|
4714
4677
|
const wallDraftEnd = useFloorplanDraftPreview((s) => s.wallDraftEnd)
|
|
4715
4678
|
const fenceDraftEnd = useFloorplanDraftPreview((s) => s.fenceDraftEnd)
|
|
4716
4679
|
const roofDraftEnd = useFloorplanDraftPreview((s) => s.roofDraftEnd)
|
|
4680
|
+
const roofDraftQuarterTurn = useFloorplanDraftPreview((s) => s.roofDraftQuarterTurn)
|
|
4717
4681
|
|
|
4718
4682
|
const draftPolygon = useMemo(() => {
|
|
4719
4683
|
if (
|
|
@@ -4750,6 +4714,21 @@ function FloorplanLinearDraftLayer({
|
|
|
4750
4714
|
return draftPolygon ? formatPolygonPoints(draftPolygon) : null
|
|
4751
4715
|
}, [draftPolygon, isRoofBuildActive, roofDraftEnd, roofDraftStart])
|
|
4752
4716
|
|
|
4717
|
+
const roofDraftDirectionLine = useMemo(() => {
|
|
4718
|
+
if (!(isRoofBuildActive && roofDraftStart && roofDraftEnd)) return null
|
|
4719
|
+
const minX = Math.min(roofDraftStart[0], roofDraftEnd[0])
|
|
4720
|
+
const maxX = Math.max(roofDraftStart[0], roofDraftEnd[0])
|
|
4721
|
+
const minY = Math.min(roofDraftStart[1], roofDraftEnd[1])
|
|
4722
|
+
const maxY = Math.max(roofDraftStart[1], roofDraftEnd[1])
|
|
4723
|
+
if (maxX - minX < 1e-6 || maxY - minY < 1e-6) return null
|
|
4724
|
+
|
|
4725
|
+
const centerX = (minX + maxX) / 2
|
|
4726
|
+
const centerY = (minY + maxY) / 2
|
|
4727
|
+
return roofDraftQuarterTurn
|
|
4728
|
+
? { x1: centerX, y1: minY, x2: centerX, y2: maxY }
|
|
4729
|
+
: { x1: minX, y1: centerY, x2: maxX, y2: centerY }
|
|
4730
|
+
}, [isRoofBuildActive, roofDraftEnd, roofDraftQuarterTurn, roofDraftStart])
|
|
4731
|
+
|
|
4753
4732
|
const fenceDraftSegment = useMemo(() => {
|
|
4754
4733
|
if (!(isFenceBuildActive && fenceDraftStart && fenceDraftEnd)) {
|
|
4755
4734
|
return null
|
|
@@ -4928,6 +4907,19 @@ function FloorplanLinearDraftLayer({
|
|
|
4928
4907
|
unitsPerPixel={unitsPerPixel}
|
|
4929
4908
|
/>
|
|
4930
4909
|
|
|
4910
|
+
{roofDraftDirectionLine && (
|
|
4911
|
+
<line
|
|
4912
|
+
pointerEvents="none"
|
|
4913
|
+
stroke={draftStroke}
|
|
4914
|
+
strokeLinecap="round"
|
|
4915
|
+
strokeWidth={unitsPerPixel * 1.5}
|
|
4916
|
+
x1={toSvgX(roofDraftDirectionLine.x1)}
|
|
4917
|
+
x2={toSvgX(roofDraftDirectionLine.x2)}
|
|
4918
|
+
y1={toSvgY(roofDraftDirectionLine.y1)}
|
|
4919
|
+
y2={toSvgY(roofDraftDirectionLine.y2)}
|
|
4920
|
+
/>
|
|
4921
|
+
)}
|
|
4922
|
+
|
|
4931
4923
|
{draftWallMeasurement && (
|
|
4932
4924
|
<FloorplanDraftWallMeasurement
|
|
4933
4925
|
labelBackground={isDark ? '#0f172a' : '#ffffff'}
|
|
@@ -5104,6 +5096,9 @@ export function FloorplanPanel({
|
|
|
5104
5096
|
if (building?.type !== 'building') return false
|
|
5105
5097
|
return building.children.some((cid) => state.nodes[cid]?.type === 'level')
|
|
5106
5098
|
})
|
|
5099
|
+
// The studio workspace (renders / materials / item builder) is a clean
|
|
5100
|
+
// stage — editor viewport chrome, compass included, stays out of it.
|
|
5101
|
+
const isStudioWorkspace = useEditor((s) => s.workspaceMode === 'studio')
|
|
5107
5102
|
const elevators = useScene(
|
|
5108
5103
|
useShallow((state) => {
|
|
5109
5104
|
const building = currentBuildingId ? state.nodes[currentBuildingId] : null
|
|
@@ -8972,12 +8967,29 @@ export function FloorplanPanel({
|
|
|
8972
8967
|
const groundY = groundHeightAt(worldX, worldZ, floorplanGridWorldY)
|
|
8973
8968
|
const worldY = groundY ?? floorplanGridWorldY
|
|
8974
8969
|
const localY = groundY === null ? floorplanGridLocalY : groundY - buildingPosition[1]
|
|
8970
|
+
const planScene =
|
|
8971
|
+
nativeEvent.currentTarget.querySelector<SVGGraphicsElement>('[data-floorplan-scene]')
|
|
8972
|
+
const screenMatrix = planScene?.getScreenCTM()
|
|
8975
8973
|
|
|
8976
|
-
|
|
8974
|
+
const gridEvent: EditorGridEvent = {
|
|
8977
8975
|
nativeEvent: nativeEvent.nativeEvent as any,
|
|
8978
8976
|
position: [worldX, worldY, worldZ],
|
|
8979
8977
|
localPosition: [planPoint[0], localY, planPoint[1]],
|
|
8980
|
-
|
|
8978
|
+
screenProjection: screenMatrix
|
|
8979
|
+
? {
|
|
8980
|
+
pointer: [nativeEvent.clientX, nativeEvent.clientY],
|
|
8981
|
+
localToScreen: [
|
|
8982
|
+
screenMatrix.a,
|
|
8983
|
+
screenMatrix.b,
|
|
8984
|
+
screenMatrix.c,
|
|
8985
|
+
screenMatrix.d,
|
|
8986
|
+
screenMatrix.e,
|
|
8987
|
+
screenMatrix.f,
|
|
8988
|
+
],
|
|
8989
|
+
}
|
|
8990
|
+
: undefined,
|
|
8991
|
+
}
|
|
8992
|
+
emitter.emit(`grid:${eventType}` as any, gridEvent)
|
|
8981
8993
|
},
|
|
8982
8994
|
[buildingPosition, buildingRotationY, floorplanGridLocalY, floorplanGridWorldY],
|
|
8983
8995
|
)
|
|
@@ -9441,10 +9453,14 @@ export function FloorplanPanel({
|
|
|
9441
9453
|
// this exclusion the catch-all would emit `grid:move` and re-drive the
|
|
9442
9454
|
// 3D MoveDoorTool's free-follow, fighting the overlay again.
|
|
9443
9455
|
if (!isWallBuildActive && !isOpeningMoveActive && isFloorplanGridInteractionActive) {
|
|
9444
|
-
const
|
|
9445
|
-
|
|
9456
|
+
const eventPoint = resolveGenericFloorplanGridEventPoint({
|
|
9457
|
+
point: planPoint,
|
|
9458
|
+
registryToolOwnsSnapping: isRegistryToolBuildActive,
|
|
9459
|
+
snap: getSnappedFloorplanPoint,
|
|
9460
|
+
})
|
|
9461
|
+
emitFloorplanGridEvent('move', eventPoint, event)
|
|
9446
9462
|
setCursorPoint((previousPoint) =>
|
|
9447
|
-
previousPoint && pointsEqual(previousPoint,
|
|
9463
|
+
previousPoint && pointsEqual(previousPoint, eventPoint) ? previousPoint : eventPoint,
|
|
9448
9464
|
)
|
|
9449
9465
|
return
|
|
9450
9466
|
}
|
|
@@ -9544,6 +9560,7 @@ export function FloorplanPanel({
|
|
|
9544
9560
|
// stale closure and float a door symbol while the window tool is armed.
|
|
9545
9561
|
showOpeningGhost,
|
|
9546
9562
|
isPolygonBuildActive,
|
|
9563
|
+
isRegistryToolBuildActive,
|
|
9547
9564
|
isRoofBuildActive,
|
|
9548
9565
|
isWallBuildActive,
|
|
9549
9566
|
levelId,
|
|
@@ -9890,6 +9907,7 @@ export function FloorplanPanel({
|
|
|
9890
9907
|
isOpeningPlacementActive: isOpeningBuildActive && !isOpeningMoveActive,
|
|
9891
9908
|
isPolygonBuildActive,
|
|
9892
9909
|
isRoofBuildActive,
|
|
9910
|
+
registryToolOwnsSnapping: isRegistryToolBuildActive,
|
|
9893
9911
|
isWallBuildActive,
|
|
9894
9912
|
isZoneBuildActive,
|
|
9895
9913
|
levelId,
|
|
@@ -9978,6 +9996,7 @@ export function FloorplanPanel({
|
|
|
9978
9996
|
canSelectElementFloorplanGeometry,
|
|
9979
9997
|
canSelectFloorplanZones,
|
|
9980
9998
|
currentSelectedIds: useViewer.getState().selection.selectedIds,
|
|
9999
|
+
expandIdsForNode: expandSessionSelectionForNode,
|
|
9981
10000
|
getFloorplanHitIdAtPoint,
|
|
9982
10001
|
isWallBuildActive,
|
|
9983
10002
|
modifierKeys,
|
|
@@ -10552,9 +10571,11 @@ export function FloorplanPanel({
|
|
|
10552
10571
|
}
|
|
10553
10572
|
|
|
10554
10573
|
if (useEditor.getState().phase !== 'site') {
|
|
10555
|
-
useEditor.
|
|
10574
|
+
useEditor.getState().setPhase('site')
|
|
10575
|
+
useEditor.getState().armToolMode({ mode: 'select' })
|
|
10576
|
+
} else {
|
|
10577
|
+
selectSiteFloorplanContext()
|
|
10556
10578
|
}
|
|
10557
|
-
selectSiteFloorplanContext()
|
|
10558
10579
|
|
|
10559
10580
|
const nextDraft = {
|
|
10560
10581
|
siteId,
|
|
@@ -10636,9 +10657,11 @@ export function FloorplanPanel({
|
|
|
10636
10657
|
]
|
|
10637
10658
|
|
|
10638
10659
|
if (useEditor.getState().phase !== 'site') {
|
|
10639
|
-
useEditor.
|
|
10660
|
+
useEditor.getState().setPhase('site')
|
|
10661
|
+
useEditor.getState().armToolMode({ mode: 'select' })
|
|
10662
|
+
} else {
|
|
10663
|
+
selectSiteFloorplanContext()
|
|
10640
10664
|
}
|
|
10641
|
-
selectSiteFloorplanContext()
|
|
10642
10665
|
|
|
10643
10666
|
const nextDraft = {
|
|
10644
10667
|
siteId,
|
|
@@ -11147,7 +11170,8 @@ export function FloorplanPanel({
|
|
|
11147
11170
|
<FloorplanRegistryActionMenu />
|
|
11148
11171
|
<FloorplanGroupActionMenu />
|
|
11149
11172
|
|
|
11150
|
-
{
|
|
11173
|
+
{!isStudioWorkspace &&
|
|
11174
|
+
(levelNode?.type === 'level' || hasAmbientBuildingLevel) &&
|
|
11151
11175
|
(compassHost ? (
|
|
11152
11176
|
createPortal(
|
|
11153
11177
|
<FloorplanCompassButton
|
|
@@ -9,7 +9,7 @@ import { color, float, fract, fwidth, mix, positionLocal, uniform } from 'three/
|
|
|
9
9
|
import { MeshBasicNodeMaterial } from 'three/webgpu'
|
|
10
10
|
import { useCeilingEvents } from '../../hooks/use-ceiling-events'
|
|
11
11
|
import { useGridEvents } from '../../hooks/use-grid-events'
|
|
12
|
-
import { getPlacementSurface } from '../../lib/active-placement-surface'
|
|
12
|
+
import { getPlacementSurface, usesOrientedPlacementPlane } from '../../lib/active-placement-surface'
|
|
13
13
|
import useEditor, { isGridSnapActive } from '../../store/use-editor'
|
|
14
14
|
import { getMovingNode } from '../../store/use-interaction-scope'
|
|
15
15
|
|
|
@@ -229,16 +229,21 @@ export const Grid = ({
|
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
const gridMesh = gridRef.current
|
|
232
|
-
const
|
|
233
|
-
if (
|
|
234
|
-
//
|
|
232
|
+
const onOrientedPlane = surfacePoint != null && usesOrientedPlacementPlane(surfaceNormal)
|
|
233
|
+
if (onOrientedPlane && surfacePoint) {
|
|
234
|
+
// Surface-anchored lattice: orient the plane into the host and pin the mesh to
|
|
235
235
|
// the plane's FOOT (the point on the wall plane closest to the world origin)
|
|
236
236
|
// — never the moving ghost. Sliding the opening along the wall then only
|
|
237
237
|
// moves the reveal patch (the cursor uniform); the snap lattice stays put.
|
|
238
238
|
// (Copying `surfacePoint` here made the grid follow the item — useless.)
|
|
239
239
|
gridMesh.quaternion.setFromUnitVectors(PLANE_LOCAL_NORMAL, surfaceNormal)
|
|
240
240
|
const planeOffset = surfacePoint.dot(surfaceNormal)
|
|
241
|
-
|
|
241
|
+
const latticeAnchor = published?.anchor ?? surfacePoint
|
|
242
|
+
gridMesh.position.copy(latticeAnchor)
|
|
243
|
+
gridMesh.position.addScaledVector(
|
|
244
|
+
surfaceNormal,
|
|
245
|
+
-latticeAnchor.dot(surfaceNormal) + planeOffset,
|
|
246
|
+
)
|
|
242
247
|
// Cursor → plane-local XY: rotate (ghost − anchor) by the inverse plane
|
|
243
248
|
// orientation. Both lie in the plane, so the resulting local Z is ~0.
|
|
244
249
|
invQuatRef.current.copy(gridMesh.quaternion).invert()
|
|
@@ -275,8 +280,8 @@ export const Grid = ({
|
|
|
275
280
|
// Floor grid depth-tests against the scene (ground occludes a sub-floor
|
|
276
281
|
// lattice); the wall grid ignores depth so it stays visible through the wall
|
|
277
282
|
// when the opening is being handled from the opposite side.
|
|
278
|
-
if (material.depthTest ===
|
|
279
|
-
material.depthTest = !
|
|
283
|
+
if (material.depthTest === onOrientedPlane) {
|
|
284
|
+
material.depthTest = !onOrientedPlane
|
|
280
285
|
material.needsUpdate = true
|
|
281
286
|
}
|
|
282
287
|
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
useLiveTransforms,
|
|
15
15
|
useScene,
|
|
16
16
|
} from '@pascal-app/core'
|
|
17
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
17
|
+
import { markPerfAction, useViewer } from '@pascal-app/viewer'
|
|
18
18
|
import { Plane, Vector2, Vector3 } from 'three'
|
|
19
19
|
import { GROUP_MOVE_DRAG_LABEL } from '../../lib/contextual-help'
|
|
20
20
|
import { clientToPlan } from '../../lib/floorplan/plan-coords'
|
|
@@ -40,9 +40,11 @@ import {
|
|
|
40
40
|
collectParticipants,
|
|
41
41
|
computeGroupBox,
|
|
42
42
|
expandToComponent,
|
|
43
|
+
groupPlanBounds,
|
|
43
44
|
levelFrame,
|
|
44
|
-
|
|
45
|
+
planBoundsCenter,
|
|
45
46
|
rotateGroupSnapshots,
|
|
47
|
+
rotatePlanBounds,
|
|
46
48
|
translateGroupPatches,
|
|
47
49
|
type Vec2,
|
|
48
50
|
} from './group-transform-shared'
|
|
@@ -104,46 +106,15 @@ export function startGroupPickUp(
|
|
|
104
106
|
if (starts.length === 0) return false
|
|
105
107
|
const affectedIds: AnyNodeId[] = [...starts.map((s) => s.id), ...links.map((l) => l.id)]
|
|
106
108
|
|
|
107
|
-
// Rest bounds in the level frame. Prefer the mounted meshes' world box
|
|
108
|
-
// (footprint-accurate), but fall back to the participant DATA when the
|
|
109
|
-
// meshes aren't up yet — Duplicate starts the pick-up synchronously after
|
|
110
|
-
// `createNodes`, one frame before the clones' renderers mount.
|
|
111
109
|
const { inverse: frameInv } = levelFrame(levelId)
|
|
112
110
|
const restBox = computeGroupBox(fullIds)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
let
|
|
117
|
-
|
|
118
|
-
const boxMin = restBox.min.clone().applyMatrix4(frameInv)
|
|
119
|
-
const boxMax = restBox.max.clone().applyMatrix4(frameInv)
|
|
120
|
-
minX = Math.min(boxMin.x, boxMax.x)
|
|
121
|
-
minZ = Math.min(boxMin.z, boxMax.z)
|
|
122
|
-
maxX = Math.max(boxMin.x, boxMax.x)
|
|
123
|
-
maxZ = Math.max(boxMin.z, boxMax.z)
|
|
124
|
-
} else {
|
|
125
|
-
const reach = (x: number, z: number) => {
|
|
126
|
-
minX = Math.min(minX, x)
|
|
127
|
-
minZ = Math.min(minZ, z)
|
|
128
|
-
maxX = Math.max(maxX, x)
|
|
129
|
-
maxZ = Math.max(maxZ, z)
|
|
130
|
-
}
|
|
131
|
-
for (const s of starts) {
|
|
132
|
-
if (s.kind === 'endpoint') {
|
|
133
|
-
reach(s.start[0], s.start[1])
|
|
134
|
-
reach(s.end[0], s.end[1])
|
|
135
|
-
} else if (s.kind === 'polygon') {
|
|
136
|
-
for (const [x, z] of s.polygon) {
|
|
137
|
-
reach(x, z)
|
|
138
|
-
}
|
|
139
|
-
} else {
|
|
140
|
-
reach(s.position[0], s.position[2])
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
if (!Number.isFinite(minX)) return false
|
|
111
|
+
const startBounds = groupPlanBounds(restBox, starts, frameInv)
|
|
112
|
+
if (!startBounds) return false
|
|
113
|
+
// Mutable: mid-carry R/T re-seeds the footprint around the same pivot.
|
|
114
|
+
let restBounds = startBounds
|
|
115
|
+
let carriedRotation = 0
|
|
145
116
|
// Rotation pivot for mid-carry R/T; stable across the whole pick-up.
|
|
146
|
-
const restCenter
|
|
117
|
+
const restCenter = planBoundsCenter(restBounds)
|
|
147
118
|
// Ground plane for the 3D surface: the meshes' base when available, floor
|
|
148
119
|
// level otherwise. Placements live in the level frame, so both surfaces
|
|
149
120
|
// resolve into it before measuring.
|
|
@@ -157,7 +128,13 @@ export function startGroupPickUp(
|
|
|
157
128
|
if (n && !movingIdSet.has(nid)) staticNodes[nid] = n
|
|
158
129
|
}
|
|
159
130
|
const candidates = collectAlignmentAnchors(staticNodes, '', levelId)
|
|
160
|
-
let restAnchors = bboxCornerAnchors(
|
|
131
|
+
let restAnchors = bboxCornerAnchors(
|
|
132
|
+
'group-move',
|
|
133
|
+
restBounds.minX,
|
|
134
|
+
restBounds.minZ,
|
|
135
|
+
restBounds.maxX,
|
|
136
|
+
restBounds.maxZ,
|
|
137
|
+
)
|
|
161
138
|
|
|
162
139
|
// Cursor → level-frame plan point, whichever surface the pointer is over.
|
|
163
140
|
const ndc = new Vector2()
|
|
@@ -274,18 +251,20 @@ export function startGroupPickUp(
|
|
|
274
251
|
// the current delta — the carried group turns exactly like the idle
|
|
275
252
|
// keyboard rotate, and the placement stays a single updateNodes.
|
|
276
253
|
const rotateCarried = (direction: 1 | -1) => {
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
{ x: restCenter[0], z: restCenter[1] },
|
|
281
|
-
-direction * (Math.PI / 4),
|
|
282
|
-
)
|
|
254
|
+
const pivot = { x: restCenter[0], z: restCenter[1] }
|
|
255
|
+
const delta = -direction * (Math.PI / 4)
|
|
256
|
+
const rotated = rotateGroupSnapshots(starts, links, pivot, delta)
|
|
283
257
|
starts = rotated.starts
|
|
284
258
|
links = rotated.links
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
259
|
+
carriedRotation += delta
|
|
260
|
+
restBounds = rotatePlanBounds(startBounds, pivot, carriedRotation)
|
|
261
|
+
restAnchors = bboxCornerAnchors(
|
|
262
|
+
'group-move',
|
|
263
|
+
restBounds.minX,
|
|
264
|
+
restBounds.minZ,
|
|
265
|
+
restBounds.maxX,
|
|
266
|
+
restBounds.maxZ,
|
|
267
|
+
)
|
|
289
268
|
sfxEmitter.emit('sfx:item-rotate')
|
|
290
269
|
applyDelta(lastDelta?.[0] ?? 0, lastDelta?.[1] ?? 0)
|
|
291
270
|
}
|
|
@@ -575,6 +554,11 @@ export function deleteSelection(): boolean {
|
|
|
575
554
|
if (selectedIds.length === 0) return false
|
|
576
555
|
|
|
577
556
|
const commitDelete = () => {
|
|
557
|
+
const detail =
|
|
558
|
+
selectedIds.length === 1
|
|
559
|
+
? (useScene.getState().nodes[selectedIds[0]!]?.type ?? selectedIds[0]!)
|
|
560
|
+
: String(selectedIds.length)
|
|
561
|
+
markPerfAction('delete', detail)
|
|
578
562
|
if (selectedIds.length === 1) {
|
|
579
563
|
emitDeleteSFX(useScene.getState().nodes[selectedIds[0]!]?.type)
|
|
580
564
|
} else {
|