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