@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,257 +0,0 @@
1
- 'use client'
2
-
3
- import { type AnyNodeId, emitter, type GridEvent, useScene, type CeilingNode } from '@pascal-app/core'
4
- import { useViewer } from '@pascal-app/viewer'
5
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
6
- import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
7
- import { sfxEmitter } from '../../../lib/sfx-bus'
8
- import useEditor from '../../../store/use-editor'
9
- import { CursorSphere } from '../shared/cursor-sphere'
10
- import { BufferGeometry, DoubleSide, Path, Shape, ShapeGeometry, Vector3 } from 'three'
11
-
12
- function snap(value: number) {
13
- return Math.round(value * 2) / 2
14
- }
15
-
16
- function translatePolygon(
17
- polygon: Array<[number, number]>,
18
- deltaX: number,
19
- deltaZ: number,
20
- ): Array<[number, number]> {
21
- return polygon.map(([x, z]) => [x + deltaX, z + deltaZ] as [number, number])
22
- }
23
-
24
- function getPolygonCenter(polygon: Array<[number, number]>): [number, number] {
25
- if (polygon.length === 0) return [0, 0]
26
- let sumX = 0
27
- let sumZ = 0
28
- for (const [x, z] of polygon) {
29
- sumX += x
30
- sumZ += z
31
- }
32
- return [sumX / polygon.length, sumZ / polygon.length]
33
- }
34
-
35
- export const MoveCeilingTool: React.FC<{ node: CeilingNode }> = ({ node }) => {
36
- const activatedAtRef = useRef<number>(Date.now())
37
- const originalPolygonRef = useRef(node.polygon.map(([x, z]) => [x, z] as [number, number]))
38
- const originalHolesRef = useRef(
39
- (node.holes ?? []).map((hole) => hole.map(([x, z]) => [x, z] as [number, number])),
40
- )
41
- const dragAnchorRef = useRef<[number, number] | null>(null)
42
- const previousGridPosRef = useRef<[number, number] | null>(null)
43
- const previousCursorPosRef = useRef<[number, number, number] | null>(null)
44
- const previousDeltaRef = useRef<[number, number] | null>(null)
45
- const previewRef = useRef<{
46
- polygon: Array<[number, number]>
47
- holes: Array<Array<[number, number]>>
48
- } | null>(null)
49
-
50
- const [cursorLocalPos, setCursorLocalPos] = useState<[number, number, number]>(() => {
51
- const center = getPolygonCenter(node.polygon)
52
- return [center[0], node.height ?? 2.5, center[1]]
53
- })
54
- const [previewPolygon, setPreviewPolygon] = useState<Array<[number, number]>>(node.polygon)
55
- const [previewHoles, setPreviewHoles] = useState<Array<Array<[number, number]>>>(node.holes ?? [])
56
-
57
- const exitMoveMode = useCallback(() => {
58
- useEditor.getState().setMovingNode(null)
59
- }, [])
60
-
61
- useEffect(() => {
62
- const originalPolygon = originalPolygonRef.current
63
- const originalHoles = originalHolesRef.current
64
-
65
- useScene.temporal.getState().pause()
66
- let wasCommitted = false
67
-
68
- const applyPreview = (
69
- polygon: Array<[number, number]>,
70
- holes: Array<Array<[number, number]>>,
71
- ) => {
72
- previewRef.current = { polygon, holes }
73
- setPreviewPolygon(polygon)
74
- setPreviewHoles(holes)
75
- const center = getPolygonCenter(polygon)
76
- const nextCursorPos: [number, number, number] = [center[0], node.height ?? 2.5, center[1]]
77
- if (
78
- !previousCursorPosRef.current ||
79
- previousCursorPosRef.current[0] !== nextCursorPos[0] ||
80
- previousCursorPosRef.current[1] !== nextCursorPos[1] ||
81
- previousCursorPosRef.current[2] !== nextCursorPos[2]
82
- ) {
83
- previousCursorPosRef.current = nextCursorPos
84
- setCursorLocalPos(nextCursorPos)
85
- }
86
- useScene.getState().updateNode(node.id, { polygon, holes })
87
- useScene.getState().markDirty(node.id as AnyNodeId)
88
- }
89
-
90
- const restoreOriginal = () => {
91
- setPreviewPolygon(originalPolygon)
92
- setPreviewHoles(originalHoles)
93
- useScene.getState().updateNode(node.id, {
94
- holes: originalHoles,
95
- polygon: originalPolygon,
96
- })
97
- useScene.getState().markDirty(node.id as AnyNodeId)
98
- }
99
-
100
- const onGridMove = (event: GridEvent) => {
101
- const localX = snap(event.localPosition[0])
102
- const localZ = snap(event.localPosition[2])
103
-
104
- if (
105
- previousGridPosRef.current &&
106
- (localX !== previousGridPosRef.current[0] || localZ !== previousGridPosRef.current[1])
107
- ) {
108
- sfxEmitter.emit('sfx:grid-snap')
109
- }
110
- previousGridPosRef.current = [localX, localZ]
111
-
112
- const anchor = dragAnchorRef.current ?? [localX, localZ]
113
- dragAnchorRef.current = anchor
114
-
115
- const deltaX = localX - anchor[0]
116
- const deltaZ = localZ - anchor[1]
117
-
118
- if (
119
- previousDeltaRef.current &&
120
- previousDeltaRef.current[0] === deltaX &&
121
- previousDeltaRef.current[1] === deltaZ
122
- ) {
123
- return
124
- }
125
- previousDeltaRef.current = [deltaX, deltaZ]
126
-
127
- applyPreview(
128
- translatePolygon(originalPolygon, deltaX, deltaZ),
129
- originalHoles.map((hole) => translatePolygon(hole, deltaX, deltaZ)),
130
- )
131
- }
132
-
133
- const onGridClick = (event: GridEvent) => {
134
- if (Date.now() - activatedAtRef.current < 150) {
135
- event.nativeEvent?.stopPropagation?.()
136
- return
137
- }
138
-
139
- const preview = previewRef.current ?? { polygon: originalPolygon, holes: originalHoles }
140
-
141
- wasCommitted = true
142
-
143
- // Restore original baseline while paused so the next resume+update
144
- // registers as a single tracked change (undo reverts to original).
145
- useScene.getState().updateNode(node.id, {
146
- polygon: originalPolygon,
147
- holes: originalHoles,
148
- })
149
-
150
- useScene.temporal.getState().resume()
151
- useScene.getState().updateNode(node.id, preview)
152
- useScene.getState().markDirty(node.id as AnyNodeId)
153
- useScene.temporal.getState().pause()
154
-
155
- sfxEmitter.emit('sfx:item-place')
156
- useViewer.getState().setSelection({ selectedIds: [node.id] })
157
- exitMoveMode()
158
- event.nativeEvent?.stopPropagation?.()
159
- }
160
-
161
- const onCancel = () => {
162
- restoreOriginal()
163
- useViewer.getState().setSelection({ selectedIds: [node.id] })
164
- useScene.temporal.getState().resume()
165
- markToolCancelConsumed()
166
- exitMoveMode()
167
- }
168
-
169
- emitter.on('grid:move', onGridMove)
170
- emitter.on('grid:click', onGridClick)
171
- emitter.on('tool:cancel', onCancel)
172
-
173
- return () => {
174
- if (!wasCommitted) {
175
- restoreOriginal()
176
- }
177
- useScene.temporal.getState().resume()
178
- emitter.off('grid:move', onGridMove)
179
- emitter.off('grid:click', onGridClick)
180
- emitter.off('tool:cancel', onCancel)
181
- }
182
- }, [exitMoveMode, node.height, node.id])
183
-
184
- const previewFillGeometry = useMemo(
185
- () => createCeilingPreviewGeometry(previewPolygon, previewHoles),
186
- [previewHoles, previewPolygon],
187
- )
188
-
189
- const previewOutlineGeometry = useMemo(
190
- () => createCeilingOutlineGeometry(previewPolygon),
191
- [previewPolygon],
192
- )
193
-
194
- return (
195
- <group>
196
- <mesh geometry={previewFillGeometry} position={[0, (node.height ?? 2.5) + 0.012, 0]}>
197
- <meshBasicMaterial
198
- color="#f5f5f4"
199
- depthWrite={false}
200
- opacity={0.3}
201
- side={DoubleSide}
202
- transparent
203
- />
204
- </mesh>
205
- <line geometry={previewOutlineGeometry} position={[0, (node.height ?? 2.5) + 0.02, 0]}>
206
- <lineBasicMaterial color="#ffffff" depthWrite={false} opacity={0.95} transparent />
207
- </line>
208
- <CursorSphere position={cursorLocalPos} showTooltip={false} />
209
- </group>
210
- )
211
- }
212
-
213
- function createCeilingPreviewGeometry(
214
- polygon: Array<[number, number]>,
215
- holes: Array<Array<[number, number]>>,
216
- ): BufferGeometry {
217
- if (polygon.length < 3) return new BufferGeometry()
218
-
219
- const shape = new Shape()
220
- const [firstX, firstZ] = polygon[0]!
221
- shape.moveTo(firstX, -firstZ)
222
-
223
- for (let i = 1; i < polygon.length; i++) {
224
- const [x, z] = polygon[i]!
225
- shape.lineTo(x, -z)
226
- }
227
- shape.closePath()
228
-
229
- for (const holePolygon of holes) {
230
- if (holePolygon.length < 3) continue
231
- const hole = new Path()
232
- const [hx, hz] = holePolygon[0]!
233
- hole.moveTo(hx, -hz)
234
- for (let i = 1; i < holePolygon.length; i++) {
235
- const [x, z] = holePolygon[i]!
236
- hole.lineTo(x, -z)
237
- }
238
- hole.closePath()
239
- shape.holes.push(hole)
240
- }
241
-
242
- const geometry = new ShapeGeometry(shape)
243
- geometry.rotateX(-Math.PI / 2)
244
- geometry.computeVertexNormals()
245
- return geometry
246
- }
247
-
248
- function createCeilingOutlineGeometry(polygon: Array<[number, number]>): BufferGeometry {
249
- const geometry = new BufferGeometry()
250
- if (polygon.length < 2) return geometry
251
-
252
- const points = polygon.map(([x, z]) => new Vector3(x, 0, z))
253
- const [firstX, firstZ] = polygon[0]!
254
- points.push(new Vector3(firstX, 0, firstZ))
255
- geometry.setFromPoints(points)
256
- return geometry
257
- }
@@ -1,97 +0,0 @@
1
- import '../../../three-types'
2
-
3
- import {
4
- COLUMN_PRESETS,
5
- ColumnNode,
6
- type ColumnNode as ColumnNodeType,
7
- type ColumnPresetId,
8
- emitter,
9
- type GridEvent,
10
- type LevelNode,
11
- useScene,
12
- } from '@pascal-app/core'
13
- import { useEffect, useRef, useState } from 'react'
14
- import type { Group } 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 COLUMN_ICON = (
20
- // eslint-disable-next-line @next/next/no-img-element
21
- <img
22
- alt="Column"
23
- src="/icons/column.png"
24
- style={{ width: '100%', height: '100%', objectFit: 'contain' }}
25
- />
26
- )
27
-
28
- const roundToHalf = (value: number) => Math.round(value * 2) / 2
29
- const DEFAULT_COLUMN_PRESET_ID = 'basicPillar' satisfies ColumnPresetId
30
-
31
- function createColumnFromPreset(presetId: ColumnPresetId, position: [number, number, number]) {
32
- const { label, ...preset } = COLUMN_PRESETS[presetId]
33
- return ColumnNode.parse({
34
- name: label,
35
- position,
36
- rotation: 0,
37
- ...preset,
38
- })
39
- }
40
-
41
- type ColumnToolProps = {
42
- currentLevelId: LevelNode['id'] | null
43
- onPlaced?: (nodeId: ColumnNodeType['id']) => void
44
- }
45
-
46
- export const ColumnTool: React.FC<ColumnToolProps> = ({ currentLevelId, onPlaced }) => {
47
- const [, setCursorPosition] = useState<[number, number, number] | null>(null)
48
- const cursorRef = useRef<Group>(null)
49
-
50
- useEffect(() => {
51
- if (!currentLevelId) return
52
-
53
- const onGridMove = (event: GridEvent) => {
54
- const nextPosition: [number, number, number] = [
55
- roundToHalf(event.localPosition[0]),
56
- 0,
57
- roundToHalf(event.localPosition[2]),
58
- ]
59
- setCursorPosition(nextPosition)
60
- cursorRef.current?.position.set(nextPosition[0], event.localPosition[1], nextPosition[2])
61
- }
62
-
63
- const onGridClick = (event: GridEvent) => {
64
- const position: [number, number, number] = [
65
- roundToHalf(event.localPosition[0]),
66
- 0,
67
- roundToHalf(event.localPosition[2]),
68
- ]
69
- const column = createColumnFromPreset(DEFAULT_COLUMN_PRESET_ID, position)
70
- useScene.getState().createNode(column, currentLevelId)
71
- onPlaced?.(column.id)
72
- sfxEmitter.emit('sfx:structure-build')
73
- useEditor.getState().setTool(null)
74
- useEditor.getState().setMode('select')
75
- }
76
-
77
- emitter.on('grid:move', onGridMove)
78
- emitter.on('grid:click', onGridClick)
79
-
80
- return () => {
81
- emitter.off('grid:move', onGridMove)
82
- emitter.off('grid:click', onGridClick)
83
- }
84
- }, [currentLevelId, onPlaced])
85
-
86
- if (!currentLevelId) return null
87
-
88
- return (
89
- <CursorSphere
90
- color="#a78bfa"
91
- height={2.8}
92
- ref={cursorRef}
93
- showTooltip
94
- tooltipContent={COLUMN_ICON}
95
- />
96
- )
97
- }
@@ -1,105 +0,0 @@
1
- import '../../../three-types'
2
-
3
- import {
4
- type AnyNodeId,
5
- ColumnNode,
6
- type ColumnNode as ColumnNodeType,
7
- emitter,
8
- type GridEvent,
9
- sceneRegistry,
10
- useLiveTransforms,
11
- useScene,
12
- } from '@pascal-app/core'
13
- import { useCallback, useEffect, useState } from 'react'
14
- import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
15
- import { sfxEmitter } from '../../../lib/sfx-bus'
16
- import useEditor from '../../../store/use-editor'
17
- import { CursorSphere } from '../shared/cursor-sphere'
18
-
19
- const roundToHalf = (value: number) => Math.round(value * 2) / 2
20
-
21
- export function MoveColumnTool({ node }: { node: ColumnNodeType }) {
22
- const [previewPosition, setPreviewPosition] = useState<[number, number, number]>(node.position)
23
-
24
- const exitMoveMode = useCallback(() => {
25
- useEditor.getState().setMovingNode(null)
26
- }, [])
27
-
28
- useEffect(() => {
29
- useScene.temporal.getState().pause()
30
- let committed = false
31
-
32
- const applyPreview = (position: [number, number, number]) => {
33
- setPreviewPosition(position)
34
- useLiveTransforms.getState().set(node.id, {
35
- position,
36
- rotation: node.rotation,
37
- })
38
- sceneRegistry.nodes.get(node.id)?.position.set(position[0], position[1], position[2])
39
- }
40
-
41
- const onGridMove = (event: GridEvent) => {
42
- applyPreview([roundToHalf(event.localPosition[0]), 0, roundToHalf(event.localPosition[2])])
43
- }
44
-
45
- const onGridClick = (event: GridEvent) => {
46
- const position: [number, number, number] = [
47
- roundToHalf(event.localPosition[0]),
48
- 0,
49
- roundToHalf(event.localPosition[2]),
50
- ]
51
- const nodeId = (node as { id?: ColumnNodeType['id'] }).id
52
-
53
- if (nodeId && useScene.getState().nodes[nodeId]) {
54
- committed = true
55
- useLiveTransforms.getState().clear(nodeId)
56
- useScene.temporal.getState().resume()
57
- useScene.getState().updateNode(nodeId, { position })
58
- } else if (node.parentId) {
59
- const column = ColumnNode.parse({
60
- ...node,
61
- id: undefined,
62
- metadata: {},
63
- position,
64
- })
65
- committed = true
66
- useScene.temporal.getState().resume()
67
- useScene.getState().createNode(column, node.parentId as AnyNodeId)
68
- }
69
-
70
- useLiveTransforms.getState().clear(node.id)
71
- sfxEmitter.emit('sfx:item-place')
72
- exitMoveMode()
73
- event.nativeEvent?.stopPropagation?.()
74
- }
75
-
76
- const onCancel = () => {
77
- useLiveTransforms.getState().clear(node.id)
78
- sceneRegistry.nodes
79
- .get(node.id)
80
- ?.position.set(node.position[0], node.position[1], node.position[2])
81
- useScene.temporal.getState().resume()
82
- markToolCancelConsumed()
83
- exitMoveMode()
84
- }
85
-
86
- emitter.on('grid:move', onGridMove)
87
- emitter.on('grid:click', onGridClick)
88
- emitter.on('tool:cancel', onCancel)
89
-
90
- return () => {
91
- emitter.off('grid:move', onGridMove)
92
- emitter.off('grid:click', onGridClick)
93
- emitter.off('tool:cancel', onCancel)
94
- useLiveTransforms.getState().clear(node.id)
95
- if (!committed) {
96
- sceneRegistry.nodes
97
- .get(node.id)
98
- ?.position.set(node.position[0], node.position[1], node.position[2])
99
- useScene.temporal.getState().resume()
100
- }
101
- }
102
- }, [exitMoveMode, node])
103
-
104
- return <CursorSphere color="#a78bfa" height={node.height} position={previewPosition} />
105
- }
@@ -1,110 +0,0 @@
1
- import {
2
- type AnyNodeId,
3
- type DoorNode,
4
- getScaledDimensions,
5
- type ItemNode,
6
- useScene,
7
- type WallNode,
8
- type WindowNode,
9
- } from '@pascal-app/core'
10
-
11
- /**
12
- * Converts wall-local (X along wall, Y = height above wall base) to world XYZ.
13
- */
14
- export function wallLocalToWorld(
15
- wallNode: WallNode,
16
- localX: number,
17
- localY: number,
18
- levelYOffset = 0,
19
- slabElevation = 0,
20
- ): [number, number, number] {
21
- const wallAngle = Math.atan2(
22
- wallNode.end[1] - wallNode.start[1],
23
- wallNode.end[0] - wallNode.start[0],
24
- )
25
- return [
26
- wallNode.start[0] + localX * Math.cos(wallAngle),
27
- slabElevation + localY + levelYOffset,
28
- wallNode.start[1] + localX * Math.sin(wallAngle),
29
- ]
30
- }
31
-
32
- /**
33
- * Clamps door center X so it stays fully within wall bounds.
34
- * Y is always height/2 — doors sit at floor level.
35
- */
36
- export function clampToWall(
37
- wallNode: WallNode,
38
- localX: number,
39
- width: number,
40
- height: number,
41
- ): { clampedX: number; clampedY: number } {
42
- const dx = wallNode.end[0] - wallNode.start[0]
43
- const dz = wallNode.end[1] - wallNode.start[1]
44
- const wallLength = Math.sqrt(dx * dx + dz * dz)
45
-
46
- const clampedX = Math.max(width / 2, Math.min(wallLength - width / 2, localX))
47
- const clampedY = height / 2 // Doors always sit at floor level
48
- return { clampedX, clampedY }
49
- }
50
-
51
- /**
52
- * Checks if a proposed door position overlaps any existing wall children.
53
- * Handles item, window, and door types.
54
- */
55
- export function hasWallChildOverlap(
56
- wallId: string,
57
- clampedX: number,
58
- clampedY: number,
59
- width: number,
60
- height: number,
61
- ignoreId?: string,
62
- ): boolean {
63
- const nodes = useScene.getState().nodes
64
- const wallNode = nodes[wallId as AnyNodeId] as WallNode | undefined
65
- if (!wallNode) return true
66
- const halfW = width / 2
67
- const halfH = height / 2
68
- const newBottom = clampedY - halfH
69
- const newTop = clampedY + halfH
70
- const newLeft = clampedX - halfW
71
- const newRight = clampedX + halfW
72
-
73
- for (const childId of Array.isArray(wallNode.children) ? wallNode.children : []) {
74
- if (childId === ignoreId) continue
75
- const child = nodes[childId as AnyNodeId]
76
- if (!child) continue
77
-
78
- let childLeft: number, childRight: number, childBottom: number, childTop: number
79
-
80
- if (child.type === 'item') {
81
- const item = child as ItemNode
82
- if (item.asset.attachTo !== 'wall' && item.asset.attachTo !== 'wall-side') continue
83
- const [w, h] = getScaledDimensions(item)
84
- childLeft = item.position[0] - w / 2
85
- childRight = item.position[0] + w / 2
86
- childBottom = item.position[1]
87
- childTop = item.position[1] + h
88
- } else if (child.type === 'window') {
89
- const win = child as WindowNode
90
- childLeft = win.position[0] - win.width / 2
91
- childRight = win.position[0] + win.width / 2
92
- childBottom = win.position[1] - win.height / 2
93
- childTop = win.position[1] + win.height / 2
94
- } else if (child.type === 'door') {
95
- const door = child as DoorNode
96
- childLeft = door.position[0] - door.width / 2
97
- childRight = door.position[0] + door.width / 2
98
- childBottom = door.position[1] - door.height / 2
99
- childTop = door.position[1] + door.height / 2
100
- } else {
101
- continue
102
- }
103
-
104
- const xOverlap = newLeft < childRight && newRight > childLeft
105
- const yOverlap = newBottom < childTop && newTop > childBottom
106
- if (xOverlap && yOverlap) return true
107
- }
108
-
109
- return false
110
- }