@pascal-app/editor 0.7.0 → 0.9.1
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 -13
- package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
- package/src/components/editor/custom-camera-controls.tsx +755 -25
- package/src/components/editor/editor-layout-mobile.tsx +3 -3
- package/src/components/editor/editor-layout-v2.tsx +62 -54
- package/src/components/editor/export-manager.tsx +32 -3
- package/src/components/editor/first-person/build-collider-world.test.ts +149 -0
- package/src/components/editor/first-person/build-collider-world.ts +203 -87
- package/src/components/editor/first-person/bvh-ecctrl.tsx +127 -62
- package/src/components/editor/first-person-controls.tsx +836 -31
- package/src/components/editor/floating-action-menu.tsx +287 -196
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-panel.tsx +5837 -12006
- package/src/components/editor/grid.tsx +51 -17
- package/src/components/editor/group-move-handle.tsx +316 -0
- package/src/components/editor/group-rotate-handle.tsx +382 -0
- package/src/components/editor/group-transform-shared.test.ts +205 -0
- package/src/components/editor/group-transform-shared.ts +256 -0
- package/src/components/editor/handles/handle-arrow.tsx +523 -0
- package/src/components/editor/handles/use-handle-drag.ts +213 -0
- package/src/components/editor/index.tsx +234 -190
- package/src/components/editor/measurement-pill.tsx +75 -0
- package/src/components/editor/node-arrow-handles.tsx +1453 -0
- package/src/components/editor/selection-manager.tsx +435 -158
- package/src/components/editor/site-edge-labels.tsx +44 -20
- package/src/components/editor/slab-hole-highlights.tsx +466 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +539 -0
- package/src/components/editor/thumbnail-generator.tsx +27 -64
- package/src/components/editor/use-floorplan-background-placement.ts +122 -35
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +14 -2
- package/src/components/editor/wall-measurement-label.tsx +11 -6
- package/src/components/editor/wall-move-side-handles.tsx +855 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +150 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +6 -1
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +227 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +616 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +75 -0
- package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -61
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +29 -14
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +30 -9
- package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
- package/src/components/systems/roof/roof-edit-system.tsx +86 -12
- package/src/components/systems/stair/stair-edit-system.tsx +2 -2
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
- package/src/components/systems/zone/zone-system.tsx +0 -0
- package/src/components/tools/elevator/elevator-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +349 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
- package/src/components/tools/fence/fence-drafting.ts +64 -22
- package/src/components/tools/item/move-tool.tsx +40 -107
- package/src/components/tools/item/placement-math.test.ts +10 -0
- package/src/components/tools/item/placement-math.ts +33 -9
- package/src/components/tools/item/placement-strategies.ts +201 -25
- package/src/components/tools/item/placement-types.ts +10 -1
- package/src/components/tools/item/use-draft-node.ts +20 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +700 -299
- package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
- package/src/components/tools/roof/roof-tool.tsx +108 -12
- package/src/components/tools/select/box-select-state.ts +77 -0
- package/src/components/tools/select/box-select-tool.tsx +324 -543
- package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
- package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
- package/src/components/tools/select/select-candidates.test.ts +109 -0
- package/src/components/tools/select/select-candidates.ts +85 -0
- package/src/components/tools/shared/affordance-dispatch.ts +30 -0
- package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
- package/src/components/tools/shared/floor-stack-preview.ts +25 -0
- package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
- package/src/components/tools/shared/placement-box-geometry.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +123 -0
- package/src/components/tools/shared/polygon-editor.tsx +617 -167
- package/src/components/tools/shared/segment-angle.ts +57 -1
- package/src/components/tools/site/site-boundary-editor.tsx +464 -12
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +1 -1
- package/src/components/tools/stair/stair-tool.tsx +269 -35
- package/src/components/tools/tool-manager.tsx +172 -56
- package/src/components/tools/wall/wall-drafting.ts +98 -98
- package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
- package/src/components/tools/zone/zone-tool.tsx +36 -11
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/camera-actions.tsx +0 -0
- package/src/components/ui/action-menu/control-modes.tsx +5 -76
- package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
- package/src/components/ui/action-menu/index.tsx +33 -196
- package/src/components/ui/action-menu/structure-tools.tsx +13 -88
- package/src/components/ui/action-menu/view-toggles.tsx +262 -37
- package/src/components/ui/command-palette/editor-commands.tsx +24 -17
- package/src/components/ui/command-palette/index.tsx +6 -256
- package/src/components/ui/controls/material-paint-panel.tsx +83 -0
- package/src/components/ui/controls/material-picker.tsx +17 -35
- package/src/components/ui/controls/metric-control.tsx +33 -11
- package/src/components/ui/floating-level-selector.tsx +48 -11
- package/src/components/ui/helpers/helper-manager.tsx +21 -17
- package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1743 -315
- package/src/components/ui/item-catalog/item-catalog.tsx +91 -46
- package/src/components/ui/level-duplicate-dialog.tsx +5 -6
- package/src/components/ui/panels/node-display.ts +1 -0
- package/src/components/ui/panels/paint-panel.tsx +66 -41
- package/src/components/ui/panels/panel-manager.tsx +33 -74
- package/src/components/ui/panels/panel-wrapper.tsx +217 -12
- package/src/components/ui/panels/parametric-inspector.tsx +469 -0
- package/src/components/ui/panels/reference-panel.tsx +28 -13
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/primitives/number-input.tsx +1 -1
- package/src/components/ui/primitives/sidebar.tsx +0 -0
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
- package/src/components/ui/sidebar/icon-rail.tsx +0 -0
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +404 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +8 -12
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -34
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +52 -3
- package/src/components/ui/sidebar/tab-bar.tsx +61 -1
- package/src/components/ui/slider.tsx +1 -1
- package/src/components/viewer-overlay.tsx +169 -52
- package/src/components/viewer-zone-system.tsx +1 -1
- package/src/hooks/use-auto-save.ts +24 -3
- package/src/hooks/use-ceiling-events.ts +175 -0
- package/src/hooks/use-drag-action.ts +124 -0
- package/src/hooks/use-keyboard.ts +125 -62
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +243 -4
- package/src/lib/constants.ts +6 -2
- package/src/lib/editor-api.ts +62 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.ts +111 -0
- package/src/lib/floorplan/index.ts +7 -0
- package/src/lib/floorplan/selection-tool.ts +40 -0
- package/src/lib/floorplan/stairs.ts +12 -1
- package/src/lib/fresh-planar-placement.test.ts +102 -0
- package/src/lib/fresh-planar-placement.ts +63 -0
- package/src/lib/history.ts +2 -1
- package/src/lib/level-duplication.test.ts +0 -2
- package/src/lib/level-duplication.ts +55 -43
- package/src/lib/level-name.ts +11 -0
- package/src/lib/material-paint.ts +212 -20
- package/src/lib/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +43 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/roof-duplication.ts +2 -2
- package/src/lib/scene-bounds.ts +1 -1
- package/src/lib/scene-clipboard.ts +267 -0
- package/src/lib/scene.ts +13 -4
- package/src/lib/sfx-bus.ts +25 -1
- package/src/lib/sfx-player.ts +75 -27
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/stair-duplication.ts +2 -2
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/world-grid-snap.ts +199 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-editor.tsx +342 -91
- package/src/store/use-placement-preview.ts +31 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +32 -0
- package/tsconfig.json +2 -1
- package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- package/src/components/feedback-dialog.tsx +0 -265
- package/src/components/pascal-radio.tsx +0 -280
- package/src/components/preview-button.tsx +0 -16
- package/src/components/tools/building/move-building-tool.tsx +0 -157
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
- package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -257
- package/src/components/tools/column/column-tool.tsx +0 -97
- package/src/components/tools/column/move-column-tool.tsx +0 -105
- package/src/components/tools/door/door-math.ts +0 -110
- package/src/components/tools/door/door-tool.tsx +0 -324
- package/src/components/tools/door/move-door-tool.tsx +0 -412
- package/src/components/tools/fence/curve-fence-tool.tsx +0 -179
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -438
- package/src/components/tools/fence/move-fence-tool.tsx +0 -298
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -359
- package/src/components/tools/slab/move-slab-tool.tsx +0 -182
- package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
- package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
- package/src/components/tools/slab/slab-tool.tsx +0 -322
- package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
- package/src/components/tools/spawn/spawn-tool.tsx +0 -130
- package/src/components/tools/wall/curve-wall-tool.tsx +0 -176
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -423
- package/src/components/tools/wall/move-wall-tool.tsx +0 -356
- package/src/components/tools/wall/wall-tool.tsx +0 -332
- package/src/components/tools/window/move-window-tool.tsx +0 -447
- package/src/components/tools/window/window-math.ts +0 -117
- package/src/components/tools/window/window-tool.tsx +0 -332
- package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
- package/src/components/ui/helpers/slab-helper.tsx +0 -20
- package/src/components/ui/helpers/wall-helper.tsx +0 -20
- package/src/components/ui/panels/ceiling-panel.tsx +0 -250
- package/src/components/ui/panels/column-panel.tsx +0 -715
- package/src/components/ui/panels/door-panel.tsx +0 -1292
- package/src/components/ui/panels/fence-panel.tsx +0 -227
- package/src/components/ui/panels/item-panel.tsx +0 -306
- package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
- package/src/components/ui/panels/roof-panel.tsx +0 -230
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
- package/src/components/ui/panels/slab-panel.tsx +0 -250
- package/src/components/ui/panels/spawn-panel.tsx +0 -155
- package/src/components/ui/panels/stair-panel.tsx +0 -525
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -177
- package/src/components/ui/panels/window-panel.tsx +0 -960
- package/src/components/ui/viewer-toolbar.tsx +0 -436
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AnyNode,
|
|
5
|
+
type AnyNodeId,
|
|
6
|
+
type CeilingNode,
|
|
7
|
+
getWallMidpointHandlePoint,
|
|
8
|
+
nodeRegistry,
|
|
9
|
+
type SlabNode,
|
|
10
|
+
useLiveNodeOverrides,
|
|
11
|
+
useScene,
|
|
12
|
+
type WallNode,
|
|
13
|
+
} from '@pascal-app/core'
|
|
14
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
15
|
+
import { useEffect, useState } from 'react'
|
|
16
|
+
import { createPortal } from 'react-dom'
|
|
17
|
+
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
18
|
+
import useEditor from '../../store/use-editor'
|
|
19
|
+
import { NodeActionMenu } from '../editor/node-action-menu'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Floating Move / Duplicate / Delete buttons that appear above the
|
|
23
|
+
* selected registered kind in the floor plan view.
|
|
24
|
+
*
|
|
25
|
+
* Lives outside the floorplan-panel.tsx monolith. Reads selection from
|
|
26
|
+
* `useViewer`, finds the rendered `[data-node-id]` <g> inside the floor
|
|
27
|
+
* plan scene, polls its bounding rect via rAF while open, and portals
|
|
28
|
+
* an HTML overlay positioned at the top of the bounding box.
|
|
29
|
+
*
|
|
30
|
+
* Buttons:
|
|
31
|
+
* - Move: sets `movingNode` in useEditor. Enabled when the kind has
|
|
32
|
+
* `capabilities.movable`, `def.floorplanMoveTarget`, OR
|
|
33
|
+
* `def.affordanceTools.move` (slab / ceiling). The
|
|
34
|
+
* `<FloorplanRegistryMoveOverlay>` / dispatcher picks the right path.
|
|
35
|
+
* Walls are excluded — their move is reached via the side-arrow
|
|
36
|
+
* handles emitted from `def.floorplan`, not via a menu button.
|
|
37
|
+
* - Add hole (slab + ceiling only): inserts a small default-square
|
|
38
|
+
* hole at the polygon centroid via `updateNode`. Mirrors the legacy
|
|
39
|
+
* `handleAddHole` in `floating-action-menu.tsx`.
|
|
40
|
+
* - Duplicate: deep-clones the node, marks it new, sets it as the
|
|
41
|
+
* movingNode (placement cursor) — same UX pattern as 3D duplicate.
|
|
42
|
+
* - Delete: calls `deleteNode(id)`. Cascade is handled by the registry's
|
|
43
|
+
* `relations.cascadeDelete` if declared on the def.
|
|
44
|
+
*
|
|
45
|
+
* Hidden while in a move state (so we don't show buttons over a ghost).
|
|
46
|
+
*/
|
|
47
|
+
export function FloorplanRegistryActionMenu() {
|
|
48
|
+
const selectedId = useViewer((s) => s.selection.selectedIds[0]) as AnyNodeId | undefined
|
|
49
|
+
const movingNode = useEditor((s) => s.movingNode)
|
|
50
|
+
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
51
|
+
|
|
52
|
+
const [position, setPosition] = useState<{ left: number; top: number } | null>(null)
|
|
53
|
+
|
|
54
|
+
// Only show for registered kinds (skip legacy kinds — they have their
|
|
55
|
+
// own FloorplanActionMenuLayer entries).
|
|
56
|
+
const selectedKind = useScene((s) => (selectedId ? (s.nodes[selectedId]?.type ?? null) : null))
|
|
57
|
+
const def = selectedKind ? nodeRegistry.get(selectedKind) : null
|
|
58
|
+
const isRegistryKind = !!def
|
|
59
|
+
const isVisible = isRegistryKind && !movingNode
|
|
60
|
+
const isWall = selectedKind === 'wall'
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!(isVisible && selectedId)) {
|
|
64
|
+
setPosition(null)
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
let raf = 0
|
|
68
|
+
const tick = () => {
|
|
69
|
+
raf = requestAnimationFrame(tick)
|
|
70
|
+
const sceneEl = document.querySelector('[data-floorplan-scene]') as SVGGElement | null
|
|
71
|
+
const svgEl = sceneEl?.ownerSVGElement ?? null
|
|
72
|
+
const ctm = sceneEl?.getScreenCTM() ?? null
|
|
73
|
+
if (!(sceneEl && svgEl && ctm)) {
|
|
74
|
+
setPosition(null)
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Walls: anchor at the wall midpoint in screen space so the menu
|
|
79
|
+
// sits over the centre of the wall (not the top of its screen-axis
|
|
80
|
+
// bounding box). Menu itself stays horizontal. Read live overrides
|
|
81
|
+
// too so the anchor tracks the wall during side-arrow / endpoint
|
|
82
|
+
// drags. For curved walls `getWallMidpointHandlePoint` returns the
|
|
83
|
+
// apex point on the arc at t=0.5, matching what the renderer draws.
|
|
84
|
+
if (isWall) {
|
|
85
|
+
const sceneNode = useScene.getState().nodes[selectedId] as WallNode | undefined
|
|
86
|
+
if (!sceneNode) {
|
|
87
|
+
setPosition(null)
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
const overrides = useLiveNodeOverrides.getState().get(selectedId) as
|
|
91
|
+
| Partial<WallNode>
|
|
92
|
+
| undefined
|
|
93
|
+
const wall = (overrides ? { ...sceneNode, ...overrides } : sceneNode) as WallNode
|
|
94
|
+
const planMid = getWallMidpointHandlePoint(wall)
|
|
95
|
+
const midPt = svgEl.createSVGPoint()
|
|
96
|
+
midPt.x = planMid.x
|
|
97
|
+
midPt.y = planMid.y
|
|
98
|
+
const midScreen = midPt.matrixTransform(ctm)
|
|
99
|
+
setPosition({ left: midScreen.x, top: midScreen.y })
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const el = sceneEl.querySelector(`[data-node-id="${selectedId}"]`) as SVGGElement | null
|
|
104
|
+
if (el) {
|
|
105
|
+
const rect = el.getBoundingClientRect()
|
|
106
|
+
setPosition({ left: rect.left + rect.width / 2, top: rect.top })
|
|
107
|
+
} else {
|
|
108
|
+
setPosition(null)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
raf = requestAnimationFrame(tick)
|
|
112
|
+
return () => cancelAnimationFrame(raf)
|
|
113
|
+
}, [isVisible, selectedId, isWall])
|
|
114
|
+
|
|
115
|
+
if (!(isVisible && selectedId && position && def)) return null
|
|
116
|
+
|
|
117
|
+
const node = useScene.getState().nodes[selectedId]
|
|
118
|
+
if (!node) return null
|
|
119
|
+
|
|
120
|
+
// Move button is enabled when any of:
|
|
121
|
+
// - `capabilities.movable` (generic translate-on-XZ — shelf / spawn / fence)
|
|
122
|
+
// - `def.floorplanMoveTarget` (anchor-aware 2D — door / window / item)
|
|
123
|
+
// - `def.affordanceTools.move` (kind-owned 3D mover — slab / ceiling / wall)
|
|
124
|
+
// From the menu's perspective all three are "this kind can move from
|
|
125
|
+
// the floor plan." The `MoveTool` dispatcher resolves the right path —
|
|
126
|
+
// walls land on their bespoke `MoveWallTool` (perpendicular slide
|
|
127
|
+
// with linked-wall cascade) via `affordanceTools.move`.
|
|
128
|
+
const canMove =
|
|
129
|
+
!!def.capabilities.movable || !!def.floorplanMoveTarget || !!def.affordanceTools?.move
|
|
130
|
+
const canDuplicate = def.capabilities.duplicable !== false
|
|
131
|
+
const canDelete = def.capabilities.deletable !== false
|
|
132
|
+
const canAddHole = node.type === 'slab' || node.type === 'ceiling'
|
|
133
|
+
|
|
134
|
+
const handleMove = () => {
|
|
135
|
+
sfxEmitter.emit('sfx:item-pick')
|
|
136
|
+
setMovingNode(node as never)
|
|
137
|
+
// Match the legacy 3D `floating-action-menu`: clear selection so
|
|
138
|
+
// selection-gated affordances unmount during the drag. Specifically
|
|
139
|
+
// the slab / ceiling boundary editor (`ToolManager` shows it when
|
|
140
|
+
// `selectedSlabId !== undefined`) would otherwise stay visible
|
|
141
|
+
// and render its vertex / edge handles on top of the moving mesh
|
|
142
|
+
// in split-view 3D. The move overlay reads `movingNode`, not the
|
|
143
|
+
// selection, so clearing it doesn't disturb the move itself; the
|
|
144
|
+
// commit path re-selects the node when it ends.
|
|
145
|
+
useViewer.getState().setSelection({ selectedIds: [] })
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const handleAddHole = () => {
|
|
149
|
+
if (!canAddHole) return
|
|
150
|
+
const surfaceNode = node as SlabNode | CeilingNode
|
|
151
|
+
const polygon = surfaceNode.polygon
|
|
152
|
+
if (!polygon || polygon.length < 3) return
|
|
153
|
+
|
|
154
|
+
let cx = 0
|
|
155
|
+
let cz = 0
|
|
156
|
+
for (const [x, z] of polygon) {
|
|
157
|
+
cx += x
|
|
158
|
+
cz += z
|
|
159
|
+
}
|
|
160
|
+
cx /= polygon.length
|
|
161
|
+
cz /= polygon.length
|
|
162
|
+
|
|
163
|
+
const holeSize = 0.5
|
|
164
|
+
const newHole: Array<[number, number]> = [
|
|
165
|
+
[cx - holeSize, cz - holeSize],
|
|
166
|
+
[cx + holeSize, cz - holeSize],
|
|
167
|
+
[cx + holeSize, cz + holeSize],
|
|
168
|
+
[cx - holeSize, cz + holeSize],
|
|
169
|
+
]
|
|
170
|
+
const currentHoles = surfaceNode.holes ?? []
|
|
171
|
+
const currentMetadata = currentHoles.map(
|
|
172
|
+
(_, index) => surfaceNode.holeMetadata?.[index] ?? { source: 'manual' as const },
|
|
173
|
+
)
|
|
174
|
+
sfxEmitter.emit('sfx:structure-build')
|
|
175
|
+
useScene.getState().updateNode(
|
|
176
|
+
selectedId as AnyNodeId,
|
|
177
|
+
{
|
|
178
|
+
holes: [...currentHoles, newHole],
|
|
179
|
+
holeMetadata: [...currentMetadata, { source: 'manual' as const }],
|
|
180
|
+
} as Partial<AnyNode>,
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const handleDuplicate = () => {
|
|
185
|
+
if (!node.parentId) return
|
|
186
|
+
sfxEmitter.emit('sfx:item-pick')
|
|
187
|
+
useScene.temporal.getState().pause()
|
|
188
|
+
const cloned = structuredClone(node) as AnyNode & { id?: AnyNodeId }
|
|
189
|
+
delete (cloned as { id?: AnyNodeId }).id
|
|
190
|
+
const prevMeta =
|
|
191
|
+
cloned.metadata && typeof cloned.metadata === 'object' && !Array.isArray(cloned.metadata)
|
|
192
|
+
? (cloned.metadata as Record<string, unknown>)
|
|
193
|
+
: {}
|
|
194
|
+
cloned.metadata = { ...prevMeta, isNew: true }
|
|
195
|
+
const parsed = def.schema.parse(cloned) as AnyNode
|
|
196
|
+
useScene.getState().createNode(parsed, node.parentId as AnyNodeId)
|
|
197
|
+
setMovingNode(parsed as never)
|
|
198
|
+
useScene.temporal.getState().resume()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const handleDelete = () => {
|
|
202
|
+
sfxEmitter.emit('sfx:item-delete')
|
|
203
|
+
useScene.getState().deleteNode(selectedId)
|
|
204
|
+
useViewer.getState().setSelection({ selectedIds: [] })
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return createPortal(
|
|
208
|
+
<div
|
|
209
|
+
className="pointer-events-none fixed z-30"
|
|
210
|
+
style={{
|
|
211
|
+
left: position.left,
|
|
212
|
+
top: position.top,
|
|
213
|
+
transform: 'translate(-50%, calc(-100% - 32px))',
|
|
214
|
+
}}
|
|
215
|
+
>
|
|
216
|
+
<NodeActionMenu
|
|
217
|
+
onAddHole={canAddHole ? handleAddHole : undefined}
|
|
218
|
+
onDelete={canDelete ? handleDelete : undefined}
|
|
219
|
+
onDuplicate={canDuplicate ? handleDuplicate : undefined}
|
|
220
|
+
onMove={canMove ? handleMove : undefined}
|
|
221
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
222
|
+
onPointerUp={(event) => event.stopPropagation()}
|
|
223
|
+
/>
|
|
224
|
+
</div>,
|
|
225
|
+
document.body,
|
|
226
|
+
)
|
|
227
|
+
}
|