@pascal-app/editor 0.5.1 → 0.7.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 +12 -7
- package/src/components/editor/bottom-sheet.tsx +149 -0
- package/src/components/editor/custom-camera-controls.tsx +75 -7
- package/src/components/editor/editor-layout-mobile.tsx +264 -0
- package/src/components/editor/editor-layout-v2.tsx +29 -0
- package/src/components/editor/first-person/build-collider-world.ts +365 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +795 -0
- package/src/components/editor/first-person-controls.tsx +496 -143
- package/src/components/editor/floating-action-menu.tsx +281 -83
- package/src/components/editor/floating-building-action-menu.tsx +4 -3
- package/src/components/editor/floorplan-background-selection.ts +113 -0
- package/src/components/editor/floorplan-panel.tsx +10442 -3275
- package/src/components/editor/index.tsx +270 -20
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/selection-manager.tsx +766 -12
- package/src/components/editor/site-edge-labels.tsx +9 -3
- package/src/components/editor/thumbnail-generator.tsx +350 -157
- 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 +377 -58
- 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 +119 -0
- package/src/components/editor-2d/renderers/floorplan-marquee-layer.tsx +58 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +197 -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 +272 -0
- package/src/components/systems/roof/roof-edit-system.tsx +5 -5
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +1 -0
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +2 -0
- package/src/components/tools/ceiling/ceiling-tool.tsx +5 -5
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +257 -0
- 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 +19 -0
- package/src/components/tools/door/move-door-tool.tsx +38 -8
- package/src/components/tools/fence/curve-fence-tool.tsx +179 -0
- package/src/components/tools/fence/fence-drafting.ts +27 -8
- package/src/components/tools/fence/fence-tool.tsx +159 -3
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +438 -0
- package/src/components/tools/fence/move-fence-tool.tsx +102 -27
- package/src/components/tools/item/move-tool.tsx +19 -1
- package/src/components/tools/item/placement-math.ts +44 -7
- package/src/components/tools/item/placement-strategies.ts +111 -33
- package/src/components/tools/item/placement-types.ts +7 -0
- package/src/components/tools/item/use-draft-node.ts +2 -0
- package/src/components/tools/item/use-placement-coordinator.tsx +701 -61
- package/src/components/tools/roof/move-roof-tool.tsx +111 -43
- package/src/components/tools/shared/polygon-editor.tsx +244 -29
- package/src/components/tools/shared/segment-angle.ts +156 -0
- package/src/components/tools/slab/move-slab-tool.tsx +182 -0
- package/src/components/tools/slab/slab-boundary-editor.tsx +1 -0
- package/src/components/tools/slab/slab-hole-editor.tsx +2 -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/stair/stair-tool.tsx +11 -3
- package/src/components/tools/tool-manager.tsx +30 -3
- package/src/components/tools/wall/curve-wall-tool.tsx +176 -0
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +423 -0
- package/src/components/tools/wall/move-wall-tool.tsx +356 -0
- package/src/components/tools/wall/wall-drafting.ts +348 -17
- package/src/components/tools/wall/wall-tool.tsx +134 -2
- package/src/components/tools/window/move-window-tool.tsx +28 -0
- package/src/components/tools/window/window-tool.tsx +17 -0
- package/src/components/ui/action-menu/camera-actions.tsx +37 -33
- package/src/components/ui/action-menu/control-modes.tsx +37 -5
- package/src/components/ui/action-menu/index.tsx +91 -1
- package/src/components/ui/action-menu/structure-tools.tsx +2 -0
- package/src/components/ui/action-menu/view-toggles.tsx +424 -35
- package/src/components/ui/command-palette/editor-commands.tsx +27 -5
- package/src/components/ui/command-palette/index.tsx +0 -1
- package/src/components/ui/controls/material-picker.tsx +189 -169
- package/src/components/ui/controls/slider-control.tsx +88 -26
- package/src/components/ui/floating-level-selector.tsx +286 -55
- package/src/components/ui/helpers/helper-manager.tsx +5 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1121 -1219
- package/src/components/ui/item-catalog/item-catalog.tsx +42 -175
- package/src/components/ui/level-duplicate-dialog.tsx +115 -0
- package/src/components/ui/panels/ceiling-panel.tsx +47 -27
- package/src/components/ui/panels/column-panel.tsx +715 -0
- package/src/components/ui/panels/door-panel.tsx +986 -294
- package/src/components/ui/panels/fence-panel.tsx +55 -12
- package/src/components/ui/panels/item-panel.tsx +5 -5
- 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 +138 -0
- package/src/components/ui/panels/panel-manager.tsx +241 -30
- package/src/components/ui/panels/panel-wrapper.tsx +48 -39
- package/src/components/ui/panels/reference-panel.tsx +243 -9
- package/src/components/ui/panels/roof-panel.tsx +30 -62
- package/src/components/ui/panels/roof-segment-panel.tsx +8 -23
- package/src/components/ui/panels/slab-panel.tsx +46 -24
- package/src/components/ui/panels/spawn-panel.tsx +155 -0
- package/src/components/ui/panels/stair-panel.tsx +117 -69
- package/src/components/ui/panels/stair-segment-panel.tsx +13 -27
- package/src/components/ui/panels/wall-panel.tsx +71 -17
- package/src/components/ui/panels/window-panel.tsx +665 -146
- package/src/components/ui/sidebar/mobile-tab-bar.tsx +46 -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 +9 -5
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +77 -0
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +138 -56
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +82 -0
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +3 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +12 -6
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +15 -8
- package/src/components/ui/sidebar/tab-bar.tsx +3 -0
- package/src/components/ui/viewer-toolbar.tsx +96 -2
- package/src/components/viewer-overlay.tsx +25 -19
- package/src/hooks/use-auto-frame.ts +45 -0
- package/src/hooks/use-contextual-tools.ts +14 -13
- package/src/hooks/use-keyboard.ts +67 -9
- package/src/hooks/use-mobile.ts +12 -12
- package/src/index.tsx +2 -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/history.ts +20 -0
- package/src/lib/level-duplication.test.ts +72 -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/sfx-player.ts +96 -13
- package/src/lib/stair-duplication.ts +126 -0
- package/src/lib/window-interaction.ts +86 -0
- package/src/store/use-editor.tsx +279 -15
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type FenceNode,
|
|
3
|
+
getWallCurveFrameAt,
|
|
4
|
+
getWallCurveLength,
|
|
5
|
+
isCurvedWall,
|
|
6
|
+
type WallNode,
|
|
7
|
+
} from '@pascal-app/core'
|
|
8
|
+
|
|
9
|
+
export type PlanPoint = [number, number]
|
|
10
|
+
|
|
11
|
+
export type SegmentAngleLike = Pick<WallNode | FenceNode, 'start' | 'end' | 'curveOffset'>
|
|
12
|
+
|
|
13
|
+
export type SegmentAngleReference = {
|
|
14
|
+
vector: PlanPoint
|
|
15
|
+
orientation: 'directed' | 'axis'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const POINT_MATCH_TOLERANCE = 1e-5
|
|
19
|
+
const SEGMENT_POINT_TOLERANCE = 0.15
|
|
20
|
+
const CURVE_TANGENT_SAMPLE_SPACING = 0.08
|
|
21
|
+
|
|
22
|
+
function distanceSquared(a: PlanPoint, b: PlanPoint) {
|
|
23
|
+
const dx = a[0] - b[0]
|
|
24
|
+
const dz = a[1] - b[1]
|
|
25
|
+
|
|
26
|
+
return dx * dx + dz * dz
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function pointsMatch(a: PlanPoint, b: PlanPoint, tolerance = POINT_MATCH_TOLERANCE) {
|
|
30
|
+
return distanceSquared(a, b) <= tolerance * tolerance
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getProjectedPointOnSegment(point: PlanPoint, segment: SegmentAngleLike): PlanPoint | null {
|
|
34
|
+
const [x1, z1] = segment.start
|
|
35
|
+
const [x2, z2] = segment.end
|
|
36
|
+
const dx = x2 - x1
|
|
37
|
+
const dz = z2 - z1
|
|
38
|
+
const lengthSquared = dx * dx + dz * dz
|
|
39
|
+
|
|
40
|
+
if (lengthSquared < 1e-9) {
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const t = ((point[0] - x1) * dx + (point[1] - z1) * dz) / lengthSquared
|
|
45
|
+
if (t <= 0 || t >= 1) {
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return [x1 + dx * t, z1 + dz * t]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getCurveTangentAtPoint(point: PlanPoint, segment: SegmentAngleLike): PlanPoint | null {
|
|
53
|
+
const curveLength = getWallCurveLength(segment)
|
|
54
|
+
const sampleCount = Math.max(24, Math.ceil(curveLength / CURVE_TANGENT_SAMPLE_SPACING))
|
|
55
|
+
let best: { distance: number; tangent: PlanPoint } | null = null
|
|
56
|
+
|
|
57
|
+
for (let index = 0; index <= sampleCount; index += 1) {
|
|
58
|
+
const frame = getWallCurveFrameAt(segment, index / sampleCount)
|
|
59
|
+
const candidate: PlanPoint = [frame.point.x, frame.point.y]
|
|
60
|
+
const distance = distanceSquared(point, candidate)
|
|
61
|
+
|
|
62
|
+
if (best && distance >= best.distance) {
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
best = {
|
|
67
|
+
distance,
|
|
68
|
+
tangent: [frame.tangent.x, frame.tangent.y],
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!best || best.distance > SEGMENT_POINT_TOLERANCE * SEGMENT_POINT_TOLERANCE) {
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return best.tangent
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function formatAngleRadians(angle: number) {
|
|
80
|
+
return `${Math.round((angle * 180) / Math.PI)}°`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function getAngleBetweenVectors(first: PlanPoint, second: PlanPoint): number | null {
|
|
84
|
+
const firstLength = Math.hypot(first[0], first[1])
|
|
85
|
+
const secondLength = Math.hypot(second[0], second[1])
|
|
86
|
+
|
|
87
|
+
if (firstLength < 1e-6 || secondLength < 1e-6) return null
|
|
88
|
+
|
|
89
|
+
const dot = first[0] * second[0] + first[1] * second[1]
|
|
90
|
+
const cosine = Math.min(1, Math.max(-1, dot / (firstLength * secondLength)))
|
|
91
|
+
|
|
92
|
+
return Math.acos(cosine)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function getAngleToSegmentReference(
|
|
96
|
+
vector: PlanPoint,
|
|
97
|
+
reference: SegmentAngleReference,
|
|
98
|
+
): number | null {
|
|
99
|
+
const angle = getAngleBetweenVectors(vector, reference.vector)
|
|
100
|
+
|
|
101
|
+
if (angle === null || reference.orientation === 'directed') {
|
|
102
|
+
return angle
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const reverseAngle = getAngleBetweenVectors(vector, [-reference.vector[0], -reference.vector[1]])
|
|
106
|
+
|
|
107
|
+
if (reverseAngle === null) {
|
|
108
|
+
return angle
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return Math.min(angle, reverseAngle)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function getSegmentAngleReferenceAtPoint(
|
|
115
|
+
point: PlanPoint,
|
|
116
|
+
segment: SegmentAngleLike,
|
|
117
|
+
): SegmentAngleReference | null {
|
|
118
|
+
if (pointsMatch(point, segment.start)) {
|
|
119
|
+
const frame = getWallCurveFrameAt(segment, 0)
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
vector: [frame.tangent.x, frame.tangent.y],
|
|
123
|
+
orientation: 'directed',
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (pointsMatch(point, segment.end)) {
|
|
128
|
+
const frame = getWallCurveFrameAt(segment, 1)
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
vector: [-frame.tangent.x, -frame.tangent.y],
|
|
132
|
+
orientation: 'directed',
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (isCurvedWall(segment)) {
|
|
137
|
+
const tangent = getCurveTangentAtPoint(point, segment)
|
|
138
|
+
|
|
139
|
+
return tangent
|
|
140
|
+
? {
|
|
141
|
+
vector: tangent,
|
|
142
|
+
orientation: 'axis',
|
|
143
|
+
}
|
|
144
|
+
: null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const projected = getProjectedPointOnSegment(point, segment)
|
|
148
|
+
if (!projected || !pointsMatch(point, projected, SEGMENT_POINT_TOLERANCE)) {
|
|
149
|
+
return null
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
vector: [segment.end[0] - segment.start[0], segment.end[1] - segment.start[1]],
|
|
154
|
+
orientation: 'axis',
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AnyNodeId,
|
|
5
|
+
emitter,
|
|
6
|
+
type FenceNode,
|
|
7
|
+
type GridEvent,
|
|
8
|
+
type LevelNode,
|
|
9
|
+
type SlabNode,
|
|
10
|
+
useScene,
|
|
11
|
+
type WallNode,
|
|
12
|
+
} from '@pascal-app/core'
|
|
13
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
14
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
15
|
+
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
|
16
|
+
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
17
|
+
import useEditor from '../../../store/use-editor'
|
|
18
|
+
import { snapFenceDraftPoint } from '../fence/fence-drafting'
|
|
19
|
+
import { CursorSphere } from '../shared/cursor-sphere'
|
|
20
|
+
|
|
21
|
+
function translatePolygon(
|
|
22
|
+
polygon: Array<[number, number]>,
|
|
23
|
+
deltaX: number,
|
|
24
|
+
deltaZ: number,
|
|
25
|
+
): Array<[number, number]> {
|
|
26
|
+
return polygon.map(([x, z]) => [x + deltaX, z + deltaZ] as [number, number])
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getPolygonCenter(polygon: Array<[number, number]>): [number, number] {
|
|
30
|
+
if (polygon.length === 0) return [0, 0]
|
|
31
|
+
let sumX = 0
|
|
32
|
+
let sumZ = 0
|
|
33
|
+
for (const [x, z] of polygon) {
|
|
34
|
+
sumX += x
|
|
35
|
+
sumZ += z
|
|
36
|
+
}
|
|
37
|
+
return [sumX / polygon.length, sumZ / polygon.length]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const MoveSlabTool: React.FC<{ node: SlabNode }> = ({ node }) => {
|
|
41
|
+
const activatedAtRef = useRef<number>(Date.now())
|
|
42
|
+
const originalPolygonRef = useRef(node.polygon.map(([x, z]) => [x, z] as [number, number]))
|
|
43
|
+
const originalHolesRef = useRef(
|
|
44
|
+
(node.holes ?? []).map((hole) => hole.map(([x, z]) => [x, z] as [number, number])),
|
|
45
|
+
)
|
|
46
|
+
const dragAnchorRef = useRef<[number, number] | null>(null)
|
|
47
|
+
const previousGridPosRef = useRef<[number, number] | null>(null)
|
|
48
|
+
const previewRef = useRef<{
|
|
49
|
+
polygon: Array<[number, number]>
|
|
50
|
+
holes: Array<Array<[number, number]>>
|
|
51
|
+
} | null>(null)
|
|
52
|
+
|
|
53
|
+
const [cursorLocalPos, setCursorLocalPos] = useState<[number, number, number]>(() => {
|
|
54
|
+
const center = getPolygonCenter(node.polygon)
|
|
55
|
+
return [center[0], 0, center[1]]
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const exitMoveMode = useCallback(() => {
|
|
59
|
+
useEditor.getState().setMovingNode(null)
|
|
60
|
+
}, [])
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const originalPolygon = originalPolygonRef.current
|
|
64
|
+
const originalHoles = originalHolesRef.current
|
|
65
|
+
const levelNode =
|
|
66
|
+
node.parentId && useScene.getState().nodes[node.parentId as AnyNodeId]?.type === 'level'
|
|
67
|
+
? (useScene.getState().nodes[node.parentId as AnyNodeId] as LevelNode)
|
|
68
|
+
: null
|
|
69
|
+
const levelChildren = levelNode?.children ?? []
|
|
70
|
+
const levelWalls = levelChildren
|
|
71
|
+
.map((childId) => useScene.getState().nodes[childId as AnyNodeId])
|
|
72
|
+
.filter((child): child is WallNode => child?.type === 'wall')
|
|
73
|
+
const levelFences = levelChildren
|
|
74
|
+
.map((childId) => useScene.getState().nodes[childId as AnyNodeId])
|
|
75
|
+
.filter((child): child is FenceNode => child?.type === 'fence')
|
|
76
|
+
|
|
77
|
+
useScene.temporal.getState().pause()
|
|
78
|
+
let wasCommitted = false
|
|
79
|
+
|
|
80
|
+
const applyPreview = (
|
|
81
|
+
polygon: Array<[number, number]>,
|
|
82
|
+
holes: Array<Array<[number, number]>>,
|
|
83
|
+
) => {
|
|
84
|
+
previewRef.current = { polygon, holes }
|
|
85
|
+
const center = getPolygonCenter(polygon)
|
|
86
|
+
setCursorLocalPos([center[0], 0, center[1]])
|
|
87
|
+
useScene.getState().updateNode(node.id, { polygon, holes })
|
|
88
|
+
useScene.getState().markDirty(node.id as AnyNodeId)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const restoreOriginal = () => {
|
|
92
|
+
useScene.getState().updateNode(node.id, {
|
|
93
|
+
holes: originalHoles,
|
|
94
|
+
polygon: originalPolygon,
|
|
95
|
+
})
|
|
96
|
+
useScene.getState().markDirty(node.id as AnyNodeId)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const onGridMove = (event: GridEvent) => {
|
|
100
|
+
const [localX, localZ] = snapFenceDraftPoint({
|
|
101
|
+
point: [event.localPosition[0], event.localPosition[2]],
|
|
102
|
+
walls: levelWalls,
|
|
103
|
+
fences: levelFences,
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
if (
|
|
107
|
+
previousGridPosRef.current &&
|
|
108
|
+
(localX !== previousGridPosRef.current[0] || localZ !== previousGridPosRef.current[1])
|
|
109
|
+
) {
|
|
110
|
+
sfxEmitter.emit('sfx:grid-snap')
|
|
111
|
+
}
|
|
112
|
+
previousGridPosRef.current = [localX, localZ]
|
|
113
|
+
|
|
114
|
+
const anchor = dragAnchorRef.current ?? [localX, localZ]
|
|
115
|
+
dragAnchorRef.current = anchor
|
|
116
|
+
|
|
117
|
+
const deltaX = localX - anchor[0]
|
|
118
|
+
const deltaZ = localZ - anchor[1]
|
|
119
|
+
|
|
120
|
+
applyPreview(
|
|
121
|
+
translatePolygon(originalPolygon, deltaX, deltaZ),
|
|
122
|
+
originalHoles.map((hole) => translatePolygon(hole, deltaX, deltaZ)),
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const onGridClick = (event: GridEvent) => {
|
|
127
|
+
if (Date.now() - activatedAtRef.current < 150) {
|
|
128
|
+
event.nativeEvent?.stopPropagation?.()
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const preview = previewRef.current ?? { polygon: originalPolygon, holes: originalHoles }
|
|
133
|
+
|
|
134
|
+
wasCommitted = true
|
|
135
|
+
|
|
136
|
+
// Restore original baseline while paused so the next resume+update
|
|
137
|
+
// registers as a single tracked change (undo reverts to original).
|
|
138
|
+
useScene.getState().updateNode(node.id, {
|
|
139
|
+
polygon: originalPolygon,
|
|
140
|
+
holes: originalHoles,
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
useScene.temporal.getState().resume()
|
|
144
|
+
useScene.getState().updateNode(node.id, preview)
|
|
145
|
+
useScene.getState().markDirty(node.id as AnyNodeId)
|
|
146
|
+
useScene.temporal.getState().pause()
|
|
147
|
+
|
|
148
|
+
sfxEmitter.emit('sfx:item-place')
|
|
149
|
+
useViewer.getState().setSelection({ selectedIds: [node.id] })
|
|
150
|
+
exitMoveMode()
|
|
151
|
+
event.nativeEvent?.stopPropagation?.()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const onCancel = () => {
|
|
155
|
+
restoreOriginal()
|
|
156
|
+
useViewer.getState().setSelection({ selectedIds: [node.id] })
|
|
157
|
+
useScene.temporal.getState().resume()
|
|
158
|
+
markToolCancelConsumed()
|
|
159
|
+
exitMoveMode()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
emitter.on('grid:move', onGridMove)
|
|
163
|
+
emitter.on('grid:click', onGridClick)
|
|
164
|
+
emitter.on('tool:cancel', onCancel)
|
|
165
|
+
|
|
166
|
+
return () => {
|
|
167
|
+
if (!wasCommitted) {
|
|
168
|
+
restoreOriginal()
|
|
169
|
+
}
|
|
170
|
+
useScene.temporal.getState().resume()
|
|
171
|
+
emitter.off('grid:move', onGridMove)
|
|
172
|
+
emitter.off('grid:click', onGridClick)
|
|
173
|
+
emitter.off('tool:cancel', onCancel)
|
|
174
|
+
}
|
|
175
|
+
}, [exitMoveMode, node.id])
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<group>
|
|
179
|
+
<CursorSphere position={cursorLocalPos} showTooltip={false} />
|
|
180
|
+
</group>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
@@ -36,6 +36,8 @@ export const SlabHoleEditor: React.FC<SlabHoleEditorProps> = ({ slabId, holeInde
|
|
|
36
36
|
|
|
37
37
|
return (
|
|
38
38
|
<PolygonEditor
|
|
39
|
+
allowEdgeMove
|
|
40
|
+
allowPolygonMove
|
|
39
41
|
color="#ef4444"
|
|
40
42
|
levelId={resolveLevelId(slab, useScene.getState().nodes)} // red for holes
|
|
41
43
|
minVertices={3}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import '../../../three-types'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
emitter,
|
|
5
|
+
type GridEvent,
|
|
6
|
+
type SpawnNode,
|
|
7
|
+
sceneRegistry,
|
|
8
|
+
useLiveTransforms,
|
|
9
|
+
useScene,
|
|
10
|
+
} from '@pascal-app/core'
|
|
11
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
12
|
+
import { Vector3 } from 'three'
|
|
13
|
+
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
14
|
+
import useEditor from '../../../store/use-editor'
|
|
15
|
+
import { CursorSphere } from '../shared/cursor-sphere'
|
|
16
|
+
|
|
17
|
+
const roundToHalf = (value: number) => Math.round(value * 2) / 2
|
|
18
|
+
const worldVector = new Vector3()
|
|
19
|
+
|
|
20
|
+
function getLevelLocalSpawnPosition(node: SpawnNode, event: GridEvent): [number, number, number] {
|
|
21
|
+
const levelObject = node.parentId ? sceneRegistry.nodes.get(node.parentId) : null
|
|
22
|
+
if (!levelObject) {
|
|
23
|
+
return [
|
|
24
|
+
roundToHalf(event.localPosition[0]),
|
|
25
|
+
event.localPosition[1],
|
|
26
|
+
roundToHalf(event.localPosition[2]),
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
worldVector.set(event.position[0], event.position[1], event.position[2])
|
|
31
|
+
levelObject.updateWorldMatrix(true, false)
|
|
32
|
+
levelObject.worldToLocal(worldVector)
|
|
33
|
+
|
|
34
|
+
return [roundToHalf(worldVector.x), worldVector.y, roundToHalf(worldVector.z)]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const MoveSpawnTool: React.FC<{
|
|
38
|
+
node: SpawnNode
|
|
39
|
+
onCommitted?: (nodeId: SpawnNode['id']) => void
|
|
40
|
+
}> = ({ node, onCommitted }) => {
|
|
41
|
+
const [previewPosition, setPreviewPosition] = useState<[number, number, number]>(node.position)
|
|
42
|
+
|
|
43
|
+
const exitMoveMode = useCallback(() => {
|
|
44
|
+
useEditor.getState().setMovingNode(null)
|
|
45
|
+
}, [])
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
useScene.temporal.getState().pause()
|
|
49
|
+
|
|
50
|
+
let committed = false
|
|
51
|
+
|
|
52
|
+
const onGridMove = (event: GridEvent) => {
|
|
53
|
+
const nextPosition: [number, number, number] = [
|
|
54
|
+
roundToHalf(event.localPosition[0]),
|
|
55
|
+
event.localPosition[1],
|
|
56
|
+
roundToHalf(event.localPosition[2]),
|
|
57
|
+
]
|
|
58
|
+
setPreviewPosition(nextPosition)
|
|
59
|
+
useLiveTransforms.getState().set(node.id, {
|
|
60
|
+
position: [...nextPosition],
|
|
61
|
+
rotation: node.rotation,
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const onGridClick = (event: GridEvent) => {
|
|
66
|
+
const nextPosition = getLevelLocalSpawnPosition(node, event)
|
|
67
|
+
|
|
68
|
+
committed = true
|
|
69
|
+
useScene.temporal.getState().resume()
|
|
70
|
+
useScene.getState().updateNode(node.id, { position: nextPosition })
|
|
71
|
+
onCommitted?.(node.id)
|
|
72
|
+
useLiveTransforms.getState().clear(node.id)
|
|
73
|
+
sfxEmitter.emit('sfx:item-place')
|
|
74
|
+
exitMoveMode()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const onCancel = () => {
|
|
78
|
+
useLiveTransforms.getState().clear(node.id)
|
|
79
|
+
useScene.temporal.getState().resume()
|
|
80
|
+
exitMoveMode()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
emitter.on('grid:move', onGridMove)
|
|
84
|
+
emitter.on('grid:click', onGridClick)
|
|
85
|
+
emitter.on('tool:cancel', onCancel)
|
|
86
|
+
|
|
87
|
+
return () => {
|
|
88
|
+
emitter.off('grid:move', onGridMove)
|
|
89
|
+
emitter.off('grid:click', onGridClick)
|
|
90
|
+
emitter.off('tool:cancel', onCancel)
|
|
91
|
+
useLiveTransforms.getState().clear(node.id)
|
|
92
|
+
if (!committed) {
|
|
93
|
+
useScene.temporal.getState().resume()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}, [exitMoveMode, node, onCommitted])
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<CursorSphere color="#60a5fa" height={2.2} position={previewPosition} showTooltip={false} />
|
|
100
|
+
)
|
|
101
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import '../../../three-types'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
emitter,
|
|
5
|
+
type GridEvent,
|
|
6
|
+
type LevelNode,
|
|
7
|
+
SpawnNode,
|
|
8
|
+
type SpawnNode as SpawnNodeType,
|
|
9
|
+
sceneRegistry,
|
|
10
|
+
useScene,
|
|
11
|
+
} from '@pascal-app/core'
|
|
12
|
+
import { useEffect, useRef, useState } from 'react'
|
|
13
|
+
import type { Group } from 'three'
|
|
14
|
+
import { Vector3 } from 'three'
|
|
15
|
+
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
16
|
+
import useEditor from '../../../store/use-editor'
|
|
17
|
+
import { CursorSphere } from '../shared/cursor-sphere'
|
|
18
|
+
|
|
19
|
+
const SPAWN_ICON = (
|
|
20
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
21
|
+
<img
|
|
22
|
+
alt="Spawn Point"
|
|
23
|
+
src="/icons/site.png"
|
|
24
|
+
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
|
25
|
+
/>
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
const roundToHalf = (value: number) => Math.round(value * 2) / 2
|
|
29
|
+
const worldVector = new Vector3()
|
|
30
|
+
|
|
31
|
+
function getExistingSpawnIds() {
|
|
32
|
+
const nodes = useScene.getState().nodes
|
|
33
|
+
return Object.values(nodes)
|
|
34
|
+
.filter((node) => node.type === 'spawn')
|
|
35
|
+
.map((node) => node.id)
|
|
36
|
+
.sort()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getLevelLocalSpawnPosition(
|
|
40
|
+
levelId: LevelNode['id'],
|
|
41
|
+
event: GridEvent,
|
|
42
|
+
): [number, number, number] {
|
|
43
|
+
const levelObject = sceneRegistry.nodes.get(levelId)
|
|
44
|
+
if (!levelObject) {
|
|
45
|
+
return [
|
|
46
|
+
roundToHalf(event.localPosition[0]),
|
|
47
|
+
event.localPosition[1],
|
|
48
|
+
roundToHalf(event.localPosition[2]),
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
worldVector.set(event.position[0], event.position[1], event.position[2])
|
|
53
|
+
levelObject.updateWorldMatrix(true, false)
|
|
54
|
+
levelObject.worldToLocal(worldVector)
|
|
55
|
+
|
|
56
|
+
return [roundToHalf(worldVector.x), worldVector.y, roundToHalf(worldVector.z)]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type SpawnToolProps = {
|
|
60
|
+
currentLevelId: LevelNode['id'] | null
|
|
61
|
+
onPlaced?: (spawnId: SpawnNodeType['id']) => void
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const SpawnTool: React.FC<SpawnToolProps> = ({ currentLevelId, onPlaced }) => {
|
|
65
|
+
const [, setCursorPosition] = useState<[number, number, number] | null>(null)
|
|
66
|
+
const cursorRef = useRef<Group>(null)
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!currentLevelId) return
|
|
70
|
+
|
|
71
|
+
const onGridMove = (event: GridEvent) => {
|
|
72
|
+
const nextPosition: [number, number, number] = [
|
|
73
|
+
roundToHalf(event.localPosition[0]),
|
|
74
|
+
event.localPosition[1],
|
|
75
|
+
roundToHalf(event.localPosition[2]),
|
|
76
|
+
]
|
|
77
|
+
setCursorPosition(nextPosition)
|
|
78
|
+
cursorRef.current?.position.set(nextPosition[0], nextPosition[1], nextPosition[2])
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const onGridClick = (event: GridEvent) => {
|
|
82
|
+
const nextPosition = getLevelLocalSpawnPosition(currentLevelId, event)
|
|
83
|
+
|
|
84
|
+
const [existingSpawnId, ...duplicateSpawnIds] = getExistingSpawnIds()
|
|
85
|
+
if (existingSpawnId) {
|
|
86
|
+
useScene.getState().updateNode(existingSpawnId, {
|
|
87
|
+
parentId: currentLevelId,
|
|
88
|
+
position: nextPosition,
|
|
89
|
+
rotation: 0,
|
|
90
|
+
})
|
|
91
|
+
if (duplicateSpawnIds.length > 0) {
|
|
92
|
+
useScene.getState().deleteNodes(duplicateSpawnIds)
|
|
93
|
+
}
|
|
94
|
+
onPlaced?.(existingSpawnId)
|
|
95
|
+
} else {
|
|
96
|
+
const spawn = SpawnNode.parse({
|
|
97
|
+
name: 'Spawn Point',
|
|
98
|
+
position: nextPosition,
|
|
99
|
+
rotation: 0,
|
|
100
|
+
})
|
|
101
|
+
useScene.getState().createNode(spawn, currentLevelId)
|
|
102
|
+
onPlaced?.(spawn.id)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
sfxEmitter.emit('sfx:structure-build')
|
|
106
|
+
useEditor.getState().setTool(null)
|
|
107
|
+
useEditor.getState().setMode('select')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
emitter.on('grid:move', onGridMove)
|
|
111
|
+
emitter.on('grid:click', onGridClick)
|
|
112
|
+
|
|
113
|
+
return () => {
|
|
114
|
+
emitter.off('grid:move', onGridMove)
|
|
115
|
+
emitter.off('grid:click', onGridClick)
|
|
116
|
+
}
|
|
117
|
+
}, [currentLevelId, onPlaced])
|
|
118
|
+
|
|
119
|
+
if (!currentLevelId) return null
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<CursorSphere
|
|
123
|
+
color="#60a5fa"
|
|
124
|
+
height={2.2}
|
|
125
|
+
ref={cursorRef}
|
|
126
|
+
showTooltip
|
|
127
|
+
tooltipContent={SPAWN_ICON}
|
|
128
|
+
/>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
@@ -93,11 +93,21 @@ function commitStairPlacement(
|
|
|
93
93
|
position: [0, 0, 0],
|
|
94
94
|
})
|
|
95
95
|
|
|
96
|
+
const sortedLevels = Object.values(nodes)
|
|
97
|
+
.filter((node): node is LevelNode => node.type === 'level')
|
|
98
|
+
.sort((left, right) => left.level - right.level)
|
|
99
|
+
const currentLevelIndex = sortedLevels.findIndex((level) => level.id === levelId)
|
|
100
|
+
const nextLevelId = sortedLevels[currentLevelIndex + 1]?.id ?? levelId
|
|
101
|
+
|
|
96
102
|
const stair = StairNode.parse({
|
|
97
103
|
name,
|
|
98
104
|
position,
|
|
99
105
|
rotation,
|
|
100
106
|
stairType: DEFAULT_STAIR_TYPE,
|
|
107
|
+
fromLevelId: levelId,
|
|
108
|
+
toLevelId: nextLevelId,
|
|
109
|
+
slabOpeningMode: 'destination',
|
|
110
|
+
openingOffset: 0.08,
|
|
101
111
|
width: DEFAULT_STAIR_WIDTH,
|
|
102
112
|
totalRise: DEFAULT_STAIR_HEIGHT,
|
|
103
113
|
stepCount: DEFAULT_STAIR_STEP_COUNT,
|
|
@@ -166,9 +176,7 @@ export const StairTool: React.FC = () => {
|
|
|
166
176
|
|
|
167
177
|
const gridX = Math.round(event.localPosition[0] * 2) / 2
|
|
168
178
|
const gridZ = Math.round(event.localPosition[2] * 2) / 2
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
commitStairPlacement(currentLevelId, [gridX, y, gridZ], rotationRef.current)
|
|
179
|
+
commitStairPlacement(currentLevelId, [gridX, 0, gridZ], rotationRef.current)
|
|
172
180
|
}
|
|
173
181
|
|
|
174
182
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
@@ -10,8 +10,11 @@ import useEditor, { type Phase, type Tool } from '../../store/use-editor'
|
|
|
10
10
|
import { CeilingBoundaryEditor } from './ceiling/ceiling-boundary-editor'
|
|
11
11
|
import { CeilingHoleEditor } from './ceiling/ceiling-hole-editor'
|
|
12
12
|
import { CeilingTool } from './ceiling/ceiling-tool'
|
|
13
|
+
import { ColumnTool } from './column/column-tool'
|
|
13
14
|
import { DoorTool } from './door/door-tool'
|
|
15
|
+
import { CurveFenceTool } from './fence/curve-fence-tool'
|
|
14
16
|
import { FenceTool } from './fence/fence-tool'
|
|
17
|
+
import { MoveFenceEndpointTool } from './fence/move-fence-endpoint-tool'
|
|
15
18
|
import { ItemTool } from './item/item-tool'
|
|
16
19
|
import { MoveTool } from './item/move-tool'
|
|
17
20
|
import { RoofTool } from './roof/roof-tool'
|
|
@@ -19,7 +22,10 @@ import { SiteBoundaryEditor } from './site/site-boundary-editor'
|
|
|
19
22
|
import { SlabBoundaryEditor } from './slab/slab-boundary-editor'
|
|
20
23
|
import { SlabHoleEditor } from './slab/slab-hole-editor'
|
|
21
24
|
import { SlabTool } from './slab/slab-tool'
|
|
25
|
+
import { SpawnTool } from './spawn/spawn-tool'
|
|
22
26
|
import { StairTool } from './stair/stair-tool'
|
|
27
|
+
import { CurveWallTool } from './wall/curve-wall-tool'
|
|
28
|
+
import { MoveWallEndpointTool } from './wall/move-wall-endpoint-tool'
|
|
23
29
|
import { WallTool } from './wall/wall-tool'
|
|
24
30
|
import { WindowTool } from './window/window-tool'
|
|
25
31
|
import { ZoneBoundaryEditor } from './zone/zone-boundary-editor'
|
|
@@ -51,10 +57,16 @@ export const ToolManager: React.FC = () => {
|
|
|
51
57
|
const mode = useEditor((state) => state.mode)
|
|
52
58
|
const tool = useEditor((state) => state.tool)
|
|
53
59
|
const movingNode = useEditor((state) => state.movingNode)
|
|
60
|
+
const movingWallEndpoint = useEditor((state) => state.movingWallEndpoint)
|
|
61
|
+
const movingFenceEndpoint = useEditor((state) => state.movingFenceEndpoint)
|
|
62
|
+
const curvingWall = useEditor((state) => state.curvingWall)
|
|
63
|
+
const curvingFence = useEditor((state) => state.curvingFence)
|
|
54
64
|
const editingHole = useEditor((state) => state.editingHole)
|
|
55
65
|
const selectedZoneId = useViewer((state) => state.selection.zoneId)
|
|
66
|
+
const selectedLevelId = useViewer((state) => state.selection.levelId)
|
|
56
67
|
const buildingId = useViewer((state) => state.selection.buildingId)
|
|
57
68
|
const selectedIds = useViewer((state) => state.selection.selectedIds)
|
|
69
|
+
const setSelection = useViewer((state) => state.setSelection)
|
|
58
70
|
const nodes = useScene((state) => state.nodes)
|
|
59
71
|
|
|
60
72
|
// Building transform for the local group — all building-relative tools live inside this group
|
|
@@ -115,12 +127,15 @@ export const ToolManager: React.FC = () => {
|
|
|
115
127
|
const showBuildTool = mode === 'build' && tool !== null
|
|
116
128
|
|
|
117
129
|
const BuildToolComponent = showBuildTool ? tools[phase]?.[tool] : null
|
|
130
|
+
const handlePlacedNodeSelected = (nodeId: AnyNodeId) => {
|
|
131
|
+
setSelection({ selectedIds: [nodeId] })
|
|
132
|
+
}
|
|
118
133
|
|
|
119
134
|
return (
|
|
120
135
|
<>
|
|
121
136
|
{showSiteBoundaryEditor && <SiteBoundaryEditor />}
|
|
122
137
|
{/* World-space tools: site boundary and building movement operate in world coordinates */}
|
|
123
|
-
{movingNode?.type === 'building' && <MoveTool />}
|
|
138
|
+
{movingNode?.type === 'building' && <MoveTool onSpawnMoved={handlePlacedNodeSelected} />}
|
|
124
139
|
|
|
125
140
|
{/* Building-local group: all other tools are relative to the selected building.
|
|
126
141
|
Cursor visuals set positions in building-local space; this group applies the
|
|
@@ -140,8 +155,20 @@ export const ToolManager: React.FC = () => {
|
|
|
140
155
|
{showCeilingHoleEditor && selectedCeilingId && editingHole && (
|
|
141
156
|
<CeilingHoleEditor ceilingId={selectedCeilingId} holeIndex={editingHole.holeIndex} />
|
|
142
157
|
)}
|
|
143
|
-
{
|
|
144
|
-
{
|
|
158
|
+
{movingWallEndpoint && <MoveWallEndpointTool target={movingWallEndpoint} />}
|
|
159
|
+
{movingFenceEndpoint && <MoveFenceEndpointTool target={movingFenceEndpoint} />}
|
|
160
|
+
{curvingWall && <CurveWallTool node={curvingWall} />}
|
|
161
|
+
{curvingFence && <CurveFenceTool node={curvingFence} />}
|
|
162
|
+
{movingNode && movingNode.type !== 'building' && (
|
|
163
|
+
<MoveTool onSpawnMoved={handlePlacedNodeSelected} />
|
|
164
|
+
)}
|
|
165
|
+
{!movingNode && showBuildTool && tool === 'spawn' && (
|
|
166
|
+
<SpawnTool currentLevelId={selectedLevelId} onPlaced={handlePlacedNodeSelected} />
|
|
167
|
+
)}
|
|
168
|
+
{!movingNode && showBuildTool && tool === 'column' && (
|
|
169
|
+
<ColumnTool currentLevelId={selectedLevelId} onPlaced={handlePlacedNodeSelected} />
|
|
170
|
+
)}
|
|
171
|
+
{!movingNode && BuildToolComponent && tool !== 'column' && <BuildToolComponent />}
|
|
145
172
|
</group>
|
|
146
173
|
</>
|
|
147
174
|
)
|