@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.
Files changed (223) hide show
  1. package/package.json +8 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
  3. package/src/components/editor/custom-camera-controls.tsx +754 -25
  4. package/src/components/editor/editor-layout-mobile.tsx +3 -3
  5. package/src/components/editor/editor-layout-v2.tsx +58 -51
  6. package/src/components/editor/export-manager.tsx +32 -3
  7. package/src/components/editor/first-person/build-collider-world.test.ts +149 -0
  8. package/src/components/editor/first-person/build-collider-world.ts +201 -83
  9. package/src/components/editor/first-person/bvh-ecctrl.tsx +8 -8
  10. package/src/components/editor/first-person-controls.tsx +825 -20
  11. package/src/components/editor/floating-action-menu.tsx +287 -196
  12. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  13. package/src/components/editor/floorplan-panel.tsx +5829 -12005
  14. package/src/components/editor/grid.tsx +51 -17
  15. package/src/components/editor/group-move-handle.tsx +316 -0
  16. package/src/components/editor/group-rotate-handle.tsx +382 -0
  17. package/src/components/editor/group-transform-shared.test.ts +205 -0
  18. package/src/components/editor/group-transform-shared.ts +256 -0
  19. package/src/components/editor/handles/handle-arrow.tsx +523 -0
  20. package/src/components/editor/handles/use-handle-drag.ts +213 -0
  21. package/src/components/editor/index.tsx +178 -149
  22. package/src/components/editor/measurement-pill.tsx +75 -0
  23. package/src/components/editor/node-arrow-handles.tsx +1453 -0
  24. package/src/components/editor/selection-manager.tsx +433 -156
  25. package/src/components/editor/site-edge-labels.tsx +44 -20
  26. package/src/components/editor/slab-hole-highlights.tsx +466 -0
  27. package/src/components/editor/snapshot-capture-overlay.tsx +136 -62
  28. package/src/components/editor/thumbnail-generator.tsx +9 -3
  29. package/src/components/editor/use-floorplan-background-placement.ts +119 -32
  30. package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
  31. package/src/components/editor/use-floorplan-scene-data.ts +14 -2
  32. package/src/components/editor/wall-measurement-label.tsx +11 -6
  33. package/src/components/editor/wall-move-side-handles.tsx +855 -0
  34. package/src/components/editor/wall-opening-highlights.tsx +181 -0
  35. package/src/components/editor/wall-snap-beacon-layer.tsx +150 -0
  36. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +6 -1
  37. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
  38. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
  39. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +227 -0
  40. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +616 -0
  41. package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
  42. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +75 -0
  43. package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
  44. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
  45. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
  46. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
  47. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
  48. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
  49. package/src/components/editor-2d/svg-paths.ts +10 -2
  50. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +26 -3
  51. package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
  52. package/src/components/systems/roof/roof-edit-system.tsx +86 -12
  53. package/src/components/systems/stair/stair-edit-system.tsx +1 -1
  54. package/src/components/tools/elevator/elevator-defaults.ts +8 -0
  55. package/src/components/tools/elevator/elevator-tool.tsx +349 -0
  56. package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
  57. package/src/components/tools/fence/fence-drafting.ts +64 -22
  58. package/src/components/tools/item/move-tool.tsx +41 -105
  59. package/src/components/tools/item/placement-math.test.ts +10 -0
  60. package/src/components/tools/item/placement-math.ts +31 -5
  61. package/src/components/tools/item/placement-strategies.ts +190 -15
  62. package/src/components/tools/item/placement-types.ts +10 -1
  63. package/src/components/tools/item/use-draft-node.ts +20 -1
  64. package/src/components/tools/item/use-placement-coordinator.tsx +694 -191
  65. package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
  66. package/src/components/tools/roof/roof-tool.tsx +108 -12
  67. package/src/components/tools/select/box-select-state.ts +77 -0
  68. package/src/components/tools/select/box-select-tool.tsx +324 -538
  69. package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
  70. package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
  71. package/src/components/tools/select/select-candidates.test.ts +109 -0
  72. package/src/components/tools/select/select-candidates.ts +85 -0
  73. package/src/components/tools/shared/affordance-dispatch.ts +30 -0
  74. package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
  75. package/src/components/tools/shared/floor-stack-preview.ts +25 -0
  76. package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
  77. package/src/components/tools/shared/placement-box-geometry.ts +123 -0
  78. package/src/components/tools/shared/placement-box.tsx +123 -0
  79. package/src/components/tools/shared/polygon-editor.tsx +617 -167
  80. package/src/components/tools/shared/segment-angle.ts +56 -0
  81. package/src/components/tools/site/site-boundary-editor.tsx +464 -12
  82. package/src/components/tools/site/site-flag-model.ts +18 -0
  83. package/src/components/tools/stair/stair-defaults.ts +1 -1
  84. package/src/components/tools/stair/stair-tool.tsx +269 -35
  85. package/src/components/tools/tool-manager.tsx +172 -56
  86. package/src/components/tools/wall/wall-drafting.ts +98 -98
  87. package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
  88. package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
  89. package/src/components/tools/zone/zone-tool.tsx +16 -6
  90. package/src/components/ui/action-menu/action-button.tsx +21 -1
  91. package/src/components/ui/action-menu/control-modes.tsx +5 -82
  92. package/src/components/ui/action-menu/index.tsx +1 -113
  93. package/src/components/ui/action-menu/structure-tools.tsx +13 -88
  94. package/src/components/ui/action-menu/view-toggles.tsx +243 -6
  95. package/src/components/ui/command-palette/editor-commands.tsx +18 -13
  96. package/src/components/ui/command-palette/index.tsx +2 -1
  97. package/src/components/ui/controls/material-paint-panel.tsx +83 -0
  98. package/src/components/ui/controls/material-picker.tsx +11 -32
  99. package/src/components/ui/controls/metric-control.tsx +33 -11
  100. package/src/components/ui/floating-level-selector.tsx +47 -10
  101. package/src/components/ui/helpers/helper-manager.tsx +16 -17
  102. package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
  103. package/src/components/ui/item-catalog/catalog-items.tsx +1 -0
  104. package/src/components/ui/item-catalog/item-catalog.tsx +3 -0
  105. package/src/components/ui/level-duplicate-dialog.tsx +2 -1
  106. package/src/components/ui/panels/node-display.ts +1 -0
  107. package/src/components/ui/panels/panel-manager.tsx +35 -47
  108. package/src/components/ui/panels/panel-wrapper.tsx +217 -12
  109. package/src/components/ui/panels/parametric-inspector.tsx +469 -0
  110. package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
  111. package/src/components/ui/scene-loader.tsx +1 -3
  112. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
  113. package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
  114. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
  115. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
  116. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
  117. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
  118. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
  119. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
  120. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
  121. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
  122. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
  123. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
  124. package/src/components/ui/sidebar/panels/site-panel/index.tsx +4 -6
  125. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
  126. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
  127. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
  128. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
  129. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
  130. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
  131. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
  132. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -36
  133. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
  134. package/src/components/ui/sidebar/tab-bar.tsx +61 -1
  135. package/src/components/viewer-overlay.tsx +169 -52
  136. package/src/components/viewer-zone-system.tsx +1 -1
  137. package/src/hooks/use-auto-save.ts +10 -3
  138. package/src/hooks/use-ceiling-events.ts +175 -0
  139. package/src/hooks/use-drag-action.ts +124 -0
  140. package/src/hooks/use-keyboard.ts +115 -62
  141. package/src/hooks/use-selection.ts +64 -0
  142. package/src/index.tsx +237 -5
  143. package/src/lib/constants.ts +6 -2
  144. package/src/lib/editor-api.ts +62 -0
  145. package/src/lib/elevator-support.ts +96 -0
  146. package/src/lib/floorplan/apply-alignment.ts +111 -0
  147. package/src/lib/floorplan/index.ts +7 -0
  148. package/src/lib/floorplan/selection-tool.ts +40 -0
  149. package/src/lib/floorplan/stairs.ts +12 -1
  150. package/src/lib/fresh-planar-placement.test.ts +102 -0
  151. package/src/lib/fresh-planar-placement.ts +63 -0
  152. package/src/lib/history.ts +2 -1
  153. package/src/lib/level-duplication.ts +54 -42
  154. package/src/lib/level-name.ts +11 -0
  155. package/src/lib/material-paint.ts +211 -19
  156. package/src/lib/placement-metadata.ts +29 -0
  157. package/src/lib/planar-cursor-placement.test.ts +43 -0
  158. package/src/lib/planar-cursor-placement.ts +42 -0
  159. package/src/lib/roof-duplication.ts +1 -1
  160. package/src/lib/scene-clipboard.ts +267 -0
  161. package/src/lib/scene.ts +13 -4
  162. package/src/lib/sfx-bus.ts +23 -1
  163. package/src/lib/sfx-player.ts +70 -22
  164. package/src/lib/site-boundary.ts +1 -0
  165. package/src/lib/stair-levels.test.ts +146 -0
  166. package/src/lib/stair-levels.ts +177 -0
  167. package/src/lib/world-grid-snap.ts +199 -0
  168. package/src/lib/zone-content.ts +130 -0
  169. package/src/store/use-alignment-guides.ts +21 -0
  170. package/src/store/use-editor.tsx +320 -37
  171. package/src/store/use-placement-preview.ts +31 -0
  172. package/src/store/use-wall-move-ghosts.ts +36 -0
  173. package/src/store/use-wall-snap-indicator.ts +32 -0
  174. package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
  175. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
  176. package/src/components/tools/building/move-building-tool.tsx +0 -157
  177. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
  178. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
  179. package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
  180. package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
  181. package/src/components/tools/column/column-tool.tsx +0 -97
  182. package/src/components/tools/column/move-column-tool.tsx +0 -105
  183. package/src/components/tools/door/door-math.ts +0 -110
  184. package/src/components/tools/door/door-tool.tsx +0 -324
  185. package/src/components/tools/door/move-door-tool.tsx +0 -412
  186. package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
  187. package/src/components/tools/fence/fence-tool.tsx +0 -346
  188. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
  189. package/src/components/tools/fence/move-fence-tool.tsx +0 -302
  190. package/src/components/tools/item/item-tool.tsx +0 -26
  191. package/src/components/tools/roof/move-roof-tool.tsx +0 -364
  192. package/src/components/tools/slab/move-slab-tool.tsx +0 -182
  193. package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
  194. package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
  195. package/src/components/tools/slab/slab-tool.tsx +0 -322
  196. package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
  197. package/src/components/tools/spawn/spawn-tool.tsx +0 -130
  198. package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
  199. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
  200. package/src/components/tools/wall/move-wall-tool.tsx +0 -358
  201. package/src/components/tools/wall/wall-tool.tsx +0 -332
  202. package/src/components/tools/window/move-window-tool.tsx +0 -447
  203. package/src/components/tools/window/window-math.ts +0 -117
  204. package/src/components/tools/window/window-tool.tsx +0 -332
  205. package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
  206. package/src/components/ui/helpers/slab-helper.tsx +0 -20
  207. package/src/components/ui/helpers/wall-helper.tsx +0 -20
  208. package/src/components/ui/panels/ceiling-panel.tsx +0 -249
  209. package/src/components/ui/panels/column-panel.tsx +0 -759
  210. package/src/components/ui/panels/door-panel.tsx +0 -1299
  211. package/src/components/ui/panels/fence-panel.tsx +0 -222
  212. package/src/components/ui/panels/item-panel.tsx +0 -306
  213. package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
  214. package/src/components/ui/panels/roof-panel.tsx +0 -280
  215. package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
  216. package/src/components/ui/panels/slab-panel.tsx +0 -250
  217. package/src/components/ui/panels/spawn-panel.tsx +0 -161
  218. package/src/components/ui/panels/stair-panel.tsx +0 -577
  219. package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
  220. package/src/components/ui/panels/wall-panel.tsx +0 -273
  221. package/src/components/ui/panels/window-panel.tsx +0 -968
  222. package/src/contexts/presets-context.tsx +0 -121
  223. package/src/hooks/use-contextual-tools.ts +0 -61
