@pascal-app/editor 0.8.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 +8 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
- package/src/components/editor/custom-camera-controls.tsx +754 -25
- package/src/components/editor/editor-layout-mobile.tsx +3 -3
- package/src/components/editor/editor-layout-v2.tsx +58 -51
- 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 +201 -83
- package/src/components/editor/first-person/bvh-ecctrl.tsx +8 -8
- package/src/components/editor/first-person-controls.tsx +825 -20
- 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 +5829 -12005
- 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 +178 -149
- 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 +433 -156
- 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 +136 -62
- package/src/components/editor/thumbnail-generator.tsx +9 -3
- package/src/components/editor/use-floorplan-background-placement.ts +119 -32
- 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-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
- 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 +24 -9
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +26 -3
- 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 +1 -1
- 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 +41 -105
- package/src/components/tools/item/placement-math.test.ts +10 -0
- package/src/components/tools/item/placement-math.ts +31 -5
- package/src/components/tools/item/placement-strategies.ts +190 -15
- package/src/components/tools/item/placement-types.ts +10 -1
- package/src/components/tools/item/use-draft-node.ts +20 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +694 -191
- 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 -538
- 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 +56 -0
- 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 +16 -6
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/control-modes.tsx +5 -82
- package/src/components/ui/action-menu/index.tsx +1 -113
- package/src/components/ui/action-menu/structure-tools.tsx +13 -88
- package/src/components/ui/action-menu/view-toggles.tsx +243 -6
- package/src/components/ui/command-palette/editor-commands.tsx +18 -13
- package/src/components/ui/command-palette/index.tsx +2 -1
- package/src/components/ui/controls/material-paint-panel.tsx +83 -0
- package/src/components/ui/controls/material-picker.tsx +11 -32
- package/src/components/ui/controls/metric-control.tsx +33 -11
- package/src/components/ui/floating-level-selector.tsx +47 -10
- package/src/components/ui/helpers/helper-manager.tsx +16 -17
- package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1 -0
- package/src/components/ui/item-catalog/item-catalog.tsx +3 -0
- package/src/components/ui/level-duplicate-dialog.tsx +2 -1
- package/src/components/ui/panels/node-display.ts +1 -0
- package/src/components/ui/panels/panel-manager.tsx +35 -47
- package/src/components/ui/panels/panel-wrapper.tsx +217 -12
- package/src/components/ui/panels/parametric-inspector.tsx +469 -0
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
- 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 +4 -6
- 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 +7 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -36
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
- package/src/components/ui/sidebar/tab-bar.tsx +61 -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 +10 -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 +115 -62
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +237 -5
- 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.ts +54 -42
- package/src/lib/level-name.ts +11 -0
- package/src/lib/material-paint.ts +211 -19
- 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 +1 -1
- package/src/lib/scene-clipboard.ts +267 -0
- package/src/lib/scene.ts +13 -4
- package/src/lib/sfx-bus.ts +23 -1
- package/src/lib/sfx-player.ts +70 -22
- package/src/lib/site-boundary.ts +1 -0
- 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 +320 -37
- 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/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- 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 -264
- 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 -178
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
- package/src/components/tools/fence/move-fence-tool.tsx +0 -302
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -364
- 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 -178
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
- package/src/components/tools/wall/move-wall-tool.tsx +0 -358
- 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 -249
- package/src/components/ui/panels/column-panel.tsx +0 -759
- package/src/components/ui/panels/door-panel.tsx +0 -1299
- package/src/components/ui/panels/fence-panel.tsx +0 -222
- 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 -280
- 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 -161
- package/src/components/ui/panels/stair-panel.tsx +0 -577
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -273
- package/src/components/ui/panels/window-panel.tsx +0 -968
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -5,16 +5,24 @@ import {
|
|
|
5
5
|
type AnyNodeId,
|
|
6
6
|
type CeilingNode,
|
|
7
7
|
ColumnNode,
|
|
8
|
+
DEFAULT_WALL_HEIGHT,
|
|
8
9
|
DoorNode,
|
|
10
|
+
ElevatorNode,
|
|
9
11
|
FenceNode,
|
|
10
12
|
generateId,
|
|
13
|
+
getWallCurveLength,
|
|
14
|
+
getWallThickness,
|
|
11
15
|
ItemNode,
|
|
16
|
+
isRegistryMovable,
|
|
17
|
+
isRegistrySelectable,
|
|
18
|
+
nodeRegistry,
|
|
12
19
|
RoofSegmentNode,
|
|
13
20
|
type SlabNode,
|
|
14
21
|
SpawnNode,
|
|
15
22
|
StairNode,
|
|
16
23
|
StairSegmentNode,
|
|
17
24
|
sceneRegistry,
|
|
25
|
+
useLiveNodeOverrides,
|
|
18
26
|
useScene,
|
|
19
27
|
WallNode,
|
|
20
28
|
WindowNode,
|
|
@@ -22,19 +30,20 @@ import {
|
|
|
22
30
|
import { useViewer } from '@pascal-app/viewer'
|
|
23
31
|
import { Html } from '@react-three/drei'
|
|
24
32
|
import { useFrame } from '@react-three/fiber'
|
|
25
|
-
import {
|
|
26
|
-
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
33
|
+
import { useCallback, useRef } from 'react'
|
|
27
34
|
import * as THREE from 'three'
|
|
28
35
|
import { duplicateRoofSubtree } from '../../lib/roof-duplication'
|
|
29
|
-
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
36
|
+
import { emitDeleteSFX, sfxEmitter } from '../../lib/sfx-bus'
|
|
30
37
|
import { duplicateStairSubtree } from '../../lib/stair-duplication'
|
|
31
38
|
import useEditor from '../../store/use-editor'
|
|
39
|
+
import { formatMeasurement, MeasurementPill } from './measurement-pill'
|
|
32
40
|
import { NodeActionMenu } from './node-action-menu'
|
|
33
41
|
|
|
34
42
|
const ALLOWED_TYPES = [
|
|
35
43
|
'item',
|
|
36
44
|
'door',
|
|
37
45
|
'window',
|
|
46
|
+
'elevator',
|
|
38
47
|
'roof',
|
|
39
48
|
'roof-segment',
|
|
40
49
|
'stair',
|
|
@@ -49,6 +58,85 @@ const ALLOWED_TYPES = [
|
|
|
49
58
|
const DELETE_ONLY_TYPES: string[] = []
|
|
50
59
|
const HOLE_TYPES = ['slab', 'ceiling']
|
|
51
60
|
|
|
61
|
+
// Menu scales with camera zoom so it feels anchored to the object, but is
|
|
62
|
+
// clamped on both ends so it stays readable when zoomed way out and doesn't
|
|
63
|
+
// dominate the screen when zoomed in close. Reference values are picked so
|
|
64
|
+
// scale = 1 lands near the editor's default framing.
|
|
65
|
+
const MIN_MENU_SCALE = 0.5
|
|
66
|
+
// Cap at 1 so zooming in doesn't grow the menu past its default pixel size —
|
|
67
|
+
// only zoom-out shrinks it (down to MIN_MENU_SCALE).
|
|
68
|
+
const MAX_MENU_SCALE = 1
|
|
69
|
+
const REF_ORTHO_ZOOM = 20
|
|
70
|
+
const REF_CAMERA_DISTANCE = 12
|
|
71
|
+
|
|
72
|
+
// World-space Y distance from a node's bbox top to the floating menu anchor.
|
|
73
|
+
// Per-type because in-world chrome above the node (height-resize arrows,
|
|
74
|
+
// measurement labels) varies in vertical reach.
|
|
75
|
+
// `EXTRA_MENU_LIFT` is a uniform global nudge — easier to tune one
|
|
76
|
+
// constant than to bump every per-type entry below.
|
|
77
|
+
const EXTRA_MENU_LIFT = 0.35
|
|
78
|
+
const MENU_Y_OFFSET_DEFAULT = 0.3
|
|
79
|
+
const MENU_Y_OFFSETS: Record<string, number> = {
|
|
80
|
+
wall: 0.5,
|
|
81
|
+
door: 0.6,
|
|
82
|
+
window: 0.6,
|
|
83
|
+
column: 0.6,
|
|
84
|
+
// Fence: still clears the height-resize arrow (sits at fence.height +
|
|
85
|
+
// 0.45) plus the chevron's visual size, but kept low so the menu sits
|
|
86
|
+
// close to the fence rather than floating well above it.
|
|
87
|
+
fence: 0.7,
|
|
88
|
+
// Elevator: clears the cab-height arrow which sits above the SHAFT
|
|
89
|
+
// top (resolved through level entries), so the menu floats above it.
|
|
90
|
+
elevator: 0.9,
|
|
91
|
+
stair: 0.2,
|
|
92
|
+
'stair-stair': 1.1,
|
|
93
|
+
'stair-landing': 0.9,
|
|
94
|
+
// Slab: clears the height arrow that sits at elevation + 0.22 plus the
|
|
95
|
+
// chevron's own visual reach, so the menu floats just above it.
|
|
96
|
+
slab: 0.7,
|
|
97
|
+
// Ceiling: clears the upward height arrow that sits ~0.22 above the
|
|
98
|
+
// ceiling plane, plus extra headroom so the menu doesn't crowd the
|
|
99
|
+
// chevron at any zoom level.
|
|
100
|
+
ceiling: 1.0,
|
|
101
|
+
// Shelf: clears the height arrow that sits at shelf.height + 0.22
|
|
102
|
+
// plus the chevron's visual reach.
|
|
103
|
+
shelf: 0.6,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getMenuYOffset(node: AnyNode | null): number {
|
|
107
|
+
if (!node) return MENU_Y_OFFSET_DEFAULT + EXTRA_MENU_LIFT
|
|
108
|
+
if (node.type === 'stair-segment') {
|
|
109
|
+
return (MENU_Y_OFFSETS[`stair-${node.segmentType}`] ?? MENU_Y_OFFSET_DEFAULT) + EXTRA_MENU_LIFT
|
|
110
|
+
}
|
|
111
|
+
return (MENU_Y_OFFSETS[node.type] ?? MENU_Y_OFFSET_DEFAULT) + EXTRA_MENU_LIFT
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Fence schema defaults — mirror packages/nodes/src/fence/definition.ts so the
|
|
115
|
+
// pill reads sensibly before an explicit height / thickness is set.
|
|
116
|
+
const FENCE_DEFAULT_HEIGHT = 1.8
|
|
117
|
+
const FENCE_DEFAULT_THICKNESS = 0.08
|
|
118
|
+
|
|
119
|
+
// Dimensions for the height-drag pill. Walls and fences both carry
|
|
120
|
+
// start/end/curveOffset, so getWallCurveLength covers length for either.
|
|
121
|
+
function getHeightPillDimensions(node: WallNode | FenceNode): {
|
|
122
|
+
height: number
|
|
123
|
+
length: number
|
|
124
|
+
thickness: number
|
|
125
|
+
} {
|
|
126
|
+
if (node.type === 'wall') {
|
|
127
|
+
return {
|
|
128
|
+
height: node.height ?? DEFAULT_WALL_HEIGHT,
|
|
129
|
+
length: getWallCurveLength(node),
|
|
130
|
+
thickness: getWallThickness(node),
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
height: node.height ?? FENCE_DEFAULT_HEIGHT,
|
|
135
|
+
length: getWallCurveLength(node),
|
|
136
|
+
thickness: node.thickness ?? FENCE_DEFAULT_THICKNESS,
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
52
140
|
export function FloatingActionMenu() {
|
|
53
141
|
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
|
54
142
|
const updateNode = useScene((s) => s.updateNode)
|
|
@@ -58,17 +146,35 @@ export function FloatingActionMenu() {
|
|
|
58
146
|
const movingFenceEndpoint = useEditor((s) => s.movingFenceEndpoint)
|
|
59
147
|
const curvingFence = useEditor((s) => s.curvingFence)
|
|
60
148
|
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
61
|
-
const setMovingWallEndpoint = useEditor((s) => s.setMovingWallEndpoint)
|
|
62
|
-
const setMovingFenceEndpoint = useEditor((s) => s.setMovingFenceEndpoint)
|
|
63
149
|
const setCurvingWall = useEditor((s) => s.setCurvingWall)
|
|
64
150
|
const setCurvingFence = useEditor((s) => s.setCurvingFence)
|
|
65
151
|
const setSelection = useViewer((s) => s.setSelection)
|
|
66
152
|
const setEditingHole = useEditor((s) => s.setEditingHole)
|
|
153
|
+
const unit = useViewer((s) => s.unit)
|
|
154
|
+
// Drives the height-drag dimension pill below the menu. `activeHandleDrag`
|
|
155
|
+
// flips only at drag start / end, so subscribing here is cheap — the live
|
|
156
|
+
// height value is written imperatively in the useFrame below.
|
|
157
|
+
const activeHandleDrag = useEditor((s) => s.activeHandleDrag)
|
|
67
158
|
|
|
68
159
|
const groupRef = useRef<THREE.Group>(null)
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
160
|
+
const menuScaleRef = useRef<HTMLDivElement>(null)
|
|
161
|
+
const pillHeightRef = useRef<HTMLSpanElement>(null)
|
|
162
|
+
|
|
163
|
+
// Cached world anchor. The anchor is derived from `Box3.setFromObject`,
|
|
164
|
+
// which traverses the selected object's children — so a node with a
|
|
165
|
+
// continuously-animating child (the spinning turbine-vent head) makes the
|
|
166
|
+
// AABB wobble a few millimetres every frame, and the menu drifts even with
|
|
167
|
+
// a still camera. We instead recompute the box only when the selection,
|
|
168
|
+
// the object's world transform, or its geometry actually changes, and
|
|
169
|
+
// reuse the cached anchor otherwise. (Also removes a per-frame
|
|
170
|
+
// `setFromObject` for every selection.)
|
|
171
|
+
const anchorRef = useRef(new THREE.Vector3())
|
|
172
|
+
const hasAnchorRef = useRef(false)
|
|
173
|
+
const lastMatrixRef = useRef(new THREE.Matrix4())
|
|
174
|
+
const lastAnchorKeyRef = useRef<{ id: string | null; node: AnyNode | null }>({
|
|
175
|
+
id: null,
|
|
176
|
+
node: null,
|
|
177
|
+
})
|
|
72
178
|
|
|
73
179
|
// Only show for single selection of specific types
|
|
74
180
|
const selectedId = selectedIds.length === 1 ? selectedIds[0] : null
|
|
@@ -76,7 +182,23 @@ export function FloatingActionMenu() {
|
|
|
76
182
|
// Subscribe just to the selected node so unrelated scene updates do not
|
|
77
183
|
// re-render this menu.
|
|
78
184
|
const node = useScene((s) => (selectedId ? (s.nodes[selectedId as AnyNodeId] ?? null) : null))
|
|
79
|
-
|
|
185
|
+
// ALLOWED_TYPES is the hardcoded set; registry-driven kinds (any
|
|
186
|
+
// NodeDefinition with `capabilities.selectable`) get the floating menu
|
|
187
|
+
// by default too. Phase 4 collapses these into a single registry check.
|
|
188
|
+
const isValidType = node
|
|
189
|
+
? ALLOWED_TYPES.includes(node.type) || isRegistrySelectable(node.type)
|
|
190
|
+
: false
|
|
191
|
+
|
|
192
|
+
// Height-drag pill: shown just above the menu only while the selected
|
|
193
|
+
// wall/fence height arrow is being dragged. Length + thickness are fixed
|
|
194
|
+
// during a height drag, so they're computed here; the live height value
|
|
195
|
+
// is updated imperatively in the useFrame (same pattern as the scale).
|
|
196
|
+
const pillNode = node?.type === 'wall' || node?.type === 'fence' ? node : null
|
|
197
|
+
const isHeightDragPill =
|
|
198
|
+
pillNode !== null &&
|
|
199
|
+
activeHandleDrag?.nodeId === selectedId &&
|
|
200
|
+
activeHandleDrag?.label === 'height'
|
|
201
|
+
const pillDims = pillNode ? getHeightPillDimensions(pillNode) : null
|
|
80
202
|
|
|
81
203
|
// Boolean selector, only re-renders when curving availability actually flips.
|
|
82
204
|
const canCurveSelectedWall = useScene((s) => {
|
|
@@ -95,111 +217,73 @@ export function FloatingActionMenu() {
|
|
|
95
217
|
})
|
|
96
218
|
})
|
|
97
219
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (event.key === 'Alt') {
|
|
101
|
-
setAltPressed(true)
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const handleKeyUp = (event: KeyboardEvent) => {
|
|
106
|
-
if (event.key === 'Alt') {
|
|
107
|
-
setAltPressed(false)
|
|
108
|
-
}
|
|
109
|
-
}
|
|
220
|
+
useFrame((state) => {
|
|
221
|
+
if (!(selectedId && isValidType && groupRef.current)) return
|
|
110
222
|
|
|
111
|
-
|
|
112
|
-
|
|
223
|
+
// Scale the HTML menu with camera zoom (ortho) or inverse distance
|
|
224
|
+
// (perspective) so it feels anchored to the world, clamped on both ends
|
|
225
|
+
// so it stays readable at extreme zoom-out and doesn't fill the screen
|
|
226
|
+
// when zoomed in close.
|
|
227
|
+
if (menuScaleRef.current) {
|
|
228
|
+
const raw =
|
|
229
|
+
state.camera instanceof THREE.OrthographicCamera
|
|
230
|
+
? state.camera.zoom / REF_ORTHO_ZOOM
|
|
231
|
+
: REF_CAMERA_DISTANCE /
|
|
232
|
+
Math.max(state.camera.position.distanceTo(groupRef.current.position), 0.001)
|
|
233
|
+
const scale = Math.min(MAX_MENU_SCALE, Math.max(MIN_MENU_SCALE, raw))
|
|
234
|
+
menuScaleRef.current.style.transform = `scale(${scale})`
|
|
113
235
|
}
|
|
114
236
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
237
|
+
// Live height readout for the drag pill. The dragged height lands in
|
|
238
|
+
// `useLiveNodeOverrides` (not the scene store) each frame, so read it
|
|
239
|
+
// imperatively here instead of forcing a per-frame React re-render.
|
|
240
|
+
if (
|
|
241
|
+
pillHeightRef.current &&
|
|
242
|
+
(node?.type === 'wall' || node?.type === 'fence') &&
|
|
243
|
+
activeHandleDrag?.nodeId === selectedId &&
|
|
244
|
+
activeHandleDrag?.label === 'height'
|
|
245
|
+
) {
|
|
246
|
+
const override = useLiveNodeOverrides.getState().overrides.get(selectedId) as
|
|
247
|
+
| { height?: number }
|
|
248
|
+
| undefined
|
|
249
|
+
const fallbackHeight = node.type === 'wall' ? DEFAULT_WALL_HEIGHT : FENCE_DEFAULT_HEIGHT
|
|
250
|
+
const liveHeight = override?.height ?? node.height ?? fallbackHeight
|
|
251
|
+
pillHeightRef.current.textContent = `H ${formatMeasurement(liveHeight, unit)}`
|
|
123
252
|
}
|
|
124
|
-
}, [])
|
|
125
|
-
|
|
126
|
-
useFrame(() => {
|
|
127
|
-
if (!(selectedId && isValidType && groupRef.current)) return
|
|
128
253
|
|
|
129
254
|
const obj = sceneRegistry.nodes.get(selectedId)
|
|
130
255
|
if (obj) {
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
256
|
+
// Recompute the anchor only when the object genuinely changes —
|
|
257
|
+
// reselected, moved (its own world matrix changed), or resized
|
|
258
|
+
// (a fresh store node on commit, or a live override / handle drag
|
|
259
|
+
// mid-resize). A spinning child changes the head's matrix, not the
|
|
260
|
+
// registered group's, so it never triggers a recompute → the menu
|
|
261
|
+
// holds still.
|
|
262
|
+
const overrideActive = useLiveNodeOverrides.getState().overrides.get(selectedId) != null
|
|
263
|
+
const dragActive = activeHandleDrag?.nodeId === selectedId
|
|
264
|
+
const selectionChanged =
|
|
265
|
+
lastAnchorKeyRef.current.id !== selectedId || lastAnchorKeyRef.current.node !== node
|
|
266
|
+
const matrixChanged = !lastMatrixRef.current.equals(obj.matrixWorld)
|
|
140
267
|
|
|
141
|
-
if (
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
node.type === 'wall'
|
|
150
|
-
? obj.localToWorld(
|
|
151
|
-
new THREE.Vector3(
|
|
152
|
-
Math.hypot(segment.end[0] - segment.start[0], segment.end[1] - segment.start[1]),
|
|
153
|
-
0,
|
|
154
|
-
0,
|
|
155
|
-
),
|
|
156
|
-
)
|
|
157
|
-
: obj.localToWorld(new THREE.Vector3(segment.end[0], 0, segment.end[1]))
|
|
158
|
-
|
|
159
|
-
if (startEndpointGroupRef.current) {
|
|
160
|
-
startEndpointGroupRef.current.position.set(
|
|
161
|
-
startWorld.x,
|
|
162
|
-
startWorld.y + endpointYOffset,
|
|
163
|
-
startWorld.z,
|
|
164
|
-
)
|
|
165
|
-
}
|
|
166
|
-
if (endEndpointGroupRef.current) {
|
|
167
|
-
endEndpointGroupRef.current.position.set(
|
|
168
|
-
endWorld.x,
|
|
169
|
-
endWorld.y + endpointYOffset,
|
|
170
|
-
endWorld.z,
|
|
171
|
-
)
|
|
268
|
+
if (selectionChanged || matrixChanged || overrideActive || dragActive) {
|
|
269
|
+
const box = new THREE.Box3().setFromObject(obj)
|
|
270
|
+
if (!box.isEmpty()) {
|
|
271
|
+
const center = box.getCenter(new THREE.Vector3())
|
|
272
|
+
// Position above the object. Per-type offsets clear each kind's
|
|
273
|
+
// in-world chrome (height-resize arrows, measurement labels).
|
|
274
|
+
anchorRef.current.set(center.x, box.max.y + getMenuYOffset(node), center.z)
|
|
275
|
+
hasAnchorRef.current = true
|
|
172
276
|
}
|
|
277
|
+
lastMatrixRef.current.copy(obj.matrixWorld)
|
|
278
|
+
lastAnchorKeyRef.current = { id: selectedId, node }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (hasAnchorRef.current) {
|
|
282
|
+
groupRef.current.position.copy(anchorRef.current)
|
|
173
283
|
}
|
|
174
284
|
}
|
|
175
285
|
})
|
|
176
286
|
|
|
177
|
-
const handleMove = useCallback(
|
|
178
|
-
(e: React.MouseEvent) => {
|
|
179
|
-
e.stopPropagation()
|
|
180
|
-
if (!node) return
|
|
181
|
-
sfxEmitter.emit('sfx:item-pick')
|
|
182
|
-
if (
|
|
183
|
-
node.type === 'item' ||
|
|
184
|
-
node.type === 'window' ||
|
|
185
|
-
node.type === 'door' ||
|
|
186
|
-
node.type === 'wall' ||
|
|
187
|
-
node.type === 'fence' ||
|
|
188
|
-
node.type === 'column' ||
|
|
189
|
-
node.type === 'slab' ||
|
|
190
|
-
node.type === 'ceiling' ||
|
|
191
|
-
node.type === 'spawn' ||
|
|
192
|
-
node.type === 'roof' ||
|
|
193
|
-
node.type === 'roof-segment' ||
|
|
194
|
-
node.type === 'stair' ||
|
|
195
|
-
node.type === 'stair-segment'
|
|
196
|
-
) {
|
|
197
|
-
setMovingNode(node as any)
|
|
198
|
-
}
|
|
199
|
-
setSelection({ selectedIds: [] })
|
|
200
|
-
},
|
|
201
|
-
[node, setMovingNode, setSelection],
|
|
202
|
-
)
|
|
203
287
|
const handleCurve = useCallback(
|
|
204
288
|
(e: React.MouseEvent) => {
|
|
205
289
|
e.stopPropagation()
|
|
@@ -217,23 +301,16 @@ export function FloatingActionMenu() {
|
|
|
217
301
|
},
|
|
218
302
|
[canCurveSelectedWall, node, setCurvingFence, setCurvingWall, setSelection],
|
|
219
303
|
)
|
|
220
|
-
const
|
|
221
|
-
(
|
|
304
|
+
const handleMove = useCallback(
|
|
305
|
+
(e: React.MouseEvent) => {
|
|
222
306
|
e.stopPropagation()
|
|
223
307
|
if (!node) return
|
|
224
308
|
sfxEmitter.emit('sfx:item-pick')
|
|
225
|
-
|
|
226
|
-
setMovingWallEndpoint({ wall: node, endpoint })
|
|
227
|
-
} else if (node.type === 'fence') {
|
|
228
|
-
setMovingFenceEndpoint({ fence: node, endpoint })
|
|
229
|
-
} else {
|
|
230
|
-
return
|
|
231
|
-
}
|
|
309
|
+
setMovingNode(node as any)
|
|
232
310
|
setSelection({ selectedIds: [] })
|
|
233
311
|
},
|
|
234
|
-
[node,
|
|
312
|
+
[node, setMovingNode, setSelection],
|
|
235
313
|
)
|
|
236
|
-
|
|
237
314
|
const handleDuplicate = useCallback(
|
|
238
315
|
(e: React.MouseEvent) => {
|
|
239
316
|
e.stopPropagation()
|
|
@@ -263,6 +340,8 @@ export function FloatingActionMenu() {
|
|
|
263
340
|
duplicate = WindowNode.parse(duplicateInfo)
|
|
264
341
|
} else if (node.type === 'item') {
|
|
265
342
|
duplicate = ItemNode.parse(duplicateInfo)
|
|
343
|
+
} else if (node.type === 'elevator') {
|
|
344
|
+
duplicate = ElevatorNode.parse(duplicateInfo)
|
|
266
345
|
} else if (node.type === 'column') {
|
|
267
346
|
duplicate = ColumnNode.parse(duplicateInfo)
|
|
268
347
|
} else if (node.type === 'wall') {
|
|
@@ -284,6 +363,16 @@ export function FloatingActionMenu() {
|
|
|
284
363
|
} else if (node.type === 'spawn') {
|
|
285
364
|
duplicate = SpawnNode.parse(duplicateInfo)
|
|
286
365
|
}
|
|
366
|
+
|
|
367
|
+
// Registry-driven fallback: any kind with a NodeDefinition can be
|
|
368
|
+
// duplicated through its schema's parse(). Future built-in kinds
|
|
369
|
+
// get duplicate for free.
|
|
370
|
+
if (!duplicate) {
|
|
371
|
+
const def = nodeRegistry.get(node.type)
|
|
372
|
+
if (def) {
|
|
373
|
+
duplicate = def.schema.parse(duplicateInfo) as AnyNode
|
|
374
|
+
}
|
|
375
|
+
}
|
|
287
376
|
} catch (error) {
|
|
288
377
|
console.error('Failed to parse duplicate', error)
|
|
289
378
|
useScene.temporal.getState().resume()
|
|
@@ -296,7 +385,11 @@ export function FloatingActionMenu() {
|
|
|
296
385
|
}
|
|
297
386
|
|
|
298
387
|
if (duplicate) {
|
|
299
|
-
if (
|
|
388
|
+
if (
|
|
389
|
+
duplicate.type === 'door' ||
|
|
390
|
+
duplicate.type === 'window' ||
|
|
391
|
+
duplicate.type === 'elevator'
|
|
392
|
+
) {
|
|
300
393
|
useScene.getState().createNode(duplicate, duplicate.parentId as AnyNodeId)
|
|
301
394
|
} else if (duplicate.type === 'wall') {
|
|
302
395
|
useScene.getState().createNode(duplicate, duplicate.parentId as AnyNodeId)
|
|
@@ -322,9 +415,39 @@ export function FloatingActionMenu() {
|
|
|
322
415
|
}
|
|
323
416
|
|
|
324
417
|
// Duplicate children for stair nodes
|
|
418
|
+
} else if (
|
|
419
|
+
duplicate.type === 'item' ||
|
|
420
|
+
duplicate.type === 'chimney' ||
|
|
421
|
+
duplicate.type === 'dormer'
|
|
422
|
+
) {
|
|
423
|
+
// Items, chimneys & dormers use pure drag-to-place: NO node is
|
|
424
|
+
// inserted into the scene until the user clicks to commit. The
|
|
425
|
+
// `setMovingNode` call below hands the clone (with
|
|
426
|
+
// `metadata.isNew = true` + no id) to its move tool —
|
|
427
|
+
// `MoveItemTool` / `MoveChimneyTool` / `MoveDormerTool` — which
|
|
428
|
+
// create a draft and call `createNode` on the commit click.
|
|
429
|
+
// Pre-creating here would drop a second copy into the scene
|
|
430
|
+
// before any click — the furnish-tab "duplicate auto-places an
|
|
431
|
+
// item without clicking" bug. (Item has its own
|
|
432
|
+
// draft-committing move tool, so it must skip the generic
|
|
433
|
+
// registry auto-create branch below.)
|
|
434
|
+
} else if (nodeRegistry.has(duplicate.type)) {
|
|
435
|
+
// Registry-driven kinds: offset the position slightly so the
|
|
436
|
+
// duplicate doesn't overlap exactly, then create + hand to the
|
|
437
|
+
// move tool. Mirrors the roof-segment / stair-segment behavior.
|
|
438
|
+
if ('position' in duplicate && Array.isArray((duplicate as any).position)) {
|
|
439
|
+
const pos = (duplicate as { position: [number, number, number] }).position
|
|
440
|
+
;(duplicate as { position: [number, number, number] }).position = [
|
|
441
|
+
pos[0] + 1,
|
|
442
|
+
pos[1],
|
|
443
|
+
pos[2] + 1,
|
|
444
|
+
]
|
|
445
|
+
}
|
|
446
|
+
useScene.getState().createNode(duplicate, duplicate.parentId as AnyNodeId)
|
|
325
447
|
}
|
|
326
448
|
if (
|
|
327
449
|
duplicate.type === 'item' ||
|
|
450
|
+
duplicate.type === 'elevator' ||
|
|
328
451
|
duplicate.type === 'column' ||
|
|
329
452
|
duplicate.type === 'wall' ||
|
|
330
453
|
duplicate.type === 'fence' ||
|
|
@@ -332,7 +455,10 @@ export function FloatingActionMenu() {
|
|
|
332
455
|
duplicate.type === 'door' ||
|
|
333
456
|
duplicate.type === 'roof-segment' ||
|
|
334
457
|
duplicate.type === 'spawn' ||
|
|
335
|
-
duplicate.type === 'stair-segment'
|
|
458
|
+
duplicate.type === 'stair-segment' ||
|
|
459
|
+
// Registry-driven kinds get picked up by MoveTool's generic
|
|
460
|
+
// fallback (MoveRegistryNodeTool) so the user can reposition.
|
|
461
|
+
nodeRegistry.has(duplicate.type)
|
|
336
462
|
) {
|
|
337
463
|
setMovingNode(duplicate as any)
|
|
338
464
|
} else if (duplicate.type === 'stair') {
|
|
@@ -388,11 +514,7 @@ export function FloatingActionMenu() {
|
|
|
388
514
|
(e: React.MouseEvent) => {
|
|
389
515
|
e.stopPropagation()
|
|
390
516
|
if (!selectedId) return
|
|
391
|
-
|
|
392
|
-
sfxEmitter.emit('sfx:item-delete')
|
|
393
|
-
} else {
|
|
394
|
-
sfxEmitter.emit('sfx:structure-delete')
|
|
395
|
-
}
|
|
517
|
+
emitDeleteSFX(node?.type)
|
|
396
518
|
setSelection({ selectedIds: [] })
|
|
397
519
|
useScene.getState().deleteNode(selectedId as AnyNodeId)
|
|
398
520
|
},
|
|
@@ -416,86 +538,55 @@ export function FloatingActionMenu() {
|
|
|
416
538
|
pointerEvents: 'auto',
|
|
417
539
|
touchAction: 'none',
|
|
418
540
|
}}
|
|
419
|
-
zIndexRange={[
|
|
541
|
+
zIndexRange={[25, 0]}
|
|
420
542
|
>
|
|
421
|
-
<
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
?
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
543
|
+
<div className="relative" ref={menuScaleRef} style={{ transformOrigin: 'center center' }}>
|
|
544
|
+
<NodeActionMenu
|
|
545
|
+
onAddHole={node && HOLE_TYPES.includes(node.type) ? handleAddHole : undefined}
|
|
546
|
+
onCurve={
|
|
547
|
+
node?.type === 'fence' || (node?.type === 'wall' && canCurveSelectedWall)
|
|
548
|
+
? handleCurve
|
|
549
|
+
: undefined
|
|
550
|
+
}
|
|
551
|
+
onMove={
|
|
552
|
+
// Fully registry-driven: any kind that declares
|
|
553
|
+
// `capabilities.movable`, a `floorplanMoveTarget`, or a
|
|
554
|
+
// 3D `affordanceTools.move` mover gets the Move button.
|
|
555
|
+
// Adding a new movable kind never touches this file.
|
|
556
|
+
node && isRegistryMovable(node.type) ? handleMove : undefined
|
|
557
|
+
}
|
|
558
|
+
onDelete={handleDelete}
|
|
559
|
+
onDuplicate={
|
|
560
|
+
node &&
|
|
561
|
+
node.type !== 'spawn' &&
|
|
562
|
+
!DELETE_ONLY_TYPES.includes(node.type) &&
|
|
563
|
+
!HOLE_TYPES.includes(node.type)
|
|
564
|
+
? handleDuplicate
|
|
565
|
+
: undefined
|
|
566
|
+
}
|
|
567
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
568
|
+
onPointerUp={(e) => e.stopPropagation()}
|
|
569
|
+
/>
|
|
570
|
+
{/* Height-drag dimension pill. Absolutely positioned just above
|
|
571
|
+
the menu (away from the height arrow below it) so it rides the
|
|
572
|
+
same scale transform + anchor, never overlaps the menu, and
|
|
573
|
+
needs no menu lift — which is what caused the click flicker.
|
|
574
|
+
Non-interactive. */}
|
|
575
|
+
{isHeightDragPill && pillDims ? (
|
|
576
|
+
<div className="-translate-x-1/2 pointer-events-none absolute bottom-full left-1/2 mb-2">
|
|
577
|
+
<MeasurementPill
|
|
578
|
+
height={pillDims.height}
|
|
579
|
+
length={pillDims.length}
|
|
580
|
+
primary="height"
|
|
581
|
+
ref={pillHeightRef}
|
|
582
|
+
thickness={pillDims.thickness}
|
|
583
|
+
unit={unit}
|
|
584
|
+
/>
|
|
585
|
+
</div>
|
|
586
|
+
) : null}
|
|
587
|
+
</div>
|
|
441
588
|
</Html>
|
|
442
589
|
</group>
|
|
443
|
-
{(node?.type === 'wall' || node?.type === 'fence') && (
|
|
444
|
-
<>
|
|
445
|
-
<group ref={startEndpointGroupRef}>
|
|
446
|
-
<Html
|
|
447
|
-
center
|
|
448
|
-
style={{ pointerEvents: 'auto', touchAction: 'none' }}
|
|
449
|
-
zIndexRange={[100, 0]}
|
|
450
|
-
>
|
|
451
|
-
<button
|
|
452
|
-
aria-label={node.type === 'wall' ? 'Move wall start' : 'Move fence start'}
|
|
453
|
-
className={`pointer-events-auto flex h-8 w-8 items-center justify-center rounded-full border bg-background/95 shadow-lg backdrop-blur-md transition-colors ${
|
|
454
|
-
altPressed
|
|
455
|
-
? 'border-amber-500/80 bg-amber-500/15 text-amber-100 hover:bg-amber-500/20 hover:text-white'
|
|
456
|
-
: 'border-border text-muted-foreground hover:bg-accent hover:text-foreground'
|
|
457
|
-
}`}
|
|
458
|
-
onClick={(e) => handleEndpointMove('start', e)}
|
|
459
|
-
onPointerDown={(e) => e.stopPropagation()}
|
|
460
|
-
title={
|
|
461
|
-
node.type === 'wall'
|
|
462
|
-
? 'Move wall start (Alt to detach)'
|
|
463
|
-
: 'Move fence start (Alt to detach)'
|
|
464
|
-
}
|
|
465
|
-
type="button"
|
|
466
|
-
>
|
|
467
|
-
<Move className="h-4 w-4" />
|
|
468
|
-
</button>
|
|
469
|
-
</Html>
|
|
470
|
-
</group>
|
|
471
|
-
<group ref={endEndpointGroupRef}>
|
|
472
|
-
<Html
|
|
473
|
-
center
|
|
474
|
-
style={{ pointerEvents: 'auto', touchAction: 'none' }}
|
|
475
|
-
zIndexRange={[100, 0]}
|
|
476
|
-
>
|
|
477
|
-
<button
|
|
478
|
-
aria-label={node.type === 'wall' ? 'Move wall end' : 'Move fence end'}
|
|
479
|
-
className={`pointer-events-auto flex h-8 w-8 items-center justify-center rounded-full border bg-background/95 shadow-lg backdrop-blur-md transition-colors ${
|
|
480
|
-
altPressed
|
|
481
|
-
? 'border-amber-500/80 bg-amber-500/15 text-amber-100 hover:bg-amber-500/20 hover:text-white'
|
|
482
|
-
: 'border-border text-muted-foreground hover:bg-accent hover:text-foreground'
|
|
483
|
-
}`}
|
|
484
|
-
onClick={(e) => handleEndpointMove('end', e)}
|
|
485
|
-
onPointerDown={(e) => e.stopPropagation()}
|
|
486
|
-
title={
|
|
487
|
-
node.type === 'wall'
|
|
488
|
-
? 'Move wall end (Alt to detach)'
|
|
489
|
-
: 'Move fence end (Alt to detach)'
|
|
490
|
-
}
|
|
491
|
-
type="button"
|
|
492
|
-
>
|
|
493
|
-
<Move className="h-4 w-4" />
|
|
494
|
-
</button>
|
|
495
|
-
</Html>
|
|
496
|
-
</group>
|
|
497
|
-
</>
|
|
498
|
-
)}
|
|
499
590
|
</group>
|
|
500
591
|
)
|
|
501
592
|
}
|