@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
|
@@ -1,332 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type AnyNodeId,
|
|
3
|
-
emitter,
|
|
4
|
-
isCurvedWall,
|
|
5
|
-
sceneRegistry,
|
|
6
|
-
spatialGridManager,
|
|
7
|
-
useScene,
|
|
8
|
-
type WallEvent,
|
|
9
|
-
WindowNode,
|
|
10
|
-
} from '@pascal-app/core'
|
|
11
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
12
|
-
import { useEffect, useRef } from 'react'
|
|
13
|
-
import { BoxGeometry, EdgesGeometry, type Group, type LineSegments } from 'three'
|
|
14
|
-
import { LineBasicNodeMaterial } from 'three/webgpu'
|
|
15
|
-
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
16
|
-
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
17
|
-
import {
|
|
18
|
-
calculateCursorRotation,
|
|
19
|
-
calculateItemRotation,
|
|
20
|
-
getSideFromNormal,
|
|
21
|
-
isValidWallSideFace,
|
|
22
|
-
snapToHalf,
|
|
23
|
-
} from '../item/placement-math'
|
|
24
|
-
import { clampToWall, hasWallChildOverlap, wallLocalToWorld } from './window-math'
|
|
25
|
-
|
|
26
|
-
// Shared edge material — reuse across renders, just toggle color
|
|
27
|
-
const edgeMaterial = new LineBasicNodeMaterial({
|
|
28
|
-
color: 0xef_44_44, // red-500 default (invalid)
|
|
29
|
-
linewidth: 3,
|
|
30
|
-
depthTest: false,
|
|
31
|
-
depthWrite: false,
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Window tool — places WindowNodes on walls only.
|
|
36
|
-
* Shows a rectangle cursor (green = valid, red = invalid) matching window dimensions.
|
|
37
|
-
*/
|
|
38
|
-
export const WindowTool: React.FC = () => {
|
|
39
|
-
const draftRef = useRef<WindowNode | null>(null)
|
|
40
|
-
const cursorGroupRef = useRef<Group>(null!)
|
|
41
|
-
const edgesRef = useRef<LineSegments>(null!)
|
|
42
|
-
|
|
43
|
-
useEffect(() => {
|
|
44
|
-
useScene.temporal.getState().pause()
|
|
45
|
-
|
|
46
|
-
const getLevelId = () => useViewer.getState().selection.levelId
|
|
47
|
-
const getLevelYOffset = () => {
|
|
48
|
-
const id = getLevelId()
|
|
49
|
-
return id ? (sceneRegistry.nodes.get(id as AnyNodeId)?.position.y ?? 0) : 0
|
|
50
|
-
}
|
|
51
|
-
const getSlabElevation = (wallEvent: WallEvent) =>
|
|
52
|
-
spatialGridManager.getSlabElevationForWall(
|
|
53
|
-
wallEvent.node.parentId ?? '',
|
|
54
|
-
wallEvent.node.start,
|
|
55
|
-
wallEvent.node.end,
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
const markWallDirty = (wallId: string) => {
|
|
59
|
-
useScene.getState().dirtyNodes.add(wallId as AnyNodeId)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const destroyDraft = () => {
|
|
63
|
-
if (!draftRef.current) return
|
|
64
|
-
const wallId = draftRef.current.parentId
|
|
65
|
-
useScene.getState().deleteNode(draftRef.current.id)
|
|
66
|
-
draftRef.current = null
|
|
67
|
-
// Rebuild wall so it removes the cutout from the deleted draft
|
|
68
|
-
if (wallId) markWallDirty(wallId)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const hideCursor = () => {
|
|
72
|
-
if (cursorGroupRef.current) cursorGroupRef.current.visible = false
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const updateCursor = (
|
|
76
|
-
worldPosition: [number, number, number],
|
|
77
|
-
cursorRotationY: number,
|
|
78
|
-
valid: boolean,
|
|
79
|
-
) => {
|
|
80
|
-
const group = cursorGroupRef.current
|
|
81
|
-
if (!group) return
|
|
82
|
-
group.visible = true
|
|
83
|
-
group.position.set(...worldPosition)
|
|
84
|
-
group.rotation.y = cursorRotationY
|
|
85
|
-
edgeMaterial.color.setHex(valid ? 0x22_c5_5e : 0xef_44_44)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const onWallEnter = (event: WallEvent) => {
|
|
89
|
-
if (!isValidWallSideFace(event.normal)) return
|
|
90
|
-
if (isCurvedWall(event.node)) {
|
|
91
|
-
destroyDraft()
|
|
92
|
-
hideCursor()
|
|
93
|
-
return
|
|
94
|
-
}
|
|
95
|
-
const levelId = getLevelId()
|
|
96
|
-
if (!levelId) return
|
|
97
|
-
// Only interact with walls on the current level
|
|
98
|
-
if (event.node.parentId !== levelId) return
|
|
99
|
-
|
|
100
|
-
destroyDraft()
|
|
101
|
-
|
|
102
|
-
const side = getSideFromNormal(event.normal)
|
|
103
|
-
const itemRotation = calculateItemRotation(event.normal)
|
|
104
|
-
const cursorRotation = calculateCursorRotation(event.normal, event.node.start, event.node.end)
|
|
105
|
-
|
|
106
|
-
const localX = snapToHalf(event.localPosition[0])
|
|
107
|
-
const localY = snapToHalf(event.localPosition[1])
|
|
108
|
-
|
|
109
|
-
const width = 1.5
|
|
110
|
-
const height = 1.5
|
|
111
|
-
|
|
112
|
-
const { clampedX, clampedY } = clampToWall(event.node, localX, localY, width, height)
|
|
113
|
-
|
|
114
|
-
const node = WindowNode.parse({
|
|
115
|
-
position: [clampedX, clampedY, 0],
|
|
116
|
-
rotation: [0, itemRotation, 0],
|
|
117
|
-
side,
|
|
118
|
-
wallId: event.node.id,
|
|
119
|
-
parentId: event.node.id,
|
|
120
|
-
metadata: { isTransient: true },
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
useScene.getState().createNode(node, event.node.id as AnyNodeId)
|
|
124
|
-
draftRef.current = node
|
|
125
|
-
|
|
126
|
-
const valid = !hasWallChildOverlap(event.node.id, clampedX, clampedY, width, height, node.id)
|
|
127
|
-
|
|
128
|
-
updateCursor(
|
|
129
|
-
wallLocalToWorld(
|
|
130
|
-
event.node,
|
|
131
|
-
clampedX,
|
|
132
|
-
clampedY,
|
|
133
|
-
getLevelYOffset(),
|
|
134
|
-
getSlabElevation(event),
|
|
135
|
-
),
|
|
136
|
-
cursorRotation,
|
|
137
|
-
valid,
|
|
138
|
-
)
|
|
139
|
-
event.stopPropagation()
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const onWallMove = (event: WallEvent) => {
|
|
143
|
-
if (!isValidWallSideFace(event.normal)) return
|
|
144
|
-
if (isCurvedWall(event.node)) {
|
|
145
|
-
destroyDraft()
|
|
146
|
-
hideCursor()
|
|
147
|
-
return
|
|
148
|
-
}
|
|
149
|
-
// Only interact with walls on the current level
|
|
150
|
-
if (event.node.parentId !== getLevelId()) return
|
|
151
|
-
|
|
152
|
-
const side = getSideFromNormal(event.normal)
|
|
153
|
-
const itemRotation = calculateItemRotation(event.normal)
|
|
154
|
-
const cursorRotation = calculateCursorRotation(event.normal, event.node.start, event.node.end)
|
|
155
|
-
|
|
156
|
-
const localX = snapToHalf(event.localPosition[0])
|
|
157
|
-
const localY = snapToHalf(event.localPosition[1])
|
|
158
|
-
|
|
159
|
-
const width = draftRef.current?.width ?? 1.5
|
|
160
|
-
const height = draftRef.current?.height ?? 1.5
|
|
161
|
-
|
|
162
|
-
const { clampedX, clampedY } = clampToWall(event.node, localX, localY, width, height)
|
|
163
|
-
|
|
164
|
-
if (draftRef.current) {
|
|
165
|
-
if (event.node.id !== draftRef.current.parentId) {
|
|
166
|
-
// Wall changed without enter/leave: must updateNode to reparent
|
|
167
|
-
useScene.getState().updateNode(draftRef.current.id, {
|
|
168
|
-
position: [clampedX, clampedY, 0],
|
|
169
|
-
rotation: [0, itemRotation, 0],
|
|
170
|
-
side,
|
|
171
|
-
parentId: event.node.id,
|
|
172
|
-
wallId: event.node.id,
|
|
173
|
-
})
|
|
174
|
-
} else {
|
|
175
|
-
// Same wall: update Three.js mesh directly to avoid store churn
|
|
176
|
-
const draftMesh = sceneRegistry.nodes.get(draftRef.current.id as AnyNodeId)
|
|
177
|
-
if (draftMesh) {
|
|
178
|
-
draftMesh.position.set(clampedX, clampedY, 0)
|
|
179
|
-
draftMesh.rotation.set(0, itemRotation, 0)
|
|
180
|
-
draftMesh.updateMatrixWorld(true)
|
|
181
|
-
}
|
|
182
|
-
markWallDirty(event.node.id)
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const valid = !hasWallChildOverlap(
|
|
187
|
-
event.node.id,
|
|
188
|
-
clampedX,
|
|
189
|
-
clampedY,
|
|
190
|
-
width,
|
|
191
|
-
height,
|
|
192
|
-
draftRef.current?.id,
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
updateCursor(
|
|
196
|
-
wallLocalToWorld(
|
|
197
|
-
event.node,
|
|
198
|
-
clampedX,
|
|
199
|
-
clampedY,
|
|
200
|
-
getLevelYOffset(),
|
|
201
|
-
getSlabElevation(event),
|
|
202
|
-
),
|
|
203
|
-
cursorRotation,
|
|
204
|
-
valid,
|
|
205
|
-
)
|
|
206
|
-
event.stopPropagation()
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const onWallClick = (event: WallEvent) => {
|
|
210
|
-
if (!draftRef.current) return
|
|
211
|
-
if (!isValidWallSideFace(event.normal)) return
|
|
212
|
-
if (isCurvedWall(event.node)) return
|
|
213
|
-
// Only interact with walls on the current level
|
|
214
|
-
if (event.node.parentId !== getLevelId()) return
|
|
215
|
-
|
|
216
|
-
const side = getSideFromNormal(event.normal)
|
|
217
|
-
const itemRotation = calculateItemRotation(event.normal)
|
|
218
|
-
|
|
219
|
-
const localX = snapToHalf(event.localPosition[0])
|
|
220
|
-
const localY = snapToHalf(event.localPosition[1])
|
|
221
|
-
const { clampedX, clampedY } = clampToWall(
|
|
222
|
-
event.node,
|
|
223
|
-
localX,
|
|
224
|
-
localY,
|
|
225
|
-
draftRef.current.width,
|
|
226
|
-
draftRef.current.height,
|
|
227
|
-
)
|
|
228
|
-
const valid = !hasWallChildOverlap(
|
|
229
|
-
event.node.id,
|
|
230
|
-
clampedX,
|
|
231
|
-
clampedY,
|
|
232
|
-
draftRef.current.width,
|
|
233
|
-
draftRef.current.height,
|
|
234
|
-
draftRef.current.id,
|
|
235
|
-
)
|
|
236
|
-
if (!valid) return
|
|
237
|
-
|
|
238
|
-
const draft = draftRef.current
|
|
239
|
-
draftRef.current = null
|
|
240
|
-
|
|
241
|
-
// Delete transient draft (paused, invisible to undo)
|
|
242
|
-
useScene.getState().deleteNode(draft.id)
|
|
243
|
-
|
|
244
|
-
// Resume → create permanent node (single undoable action)
|
|
245
|
-
useScene.temporal.getState().resume()
|
|
246
|
-
|
|
247
|
-
const levelId = getLevelId()
|
|
248
|
-
const state = useScene.getState()
|
|
249
|
-
const windowCount = Object.values(state.nodes).filter((n) => {
|
|
250
|
-
if (n.type !== 'window') return false
|
|
251
|
-
const wall = n.parentId ? state.nodes[n.parentId as AnyNodeId] : undefined
|
|
252
|
-
return wall?.parentId === levelId
|
|
253
|
-
}).length
|
|
254
|
-
const name = `Window ${windowCount + 1}`
|
|
255
|
-
|
|
256
|
-
const node = WindowNode.parse({
|
|
257
|
-
name,
|
|
258
|
-
position: [clampedX, clampedY, 0],
|
|
259
|
-
rotation: [0, itemRotation, 0],
|
|
260
|
-
side,
|
|
261
|
-
wallId: event.node.id,
|
|
262
|
-
parentId: event.node.id,
|
|
263
|
-
width: draft.width,
|
|
264
|
-
height: draft.height,
|
|
265
|
-
windowType: draft.windowType,
|
|
266
|
-
operationState: draft.operationState,
|
|
267
|
-
awningDirection: draft.awningDirection,
|
|
268
|
-
casementStyle: draft.casementStyle,
|
|
269
|
-
hingesSide: draft.hingesSide,
|
|
270
|
-
frameThickness: draft.frameThickness,
|
|
271
|
-
frameDepth: draft.frameDepth,
|
|
272
|
-
columnRatios: draft.columnRatios,
|
|
273
|
-
rowRatios: draft.rowRatios,
|
|
274
|
-
columnDividerThickness: draft.columnDividerThickness,
|
|
275
|
-
rowDividerThickness: draft.rowDividerThickness,
|
|
276
|
-
sill: draft.sill,
|
|
277
|
-
sillDepth: draft.sillDepth,
|
|
278
|
-
sillThickness: draft.sillThickness,
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
useScene.getState().createNode(node, event.node.id as AnyNodeId)
|
|
282
|
-
useViewer.getState().setSelection({ selectedIds: [node.id] })
|
|
283
|
-
useScene.temporal.getState().pause()
|
|
284
|
-
sfxEmitter.emit('sfx:item-place')
|
|
285
|
-
|
|
286
|
-
event.stopPropagation()
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const onWallLeave = () => {
|
|
290
|
-
destroyDraft()
|
|
291
|
-
hideCursor()
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const onCancel = () => {
|
|
295
|
-
destroyDraft()
|
|
296
|
-
hideCursor()
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
emitter.on('wall:enter', onWallEnter)
|
|
300
|
-
emitter.on('wall:move', onWallMove)
|
|
301
|
-
emitter.on('wall:click', onWallClick)
|
|
302
|
-
emitter.on('wall:leave', onWallLeave)
|
|
303
|
-
emitter.on('tool:cancel', onCancel)
|
|
304
|
-
|
|
305
|
-
return () => {
|
|
306
|
-
destroyDraft()
|
|
307
|
-
hideCursor()
|
|
308
|
-
useScene.temporal.getState().resume()
|
|
309
|
-
emitter.off('wall:enter', onWallEnter)
|
|
310
|
-
emitter.off('wall:move', onWallMove)
|
|
311
|
-
emitter.off('wall:click', onWallClick)
|
|
312
|
-
emitter.off('wall:leave', onWallLeave)
|
|
313
|
-
emitter.off('tool:cancel', onCancel)
|
|
314
|
-
}
|
|
315
|
-
}, [])
|
|
316
|
-
|
|
317
|
-
// Cursor geometry: window outline rectangle (width × height × frameDepth)
|
|
318
|
-
const boxGeo = new BoxGeometry(1.5, 1.5, 0.07)
|
|
319
|
-
const edgesGeo = new EdgesGeometry(boxGeo)
|
|
320
|
-
boxGeo.dispose()
|
|
321
|
-
|
|
322
|
-
return (
|
|
323
|
-
<group ref={cursorGroupRef} visible={false}>
|
|
324
|
-
<lineSegments
|
|
325
|
-
geometry={edgesGeo}
|
|
326
|
-
layers={EDITOR_LAYER}
|
|
327
|
-
material={edgeMaterial}
|
|
328
|
-
ref={edgesRef}
|
|
329
|
-
/>
|
|
330
|
-
</group>
|
|
331
|
-
)
|
|
332
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { ShortcutToken } from '../primitives/shortcut-token'
|
|
2
|
-
|
|
3
|
-
export function CeilingHelper() {
|
|
4
|
-
return (
|
|
5
|
-
<div className="pointer-events-none fixed top-1/2 right-4 z-40 flex -translate-y-1/2 flex-col gap-2 rounded-lg border border-border bg-background/95 px-4 py-3 shadow-lg backdrop-blur-md">
|
|
6
|
-
<div className="flex items-center gap-2 text-sm">
|
|
7
|
-
<ShortcutToken value="Left click" />
|
|
8
|
-
<span className="text-muted-foreground">Add point</span>
|
|
9
|
-
</div>
|
|
10
|
-
<div className="flex items-center gap-2 text-sm">
|
|
11
|
-
<ShortcutToken value="Shift" />
|
|
12
|
-
<span className="text-muted-foreground">Allow non-45° angles</span>
|
|
13
|
-
</div>
|
|
14
|
-
<div className="flex items-center gap-2 text-sm">
|
|
15
|
-
<ShortcutToken value="Esc" />
|
|
16
|
-
<span className="text-muted-foreground">Cancel</span>
|
|
17
|
-
</div>
|
|
18
|
-
</div>
|
|
19
|
-
)
|
|
20
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { ShortcutToken } from '../primitives/shortcut-token'
|
|
2
|
-
|
|
3
|
-
export function SlabHelper() {
|
|
4
|
-
return (
|
|
5
|
-
<div className="pointer-events-none fixed top-1/2 right-4 z-40 flex -translate-y-1/2 flex-col gap-2 rounded-lg border border-border bg-background/95 px-4 py-3 shadow-lg backdrop-blur-md">
|
|
6
|
-
<div className="flex items-center gap-2 text-sm">
|
|
7
|
-
<ShortcutToken value="Left click" />
|
|
8
|
-
<span className="text-muted-foreground">Add point</span>
|
|
9
|
-
</div>
|
|
10
|
-
<div className="flex items-center gap-2 text-sm">
|
|
11
|
-
<ShortcutToken value="Shift" />
|
|
12
|
-
<span className="text-muted-foreground">Allow non-45° angles</span>
|
|
13
|
-
</div>
|
|
14
|
-
<div className="flex items-center gap-2 text-sm">
|
|
15
|
-
<ShortcutToken value="Esc" />
|
|
16
|
-
<span className="text-muted-foreground">Cancel</span>
|
|
17
|
-
</div>
|
|
18
|
-
</div>
|
|
19
|
-
)
|
|
20
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { ShortcutToken } from '../primitives/shortcut-token'
|
|
2
|
-
|
|
3
|
-
export function WallHelper() {
|
|
4
|
-
return (
|
|
5
|
-
<div className="pointer-events-none fixed top-1/2 right-4 z-40 flex -translate-y-1/2 flex-col gap-2 rounded-lg border border-border bg-background/95 px-4 py-3 shadow-lg backdrop-blur-md">
|
|
6
|
-
<div className="flex items-center gap-2 text-sm">
|
|
7
|
-
<ShortcutToken value="Left click" />
|
|
8
|
-
<span className="text-muted-foreground">Set wall start / end</span>
|
|
9
|
-
</div>
|
|
10
|
-
<div className="flex items-center gap-2 text-sm">
|
|
11
|
-
<ShortcutToken value="Shift" />
|
|
12
|
-
<span className="text-muted-foreground">Allow non-45° angles</span>
|
|
13
|
-
</div>
|
|
14
|
-
<div className="flex items-center gap-2 text-sm">
|
|
15
|
-
<ShortcutToken value="Esc" />
|
|
16
|
-
<span className="text-muted-foreground">Cancel</span>
|
|
17
|
-
</div>
|
|
18
|
-
</div>
|
|
19
|
-
)
|
|
20
|
-
}
|
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { type AnyNode, type CeilingNode, useScene } from '@pascal-app/core'
|
|
4
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
5
|
-
import { Edit, Move, Plus, Trash2 } from 'lucide-react'
|
|
6
|
-
import { useCallback, useEffect } from 'react'
|
|
7
|
-
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
8
|
-
import useEditor from '../../../store/use-editor'
|
|
9
|
-
import { ActionButton, ActionGroup } from '../controls/action-button'
|
|
10
|
-
import { PanelSection } from '../controls/panel-section'
|
|
11
|
-
import { SliderControl } from '../controls/slider-control'
|
|
12
|
-
import { PanelWrapper } from './panel-wrapper'
|
|
13
|
-
|
|
14
|
-
export function CeilingPanel() {
|
|
15
|
-
const selectedId = useViewer((s) => s.selection.selectedIds[0])
|
|
16
|
-
const setSelection = useViewer((s) => s.setSelection)
|
|
17
|
-
const updateNode = useScene((s) => s.updateNode)
|
|
18
|
-
const editingHole = useEditor((s) => s.editingHole)
|
|
19
|
-
const setEditingHole = useEditor((s) => s.setEditingHole)
|
|
20
|
-
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
21
|
-
|
|
22
|
-
const node = useScene((s) =>
|
|
23
|
-
selectedId ? (s.nodes[selectedId as AnyNode['id']] as CeilingNode | undefined) : undefined,
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
const handleUpdate = useCallback(
|
|
27
|
-
(updates: Partial<CeilingNode>) => {
|
|
28
|
-
if (!selectedId) return
|
|
29
|
-
updateNode(selectedId as AnyNode['id'], updates)
|
|
30
|
-
},
|
|
31
|
-
[selectedId, updateNode],
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
const handleClose = useCallback(() => {
|
|
35
|
-
setSelection({ selectedIds: [] })
|
|
36
|
-
setEditingHole(null)
|
|
37
|
-
}, [setSelection, setEditingHole])
|
|
38
|
-
|
|
39
|
-
useEffect(() => {
|
|
40
|
-
if (!node) {
|
|
41
|
-
setEditingHole(null)
|
|
42
|
-
}
|
|
43
|
-
}, [node, setEditingHole])
|
|
44
|
-
|
|
45
|
-
useEffect(() => {
|
|
46
|
-
return () => {
|
|
47
|
-
setEditingHole(null)
|
|
48
|
-
}
|
|
49
|
-
}, [setEditingHole])
|
|
50
|
-
|
|
51
|
-
const handleAddHole = useCallback(() => {
|
|
52
|
-
if (!(node && selectedId)) return
|
|
53
|
-
|
|
54
|
-
const polygon = node.polygon
|
|
55
|
-
let cx = 0
|
|
56
|
-
let cz = 0
|
|
57
|
-
for (const [x, z] of polygon) {
|
|
58
|
-
cx += x
|
|
59
|
-
cz += z
|
|
60
|
-
}
|
|
61
|
-
cx /= polygon.length
|
|
62
|
-
cz /= polygon.length
|
|
63
|
-
|
|
64
|
-
const holeSize = 0.5
|
|
65
|
-
const newHole: Array<[number, number]> = [
|
|
66
|
-
[cx - holeSize, cz - holeSize],
|
|
67
|
-
[cx + holeSize, cz - holeSize],
|
|
68
|
-
[cx + holeSize, cz + holeSize],
|
|
69
|
-
[cx - holeSize, cz + holeSize],
|
|
70
|
-
]
|
|
71
|
-
const currentHoles = node?.holes || []
|
|
72
|
-
const currentMetadata = currentHoles.map(
|
|
73
|
-
(_, index) => node?.holeMetadata?.[index] ?? { source: 'manual' as const },
|
|
74
|
-
)
|
|
75
|
-
handleUpdate({
|
|
76
|
-
holes: [...currentHoles, newHole],
|
|
77
|
-
holeMetadata: [...currentMetadata, { source: 'manual' }],
|
|
78
|
-
})
|
|
79
|
-
setEditingHole({ nodeId: selectedId, holeIndex: currentHoles.length })
|
|
80
|
-
}, [node, selectedId, handleUpdate, setEditingHole])
|
|
81
|
-
|
|
82
|
-
const handleEditHole = useCallback(
|
|
83
|
-
(index: number) => {
|
|
84
|
-
if (!selectedId) return
|
|
85
|
-
setEditingHole({ nodeId: selectedId, holeIndex: index })
|
|
86
|
-
},
|
|
87
|
-
[selectedId, setEditingHole],
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
const handleDeleteHole = useCallback(
|
|
91
|
-
(index: number) => {
|
|
92
|
-
if (!selectedId) return
|
|
93
|
-
const currentHoles = node?.holes || []
|
|
94
|
-
if (node?.holeMetadata?.[index]?.source === 'stair') return
|
|
95
|
-
const newHoles = currentHoles.filter((_, i) => i !== index)
|
|
96
|
-
const currentMetadata = currentHoles.map(
|
|
97
|
-
(_, metadataIndex) => node?.holeMetadata?.[metadataIndex] ?? { source: 'manual' as const },
|
|
98
|
-
)
|
|
99
|
-
const newMetadata = currentMetadata.filter((_, i) => i !== index)
|
|
100
|
-
handleUpdate({ holes: newHoles, holeMetadata: newMetadata })
|
|
101
|
-
if (editingHole?.nodeId === selectedId && editingHole?.holeIndex === index) {
|
|
102
|
-
setEditingHole(null)
|
|
103
|
-
}
|
|
104
|
-
},
|
|
105
|
-
[selectedId, node?.holes, node?.holeMetadata, handleUpdate, editingHole, setEditingHole],
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
const handleMove = useCallback(() => {
|
|
109
|
-
if (!node) return
|
|
110
|
-
sfxEmitter.emit('sfx:item-pick')
|
|
111
|
-
setMovingNode(node)
|
|
112
|
-
setSelection({ selectedIds: [] })
|
|
113
|
-
}, [node, setMovingNode, setSelection])
|
|
114
|
-
|
|
115
|
-
if (!(node && node.type === 'ceiling' && selectedId)) return null
|
|
116
|
-
|
|
117
|
-
const calculateArea = (polygon: Array<[number, number]>): number => {
|
|
118
|
-
if (polygon.length < 3) return 0
|
|
119
|
-
let area = 0
|
|
120
|
-
const n = polygon.length
|
|
121
|
-
for (let i = 0; i < n; i++) {
|
|
122
|
-
const j = (i + 1) % n
|
|
123
|
-
const current = polygon[i]!
|
|
124
|
-
const next = polygon[j]!
|
|
125
|
-
area += current[0] * next[1]
|
|
126
|
-
area -= next[0] * current[1]
|
|
127
|
-
}
|
|
128
|
-
return Math.abs(area) / 2
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const area = calculateArea(node.polygon)
|
|
132
|
-
|
|
133
|
-
return (
|
|
134
|
-
<PanelWrapper
|
|
135
|
-
icon="/icons/ceiling.png"
|
|
136
|
-
onClose={handleClose}
|
|
137
|
-
title={node.name || 'Ceiling'}
|
|
138
|
-
width={320}
|
|
139
|
-
>
|
|
140
|
-
<PanelSection title="Height">
|
|
141
|
-
<SliderControl
|
|
142
|
-
label="Height"
|
|
143
|
-
max={6}
|
|
144
|
-
min={0}
|
|
145
|
-
onChange={(v) => handleUpdate({ height: v })}
|
|
146
|
-
precision={3}
|
|
147
|
-
step={0.01}
|
|
148
|
-
unit="m"
|
|
149
|
-
value={Math.round(node.height * 1000) / 1000}
|
|
150
|
-
/>
|
|
151
|
-
|
|
152
|
-
<div className="mt-2 grid grid-cols-3 gap-1.5 px-1 pb-1">
|
|
153
|
-
<ActionButton label="Low (2.4m)" onClick={() => handleUpdate({ height: 2.4 })} />
|
|
154
|
-
<ActionButton label="Standard (2.5m)" onClick={() => handleUpdate({ height: 2.5 })} />
|
|
155
|
-
<ActionButton label="High (3.0m)" onClick={() => handleUpdate({ height: 3.0 })} />
|
|
156
|
-
</div>
|
|
157
|
-
</PanelSection>
|
|
158
|
-
|
|
159
|
-
<PanelSection title="Info">
|
|
160
|
-
<div className="flex items-center justify-between px-2 py-1 text-muted-foreground text-sm">
|
|
161
|
-
<span>Area</span>
|
|
162
|
-
<span className="font-mono text-white">{area.toFixed(2)} m²</span>
|
|
163
|
-
</div>
|
|
164
|
-
</PanelSection>
|
|
165
|
-
|
|
166
|
-
<PanelSection title="Holes">
|
|
167
|
-
{node.holes && node.holes.length > 0 ? (
|
|
168
|
-
<div className="flex flex-col gap-1 pb-2">
|
|
169
|
-
{node.holes.map((hole, index) => {
|
|
170
|
-
const holeArea = calculateArea(hole)
|
|
171
|
-
const isEditing =
|
|
172
|
-
editingHole?.nodeId === selectedId && editingHole?.holeIndex === index
|
|
173
|
-
const source = node.holeMetadata?.[index]?.source ?? 'manual'
|
|
174
|
-
const isAutoHole = source === 'stair'
|
|
175
|
-
return (
|
|
176
|
-
<div
|
|
177
|
-
className={`flex items-center justify-between rounded-lg border p-2 transition-colors ${
|
|
178
|
-
isEditing
|
|
179
|
-
? 'border-primary/50 bg-primary/10'
|
|
180
|
-
: 'border-transparent hover:bg-accent/30'
|
|
181
|
-
}`}
|
|
182
|
-
key={index}
|
|
183
|
-
>
|
|
184
|
-
<div className="min-w-0 flex-1">
|
|
185
|
-
<p
|
|
186
|
-
className={`font-medium text-xs ${isEditing ? 'text-primary' : 'text-white'}`}
|
|
187
|
-
>
|
|
188
|
-
Hole {index + 1} {isEditing && '(Editing)'}
|
|
189
|
-
</p>
|
|
190
|
-
<p className="text-[10px] text-muted-foreground">
|
|
191
|
-
{holeArea.toFixed(2)} m² · {hole.length} pts ·{' '}
|
|
192
|
-
{isAutoHole ? 'Auto stair cutout' : 'Manual'}
|
|
193
|
-
</p>
|
|
194
|
-
</div>
|
|
195
|
-
<div className="flex items-center gap-1">
|
|
196
|
-
{isEditing ? (
|
|
197
|
-
<ActionButton
|
|
198
|
-
className="h-7 bg-primary text-primary-foreground hover:bg-primary/90"
|
|
199
|
-
label="Done"
|
|
200
|
-
onClick={() => setEditingHole(null)}
|
|
201
|
-
/>
|
|
202
|
-
) : isAutoHole ? (
|
|
203
|
-
<div className="rounded-md bg-[#2C2C2E] px-2 py-1 text-[10px] text-muted-foreground">
|
|
204
|
-
Auto
|
|
205
|
-
</div>
|
|
206
|
-
) : (
|
|
207
|
-
<>
|
|
208
|
-
<button
|
|
209
|
-
className="flex h-7 w-7 items-center justify-center rounded-md bg-[#2C2C2E] text-muted-foreground hover:bg-[#3e3e3e] hover:text-foreground"
|
|
210
|
-
onClick={() => handleEditHole(index)}
|
|
211
|
-
type="button"
|
|
212
|
-
>
|
|
213
|
-
<Edit className="h-3.5 w-3.5" />
|
|
214
|
-
</button>
|
|
215
|
-
<button
|
|
216
|
-
className="flex h-7 w-7 items-center justify-center rounded-md bg-red-500/10 text-red-400 hover:bg-red-500/20 hover:text-red-300"
|
|
217
|
-
onClick={() => handleDeleteHole(index)}
|
|
218
|
-
type="button"
|
|
219
|
-
>
|
|
220
|
-
<Trash2 className="h-3.5 w-3.5" />
|
|
221
|
-
</button>
|
|
222
|
-
</>
|
|
223
|
-
)}
|
|
224
|
-
</div>
|
|
225
|
-
</div>
|
|
226
|
-
)
|
|
227
|
-
})}
|
|
228
|
-
</div>
|
|
229
|
-
) : (
|
|
230
|
-
<div className="px-2 py-3 text-center text-muted-foreground text-xs">No holes</div>
|
|
231
|
-
)}
|
|
232
|
-
|
|
233
|
-
<div className="px-1 pt-1 pb-1">
|
|
234
|
-
<ActionButton
|
|
235
|
-
className="w-full"
|
|
236
|
-
disabled={editingHole?.nodeId === selectedId}
|
|
237
|
-
icon={<Plus className="h-3.5 w-3.5" />}
|
|
238
|
-
label="Add Hole"
|
|
239
|
-
onClick={handleAddHole}
|
|
240
|
-
/>
|
|
241
|
-
</div>
|
|
242
|
-
</PanelSection>
|
|
243
|
-
|
|
244
|
-
<ActionGroup>
|
|
245
|
-
<ActionButton icon={<Move className="h-3.5 w-3.5" />} label="Move" onClick={handleMove} />
|
|
246
|
-
</ActionGroup>
|
|
247
|
-
</PanelWrapper>
|
|
248
|
-
)
|
|
249
|
-
}
|