@pascal-app/editor 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +13 -9
- package/src/components/editor/bottom-sheet.tsx +149 -0
- package/src/components/editor/custom-camera-controls.tsx +74 -5
- package/src/components/editor/editor-layout-mobile.tsx +264 -0
- package/src/components/editor/editor-layout-v2.tsx +24 -3
- package/src/components/editor/first-person/build-collider-world.ts +363 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +860 -0
- package/src/components/editor/first-person-controls.tsx +496 -143
- package/src/components/editor/floating-action-menu.tsx +32 -55
- package/src/components/editor/floorplan-background-selection.ts +113 -0
- package/src/components/editor/floorplan-panel.tsx +9861 -3297
- package/src/components/editor/index.tsx +295 -32
- package/src/components/editor/selection-manager.tsx +575 -13
- package/src/components/editor/snapshot-capture-overlay.tsx +465 -0
- package/src/components/editor/thumbnail-generator.tsx +56 -68
- package/src/components/editor/use-floorplan-background-placement.ts +257 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +171 -0
- package/src/components/editor/use-floorplan-scene-data.ts +189 -0
- package/src/components/editor/wall-measurement-label.tsx +267 -36
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +95 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +160 -0
- package/src/components/editor-2d/floorplan-hotkey-handlers.tsx +92 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +124 -0
- package/src/components/editor-2d/renderers/floorplan-marquee-layer.tsx +58 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +202 -0
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +113 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +474 -0
- package/src/components/editor-2d/svg-paths.ts +119 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +10 -12
- package/src/components/systems/roof/roof-edit-system.tsx +1 -1
- package/src/components/systems/stair/stair-edit-system.tsx +1 -1
- 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/ceiling/ceiling-boundary-editor.tsx +1 -0
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +1 -0
- package/src/components/tools/ceiling/ceiling-tool.tsx +5 -5
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +9 -2
- package/src/components/tools/column/column-tool.tsx +97 -0
- package/src/components/tools/column/move-column-tool.tsx +105 -0
- package/src/components/tools/door/door-tool.tsx +7 -0
- package/src/components/tools/door/move-door-tool.tsx +28 -8
- package/src/components/tools/fence/curve-fence-tool.tsx +4 -5
- package/src/components/tools/fence/fence-drafting.ts +10 -3
- package/src/components/tools/fence/fence-tool.tsx +160 -4
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +139 -25
- package/src/components/tools/fence/move-fence-tool.tsx +111 -40
- package/src/components/tools/item/move-tool.tsx +7 -1
- package/src/components/tools/item/placement-math.ts +32 -5
- package/src/components/tools/item/placement-strategies.ts +110 -31
- package/src/components/tools/item/placement-types.ts +7 -0
- package/src/components/tools/item/use-draft-node.ts +1 -0
- package/src/components/tools/item/use-placement-coordinator.tsx +558 -52
- package/src/components/tools/roof/move-roof-tool.tsx +29 -17
- package/src/components/tools/select/box-select-tool.tsx +12 -17
- package/src/components/tools/shared/polygon-editor.tsx +153 -28
- package/src/components/tools/shared/segment-angle.ts +156 -0
- package/src/components/tools/slab/slab-boundary-editor.tsx +1 -0
- package/src/components/tools/slab/slab-hole-editor.tsx +1 -0
- package/src/components/tools/spawn/move-spawn-tool.tsx +101 -0
- package/src/components/tools/spawn/spawn-tool.tsx +130 -0
- package/src/components/tools/tool-manager.tsx +20 -5
- package/src/components/tools/wall/curve-wall-tool.tsx +8 -6
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +131 -27
- package/src/components/tools/wall/move-wall-tool.tsx +6 -4
- package/src/components/tools/wall/wall-drafting.ts +18 -9
- package/src/components/tools/wall/wall-tool.tsx +136 -4
- package/src/components/tools/window/move-window-tool.tsx +18 -0
- package/src/components/tools/window/window-tool.tsx +5 -0
- package/src/components/tools/zone/zone-tool.tsx +20 -5
- package/src/components/ui/action-menu/camera-actions.tsx +37 -33
- package/src/components/ui/action-menu/control-modes.tsx +34 -1
- package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
- package/src/components/ui/action-menu/index.tsx +98 -59
- package/src/components/ui/action-menu/structure-tools.tsx +2 -0
- package/src/components/ui/action-menu/view-toggles.tsx +418 -41
- package/src/components/ui/command-palette/editor-commands.tsx +24 -5
- package/src/components/ui/command-palette/index.tsx +4 -255
- package/src/components/ui/controls/material-picker.tsx +154 -164
- package/src/components/ui/controls/slider-control.tsx +66 -18
- package/src/components/ui/floating-level-selector.tsx +286 -55
- package/src/components/ui/helpers/helper-manager.tsx +10 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +2563 -1239
- package/src/components/ui/item-catalog/item-catalog.tsx +96 -187
- package/src/components/ui/level-duplicate-dialog.tsx +113 -0
- package/src/components/ui/panels/ceiling-panel.tsx +3 -28
- package/src/components/ui/panels/column-panel.tsx +759 -0
- package/src/components/ui/panels/door-panel.tsx +989 -290
- package/src/components/ui/panels/fence-panel.tsx +2 -49
- package/src/components/ui/panels/mobile-panel-sheet.tsx +108 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +100 -0
- package/src/components/ui/panels/node-display.ts +39 -0
- package/src/components/ui/panels/paint-panel.tsx +163 -0
- package/src/components/ui/panels/panel-manager.tsx +208 -28
- package/src/components/ui/panels/panel-wrapper.tsx +48 -39
- package/src/components/ui/panels/reference-panel.tsx +253 -5
- package/src/components/ui/panels/roof-panel.tsx +13 -64
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -25
- package/src/components/ui/panels/slab-panel.tsx +4 -30
- package/src/components/ui/panels/spawn-panel.tsx +161 -0
- package/src/components/ui/panels/stair-panel.tsx +20 -74
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -25
- package/src/components/ui/panels/wall-panel.tsx +10 -8
- package/src/components/ui/panels/window-panel.tsx +668 -139
- package/src/components/ui/primitives/number-input.tsx +1 -1
- package/src/components/ui/primitives/sidebar.tsx +0 -0
- 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/mobile-tab-bar.tsx +46 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +330 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +0 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +0 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +77 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +105 -22
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +0 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +76 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +11 -3
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +10 -5
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +1 -1
- package/src/components/ui/sidebar/tab-bar.tsx +3 -0
- package/src/components/ui/slider.tsx +1 -1
- package/src/components/viewer-overlay.tsx +0 -0
- package/src/components/viewer-zone-system.tsx +0 -0
- package/src/hooks/use-auto-frame.ts +45 -0
- package/src/hooks/use-auto-save.ts +14 -0
- package/src/hooks/use-keyboard.ts +74 -7
- package/src/hooks/use-mobile.ts +12 -12
- package/src/index.tsx +8 -1
- package/src/lib/door-interaction.ts +88 -0
- package/src/lib/floorplan/geometry.ts +263 -0
- package/src/lib/floorplan/index.ts +38 -0
- package/src/lib/floorplan/items.ts +179 -0
- package/src/lib/floorplan/selection-tool.ts +231 -0
- package/src/lib/floorplan/stairs.ts +478 -0
- package/src/lib/floorplan/types.ts +57 -0
- package/src/lib/floorplan/walls.ts +23 -0
- package/src/lib/guide-events.ts +10 -0
- package/src/lib/level-duplication.test.ts +70 -0
- package/src/lib/level-duplication.ts +153 -0
- package/src/lib/local-guide-image.ts +42 -0
- package/src/lib/material-paint.ts +284 -0
- package/src/lib/roof-duplication.ts +214 -0
- package/src/lib/scene-bounds.test.ts +183 -0
- package/src/lib/scene-bounds.ts +169 -0
- package/src/lib/scene.ts +0 -0
- package/src/lib/sfx-bus.ts +2 -0
- package/src/lib/sfx-player.ts +5 -5
- package/src/lib/stair-duplication.ts +126 -0
- package/src/lib/window-interaction.ts +86 -0
- package/src/store/use-editor.tsx +186 -62
- package/tsconfig.json +2 -1
- 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/ui/viewer-toolbar.tsx +0 -395
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AnyNode,
|
|
5
|
+
type BuildingNode,
|
|
6
|
+
type CeilingNode,
|
|
7
|
+
type DoorNode,
|
|
8
|
+
type FenceNode,
|
|
9
|
+
type GuideNode,
|
|
10
|
+
type LevelNode,
|
|
11
|
+
type RoofNode,
|
|
12
|
+
type SiteNode,
|
|
13
|
+
type SlabNode,
|
|
14
|
+
type SpawnNode,
|
|
15
|
+
useScene,
|
|
16
|
+
type WallNode,
|
|
17
|
+
type WindowNode,
|
|
18
|
+
type ZoneNode as ZoneNodeType,
|
|
19
|
+
} from '@pascal-app/core'
|
|
20
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
21
|
+
import { collectLevelDescendants } from '../../lib/floorplan'
|
|
22
|
+
|
|
23
|
+
type OpeningNode = WindowNode | DoorNode
|
|
24
|
+
|
|
25
|
+
const DEFAULT_BUILDING_POSITION = [0, 0, 0] as const satisfies [number, number, number]
|
|
26
|
+
|
|
27
|
+
function useLevelChildren<TNode extends AnyNode>(
|
|
28
|
+
levelId: LevelNode['id'] | null,
|
|
29
|
+
typeGuard: (node: AnyNode | undefined) => node is TNode,
|
|
30
|
+
) {
|
|
31
|
+
return useScene(
|
|
32
|
+
useShallow((state) => {
|
|
33
|
+
if (!levelId) {
|
|
34
|
+
return [] as TNode[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const levelNode = state.nodes[levelId]
|
|
38
|
+
if (!levelNode || levelNode.type !== 'level') {
|
|
39
|
+
return [] as TNode[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return levelNode.children.map((childId) => state.nodes[childId]).filter(typeGuard)
|
|
43
|
+
}),
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function useFloorplanSceneData({
|
|
48
|
+
buildingId,
|
|
49
|
+
levelId,
|
|
50
|
+
}: {
|
|
51
|
+
buildingId: BuildingNode['id'] | null
|
|
52
|
+
levelId: LevelNode['id'] | null
|
|
53
|
+
}) {
|
|
54
|
+
const levelNode = useScene((state) =>
|
|
55
|
+
levelId ? (state.nodes[levelId] as LevelNode | undefined) : undefined,
|
|
56
|
+
)
|
|
57
|
+
const currentBuildingId =
|
|
58
|
+
levelNode?.type === 'level' && levelNode.parentId
|
|
59
|
+
? (levelNode.parentId as BuildingNode['id'])
|
|
60
|
+
: buildingId
|
|
61
|
+
|
|
62
|
+
const buildingRotationY = useScene((state) => {
|
|
63
|
+
if (!currentBuildingId) return 0
|
|
64
|
+
const node = state.nodes[currentBuildingId]
|
|
65
|
+
return node?.type === 'building' ? (node.rotation[1] ?? 0) : 0
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const buildingPosition = useScene((state) => {
|
|
69
|
+
if (!currentBuildingId) {
|
|
70
|
+
return DEFAULT_BUILDING_POSITION
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const node = state.nodes[currentBuildingId]
|
|
74
|
+
return node?.type === 'building'
|
|
75
|
+
? (node.position as [number, number, number])
|
|
76
|
+
: DEFAULT_BUILDING_POSITION
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const site = useScene((state) => {
|
|
80
|
+
for (const rootNodeId of state.rootNodeIds) {
|
|
81
|
+
const node = state.nodes[rootNodeId]
|
|
82
|
+
if (node?.type === 'site') {
|
|
83
|
+
return node as SiteNode
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return null
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const floorplanLevels = useScene(
|
|
91
|
+
useShallow((state) => {
|
|
92
|
+
if (!currentBuildingId) {
|
|
93
|
+
return [] as LevelNode[]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const buildingNode = state.nodes[currentBuildingId]
|
|
97
|
+
if (!buildingNode || buildingNode.type !== 'building') {
|
|
98
|
+
return [] as LevelNode[]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return buildingNode.children
|
|
102
|
+
.map((childId) => state.nodes[childId])
|
|
103
|
+
.filter((node): node is LevelNode => node?.type === 'level')
|
|
104
|
+
.sort((a, b) => a.level - b.level)
|
|
105
|
+
}),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
const walls = useLevelChildren(levelId, (node): node is WallNode => node?.type === 'wall')
|
|
109
|
+
const fences = useLevelChildren(levelId, (node): node is FenceNode => node?.type === 'fence')
|
|
110
|
+
const slabs = useLevelChildren(levelId, (node): node is SlabNode => node?.type === 'slab')
|
|
111
|
+
const ceilings = useLevelChildren(
|
|
112
|
+
levelId,
|
|
113
|
+
(node): node is CeilingNode => node?.type === 'ceiling',
|
|
114
|
+
)
|
|
115
|
+
const levelGuides = useLevelChildren(levelId, (node): node is GuideNode => node?.type === 'guide')
|
|
116
|
+
const zones = useLevelChildren(levelId, (node): node is ZoneNodeType => node?.type === 'zone')
|
|
117
|
+
const spawns = useLevelChildren(levelId, (node): node is SpawnNode => node?.type === 'spawn')
|
|
118
|
+
const roofs = useScene(
|
|
119
|
+
useShallow((state) => {
|
|
120
|
+
if (!levelId) {
|
|
121
|
+
return [] as RoofNode[]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const nextLevelNode = state.nodes[levelId]
|
|
125
|
+
if (!nextLevelNode || nextLevelNode.type !== 'level') {
|
|
126
|
+
return [] as RoofNode[]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return nextLevelNode.children
|
|
130
|
+
.map((childId) => state.nodes[childId])
|
|
131
|
+
.filter((node): node is RoofNode => node?.type === 'roof' && node.visible !== false)
|
|
132
|
+
}),
|
|
133
|
+
)
|
|
134
|
+
const openings = useScene(
|
|
135
|
+
useShallow((state) => {
|
|
136
|
+
if (!levelId) {
|
|
137
|
+
return [] as OpeningNode[]
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const nextLevelNode = state.nodes[levelId]
|
|
141
|
+
if (!nextLevelNode || nextLevelNode.type !== 'level') {
|
|
142
|
+
return [] as OpeningNode[]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const nextWalls = nextLevelNode.children
|
|
146
|
+
.map((childId) => state.nodes[childId])
|
|
147
|
+
.filter((node): node is WallNode => node?.type === 'wall')
|
|
148
|
+
|
|
149
|
+
return nextWalls.flatMap((wall) =>
|
|
150
|
+
wall.children
|
|
151
|
+
.map((childId) => state.nodes[childId])
|
|
152
|
+
.filter((node): node is OpeningNode => node?.type === 'window' || node?.type === 'door'),
|
|
153
|
+
)
|
|
154
|
+
}),
|
|
155
|
+
)
|
|
156
|
+
const levelDescendantNodes = useScene(
|
|
157
|
+
useShallow((state) => {
|
|
158
|
+
if (!levelId) {
|
|
159
|
+
return [] as AnyNode[]
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const nextLevelNode = state.nodes[levelId]
|
|
163
|
+
if (!nextLevelNode || nextLevelNode.type !== 'level') {
|
|
164
|
+
return [] as AnyNode[]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return collectLevelDescendants(nextLevelNode, state.nodes as Record<string, AnyNode>)
|
|
168
|
+
}),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
buildingPosition,
|
|
173
|
+
buildingRotationY,
|
|
174
|
+
currentBuildingId,
|
|
175
|
+
ceilings,
|
|
176
|
+
fences,
|
|
177
|
+
floorplanLevels,
|
|
178
|
+
levelDescendantNodes,
|
|
179
|
+
levelGuides,
|
|
180
|
+
levelNode,
|
|
181
|
+
openings,
|
|
182
|
+
roofs,
|
|
183
|
+
site,
|
|
184
|
+
slabs,
|
|
185
|
+
spawns,
|
|
186
|
+
walls,
|
|
187
|
+
zones,
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
getWallMiterBoundaryPoints,
|
|
9
9
|
getWallPlanFootprint,
|
|
10
10
|
getWallSurfacePolygon,
|
|
11
|
+
type ItemNode,
|
|
11
12
|
isCurvedWall,
|
|
12
13
|
type Point2D,
|
|
13
14
|
pointToKey,
|
|
@@ -27,6 +28,8 @@ const GUIDE_Y_OFFSET = 0.08
|
|
|
27
28
|
const LABEL_LIFT = 0.08
|
|
28
29
|
const BAR_THICKNESS = 0.012
|
|
29
30
|
const LINE_OPACITY = 0.95
|
|
31
|
+
const HEIGHT_TICK_HALF_LENGTH = 0.14
|
|
32
|
+
const HEIGHT_GUIDE_OUTSIDE_OFFSET = 0.16
|
|
30
33
|
|
|
31
34
|
const BAR_AXIS = new THREE.Vector3(0, 1, 0)
|
|
32
35
|
|
|
@@ -39,6 +42,18 @@ type MeasurementGuide = {
|
|
|
39
42
|
extEndStart: Vec3
|
|
40
43
|
extEndEnd: Vec3
|
|
41
44
|
labelPosition: Vec3
|
|
45
|
+
heightStart: Vec3
|
|
46
|
+
heightEnd: Vec3
|
|
47
|
+
heightBottomTickStart: Vec3
|
|
48
|
+
heightBottomTickEnd: Vec3
|
|
49
|
+
heightTopTickStart: Vec3
|
|
50
|
+
heightTopTickEnd: Vec3
|
|
51
|
+
heightLabelPosition: Vec3
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type WallFaceLine = {
|
|
55
|
+
start: Point2D
|
|
56
|
+
end: Point2D
|
|
42
57
|
}
|
|
43
58
|
|
|
44
59
|
function formatMeasurement(value: number, unit: 'metric' | 'imperial') {
|
|
@@ -57,28 +72,28 @@ export function WallMeasurementLabel() {
|
|
|
57
72
|
const nodes = useScene((state) => state.nodes)
|
|
58
73
|
|
|
59
74
|
const selectedId = selectedIds.length === 1 ? selectedIds[0] : null
|
|
60
|
-
const selectedNode = selectedId ? nodes[selectedId as
|
|
61
|
-
const
|
|
75
|
+
const selectedNode = selectedId ? nodes[selectedId as AnyNodeId] : null
|
|
76
|
+
const measurableNode =
|
|
77
|
+
selectedNode?.type === 'wall' || selectedNode?.type === 'item' ? selectedNode : null
|
|
62
78
|
|
|
63
|
-
const [
|
|
64
|
-
id:
|
|
79
|
+
const [objectState, setObjectState] = useState<{
|
|
80
|
+
id: AnyNodeId
|
|
65
81
|
object: THREE.Object3D
|
|
66
82
|
} | null>(null)
|
|
67
|
-
const
|
|
68
|
-
selectedId && wallObjectState?.id === selectedId ? wallObjectState.object : null
|
|
83
|
+
const selectedObject = selectedId && objectState?.id === selectedId ? objectState.object : null
|
|
69
84
|
|
|
70
85
|
useFrame(() => {
|
|
71
|
-
if (!selectedId ||
|
|
86
|
+
if (!selectedId || selectedObject) return
|
|
72
87
|
|
|
73
|
-
const
|
|
74
|
-
if (
|
|
75
|
-
|
|
88
|
+
const nextObject = sceneRegistry.nodes.get(selectedId)
|
|
89
|
+
if (nextObject) {
|
|
90
|
+
setObjectState({ id: selectedId as AnyNodeId, object: nextObject })
|
|
76
91
|
}
|
|
77
92
|
})
|
|
78
93
|
|
|
79
|
-
if (!(
|
|
94
|
+
if (!(measurableNode && selectedObject)) return null
|
|
80
95
|
|
|
81
|
-
return createPortal(<
|
|
96
|
+
return createPortal(<SelectedMeasurementAnnotation node={measurableNode} />, selectedObject)
|
|
82
97
|
}
|
|
83
98
|
|
|
84
99
|
function getLevelWalls(
|
|
@@ -97,6 +112,114 @@ function getLevelWalls(
|
|
|
97
112
|
.filter((node): node is WallNode => Boolean(node && node.type === 'wall'))
|
|
98
113
|
}
|
|
99
114
|
|
|
115
|
+
function pointMatchesWallPlanPoint(point: Point2D | undefined, planPoint: [number, number]) {
|
|
116
|
+
if (!point) return false
|
|
117
|
+
|
|
118
|
+
return Math.abs(point.x - planPoint[0]) < 1e-6 && Math.abs(point.y - planPoint[1]) < 1e-6
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function getWallFaceLines(
|
|
122
|
+
wall: WallNode,
|
|
123
|
+
miterData: WallMiterData,
|
|
124
|
+
): { left: WallFaceLine; right: WallFaceLine } | null {
|
|
125
|
+
if (isCurvedWall(wall)) return null
|
|
126
|
+
|
|
127
|
+
const footprint = getWallPlanFootprint(wall, miterData)
|
|
128
|
+
if (footprint.length < 4) return null
|
|
129
|
+
|
|
130
|
+
const startRight = footprint[0]
|
|
131
|
+
const endRight = footprint[1]
|
|
132
|
+
const hasEndCenterPoint = pointMatchesWallPlanPoint(footprint[2], wall.end)
|
|
133
|
+
const endLeft = footprint[hasEndCenterPoint ? 3 : 2]
|
|
134
|
+
const lastPoint = footprint[footprint.length - 1]
|
|
135
|
+
const hasStartCenterPoint = pointMatchesWallPlanPoint(lastPoint, wall.start)
|
|
136
|
+
const startLeft = footprint[hasStartCenterPoint ? footprint.length - 2 : footprint.length - 1]
|
|
137
|
+
|
|
138
|
+
if (!(startRight && endRight && endLeft && startLeft)) return null
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
left: {
|
|
142
|
+
start: startLeft,
|
|
143
|
+
end: endLeft,
|
|
144
|
+
},
|
|
145
|
+
right: {
|
|
146
|
+
start: startRight,
|
|
147
|
+
end: endRight,
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function getLineMidpoint(line: WallFaceLine): Point2D {
|
|
153
|
+
return {
|
|
154
|
+
x: (line.start.x + line.end.x) / 2,
|
|
155
|
+
y: (line.start.y + line.end.y) / 2,
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function getLevelWallsCenter(levelWalls: WallNode[]): Point2D {
|
|
160
|
+
let minX = Number.POSITIVE_INFINITY
|
|
161
|
+
let maxX = Number.NEGATIVE_INFINITY
|
|
162
|
+
let minY = Number.POSITIVE_INFINITY
|
|
163
|
+
let maxY = Number.NEGATIVE_INFINITY
|
|
164
|
+
|
|
165
|
+
for (const candidateWall of levelWalls) {
|
|
166
|
+
minX = Math.min(minX, candidateWall.start[0], candidateWall.end[0])
|
|
167
|
+
maxX = Math.max(maxX, candidateWall.start[0], candidateWall.end[0])
|
|
168
|
+
minY = Math.min(minY, candidateWall.start[1], candidateWall.end[1])
|
|
169
|
+
maxY = Math.max(maxY, candidateWall.start[1], candidateWall.end[1])
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
x: minX === Number.POSITIVE_INFINITY ? 0 : (minX + maxX) / 2,
|
|
174
|
+
y: minY === Number.POSITIVE_INFINITY ? 0 : (minY + maxY) / 2,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function getWallOuterFaceLine(
|
|
179
|
+
wall: WallNode,
|
|
180
|
+
miterData: WallMiterData,
|
|
181
|
+
levelWalls: WallNode[],
|
|
182
|
+
): WallFaceLine | null {
|
|
183
|
+
const faceLines = getWallFaceLines(wall, miterData)
|
|
184
|
+
if (!faceLines) return null
|
|
185
|
+
|
|
186
|
+
if (wall.frontSide === 'exterior' && wall.backSide !== 'exterior') {
|
|
187
|
+
return faceLines.left
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (wall.backSide === 'exterior' && wall.frontSide !== 'exterior') {
|
|
191
|
+
return faceLines.right
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const dx = wall.end[0] - wall.start[0]
|
|
195
|
+
const dy = wall.end[1] - wall.start[1]
|
|
196
|
+
const length = Math.hypot(dx, dy)
|
|
197
|
+
if (length < 1e-6) return null
|
|
198
|
+
|
|
199
|
+
const wallMidpoint = {
|
|
200
|
+
x: (wall.start[0] + wall.end[0]) / 2,
|
|
201
|
+
y: (wall.start[1] + wall.end[1]) / 2,
|
|
202
|
+
}
|
|
203
|
+
const levelCenter = getLevelWallsCenter(levelWalls)
|
|
204
|
+
const normal = { x: -dy / length, y: dx / length }
|
|
205
|
+
const fromCenter = {
|
|
206
|
+
x: wallMidpoint.x - levelCenter.x,
|
|
207
|
+
y: wallMidpoint.y - levelCenter.y,
|
|
208
|
+
}
|
|
209
|
+
const outwardNormal =
|
|
210
|
+
fromCenter.x * normal.x + fromCenter.y * normal.y >= 0 ? normal : { x: -normal.x, y: -normal.y }
|
|
211
|
+
const rightMidpoint = getLineMidpoint(faceLines.right)
|
|
212
|
+
const leftMidpoint = getLineMidpoint(faceLines.left)
|
|
213
|
+
const rightScore =
|
|
214
|
+
(rightMidpoint.x - wallMidpoint.x) * outwardNormal.x +
|
|
215
|
+
(rightMidpoint.y - wallMidpoint.y) * outwardNormal.y
|
|
216
|
+
const leftScore =
|
|
217
|
+
(leftMidpoint.x - wallMidpoint.x) * outwardNormal.x +
|
|
218
|
+
(leftMidpoint.y - wallMidpoint.y) * outwardNormal.y
|
|
219
|
+
|
|
220
|
+
return rightScore >= leftScore ? faceLines.right : faceLines.left
|
|
221
|
+
}
|
|
222
|
+
|
|
100
223
|
function getWallMiddlePoints(
|
|
101
224
|
wall: WallNode,
|
|
102
225
|
miterData: WallMiterData,
|
|
@@ -136,7 +259,10 @@ function worldPointToWallLocal(wall: WallNode, point: Point2D): Vec3 {
|
|
|
136
259
|
return [dx * cosA - dz * sinA, 0, dx * sinA + dz * cosA]
|
|
137
260
|
}
|
|
138
261
|
|
|
139
|
-
function getWallExteriorOffsetSign(
|
|
262
|
+
function getWallExteriorOffsetSign(
|
|
263
|
+
wall: Pick<WallNode, 'start' | 'end' | 'frontSide' | 'backSide'>,
|
|
264
|
+
levelWalls: WallNode[],
|
|
265
|
+
) {
|
|
140
266
|
if (wall.frontSide === 'exterior' && wall.backSide !== 'exterior') {
|
|
141
267
|
return 1
|
|
142
268
|
}
|
|
@@ -145,10 +271,31 @@ function getWallExteriorOffsetSign(wall: Pick<WallNode, 'frontSide' | 'backSide'
|
|
|
145
271
|
return -1
|
|
146
272
|
}
|
|
147
273
|
|
|
148
|
-
|
|
274
|
+
const dx = wall.end[0] - wall.start[0]
|
|
275
|
+
const dy = wall.end[1] - wall.start[1]
|
|
276
|
+
const length = Math.hypot(dx, dy)
|
|
277
|
+
|
|
278
|
+
if (length < 1e-6) return 1
|
|
279
|
+
|
|
280
|
+
const wallMidpoint = {
|
|
281
|
+
x: (wall.start[0] + wall.end[0]) / 2,
|
|
282
|
+
y: (wall.start[1] + wall.end[1]) / 2,
|
|
283
|
+
}
|
|
284
|
+
const levelCenter = getLevelWallsCenter(levelWalls)
|
|
285
|
+
const normal = { x: -dy / length, y: dx / length }
|
|
286
|
+
const fromCenter = {
|
|
287
|
+
x: wallMidpoint.x - levelCenter.x,
|
|
288
|
+
y: wallMidpoint.y - levelCenter.y,
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return fromCenter.x * normal.x + fromCenter.y * normal.y >= 0 ? 1 : -1
|
|
149
292
|
}
|
|
150
293
|
|
|
151
|
-
function getCurvedWallMeasurementPath(
|
|
294
|
+
function getCurvedWallMeasurementPath(
|
|
295
|
+
wall: WallNode,
|
|
296
|
+
miterData: WallMiterData,
|
|
297
|
+
levelWalls: WallNode[],
|
|
298
|
+
): Point2D[] | null {
|
|
152
299
|
const boundaryPoints = getWallMiterBoundaryPoints(wall, miterData)
|
|
153
300
|
if (!boundaryPoints) return null
|
|
154
301
|
|
|
@@ -156,7 +303,7 @@ function getCurvedWallMeasurementPath(wall: WallNode, miterData: WallMiterData):
|
|
|
156
303
|
const sidePointCount = 25
|
|
157
304
|
if (surface.length < sidePointCount * 2) return null
|
|
158
305
|
|
|
159
|
-
const offsetSign = getWallExteriorOffsetSign(wall)
|
|
306
|
+
const offsetSign = getWallExteriorOffsetSign(wall, levelWalls)
|
|
160
307
|
if (offsetSign >= 0) {
|
|
161
308
|
return surface.slice(sidePointCount).reverse()
|
|
162
309
|
}
|
|
@@ -170,14 +317,16 @@ function buildMeasurementGuide(
|
|
|
170
317
|
): MeasurementGuide | null {
|
|
171
318
|
const levelWalls = getLevelWalls(wall, nodes)
|
|
172
319
|
const miterData = calculateLevelMiters(levelWalls)
|
|
173
|
-
const
|
|
174
|
-
|
|
320
|
+
const measurementLine = getWallOuterFaceLine(wall, miterData, levelWalls)
|
|
321
|
+
const fallbackMiddlePoints = measurementLine ? null : getWallMiddlePoints(wall, miterData)
|
|
322
|
+
const measurementPoints = measurementLine ?? fallbackMiddlePoints
|
|
323
|
+
if (!measurementPoints) return null
|
|
175
324
|
|
|
176
325
|
const height = wall.height ?? DEFAULT_WALL_HEIGHT
|
|
177
|
-
const startLocal = worldPointToWallLocal(wall,
|
|
178
|
-
const endLocal = worldPointToWallLocal(wall,
|
|
326
|
+
const startLocal = worldPointToWallLocal(wall, measurementPoints.start)
|
|
327
|
+
const endLocal = worldPointToWallLocal(wall, measurementPoints.end)
|
|
179
328
|
const curvedMeasurementPath = isCurvedWall(wall)
|
|
180
|
-
? getCurvedWallMeasurementPath(wall, miterData)
|
|
329
|
+
? getCurvedWallMeasurementPath(wall, miterData, levelWalls)
|
|
181
330
|
: null
|
|
182
331
|
const guidePath: Vec3[] = curvedMeasurementPath
|
|
183
332
|
? curvedMeasurementPath.map((point) => {
|
|
@@ -224,6 +373,38 @@ function buildMeasurementGuide(
|
|
|
224
373
|
guideStart[1],
|
|
225
374
|
(guideStart[2] + guideEnd[2]) / 2,
|
|
226
375
|
] as Vec3)
|
|
376
|
+
const rawHeightGuidePosition = [guideEnd[0], 0, guideEnd[2]] as Vec3
|
|
377
|
+
const beforeGuideEnd = guidePath[guidePath.length - 2] ?? guideStart
|
|
378
|
+
const tickDx = guideEnd[0] - beforeGuideEnd[0]
|
|
379
|
+
const tickDz = guideEnd[2] - beforeGuideEnd[2]
|
|
380
|
+
const tickLength = Math.hypot(tickDx, tickDz)
|
|
381
|
+
const tangentX = tickLength > 1e-6 ? tickDx / tickLength : 1
|
|
382
|
+
const tangentZ = tickLength > 1e-6 ? tickDz / tickLength : 0
|
|
383
|
+
const tickUnitX = -tangentZ
|
|
384
|
+
const tickUnitZ = tangentX
|
|
385
|
+
const wallEndLocal = worldPointToWallLocal(wall, { x: wall.end[0], y: wall.end[1] })
|
|
386
|
+
const endOutwardX = rawHeightGuidePosition[0] - wallEndLocal[0]
|
|
387
|
+
const endOutwardZ = rawHeightGuidePosition[2] - wallEndLocal[2]
|
|
388
|
+
const outsideSign = endOutwardX * tickUnitX + endOutwardZ * tickUnitZ >= 0 ? 1 : -1
|
|
389
|
+
const heightGuidePosition = [
|
|
390
|
+
rawHeightGuidePosition[0] + tickUnitX * outsideSign * HEIGHT_GUIDE_OUTSIDE_OFFSET,
|
|
391
|
+
0,
|
|
392
|
+
rawHeightGuidePosition[2] + tickUnitZ * outsideSign * HEIGHT_GUIDE_OUTSIDE_OFFSET,
|
|
393
|
+
] as Vec3
|
|
394
|
+
const getHorizontalHeightTick = (y: number): { start: Vec3; end: Vec3 } => ({
|
|
395
|
+
start: [
|
|
396
|
+
heightGuidePosition[0] - tickUnitX * HEIGHT_TICK_HALF_LENGTH,
|
|
397
|
+
y,
|
|
398
|
+
heightGuidePosition[2] - tickUnitZ * HEIGHT_TICK_HALF_LENGTH,
|
|
399
|
+
],
|
|
400
|
+
end: [
|
|
401
|
+
heightGuidePosition[0] + tickUnitX * HEIGHT_TICK_HALF_LENGTH,
|
|
402
|
+
y,
|
|
403
|
+
heightGuidePosition[2] + tickUnitZ * HEIGHT_TICK_HALF_LENGTH,
|
|
404
|
+
],
|
|
405
|
+
})
|
|
406
|
+
const bottomHeightTick = getHorizontalHeightTick(0)
|
|
407
|
+
const topHeightTick = getHorizontalHeightTick(height)
|
|
227
408
|
|
|
228
409
|
return {
|
|
229
410
|
guidePath,
|
|
@@ -236,6 +417,13 @@ function buildMeasurementGuide(
|
|
|
236
417
|
extEndStart: [extensionEndBase[0], height, extensionEndBase[2]],
|
|
237
418
|
extEndEnd: [extensionEndBase[0], height + GUIDE_Y_OFFSET + extOvershoot, extensionEndBase[2]],
|
|
238
419
|
labelPosition: [midpoint[0], midpoint[1] + LABEL_LIFT, midpoint[2]],
|
|
420
|
+
heightStart: [heightGuidePosition[0], 0, heightGuidePosition[2]],
|
|
421
|
+
heightEnd: [heightGuidePosition[0], height, heightGuidePosition[2]],
|
|
422
|
+
heightBottomTickStart: bottomHeightTick.start,
|
|
423
|
+
heightBottomTickEnd: bottomHeightTick.end,
|
|
424
|
+
heightTopTickStart: topHeightTick.start,
|
|
425
|
+
heightTopTickEnd: topHeightTick.end,
|
|
426
|
+
heightLabelPosition: [heightGuidePosition[0], height / 2, heightGuidePosition[2]],
|
|
239
427
|
}
|
|
240
428
|
}
|
|
241
429
|
|
|
@@ -286,6 +474,45 @@ function MeasurementPath({ path, color }: { path: Vec3[]; color: string }) {
|
|
|
286
474
|
)
|
|
287
475
|
}
|
|
288
476
|
|
|
477
|
+
function MeasurementLabel({
|
|
478
|
+
label,
|
|
479
|
+
position,
|
|
480
|
+
color,
|
|
481
|
+
shadowColor,
|
|
482
|
+
}: {
|
|
483
|
+
label: string
|
|
484
|
+
position: Vec3
|
|
485
|
+
color: string
|
|
486
|
+
shadowColor: string
|
|
487
|
+
}) {
|
|
488
|
+
return (
|
|
489
|
+
<Html
|
|
490
|
+
center
|
|
491
|
+
position={position}
|
|
492
|
+
style={{ pointerEvents: 'none', userSelect: 'none' }}
|
|
493
|
+
zIndexRange={[20, 0]}
|
|
494
|
+
>
|
|
495
|
+
<div
|
|
496
|
+
className="whitespace-nowrap font-bold font-mono text-[15px]"
|
|
497
|
+
style={{
|
|
498
|
+
color,
|
|
499
|
+
textShadow: `-1.5px -1.5px 0 ${shadowColor}, 1.5px -1.5px 0 ${shadowColor}, -1.5px 1.5px 0 ${shadowColor}, 1.5px 1.5px 0 ${shadowColor}, 0 0 4px ${shadowColor}, 0 0 4px ${shadowColor}`,
|
|
500
|
+
}}
|
|
501
|
+
>
|
|
502
|
+
{label}
|
|
503
|
+
</div>
|
|
504
|
+
</Html>
|
|
505
|
+
)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function SelectedMeasurementAnnotation({ node }: { node: WallNode | ItemNode }) {
|
|
509
|
+
if (node.type === 'wall') {
|
|
510
|
+
return <WallMeasurementAnnotation wall={node} />
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return null
|
|
514
|
+
}
|
|
515
|
+
|
|
289
516
|
function WallMeasurementAnnotation({ wall }: { wall: WallNode }) {
|
|
290
517
|
const nodes = useScene((state) => state.nodes)
|
|
291
518
|
const theme = useViewer((state) => state.theme)
|
|
@@ -316,6 +543,7 @@ function WallMeasurementAnnotation({ wall }: { wall: WallNode }) {
|
|
|
316
543
|
return total
|
|
317
544
|
}, [guide, wall])
|
|
318
545
|
const label = formatMeasurement(length, unit)
|
|
546
|
+
const heightLabel = `H ${formatMeasurement(wall.height ?? DEFAULT_WALL_HEIGHT, unit)}`
|
|
319
547
|
|
|
320
548
|
if (!(guide && Number.isFinite(length) && length >= 0.01)) return null
|
|
321
549
|
|
|
@@ -324,23 +552,26 @@ function WallMeasurementAnnotation({ wall }: { wall: WallNode }) {
|
|
|
324
552
|
<MeasurementPath color={color} path={guide.guidePath} />
|
|
325
553
|
<MeasurementBar color={color} end={guide.extStartEnd} start={guide.extStartStart} />
|
|
326
554
|
<MeasurementBar color={color} end={guide.extEndEnd} start={guide.extEndStart} />
|
|
555
|
+
<MeasurementBar color={color} end={guide.heightEnd} start={guide.heightStart} />
|
|
556
|
+
<MeasurementBar
|
|
557
|
+
color={color}
|
|
558
|
+
end={guide.heightBottomTickEnd}
|
|
559
|
+
start={guide.heightBottomTickStart}
|
|
560
|
+
/>
|
|
561
|
+
<MeasurementBar color={color} end={guide.heightTopTickEnd} start={guide.heightTopTickStart} />
|
|
327
562
|
|
|
328
|
-
<
|
|
329
|
-
|
|
563
|
+
<MeasurementLabel
|
|
564
|
+
color={color}
|
|
565
|
+
label={label}
|
|
330
566
|
position={guide.labelPosition}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}}
|
|
340
|
-
>
|
|
341
|
-
{label}
|
|
342
|
-
</div>
|
|
343
|
-
</Html>
|
|
567
|
+
shadowColor={shadowColor}
|
|
568
|
+
/>
|
|
569
|
+
<MeasurementLabel
|
|
570
|
+
color={color}
|
|
571
|
+
label={heightLabel}
|
|
572
|
+
position={guide.heightLabelPosition}
|
|
573
|
+
shadowColor={shadowColor}
|
|
574
|
+
/>
|
|
344
575
|
</group>
|
|
345
576
|
)
|
|
346
577
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { memo, type MouseEvent as ReactMouseEvent } from 'react'
|
|
4
|
+
import useEditor from '../../store/use-editor'
|
|
5
|
+
import { NodeActionMenu } from '../editor/node-action-menu'
|
|
6
|
+
|
|
7
|
+
type SvgPoint = {
|
|
8
|
+
x: number
|
|
9
|
+
y: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type FloorplanActionMenuHandler = (event: ReactMouseEvent<HTMLButtonElement>) => void
|
|
13
|
+
|
|
14
|
+
export type FloorplanActionMenuEntry = {
|
|
15
|
+
position: SvgPoint | null
|
|
16
|
+
onDelete: FloorplanActionMenuHandler
|
|
17
|
+
onMove: FloorplanActionMenuHandler
|
|
18
|
+
onAddHole?: FloorplanActionMenuHandler
|
|
19
|
+
onDuplicate?: FloorplanActionMenuHandler
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type FloorplanActionMenuLayerProps = {
|
|
23
|
+
item: FloorplanActionMenuEntry
|
|
24
|
+
wall: FloorplanActionMenuEntry
|
|
25
|
+
fence: FloorplanActionMenuEntry
|
|
26
|
+
slab: FloorplanActionMenuEntry
|
|
27
|
+
ceiling: FloorplanActionMenuEntry
|
|
28
|
+
opening: FloorplanActionMenuEntry
|
|
29
|
+
spawn: FloorplanActionMenuEntry
|
|
30
|
+
stair: FloorplanActionMenuEntry
|
|
31
|
+
roof: FloorplanActionMenuEntry
|
|
32
|
+
offsetY?: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const FloorplanActionMenuLayer = memo(function FloorplanActionMenuLayer({
|
|
36
|
+
item,
|
|
37
|
+
wall,
|
|
38
|
+
fence,
|
|
39
|
+
slab,
|
|
40
|
+
ceiling,
|
|
41
|
+
opening,
|
|
42
|
+
spawn,
|
|
43
|
+
stair,
|
|
44
|
+
roof,
|
|
45
|
+
offsetY = 10,
|
|
46
|
+
}: FloorplanActionMenuLayerProps) {
|
|
47
|
+
const isFloorplanHovered = useEditor((state) => state.isFloorplanHovered)
|
|
48
|
+
const movingNode = useEditor((state) => state.movingNode)
|
|
49
|
+
const movingFenceEndpoint = useEditor((state) => state.movingFenceEndpoint)
|
|
50
|
+
const curvingWall = useEditor((state) => state.curvingWall)
|
|
51
|
+
const curvingFence = useEditor((state) => state.curvingFence)
|
|
52
|
+
|
|
53
|
+
if (!isFloorplanHovered || movingNode || movingFenceEndpoint || curvingWall || curvingFence) {
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const entries: FloorplanActionMenuEntry[] = [
|
|
58
|
+
item,
|
|
59
|
+
wall,
|
|
60
|
+
fence,
|
|
61
|
+
slab,
|
|
62
|
+
ceiling,
|
|
63
|
+
opening,
|
|
64
|
+
spawn,
|
|
65
|
+
stair,
|
|
66
|
+
roof,
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<>
|
|
71
|
+
{entries.map((entry, index) =>
|
|
72
|
+
entry.position ? (
|
|
73
|
+
<div
|
|
74
|
+
className="absolute z-30"
|
|
75
|
+
key={index}
|
|
76
|
+
style={{
|
|
77
|
+
left: entry.position.x,
|
|
78
|
+
top: entry.position.y,
|
|
79
|
+
transform: `translate(-50%, calc(-100% - ${offsetY}px))`,
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
<NodeActionMenu
|
|
83
|
+
onAddHole={entry.onAddHole}
|
|
84
|
+
onDelete={entry.onDelete}
|
|
85
|
+
onDuplicate={entry.onDuplicate}
|
|
86
|
+
onMove={entry.onMove}
|
|
87
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
88
|
+
onPointerUp={(event) => event.stopPropagation()}
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
) : null,
|
|
92
|
+
)}
|
|
93
|
+
</>
|
|
94
|
+
)
|
|
95
|
+
})
|