@@ -1,441 +0,0 @@
1
- 'use client'
2
-
3
- import {
4
- type AnyNodeId,
5
- emitter,
6
- type FenceNode,
7
- type GridEvent,
8
- pauseSceneHistory,
9
- resumeSceneHistory,
10
- useScene,
11
- type WallNode,
12
- } from '@pascal-app/core'
13
- import { useViewer } from '@pascal-app/viewer'
14
- import { Html } from '@react-three/drei'
15
- import { useCallback, useEffect, useRef, useState } from 'react'
16
- import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
17
- import { sfxEmitter } from '../../../lib/sfx-bus'
18
- import useEditor, { type MovingFenceEndpoint } from '../../../store/use-editor'
19
- import { CursorSphere } from '../shared/cursor-sphere'
20
- import {
21
- formatAngleRadians,
22
- getAngleToSegmentReference,
23
- getSegmentAngleReferenceAtPoint,
24
- } from '../shared/segment-angle'
25
- import { isWallLongEnough } from '../wall/wall-drafting'
26
- import { type FencePlanPoint, snapFenceDraftPoint } from './fence-drafting'
27
-
28
- function samePoint(a: FencePlanPoint, b: FencePlanPoint) {
29
- return a[0] === b[0] && a[1] === b[1]
30
- }
31
-
32
- type SegmentLike = {
33
- id: string
34
- start: FencePlanPoint
35
- end: FencePlanPoint
36
- curveOffset?: number
37
- }
38
-
39
- type AngleLabelState = {
40
- label: string
41
- position: [number, number, number]
42
- } | null
43
-
44
- function getEndpointAngleLabel(args: {
45
- preview: { start: FencePlanPoint; end: FencePlanPoint; curveOffset?: number }
46
- segments: SegmentLike[]
47
- nodeId: FenceNode['id']
48
- }): AngleLabelState {
49
- const { preview, segments, nodeId } = args
50
- const endpoints = [
51
- {
52
- point: preview.start,
53
- },
54
- {
55
- point: preview.end,
56
- },
57
- ]
58
- const targetSegment: SegmentLike = {
59
- id: nodeId,
60
- start: preview.start,
61
- end: preview.end,
62
- curveOffset: preview.curveOffset,
63
- }
64
-
65
- for (const endpoint of endpoints) {
66
- const targetReference = getSegmentAngleReferenceAtPoint(endpoint.point, targetSegment)
67
- if (!targetReference) continue
68
-
69
- const connectedSegment = segments.find(
70
- (segment) =>
71
- segment.id !== nodeId && Boolean(getSegmentAngleReferenceAtPoint(endpoint.point, segment)),
72
- )
73
- if (!connectedSegment) continue
74
-
75
- const connectedReference = getSegmentAngleReferenceAtPoint(endpoint.point, connectedSegment)
76
- if (!connectedReference) continue
77
-
78
- const angle = getAngleToSegmentReference(targetReference.vector, connectedReference)
79
- if (angle === null) continue
80
-
81
- return {
82
- label: formatAngleRadians(angle),
83
- position: [endpoint.point[0], 0.34, endpoint.point[1]],
84
- }
85
- }
86
-
87
- return null
88
- }
89
-
90
- function getReferenceSegments(walls: WallNode[], fences: FenceNode[]): SegmentLike[] {
91
- return [
92
- ...walls.map((wall) => ({
93
- id: wall.id,
94
- start: wall.start,
95
- end: wall.end,
96
- curveOffset: wall.curveOffset,
97
- })),
98
- ...fences.map((fence) => ({
99
- id: fence.id,
100
- start: fence.start,
101
- end: fence.end,
102
- curveOffset: fence.curveOffset,
103
- })),
104
- ]
105
- }
106
-
107
- type LinkedFenceSnapshot = {
108
- id: FenceNode['id']
109
- start: FencePlanPoint
110
- end: FencePlanPoint
111
- curveOffset?: number
112
- }
113
-
114
- function getLinkedFenceSnapshots(args: {
115
- fenceId: FenceNode['id']
116
- fenceParentId: string | null
117
- originalStart: FencePlanPoint
118
- originalEnd: FencePlanPoint
119
- }) {
120
- const { fenceId, fenceParentId, originalStart, originalEnd } = args
121
- const { nodes } = useScene.getState()
122
- const snapshots: LinkedFenceSnapshot[] = []
123
-
124
- for (const node of Object.values(nodes)) {
125
- if (!(node?.type === 'fence' && node.id !== fenceId)) {
126
- continue
127
- }
128
-
129
- if ((node.parentId ?? null) !== fenceParentId) {
130
- continue
131
- }
132
-
133
- if (
134
- !(
135
- samePoint(node.start, originalStart) ||
136
- samePoint(node.start, originalEnd) ||
137
- samePoint(node.end, originalStart) ||
138
- samePoint(node.end, originalEnd)
139
- )
140
- ) {
141
- continue
142
- }
143
-
144
- snapshots.push({
145
- id: node.id,
146
- start: [...node.start] as FencePlanPoint,
147
- end: [...node.end] as FencePlanPoint,
148
- curveOffset: node.curveOffset,
149
- })
150
- }
151
-
152
- return snapshots
153
- }
154
-
155
- function getLinkedFenceUpdates(
156
- linkedFences: LinkedFenceSnapshot[],
157
- originalStart: FencePlanPoint,
158
- originalEnd: FencePlanPoint,
159
- nextStart: FencePlanPoint,
160
- nextEnd: FencePlanPoint,
161
- ) {
162
- return linkedFences.map((fence) => ({
163
- id: fence.id,
164
- curveOffset: fence.curveOffset,
165
- start: samePoint(fence.start, originalStart)
166
- ? nextStart
167
- : samePoint(fence.start, originalEnd)
168
- ? nextEnd
169
- : fence.start,
170
- end: samePoint(fence.end, originalStart)
171
- ? nextStart
172
- : samePoint(fence.end, originalEnd)
173
- ? nextEnd
174
- : fence.end,
175
- }))
176
- }
177
-
178
- export const MoveFenceEndpointTool: React.FC<{ target: MovingFenceEndpoint }> = ({ target }) => {
179
- const activatedAtRef = useRef<number>(Date.now())
180
- const previousGridPosRef = useRef<FencePlanPoint | null>(null)
181
- const shiftPressedRef = useRef(false)
182
- const altPressedRef = useRef(false)
183
- const nodeIdRef = useRef(target.fence.id)
184
- const originalStartRef = useRef<FencePlanPoint>([...target.fence.start] as FencePlanPoint)
185
- const originalEndRef = useRef<FencePlanPoint>([...target.fence.end] as FencePlanPoint)
186
- const fixedPointRef = useRef<FencePlanPoint>(
187
- target.endpoint === 'start'
188
- ? ([...target.fence.end] as FencePlanPoint)
189
- : ([...target.fence.start] as FencePlanPoint),
190
- )
191
- const linkedOriginalsRef = useRef(
192
- getLinkedFenceSnapshots({
193
- fenceId: target.fence.id,
194
- fenceParentId: target.fence.parentId ?? null,
195
- originalStart: target.fence.start,
196
- originalEnd: target.fence.end,
197
- }),
198
- )
199
- const previewRef = useRef<{ start: FencePlanPoint; end: FencePlanPoint } | null>(null)
200
- const [angleLabel, setAngleLabel] = useState<AngleLabelState>(null)
201
-
202
- const [cursorLocalPos, setCursorLocalPos] = useState<[number, number, number]>(() => {
203
- const point = target.endpoint === 'start' ? target.fence.start : target.fence.end
204
- return [point[0], 0, point[1]]
205
- })
206
- const [altPressed, setAltPressed] = useState(false)
207
-
208
- const exitMoveMode = useCallback(() => {
209
- useEditor.getState().setMovingFenceEndpoint(null)
210
- }, [])
211
-
212
- useEffect(() => {
213
- const nodeId = nodeIdRef.current
214
- const originalStart = originalStartRef.current
215
- const originalEnd = originalEndRef.current
216
- const fixedPoint = fixedPointRef.current
217
- const siblings = Object.values(useScene.getState().nodes)
218
- const levelWalls = siblings.filter(
219
- (node): node is WallNode =>
220
- node?.type === 'wall' && (node.parentId ?? null) === (target.fence.parentId ?? null),
221
- )
222
- const levelFences = siblings.filter(
223
- (node): node is FenceNode =>
224
- node?.type === 'fence' && (node.parentId ?? null) === (target.fence.parentId ?? null),
225
- )
226
-
227
- pauseSceneHistory(useScene)
228
- let wasCommitted = false
229
-
230
- const applyNodePreview = (
231
- updates: Array<{ id: FenceNode['id']; start: FencePlanPoint; end: FencePlanPoint }>,
232
- ) => {
233
- useScene.getState().updateNodes(
234
- updates.map((entry) => ({
235
- id: entry.id as AnyNodeId,
236
- data: { start: entry.start, end: entry.end },
237
- })),
238
- )
239
- for (const entry of updates) {
240
- useScene.getState().markDirty(entry.id as AnyNodeId)
241
- }
242
- }
243
-
244
- const applyPreview = (movingPoint: FencePlanPoint, detachLinkedFences = false) => {
245
- const nextStart = target.endpoint === 'start' ? movingPoint : fixedPoint
246
- const nextEnd = target.endpoint === 'end' ? movingPoint : fixedPoint
247
- const linkedUpdates = detachLinkedFences
248
- ? []
249
- : getLinkedFenceUpdates(
250
- linkedOriginalsRef.current,
251
- originalStart,
252
- originalEnd,
253
- nextStart,
254
- nextEnd,
255
- )
256
- previewRef.current = { start: nextStart, end: nextEnd }
257
- setCursorLocalPos([movingPoint[0], 0, movingPoint[1]])
258
- setAngleLabel(
259
- getEndpointAngleLabel({
260
- preview: { start: nextStart, end: nextEnd, curveOffset: target.fence.curveOffset },
261
- segments: [...getReferenceSegments(levelWalls, levelFences), ...linkedUpdates],
262
- nodeId,
263
- }),
264
- )
265
- applyNodePreview([{ id: nodeId, start: nextStart, end: nextEnd }, ...linkedUpdates])
266
- }
267
-
268
- const restoreOriginal = (clearAngleLabel = true) => {
269
- applyNodePreview([
270
- { id: nodeId, start: originalStart, end: originalEnd },
271
- ...linkedOriginalsRef.current,
272
- ])
273
- if (clearAngleLabel) {
274
- setAngleLabel(null)
275
- }
276
- }
277
-
278
- const onGridMove = (event: GridEvent) => {
279
- const planPoint: FencePlanPoint = [event.localPosition[0], event.localPosition[2]]
280
- const snappedPoint = snapFenceDraftPoint({
281
- point: planPoint,
282
- walls: levelWalls,
283
- fences: levelFences,
284
- start: fixedPoint,
285
- angleSnap: !shiftPressedRef.current,
286
- ignoreFenceIds: [nodeId],
287
- })
288
-
289
- if (
290
- previousGridPosRef.current &&
291
- (snappedPoint[0] !== previousGridPosRef.current[0] ||
292
- snappedPoint[1] !== previousGridPosRef.current[1])
293
- ) {
294
- sfxEmitter.emit('sfx:grid-snap')
295
- }
296
- previousGridPosRef.current = snappedPoint
297
-
298
- applyPreview(snappedPoint, event.nativeEvent.altKey)
299
- }
300
-
301
- const onGridClick = (event: GridEvent) => {
302
- if (Date.now() - activatedAtRef.current < 150) {
303
- event.nativeEvent?.stopPropagation?.()
304
- return
305
- }
306
-
307
- const preview = previewRef.current ?? { start: originalStart, end: originalEnd }
308
- const hasChanged = !(
309
- samePoint(preview.start, originalStart) && samePoint(preview.end, originalEnd)
310
- )
311
-
312
- if (hasChanged && isWallLongEnough(preview.start, preview.end)) {
313
- wasCommitted = true
314
-
315
- applyNodePreview([
316
- { id: nodeId, start: originalStart, end: originalEnd },
317
- ...linkedOriginalsRef.current,
318
- ])
319
-
320
- resumeSceneHistory(useScene)
321
- applyNodePreview([
322
- { id: nodeId, start: preview.start, end: preview.end },
323
- ...(altPressedRef.current
324
- ? []
325
- : getLinkedFenceUpdates(
326
- linkedOriginalsRef.current,
327
- originalStart,
328
- originalEnd,
329
- preview.start,
330
- preview.end,
331
- )),
332
- ])
333
- pauseSceneHistory(useScene)
334
- sfxEmitter.emit('sfx:item-place')
335
- }
336
-
337
- useViewer.getState().setSelection({ selectedIds: [nodeId] })
338
- setAngleLabel(null)
339
- exitMoveMode()
340
- event.nativeEvent?.stopPropagation?.()
341
- }
342
-
343
- const onCancel = () => {
344
- restoreOriginal()
345
- useViewer.getState().setSelection({ selectedIds: [nodeId] })
346
- resumeSceneHistory(useScene)
347
- setAngleLabel(null)
348
- markToolCancelConsumed()
349
- exitMoveMode()
350
- }
351
-
352
- const onKeyDown = (event: KeyboardEvent) => {
353
- if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
354
- return
355
- }
356
- if (event.key === 'Shift') {
357
- shiftPressedRef.current = true
358
- }
359
- if (event.key === 'Alt') {
360
- altPressedRef.current = true
361
- setAltPressed(true)
362
- }
363
- }
364
-
365
- const onKeyUp = (event: KeyboardEvent) => {
366
- if (event.key === 'Shift') {
367
- shiftPressedRef.current = false
368
- }
369
- if (event.key === 'Alt') {
370
- altPressedRef.current = false
371
- setAltPressed(false)
372
- }
373
- }
374
-
375
- const onWindowBlur = () => {
376
- shiftPressedRef.current = false
377
- altPressedRef.current = false
378
- setAltPressed(false)
379
- }
380
-
381
- emitter.on('grid:move', onGridMove)
382
- emitter.on('grid:click', onGridClick)
383
- emitter.on('tool:cancel', onCancel)
384
- window.addEventListener('keydown', onKeyDown)
385
- window.addEventListener('keyup', onKeyUp)
386
- window.addEventListener('blur', onWindowBlur)
387
-
388
- return () => {
389
- if (!wasCommitted) {
390
- restoreOriginal(false)
391
- }
392
- resumeSceneHistory(useScene)
393
- emitter.off('grid:move', onGridMove)
394
- emitter.off('grid:click', onGridClick)
395
- emitter.off('tool:cancel', onCancel)
396
- window.removeEventListener('keydown', onKeyDown)
397
- window.removeEventListener('keyup', onKeyUp)
398
- window.removeEventListener('blur', onWindowBlur)
399
- }
400
- }, [exitMoveMode, target])
401
-
402
- return (
403
- <group>
404
- <CursorSphere position={cursorLocalPos} showTooltip={false} />
405
- <Html
406
- position={[cursorLocalPos[0], 0, cursorLocalPos[2]]}
407
- style={{ pointerEvents: 'none', touchAction: 'none' }}
408
- zIndexRange={[100, 0]}
409
- >
410
- <div className="translate-y-10">
411
- <div
412
- className={`whitespace-nowrap rounded-full border px-2 py-1 font-medium text-[11px] shadow-lg backdrop-blur-md transition-colors ${
413
- altPressed
414
- ? 'border-amber-500/70 bg-amber-500/15 text-amber-100'
415
- : 'border-border/70 bg-background/90 text-foreground/80'
416
- }`}
417
- >
418
- {altPressed ? 'Detach endpoint' : 'Drag endpoint'}
419
- </div>
420
- </div>
421
- </Html>
422
- {angleLabel && <EndpointAngleLabel label={angleLabel.label} position={angleLabel.position} />}
423
- </group>
424
- )
425
- }
426
-
427
- function EndpointAngleLabel({
428
- label,
429
- position,
430
- }: {
431
- label: string
432
- position: [number, number, number]
433
- }) {
434
- return (
435
- <Html center position={position} style={{ pointerEvents: 'none' }} zIndexRange={[100, 0]}>
436
- <div className="whitespace-nowrap rounded-full border border-border bg-background/95 px-2 py-1 font-mono font-semibold text-[11px] text-foreground shadow-lg backdrop-blur-md">
437
- {label}
438
- </div>
439
- </Html>
440
- )
441
- }