@pascal-app/editor 0.9.1 → 0.9.2
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/package.json +12 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
- package/src/components/editor/bake-exporter.tsx +47 -0
- package/src/components/editor/custom-camera-controls.tsx +63 -25
- package/src/components/editor/editor-layout-mobile.tsx +5 -0
- package/src/components/editor/editor-layout-v2.tsx +18 -0
- package/src/components/editor/export-manager.tsx +71 -63
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +36 -3
- package/src/components/editor/first-person/build-collider-world.ts +32 -5
- package/src/components/editor/first-person-controls.tsx +118 -7
- package/src/components/editor/floating-action-menu.tsx +410 -33
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-background-selection.test.ts +58 -0
- package/src/components/editor/floorplan-background-selection.ts +3 -2
- package/src/components/editor/floorplan-panel.tsx +1804 -849
- package/src/components/editor/grid.tsx +166 -39
- package/src/components/editor/group-actions.ts +457 -0
- package/src/components/editor/group-floating-action-menu.tsx +130 -0
- package/src/components/editor/group-move-3d.ts +397 -0
- package/src/components/editor/group-rotate-handle.tsx +102 -64
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +192 -1
- package/src/components/editor/group-transform-shared.ts +240 -7
- package/src/components/editor/handles/handle-arrow.tsx +81 -23
- package/src/components/editor/handles/use-handle-drag.ts +37 -4
- package/src/components/editor/index.tsx +248 -27
- package/src/components/editor/measurement-pill.tsx +62 -22
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +366 -173
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +70 -0
- package/src/components/editor/quick-measurement-hud.tsx +28 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +812 -466
- package/src/components/editor/site-edge-labels.tsx +4 -4
- package/src/components/editor/slab-hole-highlights.tsx +13 -5
- package/src/components/editor/snapshot-capture-overlay.tsx +255 -115
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +136 -43
- package/src/components/editor/use-floorplan-background-placement.ts +113 -45
- package/src/components/editor/use-floorplan-scene-data.ts +5 -5
- package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
- package/src/components/editor/wall-measurement-label.tsx +3 -13
- package/src/components/editor/wall-move-side-handles.tsx +36 -21
- package/src/components/editor/wall-snap-beacon-layer.tsx +160 -6
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +1 -1
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +169 -4
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +300 -59
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1704 -342
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +419 -60
- package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
- package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-tool.tsx +34 -8
- package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
- package/src/components/tools/fence/fence-drafting.ts +80 -8
- package/src/components/tools/item/move-tool.tsx +17 -13
- package/src/components/tools/item/placement-math.test.ts +13 -1
- package/src/components/tools/item/placement-math.ts +28 -2
- package/src/components/tools/item/placement-strategies.ts +246 -5
- package/src/components/tools/item/placement-types.ts +13 -1
- package/src/components/tools/item/use-draft-node.ts +31 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +761 -121
- package/src/components/tools/registry/move-registry-node-tool.tsx +561 -99
- package/src/components/tools/roof/roof-tool.tsx +320 -94
- package/src/components/tools/select/box-select-state.ts +18 -7
- package/src/components/tools/select/box-select-tool.tsx +176 -40
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
- package/src/components/tools/select/select-candidates.ts +1 -1
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
- package/src/components/tools/shared/placement-box.tsx +183 -1
- package/src/components/tools/shared/polygon-editor.tsx +276 -35
- package/src/components/tools/site/site-boundary-editor.tsx +88 -36
- package/src/components/tools/stair/stair-defaults.ts +1 -0
- package/src/components/tools/stair/stair-tool.tsx +82 -11
- package/src/components/tools/tool-manager.tsx +86 -25
- package/src/components/tools/wall/wall-drafting.test.ts +330 -0
- package/src/components/tools/wall/wall-drafting.ts +180 -103
- package/src/components/tools/wall/wall-snap-geometry.test.ts +44 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +67 -6
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +82 -88
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +40 -93
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +2 -4
- package/src/components/ui/action-menu/measurement-control.tsx +188 -0
- package/src/components/ui/action-menu/structure-tools.tsx +22 -13
- package/src/components/ui/action-menu/view-toggles.tsx +42 -90
- package/src/components/ui/command-palette/editor-commands.tsx +2 -2
- package/src/components/ui/command-palette/index.tsx +4 -3
- package/src/components/ui/controls/material-paint-panel.tsx +86 -17
- package/src/components/ui/controls/material-picker.tsx +83 -152
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +118 -44
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/floating-level-selector.tsx +1 -1
- package/src/components/ui/helpers/building-helper.tsx +10 -23
- package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
- package/src/components/ui/helpers/helper-manager.tsx +227 -14
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/node-display.ts +17 -17
- package/src/components/ui/panels/panel-manager.tsx +22 -13
- package/src/components/ui/panels/panel-wrapper.tsx +24 -3
- package/src/components/ui/panels/parametric-inspector.tsx +15 -2
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +71 -40
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +60 -6
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +40 -17
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/tab-bar.tsx +42 -28
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer-overlay.tsx +85 -10
- package/src/components/viewer-zone-system.tsx +6 -1
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +54 -7
- package/src/hooks/use-ceiling-events.ts +3 -2
- package/src/hooks/use-drag-action.ts +4 -1
- package/src/hooks/use-keyboard.ts +385 -40
- package/src/index.tsx +219 -6
- package/src/lib/active-placement-surface.ts +35 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +192 -0
- package/src/lib/direct-manipulation.ts +109 -0
- package/src/lib/editor-api.ts +23 -35
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +18 -12
- package/src/lib/floorplan/floorplan-export.tsx +364 -0
- package/src/lib/floorplan/geometry.ts +53 -0
- package/src/lib/floorplan/index.ts +3 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/fresh-planar-placement.test.ts +240 -3
- package/src/lib/fresh-planar-placement.ts +68 -1
- package/src/lib/glb-export.test.ts +441 -0
- package/src/lib/glb-export.ts +874 -0
- package/src/lib/history.ts +9 -0
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +51 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +211 -0
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +36 -49
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +183 -0
- package/src/lib/measurements.ts +199 -0
- package/src/lib/paint-scope.test.ts +454 -0
- package/src/lib/paint-scope.ts +461 -0
- package/src/lib/placement-drag-release.ts +23 -0
- package/src/lib/planar-cursor-placement.test.ts +57 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +6 -6
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +196 -0
- package/src/lib/scene-clipboard.ts +73 -8
- package/src/lib/scene.ts +28 -5
- package/src/lib/selection-routing.test.ts +298 -0
- package/src/lib/selection-routing.ts +175 -0
- package/src/lib/sfx/index.ts +1 -0
- package/src/lib/sfx/movement-tick.test.ts +65 -0
- package/src/lib/sfx/movement-tick.ts +18 -0
- package/src/lib/sfx-bus.ts +50 -13
- package/src/lib/sfx-player.test.ts +124 -0
- package/src/lib/sfx-player.ts +111 -66
- package/src/lib/slab-plan-snap.test.ts +93 -0
- package/src/lib/slab-plan-snap.ts +108 -0
- package/src/lib/snapping-mode.test.ts +138 -0
- package/src/lib/snapping-mode.ts +179 -0
- package/src/lib/stair-duplication.ts +4 -4
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/world-grid-snap.ts +37 -5
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-editor.tsx +484 -114
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +21 -0
- package/src/store/use-floorplan-draft-preview.ts +101 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-interaction-scope.test.ts +186 -0
- package/src/store/use-interaction-scope.ts +132 -0
- package/src/store/use-measurement-draft.test.ts +530 -0
- package/src/store/use-measurement-draft.ts +543 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-placement-preview.ts +11 -3
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +43 -0
- package/src/store/use-wall-snap-indicator.ts +2 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/group-move-handle.tsx +0 -316
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/lib/level-name.ts +0 -11
|
@@ -36,7 +36,7 @@ function useLevelChildren<TNode extends AnyNode>(
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const levelNode = state.nodes[levelId]
|
|
39
|
-
if (
|
|
39
|
+
if (levelNode?.type !== 'level') {
|
|
40
40
|
return [] as TNode[]
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -105,7 +105,7 @@ export function useFloorplanSceneData({
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
const buildingNode = state.nodes[currentBuildingId]
|
|
108
|
-
if (
|
|
108
|
+
if (buildingNode?.type !== 'building') {
|
|
109
109
|
return [] as LevelNode[]
|
|
110
110
|
}
|
|
111
111
|
|
|
@@ -133,7 +133,7 @@ export function useFloorplanSceneData({
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
const nextLevelNode = state.nodes[levelId]
|
|
136
|
-
if (
|
|
136
|
+
if (nextLevelNode?.type !== 'level') {
|
|
137
137
|
return [] as RoofNode[]
|
|
138
138
|
}
|
|
139
139
|
|
|
@@ -149,7 +149,7 @@ export function useFloorplanSceneData({
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
const nextLevelNode = state.nodes[levelId]
|
|
152
|
-
if (
|
|
152
|
+
if (nextLevelNode?.type !== 'level') {
|
|
153
153
|
return [] as OpeningNode[]
|
|
154
154
|
}
|
|
155
155
|
|
|
@@ -171,7 +171,7 @@ export function useFloorplanSceneData({
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
const nextLevelNode = state.nodes[levelId]
|
|
174
|
-
if (
|
|
174
|
+
if (nextLevelNode?.type !== 'level') {
|
|
175
175
|
return [] as AnyNode[]
|
|
176
176
|
}
|
|
177
177
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bumps after the 3D meshes have had a chance to settle following a scene
|
|
5
|
+
* change. `computeGroupBox` reads mesh WORLD bounds, but a scene commit
|
|
6
|
+
* (undo/redo included) reaches the meshes asynchronously — renderers
|
|
7
|
+
* reconcile on the next React commit and the geometry systems rebuild in the
|
|
8
|
+
* next frame — so anything that derives geometry from the meshes in the same
|
|
9
|
+
* render as the `nodes` change reads STALE positions. Depend on this epoch
|
|
10
|
+
* (alongside `nodes`) to recompute once more after two animation frames,
|
|
11
|
+
* when transforms and rebuilt geometry are in place.
|
|
12
|
+
*/
|
|
13
|
+
export function useMeshSettleEpoch(nodes: unknown): number {
|
|
14
|
+
const [epoch, setEpoch] = useState(0)
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
let raf2 = 0
|
|
17
|
+
const raf1 = requestAnimationFrame(() => {
|
|
18
|
+
raf2 = requestAnimationFrame(() => setEpoch((e) => e + 1))
|
|
19
|
+
})
|
|
20
|
+
return () => {
|
|
21
|
+
cancelAnimationFrame(raf1)
|
|
22
|
+
cancelAnimationFrame(raf2)
|
|
23
|
+
}
|
|
24
|
+
}, [nodes])
|
|
25
|
+
return epoch
|
|
26
|
+
}
|
|
@@ -23,6 +23,7 @@ import { Html } from '@react-three/drei'
|
|
|
23
23
|
import { createPortal, useFrame } from '@react-three/fiber'
|
|
24
24
|
import { useMemo, useState } from 'react'
|
|
25
25
|
import * as THREE from 'three'
|
|
26
|
+
import { formatLinearMeasurement } from '../../lib/measurements'
|
|
26
27
|
|
|
27
28
|
const GUIDE_Y_OFFSET = 0.08
|
|
28
29
|
const LABEL_LIFT = 0.08
|
|
@@ -62,17 +63,6 @@ type WallFaceLine = {
|
|
|
62
63
|
end: Point2D
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
function formatMeasurement(value: number, unit: 'metric' | 'imperial') {
|
|
66
|
-
if (unit === 'imperial') {
|
|
67
|
-
const feet = value * 3.280_84
|
|
68
|
-
const wholeFeet = Math.floor(feet)
|
|
69
|
-
const inches = Math.round((feet - wholeFeet) * 12)
|
|
70
|
-
if (inches === 12) return `${wholeFeet + 1}'0"`
|
|
71
|
-
return `${wholeFeet}'${inches}"`
|
|
72
|
-
}
|
|
73
|
-
return `${Number.parseFloat(value.toFixed(2))}m`
|
|
74
|
-
}
|
|
75
|
-
|
|
76
66
|
export function WallMeasurementLabel() {
|
|
77
67
|
const selectedIds = useViewer((state) => state.selection.selectedIds)
|
|
78
68
|
const nodes = useScene((state) => state.nodes)
|
|
@@ -547,8 +537,8 @@ function WallMeasurementAnnotation({ wall }: { wall: WallNode }) {
|
|
|
547
537
|
}
|
|
548
538
|
return total
|
|
549
539
|
}, [guide, wall])
|
|
550
|
-
const label =
|
|
551
|
-
const heightLabel = `H ${
|
|
540
|
+
const label = formatLinearMeasurement(length, unit)
|
|
541
|
+
const heightLabel = `H ${formatLinearMeasurement(wall.height ?? DEFAULT_WALL_HEIGHT, unit)}`
|
|
552
542
|
|
|
553
543
|
if (!(guide && Number.isFinite(length) && length >= 0.01)) return null
|
|
554
544
|
|
|
@@ -32,8 +32,15 @@ import {
|
|
|
32
32
|
} from 'three'
|
|
33
33
|
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
34
34
|
import { MeshBasicNodeMaterial } from 'three/webgpu'
|
|
35
|
+
import { isHistoryShortcut } from '../../lib/history'
|
|
36
|
+
import { endpointReshapeScope } from '../../lib/interaction/scope'
|
|
35
37
|
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
36
38
|
import useEditor from '../../store/use-editor'
|
|
39
|
+
import useInteractionScope, {
|
|
40
|
+
useEndpointReshape,
|
|
41
|
+
useIsCurveReshape,
|
|
42
|
+
useMovingNode,
|
|
43
|
+
} from '../../store/use-interaction-scope'
|
|
37
44
|
import { suppressBoxSelectForPointer } from '../tools/select/box-select-state'
|
|
38
45
|
import {
|
|
39
46
|
createArrowHitAreaGeometry,
|
|
@@ -54,7 +61,7 @@ const ARROW_HOVER_COLOR = '#a5b4fc'
|
|
|
54
61
|
// Match the door arrows: scale the rendered chevron down to ~two-thirds
|
|
55
62
|
// so the in-world handles read as a single UI family.
|
|
56
63
|
const ARROW_SCALE = 0.65
|
|
57
|
-
const CORNER_HEX_RADIUS = 0.
|
|
64
|
+
const CORNER_HEX_RADIUS = 0.11
|
|
58
65
|
const CORNER_DASH_SIZE = 0.1
|
|
59
66
|
const CORNER_GAP_SIZE = 0.07
|
|
60
67
|
const CORNER_DASH_THICKNESS = 0.006
|
|
@@ -118,11 +125,9 @@ export function WallMoveSideHandles() {
|
|
|
118
125
|
const selectedIds = useViewer((state) => state.selection.selectedIds)
|
|
119
126
|
const mode = useEditor((state) => state.mode)
|
|
120
127
|
const isFloorplanHovered = useEditor((state) => state.isFloorplanHovered)
|
|
121
|
-
const movingNode =
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
const curvingWall = useEditor((state) => state.curvingWall)
|
|
125
|
-
const curvingFence = useEditor((state) => state.curvingFence)
|
|
128
|
+
const movingNode = useMovingNode()
|
|
129
|
+
const endpointReshape = useEndpointReshape()
|
|
130
|
+
const isCurveReshape = useIsCurveReshape()
|
|
126
131
|
|
|
127
132
|
const selectedId = selectedIds.length === 1 ? selectedIds[0] : null
|
|
128
133
|
// Fence side-move / height / corner-pickers now flow through the
|
|
@@ -140,10 +145,8 @@ export function WallMoveSideHandles() {
|
|
|
140
145
|
!isFloorplanHovered &&
|
|
141
146
|
mode !== 'delete' &&
|
|
142
147
|
!movingNode &&
|
|
143
|
-
!
|
|
144
|
-
!
|
|
145
|
-
!curvingWall &&
|
|
146
|
-
!curvingFence
|
|
148
|
+
!endpointReshape &&
|
|
149
|
+
!isCurveReshape
|
|
147
150
|
|
|
148
151
|
if (!shouldRender || !selectedNode) return null
|
|
149
152
|
|
|
@@ -329,11 +332,12 @@ function WallCornerLeaderHandle({ wall, endpoint }: { wall: WallNode; endpoint:
|
|
|
329
332
|
}, [])
|
|
330
333
|
|
|
331
334
|
const activateEndpointMove = (event: ThreeEvent<PointerEvent>) => {
|
|
335
|
+
if (event.button !== 0) return
|
|
332
336
|
event.stopPropagation()
|
|
333
337
|
suppressBoxSelectForPointer(event)
|
|
334
338
|
sfxEmitter.emit('sfx:item-pick')
|
|
335
339
|
document.body.style.cursor = 'grabbing'
|
|
336
|
-
|
|
340
|
+
useInteractionScope.getState().begin(endpointReshapeScope(wall.id, endpoint))
|
|
337
341
|
}
|
|
338
342
|
|
|
339
343
|
return (
|
|
@@ -433,6 +437,7 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
|
|
|
433
437
|
const handleY = wallHeight + HEIGHT_HANDLE_OFFSET
|
|
434
438
|
|
|
435
439
|
const activateHeightResize = (event: ThreeEvent<PointerEvent>) => {
|
|
440
|
+
if (event.button !== 0) return
|
|
436
441
|
event.stopPropagation()
|
|
437
442
|
suppressBoxSelectForPointer(event)
|
|
438
443
|
const levelObject = wall.parentId ? sceneRegistry.nodes.get(wall.parentId) : null
|
|
@@ -468,7 +473,7 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
|
|
|
468
473
|
|
|
469
474
|
document.body.style.cursor = 'ns-resize'
|
|
470
475
|
sfxEmitter.emit('sfx:item-pick')
|
|
471
|
-
|
|
476
|
+
useInteractionScope.getState().begin({ kind: 'handle-drag', nodeId: wallId, handle: 'height' })
|
|
472
477
|
// Suppress R3F node pointer events until pointerup completes so the
|
|
473
478
|
// synthesized click doesn't reroute selection to whatever mesh sits
|
|
474
479
|
// under the cursor at release.
|
|
@@ -494,11 +499,12 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
|
|
|
494
499
|
window.removeEventListener('pointermove', onMove)
|
|
495
500
|
window.removeEventListener('pointerup', onUp)
|
|
496
501
|
window.removeEventListener('pointercancel', onCancel)
|
|
502
|
+
window.removeEventListener('keydown', onKeyDown, true)
|
|
497
503
|
if (document.body.style.cursor === 'ns-resize') {
|
|
498
504
|
document.body.style.cursor = ''
|
|
499
505
|
}
|
|
500
506
|
useScene.temporal.getState().resume()
|
|
501
|
-
|
|
507
|
+
useInteractionScope.getState().endIf((sc) => sc.kind === 'handle-drag')
|
|
502
508
|
useViewer.getState().setInputDragging(false)
|
|
503
509
|
dragCleanupRef.current = null
|
|
504
510
|
}
|
|
@@ -523,10 +529,21 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
|
|
|
523
529
|
cleanup()
|
|
524
530
|
}
|
|
525
531
|
|
|
532
|
+
// Escape / ⌘Z abort the drag — capture phase so they win over the global
|
|
533
|
+
// use-keyboard arms (⌘Z must never history-jump under a live pointer).
|
|
534
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
535
|
+
if (e.key !== 'Escape' && !isHistoryShortcut(e)) return
|
|
536
|
+
e.preventDefault()
|
|
537
|
+
e.stopPropagation()
|
|
538
|
+
swallowNextClick()
|
|
539
|
+
onCancel()
|
|
540
|
+
}
|
|
541
|
+
|
|
526
542
|
dragCleanupRef.current = cleanup
|
|
527
543
|
window.addEventListener('pointermove', onMove)
|
|
528
544
|
window.addEventListener('pointerup', onUp)
|
|
529
545
|
window.addEventListener('pointercancel', onCancel)
|
|
546
|
+
window.addEventListener('keydown', onKeyDown, true)
|
|
530
547
|
}
|
|
531
548
|
|
|
532
549
|
return (
|
|
@@ -605,16 +622,15 @@ function WallMoveArrowHandle({ wall, handle }: { wall: WallNode; handle: WallMov
|
|
|
605
622
|
useEffect(() => () => arrowMaterial.dispose(), [arrowMaterial])
|
|
606
623
|
|
|
607
624
|
const activateWallMove = (event: ThreeEvent<PointerEvent>) => {
|
|
625
|
+
if (event.button !== 0) return
|
|
608
626
|
event.stopPropagation()
|
|
609
627
|
suppressBoxSelectForPointer(event)
|
|
610
628
|
document.body.style.cursor = 'grabbing'
|
|
611
629
|
|
|
612
630
|
sfxEmitter.emit('sfx:item-pick')
|
|
613
631
|
useEditor.getState().setMovingNode(wall)
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
useEditor.getState().setCurvingWall(null)
|
|
617
|
-
useEditor.getState().setCurvingFence(null)
|
|
632
|
+
useInteractionScope.getState().endIf((s) => s.kind === 'reshaping' && s.reshape === 'endpoint')
|
|
633
|
+
useInteractionScope.getState().endIf((s) => s.kind === 'reshaping' && s.reshape === 'curve')
|
|
618
634
|
// Keep the wall selected so it stays the active item once the move
|
|
619
635
|
// commits; the `!movingNode` guard on the handles hides them mid-drag.
|
|
620
636
|
}
|
|
@@ -696,16 +712,15 @@ function FenceMoveArrowHandle({ fence, handle }: { fence: FenceNode; handle: Wal
|
|
|
696
712
|
useEffect(() => () => arrowMaterial.dispose(), [arrowMaterial])
|
|
697
713
|
|
|
698
714
|
const activateFenceMove = (event: ThreeEvent<PointerEvent>) => {
|
|
715
|
+
if (event.button !== 0) return
|
|
699
716
|
event.stopPropagation()
|
|
700
717
|
suppressBoxSelectForPointer(event)
|
|
701
718
|
document.body.style.cursor = 'grabbing'
|
|
702
719
|
|
|
703
720
|
sfxEmitter.emit('sfx:item-pick')
|
|
704
721
|
useEditor.getState().setMovingNode(fence)
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
useEditor.getState().setCurvingWall(null)
|
|
708
|
-
useEditor.getState().setCurvingFence(null)
|
|
722
|
+
useInteractionScope.getState().endIf((s) => s.kind === 'reshaping' && s.reshape === 'endpoint')
|
|
723
|
+
useInteractionScope.getState().endIf((s) => s.kind === 'reshaping' && s.reshape === 'curve')
|
|
709
724
|
// Keep the fence selected so it stays active once the move commits.
|
|
710
725
|
}
|
|
711
726
|
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type AnyNode,
|
|
5
|
+
type AnyNodeId,
|
|
6
|
+
DEFAULT_WALL_HEIGHT,
|
|
7
|
+
getWallCurveFrameAt,
|
|
8
|
+
getWallCurveLength,
|
|
9
|
+
getWallThickness,
|
|
10
|
+
isCurvedWall,
|
|
11
|
+
resolveLevelId,
|
|
12
|
+
sceneRegistry,
|
|
13
|
+
spatialGridManager,
|
|
14
|
+
useScene,
|
|
15
|
+
type WallNode,
|
|
16
|
+
} from '@pascal-app/core'
|
|
4
17
|
import { useWallSnapIndicator, type WallSnapKind } from '@pascal-app/editor'
|
|
5
18
|
import { useViewer } from '@pascal-app/viewer'
|
|
6
19
|
import { useFrame } from '@react-three/fiber'
|
|
7
|
-
import { memo, useRef } from 'react'
|
|
20
|
+
import { memo, useMemo, useRef } from 'react'
|
|
8
21
|
import { BoxGeometry, CircleGeometry, CylinderGeometry, type Group } from 'three'
|
|
9
22
|
import { MeshBasicNodeMaterial } from 'three/webgpu'
|
|
10
23
|
import { EDITOR_LAYER } from '../../lib/constants'
|
|
@@ -20,8 +33,8 @@ import { EDITOR_LAYER } from '../../lib/constants'
|
|
|
20
33
|
* intersection → ✕ cross wall body (edge) → circle
|
|
21
34
|
*
|
|
22
35
|
* Subscribes to the shared `useWallSnapIndicator` store (published by the wall
|
|
23
|
-
* draft + endpoint-move tools).
|
|
24
|
-
*
|
|
36
|
+
* draft + endpoint-move tools). The vertical mouse pillar and the corner
|
|
37
|
+
* (endpoint) square are green; the other floor glyphs are indigo.
|
|
25
38
|
*
|
|
26
39
|
* The point carries only XZ (building-local plan coords); like the alignment
|
|
27
40
|
* guides it's lifted to the active level's building-local Y each frame so it
|
|
@@ -30,10 +43,19 @@ import { EDITOR_LAYER } from '../../lib/constants'
|
|
|
30
43
|
*/
|
|
31
44
|
|
|
32
45
|
const BEACON_COLOR = 0x81_8c_f8 // indigo-400 — matches the alignment guide accent
|
|
46
|
+
const MARKER_GREEN = 0x22_c5_5e // green-500 — vertical mouse marker + corner (endpoint) glyph
|
|
33
47
|
const BEACON_HEIGHT = 2.5 // world-meter height of the pillar
|
|
34
48
|
const BEACON_RADIUS = 0.018 // world-meter radius of the pillar
|
|
35
49
|
const MARKER = 0.13 // world-meter base size of the floor glyph
|
|
36
50
|
const FLOOR_LIFT = 0.012 // tiny lift so the marker reads above the floor grid
|
|
51
|
+
const WALL_TOP_HIGHLIGHT_LIFT = 0.035
|
|
52
|
+
const WALL_TOP_HIGHLIGHT_HEIGHT = 0.018
|
|
53
|
+
const WALL_TOP_HIGHLIGHT_OVERHANG = 0.14
|
|
54
|
+
const WALL_TOP_GLOW_HEIGHT = 0.026
|
|
55
|
+
const WALL_TOP_GLOW_OVERHANG = 0.36
|
|
56
|
+
const WALL_TOP_END_OVERHANG = 0.08
|
|
57
|
+
const CURVED_WALL_HIGHLIGHT_SEGMENT_LENGTH = 0.45
|
|
58
|
+
const NO_RAYCAST = () => null
|
|
37
59
|
|
|
38
60
|
// Shared resources — one material + unit geometries, so snap churn during a
|
|
39
61
|
// drag doesn't rebuild GPU buffers (mirrors the alignment guide layer).
|
|
@@ -45,17 +67,49 @@ const beaconMaterial = new MeshBasicNodeMaterial({
|
|
|
45
67
|
transparent: true,
|
|
46
68
|
opacity: 0.9,
|
|
47
69
|
})
|
|
70
|
+
const greenMarkerMaterial = new MeshBasicNodeMaterial({
|
|
71
|
+
color: MARKER_GREEN,
|
|
72
|
+
depthTest: false,
|
|
73
|
+
depthWrite: false,
|
|
74
|
+
toneMapped: false,
|
|
75
|
+
transparent: true,
|
|
76
|
+
opacity: 0.9,
|
|
77
|
+
})
|
|
78
|
+
const wallTopHighlightMaterial = new MeshBasicNodeMaterial({
|
|
79
|
+
color: BEACON_COLOR,
|
|
80
|
+
depthTest: false,
|
|
81
|
+
depthWrite: false,
|
|
82
|
+
toneMapped: false,
|
|
83
|
+
transparent: true,
|
|
84
|
+
opacity: 0.88,
|
|
85
|
+
})
|
|
86
|
+
const wallTopHighlightGlowMaterial = new MeshBasicNodeMaterial({
|
|
87
|
+
color: BEACON_COLOR,
|
|
88
|
+
depthTest: false,
|
|
89
|
+
depthWrite: false,
|
|
90
|
+
toneMapped: false,
|
|
91
|
+
transparent: true,
|
|
92
|
+
opacity: 0.26,
|
|
93
|
+
})
|
|
48
94
|
const PILLAR_GEOMETRY = new CylinderGeometry(BEACON_RADIUS, BEACON_RADIUS, BEACON_HEIGHT, 8)
|
|
49
95
|
// Flat unit geometries scaled per marker. Boxes are 0.002 tall so they read as
|
|
50
96
|
// a flat plate; circles/triangles lie flat via an X rotation at the mesh.
|
|
51
97
|
const FLAT_BOX_GEOMETRY = new BoxGeometry(1, 0.002, 1)
|
|
98
|
+
const WALL_TOP_HIGHLIGHT_GEOMETRY = new BoxGeometry(1, 1, 1)
|
|
52
99
|
const TRIANGLE_GEOMETRY = new CircleGeometry(1, 3)
|
|
53
100
|
const CIRCLE_GEOMETRY = new CircleGeometry(1, 28)
|
|
54
101
|
|
|
55
102
|
export const WallSnapBeaconLayer = memo(function WallSnapBeaconLayer() {
|
|
56
103
|
const point = useWallSnapIndicator((s) => s.point)
|
|
57
104
|
const levelId = useViewer((s) => s.selection.levelId)
|
|
105
|
+
const nodes = useScene((s) => s.nodes)
|
|
58
106
|
const groupRef = useRef<Group>(null)
|
|
107
|
+
const highlightedWalls = useMemo(() => {
|
|
108
|
+
if (!point?.wallIds?.length) return []
|
|
109
|
+
return point.wallIds
|
|
110
|
+
.map((wallId) => nodes[wallId as AnyNodeId])
|
|
111
|
+
.filter((node): node is WallNode => node?.type === 'wall' && node.visible !== false)
|
|
112
|
+
}, [nodes, point?.wallIds])
|
|
59
113
|
|
|
60
114
|
// Track the active level's building-local Y each frame so the beacon stands
|
|
61
115
|
// on the floor being edited, not the building base — same source the
|
|
@@ -70,10 +124,13 @@ export const WallSnapBeaconLayer = memo(function WallSnapBeaconLayer() {
|
|
|
70
124
|
if (!point) return null
|
|
71
125
|
return (
|
|
72
126
|
<group ref={groupRef}>
|
|
127
|
+
{highlightedWalls.map((wall) => (
|
|
128
|
+
<WallTopHighlight key={wall.id} nodes={nodes} wall={wall} />
|
|
129
|
+
))}
|
|
73
130
|
<mesh
|
|
74
131
|
geometry={PILLAR_GEOMETRY}
|
|
75
132
|
layers={EDITOR_LAYER}
|
|
76
|
-
material={
|
|
133
|
+
material={greenMarkerMaterial}
|
|
77
134
|
position={[point.x, BEACON_HEIGHT / 2, point.z]}
|
|
78
135
|
renderOrder={1002}
|
|
79
136
|
/>
|
|
@@ -82,6 +139,103 @@ export const WallSnapBeaconLayer = memo(function WallSnapBeaconLayer() {
|
|
|
82
139
|
)
|
|
83
140
|
})
|
|
84
141
|
|
|
142
|
+
type WallTopHighlightSegment = {
|
|
143
|
+
angle: number
|
|
144
|
+
center: [number, number]
|
|
145
|
+
length: number
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function getWallTopY(wall: WallNode, nodes: Readonly<Record<string, AnyNode>>) {
|
|
149
|
+
const levelId = resolveLevelId(wall, nodes as Record<string, AnyNode>)
|
|
150
|
+
const slabElevation = spatialGridManager.getSlabElevationForWall(
|
|
151
|
+
levelId,
|
|
152
|
+
wall.start,
|
|
153
|
+
wall.end,
|
|
154
|
+
wall.curveOffset ?? 0,
|
|
155
|
+
wall.thickness,
|
|
156
|
+
)
|
|
157
|
+
const wallHeight = wall.height ?? DEFAULT_WALL_HEIGHT
|
|
158
|
+
return (slabElevation > 0 ? slabElevation + wallHeight : wallHeight) + WALL_TOP_HIGHLIGHT_LIFT
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function buildHighlightSegment(start: [number, number], end: [number, number]) {
|
|
162
|
+
const dx = end[0] - start[0]
|
|
163
|
+
const dz = end[1] - start[1]
|
|
164
|
+
const length = Math.hypot(dx, dz)
|
|
165
|
+
if (length < 1e-6) return null
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
angle: -Math.atan2(dz, dx),
|
|
169
|
+
center: [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2] as [number, number],
|
|
170
|
+
length,
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function buildWallTopHighlightSegments(wall: WallNode): WallTopHighlightSegment[] {
|
|
175
|
+
if (!isCurvedWall(wall)) {
|
|
176
|
+
const segment = buildHighlightSegment(wall.start, wall.end)
|
|
177
|
+
return segment ? [segment] : []
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const sampleCount = Math.max(
|
|
181
|
+
8,
|
|
182
|
+
Math.ceil(getWallCurveLength(wall) / CURVED_WALL_HIGHLIGHT_SEGMENT_LENGTH),
|
|
183
|
+
)
|
|
184
|
+
const segments: WallTopHighlightSegment[] = []
|
|
185
|
+
let previous = getWallCurveFrameAt(wall, 0).point
|
|
186
|
+
for (let index = 1; index <= sampleCount; index += 1) {
|
|
187
|
+
const current = getWallCurveFrameAt(wall, index / sampleCount).point
|
|
188
|
+
const segment = buildHighlightSegment([previous.x, previous.y], [current.x, current.y])
|
|
189
|
+
if (segment) segments.push(segment)
|
|
190
|
+
previous = current
|
|
191
|
+
}
|
|
192
|
+
return segments
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function WallTopHighlight({
|
|
196
|
+
nodes,
|
|
197
|
+
wall,
|
|
198
|
+
}: {
|
|
199
|
+
nodes: Readonly<Record<string, AnyNode>>
|
|
200
|
+
wall: WallNode
|
|
201
|
+
}) {
|
|
202
|
+
const segments = useMemo(() => buildWallTopHighlightSegments(wall), [wall])
|
|
203
|
+
const y = getWallTopY(wall, nodes)
|
|
204
|
+
const width = Math.max(getWallThickness(wall) + WALL_TOP_HIGHLIGHT_OVERHANG, 0.24)
|
|
205
|
+
const glowWidth = Math.max(getWallThickness(wall) + WALL_TOP_GLOW_OVERHANG, 0.42)
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<>
|
|
209
|
+
{segments.map((segment, index) => (
|
|
210
|
+
<group key={`${wall.id}:${index}`}>
|
|
211
|
+
<mesh
|
|
212
|
+
frustumCulled={false}
|
|
213
|
+
geometry={WALL_TOP_HIGHLIGHT_GEOMETRY}
|
|
214
|
+
layers={EDITOR_LAYER}
|
|
215
|
+
material={wallTopHighlightGlowMaterial}
|
|
216
|
+
position={[segment.center[0], y - 0.003, segment.center[1]]}
|
|
217
|
+
raycast={NO_RAYCAST}
|
|
218
|
+
renderOrder={1003}
|
|
219
|
+
rotation={[0, segment.angle, 0]}
|
|
220
|
+
scale={[segment.length + WALL_TOP_END_OVERHANG, WALL_TOP_GLOW_HEIGHT, glowWidth]}
|
|
221
|
+
/>
|
|
222
|
+
<mesh
|
|
223
|
+
frustumCulled={false}
|
|
224
|
+
geometry={WALL_TOP_HIGHLIGHT_GEOMETRY}
|
|
225
|
+
layers={EDITOR_LAYER}
|
|
226
|
+
material={wallTopHighlightMaterial}
|
|
227
|
+
position={[segment.center[0], y + 0.002, segment.center[1]]}
|
|
228
|
+
raycast={NO_RAYCAST}
|
|
229
|
+
renderOrder={1004}
|
|
230
|
+
rotation={[0, segment.angle, 0]}
|
|
231
|
+
scale={[segment.length + WALL_TOP_END_OVERHANG, WALL_TOP_HIGHLIGHT_HEIGHT, width]}
|
|
232
|
+
/>
|
|
233
|
+
</group>
|
|
234
|
+
))}
|
|
235
|
+
</>
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
|
|
85
239
|
/** Floor glyph whose shape encodes which kind of geometry the point snapped to. */
|
|
86
240
|
function SnapMarker({ kind, x, z }: { kind: WallSnapKind; x: number; z: number }) {
|
|
87
241
|
const y = FLOOR_LIFT
|
|
@@ -90,7 +244,7 @@ function SnapMarker({ kind, x, z }: { kind: WallSnapKind; x: number; z: number }
|
|
|
90
244
|
<mesh
|
|
91
245
|
geometry={FLAT_BOX_GEOMETRY}
|
|
92
246
|
layers={EDITOR_LAYER}
|
|
93
|
-
material={
|
|
247
|
+
material={greenMarkerMaterial}
|
|
94
248
|
position={[x, y, z]}
|
|
95
249
|
renderOrder={1001}
|
|
96
250
|
scale={[MARKER * 2, 1, MARKER * 2]}
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import { memo, type MouseEvent as ReactMouseEvent } from 'react'
|
|
4
4
|
import useEditor from '../../store/use-editor'
|
|
5
|
+
import {
|
|
6
|
+
useEndpointReshape,
|
|
7
|
+
useIsCurveReshape,
|
|
8
|
+
useMovingNode,
|
|
9
|
+
} from '../../store/use-interaction-scope'
|
|
5
10
|
import { NodeActionMenu } from '../editor/node-action-menu'
|
|
6
11
|
|
|
7
12
|
type SvgPoint = {
|
|
@@ -48,12 +53,11 @@ export const FloorplanActionMenuLayer = memo(function FloorplanActionMenuLayer({
|
|
|
48
53
|
offsetY = 10,
|
|
49
54
|
}: FloorplanActionMenuLayerProps) {
|
|
50
55
|
const isFloorplanHovered = useEditor((state) => state.isFloorplanHovered)
|
|
51
|
-
const movingNode =
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const curvingFence = useEditor((state) => state.curvingFence)
|
|
56
|
+
const movingNode = useMovingNode()
|
|
57
|
+
const endpointReshape = useEndpointReshape()
|
|
58
|
+
const isCurveReshape = useIsCurveReshape()
|
|
55
59
|
|
|
56
|
-
if (!isFloorplanHovered || movingNode ||
|
|
60
|
+
if (!isFloorplanHovered || movingNode || endpointReshape || isCurveReshape) {
|
|
57
61
|
return null
|
|
58
62
|
}
|
|
59
63
|
|
|
@@ -77,7 +77,7 @@ export const FloorplanCursorIndicatorOverlay = memo(function FloorplanCursorIndi
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
if (mode === 'material-paint') {
|
|
80
|
-
return { kind: 'asset', iconSrc: '/icons/paint.
|
|
80
|
+
return { kind: 'asset', iconSrc: '/icons/paint.webp' }
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
return null
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
4
|
+
import { useEffect, useState } from 'react'
|
|
5
|
+
import { createPortal } from 'react-dom'
|
|
6
|
+
import { isActive } from '../../lib/interaction/scope'
|
|
7
|
+
import useEditor from '../../store/use-editor'
|
|
8
|
+
import useInteractionScope, { useMovingNode } from '../../store/use-interaction-scope'
|
|
9
|
+
import {
|
|
10
|
+
deleteSelection,
|
|
11
|
+
duplicateSelectionAndPickUp,
|
|
12
|
+
startGroupPickUp,
|
|
13
|
+
} from '../editor/group-actions'
|
|
14
|
+
import { NodeActionMenu } from '../editor/node-action-menu'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Floating Move / Duplicate / Delete pill for a MULTI-selection in the 2D
|
|
18
|
+
* floor plan — the group sibling of `FloorplanRegistryActionMenu` (which is
|
|
19
|
+
* sole-selection only). Anchored above the dashed group selection box; every
|
|
20
|
+
* action targets the whole selection: Move picks the group up (it rides the
|
|
21
|
+
* cursor until a click places it), Duplicate clones the selection and picks
|
|
22
|
+
* the clones up, Delete removes everything selected.
|
|
23
|
+
*
|
|
24
|
+
* Gated on floorplan hover so it never coexists with the 3D group menu in
|
|
25
|
+
* split view (that one hides while the floor plan is hovered), and hidden
|
|
26
|
+
* during any active interaction so it never competes with a live drag.
|
|
27
|
+
*/
|
|
28
|
+
export function FloorplanGroupActionMenu() {
|
|
29
|
+
const isMultiSelect = useViewer((s) => s.selection.selectedIds.length > 1)
|
|
30
|
+
const movingNode = useMovingNode()
|
|
31
|
+
const isFloorplanHovered = useEditor((s) => s.isFloorplanHovered)
|
|
32
|
+
const scopeActive = useInteractionScope((s) => isActive(s.scope))
|
|
33
|
+
|
|
34
|
+
const [position, setPosition] = useState<{ left: number; top: number } | null>(null)
|
|
35
|
+
|
|
36
|
+
const isVisible = isMultiSelect && !movingNode && isFloorplanHovered && !scopeActive
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!isVisible) {
|
|
40
|
+
setPosition(null)
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
let raf = 0
|
|
44
|
+
const tick = () => {
|
|
45
|
+
raf = requestAnimationFrame(tick)
|
|
46
|
+
// The dashed group box exists exactly while the multi-selection has
|
|
47
|
+
// transformable participants — anchor to its top edge. Only publish
|
|
48
|
+
// actual changes so the idle poll doesn't re-render every frame.
|
|
49
|
+
const box = document.querySelector('[data-group-selection-box]') as SVGGElement | null
|
|
50
|
+
if (!box) {
|
|
51
|
+
setPosition((prev) => (prev === null ? prev : null))
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
const rect = box.getBoundingClientRect()
|
|
55
|
+
const next = { left: rect.left + rect.width / 2, top: rect.top }
|
|
56
|
+
setPosition((prev) =>
|
|
57
|
+
prev && Math.abs(prev.left - next.left) < 0.5 && Math.abs(prev.top - next.top) < 0.5
|
|
58
|
+
? prev
|
|
59
|
+
: next,
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
raf = requestAnimationFrame(tick)
|
|
63
|
+
return () => cancelAnimationFrame(raf)
|
|
64
|
+
}, [isVisible])
|
|
65
|
+
|
|
66
|
+
if (!(isVisible && position)) return null
|
|
67
|
+
|
|
68
|
+
return createPortal(
|
|
69
|
+
<div
|
|
70
|
+
className="pointer-events-none fixed z-30"
|
|
71
|
+
style={{
|
|
72
|
+
left: position.left,
|
|
73
|
+
top: position.top,
|
|
74
|
+
transform: 'translate(-50%, calc(-100% - 12px))',
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
<NodeActionMenu
|
|
78
|
+
onDelete={() => deleteSelection()}
|
|
79
|
+
onDuplicate={() => duplicateSelectionAndPickUp()}
|
|
80
|
+
onMove={() => startGroupPickUp()}
|
|
81
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
82
|
+
onPointerUp={(event) => event.stopPropagation()}
|
|
83
|
+
/>
|
|
84
|
+
</div>,
|
|
85
|
+
document.body,
|
|
86
|
+
)
|
|
87
|
+
}
|