@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
@@ -0,0 +1,36 @@
1
+ import { create } from 'zustand'
2
+
3
+ /**
4
+ * Ephemeral preview state for wall move's bridge walls.
5
+ *
6
+ * When the user drags a wall whose corner neighbours are off-axis to
7
+ * the move direction, the junction planner emits a `bridgePlan` for
8
+ * each — a new wall that would be inserted between the original and
9
+ * new corner on commit. The 3D `MoveWallTool` already renders these
10
+ * as translucent ghost meshes mid-drag; the 2D `wallFloorplanMoveTarget`
11
+ * writes here so the floor-plan SVG layer can render the same hint.
12
+ *
13
+ * Writer: `packages/nodes/src/wall/floorplan-move.ts` (on each `apply`,
14
+ * cleared on `commit` and by the move overlay's cleanup).
15
+ * Reader: `packages/editor/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx`.
16
+ */
17
+ export type WallMoveGhostBridge = {
18
+ id: string
19
+ start: [number, number]
20
+ end: [number, number]
21
+ /** Plan-space thickness already passed through `getFloorplanWallThickness`. */
22
+ thickness: number
23
+ color: string
24
+ }
25
+
26
+ type WallMoveGhostsState = {
27
+ bridges: WallMoveGhostBridge[]
28
+ setBridges: (bridges: WallMoveGhostBridge[]) => void
29
+ clear: () => void
30
+ }
31
+
32
+ export const useWallMoveGhosts = create<WallMoveGhostsState>((set) => ({
33
+ bridges: [],
34
+ setBridges: (bridges) => set({ bridges }),
35
+ clear: () => set({ bridges: [] }),
36
+ }))
@@ -0,0 +1,32 @@
1
+ // Ephemeral store for the "magnetic" wall-snap beacon shown during wall
2
+ // drafting. The wall tool writes the active snap point on pointermove when
3
+ // the draft endpoint locks onto existing wall geometry (a corner or a point
4
+ // along a wall); the 3D beacon layer subscribes and draws a vertical marker
5
+ // there. Cleared on commit, cancel, and unmount — same lifecycle as
6
+ // `use-alignment-guides`.
7
+
8
+ import { create } from 'zustand'
9
+
10
+ /** Which kind of wall geometry the draft point snapped to. */
11
+ export type WallSnapKind = 'endpoint' | 'midpoint' | 'intersection' | 'wall'
12
+
13
+ export type WallSnapPoint = {
14
+ /** Building-local plan coordinates (XZ meters). */
15
+ x: number
16
+ z: number
17
+ kind: WallSnapKind
18
+ }
19
+
20
+ type WallSnapIndicatorState = {
21
+ point: WallSnapPoint | null
22
+ set(point: WallSnapPoint | null): void
23
+ clear(): void
24
+ }
25
+
26
+ const useWallSnapIndicator = create<WallSnapIndicatorState>((set) => ({
27
+ point: null,
28
+ set: (point) => set({ point }),
29
+ clear: () => set({ point: null }),
30
+ }))
31
+
32
+ export default useWallSnapIndicator
@@ -1,125 +0,0 @@
1
- 'use client'
2
-
3
- import { emitter, sceneRegistry } from '@pascal-app/core'
4
- import { useThree } from '@react-three/fiber'
5
- import { useCallback, useEffect } from 'react'
6
- import * as THREE from 'three'
7
- import { usePresetsAdapter } from '../../contexts/presets-context'
8
-
9
- const THUMBNAIL_SIZE = 1080
10
- const CAMERA_FOV = 45
11
-
12
- export const PresetThumbnailGenerator = () => {
13
- const gl = useThree((state) => state.gl)
14
- const scene = useThree((state) => state.scene)
15
- const adapter = usePresetsAdapter()
16
-
17
- const generate = useCallback(
18
- async ({ presetId, nodeId }: { presetId: string; nodeId: string }) => {
19
- const target = sceneRegistry.nodes.get(nodeId)
20
- if (!target) {
21
- console.error('❌ PresetThumbnail: node not found', nodeId)
22
- return
23
- }
24
-
25
- // Compute each mesh's transform relative to the target node (cancels world
26
- // position/rotation), so the item is always rendered at origin with a known
27
- // neutral orientation regardless of where it's placed in the scene.
28
- target.updateWorldMatrix(true, true)
29
- const targetInverse = new THREE.Matrix4().copy(target.matrixWorld).invert()
30
- const relMatrix = new THREE.Matrix4()
31
-
32
- const clones: THREE.Object3D[] = []
33
- target.traverse((obj) => {
34
- if (
35
- !(obj instanceof THREE.Mesh || obj instanceof THREE.Line || obj instanceof THREE.Points)
36
- )
37
- return
38
- const c = obj.clone(false) // shallow clone: copies geometry, material, visible — no children
39
- relMatrix.multiplyMatrices(targetInverse, obj.matrixWorld)
40
- relMatrix.decompose(c.position, c.quaternion, c.scale)
41
- scene.add(c)
42
- clones.push(c)
43
- })
44
-
45
- if (clones.length === 0) {
46
- console.error('❌ PresetThumbnail: no renderable objects found', nodeId)
47
- return
48
- }
49
-
50
- // Combined bounding box across all clones
51
- const box = new THREE.Box3()
52
- for (const c of clones) box.expandByObject(c)
53
-
54
- if (box.isEmpty()) {
55
- for (const c of clones) scene.remove(c)
56
- console.error('❌ PresetThumbnail: empty bounding box', nodeId)
57
- return
58
- }
59
-
60
- const sphere = new THREE.Sphere()
61
- box.getBoundingSphere(sphere)
62
-
63
- // Camera: aspect matches canvas (center-cropped to square after render)
64
- const { width, height } = gl.domElement
65
- const camera = new THREE.PerspectiveCamera(CAMERA_FOV, width / height, 0.01, 1000)
66
- const dir = new THREE.Vector3(-0.5, 0.5, 0.5).normalize()
67
- const fovRad = (CAMERA_FOV * Math.PI) / 180
68
- const dist = (sphere.radius / Math.tan(fovRad / 2)) * 1.3
69
- camera.position.copy(sphere.center).addScaledVector(dir, dist)
70
- camera.lookAt(sphere.center)
71
- camera.updateProjectionMatrix()
72
-
73
- // Hide all scene geometry except the clones — leave lights, cameras, etc. intact
74
- const cloneSet = new Set<THREE.Object3D>(clones)
75
- const snapshot = new Map<THREE.Object3D, boolean>()
76
- scene.traverse((obj) => {
77
- if (cloneSet.has(obj)) return
78
- if (
79
- !(obj instanceof THREE.Mesh || obj instanceof THREE.Line || obj instanceof THREE.Points)
80
- )
81
- return
82
- snapshot.set(obj, obj.visible)
83
- obj.visible = false
84
- })
85
-
86
- gl.render(scene, camera)
87
-
88
- // Restore visibility and remove clones
89
- snapshot.forEach((wasVisible, obj) => {
90
- obj.visible = wasVisible
91
- })
92
- for (const c of clones) scene.remove(c)
93
-
94
- // Center-crop to square and scale to THUMBNAIL_SIZE
95
- const minDim = Math.min(width, height)
96
- const sx = Math.round((width - minDim) / 2)
97
- const sy = Math.round((height - minDim) / 2)
98
- const offscreen = document.createElement('canvas')
99
- offscreen.width = THUMBNAIL_SIZE
100
- offscreen.height = THUMBNAIL_SIZE
101
- const ctx = offscreen.getContext('2d')!
102
- ctx.drawImage(gl.domElement, sx, sy, minDim, minDim, 0, 0, THUMBNAIL_SIZE, THUMBNAIL_SIZE)
103
-
104
- offscreen.toBlob(async (blob) => {
105
- if (!blob) {
106
- console.error('❌ PresetThumbnail: failed to create blob')
107
- return
108
- }
109
- if (!adapter.uploadPresetThumbnail) return
110
- const thumbnailUrl = await adapter.uploadPresetThumbnail(presetId, blob)
111
- if (thumbnailUrl) {
112
- emitter.emit('preset:thumbnail-updated', { presetId, thumbnailUrl })
113
- }
114
- }, 'image/png')
115
- },
116
- [gl, scene, adapter],
117
- )
118
-
119
- useEffect(() => {
120
- emitter.on('preset:generate-thumbnail', generate)
121
- return () => emitter.off('preset:generate-thumbnail', generate)
122
- }, [generate])
123
-
124
- return null
125
- }
@@ -1,113 +0,0 @@
1
- 'use client'
2
-
3
- import type { Point2D, RoofNode, RoofSegmentNode } from '@pascal-app/core'
4
- import { memo } from 'react'
5
- import { toSvgX, toSvgY } from '../svg-paths'
6
-
7
- type FloorplanLineSegment = {
8
- start: Point2D
9
- end: Point2D
10
- }
11
-
12
- type FloorplanRoofSegmentEntry = {
13
- segment: RoofSegmentNode
14
- points: string
15
- ridgeLine: FloorplanLineSegment | null
16
- }
17
-
18
- type FloorplanRoofEntry = {
19
- roof: RoofNode
20
- segments: FloorplanRoofSegmentEntry[]
21
- }
22
-
23
- type FloorplanRoofPalette = {
24
- roofFill: string
25
- roofActiveFill: string
26
- roofSelectedFill: string
27
- roofStroke: string
28
- roofActiveStroke: string
29
- roofSelectedStroke: string
30
- roofRidgeStroke: string
31
- roofSelectedRidgeStroke: string
32
- }
33
-
34
- type FloorplanRoofLayerProps = {
35
- highlightedIdSet: ReadonlySet<string>
36
- palette: FloorplanRoofPalette
37
- roofEntries: FloorplanRoofEntry[]
38
- selectedIdSet: ReadonlySet<string>
39
- }
40
-
41
- export const FloorplanRoofLayer = memo(function FloorplanRoofLayer({
42
- highlightedIdSet,
43
- palette,
44
- roofEntries,
45
- selectedIdSet,
46
- }: FloorplanRoofLayerProps) {
47
- if (roofEntries.length === 0) {
48
- return null
49
- }
50
-
51
- return (
52
- <>
53
- {roofEntries.map(({ roof, segments }) => {
54
- const roofSelected = selectedIdSet.has(roof.id)
55
- const roofHighlighted = highlightedIdSet.has(roof.id)
56
- const hasSelectedSegment = segments.some(({ segment }) => selectedIdSet.has(segment.id))
57
- const hasHighlightedSegment = segments.some(({ segment }) =>
58
- highlightedIdSet.has(segment.id),
59
- )
60
- const isRoofActive =
61
- roofSelected || roofHighlighted || hasSelectedSegment || hasHighlightedSegment
62
-
63
- return (
64
- <g key={roof.id} pointerEvents="none">
65
- {segments.map(({ points, ridgeLine, segment }) => {
66
- const isSegmentSelected = selectedIdSet.has(segment.id)
67
- const isSegmentHighlighted = highlightedIdSet.has(segment.id)
68
- const isSegmentActive = isSegmentSelected || isSegmentHighlighted
69
-
70
- return (
71
- <g key={segment.id}>
72
- <polygon
73
- fill={
74
- isSegmentActive
75
- ? palette.roofSelectedFill
76
- : isRoofActive
77
- ? palette.roofActiveFill
78
- : palette.roofFill
79
- }
80
- points={points}
81
- stroke={
82
- isSegmentActive
83
- ? palette.roofSelectedStroke
84
- : isRoofActive
85
- ? palette.roofActiveStroke
86
- : palette.roofStroke
87
- }
88
- strokeWidth={isSegmentActive ? '2.25' : isRoofActive ? '1.75' : '1.1'}
89
- vectorEffect="non-scaling-stroke"
90
- />
91
- {ridgeLine ? (
92
- <line
93
- fill="none"
94
- stroke={
95
- isSegmentActive ? palette.roofSelectedRidgeStroke : palette.roofRidgeStroke
96
- }
97
- strokeWidth={isSegmentActive ? '2' : '1.4'}
98
- vectorEffect="non-scaling-stroke"
99
- x1={toSvgX(ridgeLine.start.x)}
100
- x2={toSvgX(ridgeLine.end.x)}
101
- y1={toSvgY(ridgeLine.start.y)}
102
- y2={toSvgY(ridgeLine.end.y)}
103
- />
104
- ) : null}
105
- </g>
106
- )
107
- })}
108
- </g>
109
- )
110
- })}
111
- </>
112
- )
113
- })
@@ -1,157 +0,0 @@
1
- 'use client'
2
-
3
- import {
4
- type BuildingNode,
5
- emitter,
6
- type GridEvent,
7
- sceneRegistry,
8
- useScene,
9
- } from '@pascal-app/core'
10
- import { useViewer } from '@pascal-app/viewer'
11
- import { useFrame } from '@react-three/fiber'
12
- import { useCallback, useEffect, useRef, useState } from 'react'
13
- import * as THREE from 'three'
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
- export function MoveBuildingContent({ node }: { node: BuildingNode }) {
20
- const previousGridPosRef = useRef<[number, number] | null>(null)
21
-
22
- // Stable refs so the effect never needs node in its dependency array
23
- const nodeIdRef = useRef(node.id)
24
- const originalPositionRef = useRef<[number, number, number]>([...node.position] as [
25
- number,
26
- number,
27
- number,
28
- ])
29
- const originalRotationRef = useRef<number>(node.rotation[1] ?? 0)
30
- const pendingRotationRef = useRef<number>(node.rotation[1] ?? 0)
31
-
32
- const [cursorWorldPos, setCursorWorldPos] = useState<[number, number, number]>(() => {
33
- const obj = sceneRegistry.nodes.get(node.id)
34
- if (obj) {
35
- const pos = new THREE.Vector3()
36
- obj.getWorldPosition(pos)
37
- return [pos.x, pos.y, pos.z]
38
- }
39
- return [node.position[0], node.position[1], node.position[2]]
40
- })
41
-
42
- const exitMoveMode = useCallback(() => {
43
- useEditor.getState().setMovingNode(null)
44
- }, [])
45
-
46
- useEffect(() => {
47
- const nodeId = nodeIdRef.current
48
- const originalPosition = originalPositionRef.current
49
-
50
- useScene.temporal.getState().pause()
51
-
52
- let wasCommitted = false
53
-
54
- const onKeyDown = (event: KeyboardEvent) => {
55
- if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
56
- return
57
- }
58
-
59
- const ROTATION_STEP = Math.PI / 2
60
- let rotationDelta = 0
61
- if (event.key === 'r' || event.key === 'R') rotationDelta = ROTATION_STEP
62
- else if (event.key === 't' || event.key === 'T') rotationDelta = -ROTATION_STEP
63
-
64
- if (rotationDelta !== 0) {
65
- event.preventDefault()
66
- sfxEmitter.emit('sfx:item-rotate')
67
- pendingRotationRef.current += rotationDelta
68
-
69
- const mesh = sceneRegistry.nodes.get(nodeId)
70
- if (mesh) mesh.rotation.y = pendingRotationRef.current
71
- }
72
- }
73
-
74
- const onGridMove = (event: GridEvent) => {
75
- const gridX = Math.round(event.position[0] * 2) / 2
76
- const gridZ = Math.round(event.position[2] * 2) / 2
77
-
78
- if (
79
- previousGridPosRef.current &&
80
- (gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])
81
- ) {
82
- sfxEmitter.emit('sfx:grid-snap')
83
- }
84
-
85
- previousGridPosRef.current = [gridX, gridZ]
86
- setCursorWorldPos([gridX, 0, gridZ])
87
-
88
- // Directly update the Three.js group — no store update during drag
89
- const mesh = sceneRegistry.nodes.get(nodeId)
90
- if (mesh) {
91
- mesh.position.x = gridX
92
- mesh.position.z = gridZ
93
- }
94
- }
95
-
96
- const onGridClick = (event: GridEvent) => {
97
- const gridX = Math.round(event.position[0] * 2) / 2
98
- const gridZ = Math.round(event.position[2] * 2) / 2
99
-
100
- wasCommitted = true
101
-
102
- useScene.temporal.getState().resume()
103
- useScene.getState().updateNode(nodeId, {
104
- position: [gridX, originalPosition[1], gridZ],
105
- rotation: [0, pendingRotationRef.current, 0],
106
- })
107
- useScene.temporal.getState().pause()
108
-
109
- sfxEmitter.emit('sfx:item-place')
110
- useViewer.getState().setSelection({ buildingId: nodeId as BuildingNode['id'] })
111
- exitMoveMode()
112
- event.nativeEvent?.stopPropagation?.()
113
- }
114
-
115
- const onCancel = () => {
116
- // Revert mesh position and rotation immediately
117
- const mesh = sceneRegistry.nodes.get(nodeId)
118
- if (mesh) {
119
- mesh.position.x = originalPosition[0]
120
- mesh.position.z = originalPosition[2]
121
- mesh.rotation.y = originalRotationRef.current
122
- }
123
- pendingRotationRef.current = originalRotationRef.current
124
- // Restore building selection
125
- useViewer.getState().setSelection({ buildingId: nodeId as BuildingNode['id'] })
126
- useScene.temporal.getState().resume()
127
- // Tell the keyboard handler we handled this, so it doesn't also clear the selection
128
- markToolCancelConsumed()
129
- exitMoveMode()
130
- }
131
-
132
- emitter.on('grid:move', onGridMove)
133
- emitter.on('grid:click', onGridClick)
134
- emitter.on('tool:cancel', onCancel)
135
- window.addEventListener('keydown', onKeyDown)
136
-
137
- return () => {
138
- if (!wasCommitted) {
139
- useScene.getState().updateNode(nodeId, {
140
- position: originalPosition,
141
- rotation: [0, originalRotationRef.current, 0],
142
- })
143
- }
144
- useScene.temporal.getState().resume()
145
- emitter.off('grid:move', onGridMove)
146
- emitter.off('grid:click', onGridClick)
147
- emitter.off('tool:cancel', onCancel)
148
- window.removeEventListener('keydown', onKeyDown)
149
- }
150
- }, [exitMoveMode]) // stable — node values captured via refs at mount
151
-
152
- return (
153
- <group>
154
- <CursorSphere position={cursorWorldPos} showTooltip={false} />
155
- </group>
156
- )
157
- }
@@ -1,43 +0,0 @@
1
- import { type CeilingNode, resolveLevelId, useScene } from '@pascal-app/core'
2
- import { useViewer } from '@pascal-app/viewer'
3
- import { useCallback } from 'react'
4
- import { PolygonEditor } from '../shared/polygon-editor'
5
-
6
- interface CeilingBoundaryEditorProps {
7
- ceilingId: CeilingNode['id']
8
- }
9
-
10
- /**
11
- * Ceiling boundary editor - allows editing ceiling polygon vertices for a specific ceiling
12
- * Uses the generic PolygonEditor component
13
- */
14
- export const CeilingBoundaryEditor: React.FC<CeilingBoundaryEditorProps> = ({ ceilingId }) => {
15
- const ceilingNode = useScene((state) => state.nodes[ceilingId])
16
- const updateNode = useScene((state) => state.updateNode)
17
- const setSelection = useViewer((state) => state.setSelection)
18
-
19
- const ceiling = ceilingNode?.type === 'ceiling' ? (ceilingNode as CeilingNode) : null
20
-
21
- const handlePolygonChange = useCallback(
22
- (newPolygon: Array<[number, number]>) => {
23
- updateNode(ceilingId, { polygon: newPolygon })
24
- // Re-assert selection so the ceiling stays selected after the edit
25
- setSelection({ selectedIds: [ceilingId] })
26
- },
27
- [ceilingId, updateNode, setSelection],
28
- )
29
-
30
- if (!ceiling?.polygon || ceiling.polygon.length < 3) return null
31
-
32
- return (
33
- <PolygonEditor
34
- allowEdgeMove
35
- color="#d4d4d4"
36
- levelId={resolveLevelId(ceiling, useScene.getState().nodes)}
37
- minVertices={3}
38
- onPolygonChange={handlePolygonChange}
39
- polygon={ceiling.polygon}
40
- surfaceHeight={ceiling.height ?? 2.5}
41
- />
42
- )
43
- }
@@ -1,49 +0,0 @@
1
- import { type CeilingNode, resolveLevelId, useScene } from '@pascal-app/core'
2
- import { useViewer } from '@pascal-app/viewer'
3
- import { useCallback } from 'react'
4
- import { PolygonEditor } from '../shared/polygon-editor'
5
-
6
- interface CeilingHoleEditorProps {
7
- ceilingId: CeilingNode['id']
8
- holeIndex: number
9
- }
10
-
11
- /**
12
- * Ceiling hole editor - allows editing a specific hole polygon within a ceiling
13
- * Uses the generic PolygonEditor component
14
- */
15
- export const CeilingHoleEditor: React.FC<CeilingHoleEditorProps> = ({ ceilingId, holeIndex }) => {
16
- const ceilingNode = useScene((state) => state.nodes[ceilingId])
17
- const updateNode = useScene((state) => state.updateNode)
18
- const setSelection = useViewer((state) => state.setSelection)
19
-
20
- const ceiling = ceilingNode?.type === 'ceiling' ? (ceilingNode as CeilingNode) : null
21
- const holes = ceiling?.holes || []
22
- const hole = holes[holeIndex]
23
-
24
- const handlePolygonChange = useCallback(
25
- (newPolygon: Array<[number, number]>) => {
26
- const updatedHoles = [...holes]
27
- updatedHoles[holeIndex] = newPolygon
28
- updateNode(ceilingId, { holes: updatedHoles })
29
- // Re-assert selection so the ceiling stays selected after the edit
30
- setSelection({ selectedIds: [ceilingId] })
31
- },
32
- [ceilingId, holeIndex, holes, updateNode, setSelection],
33
- )
34
-
35
- if (!(ceiling && hole) || hole.length < 3) return null
36
-
37
- return (
38
- <PolygonEditor
39
- allowEdgeMove
40
- allowPolygonMove
41
- color="#ef4444"
42
- levelId={resolveLevelId(ceiling, useScene.getState().nodes)} // red for holes
43
- minVertices={3}
44
- onPolygonChange={handlePolygonChange}
45
- polygon={hole}
46
- surfaceHeight={ceiling.height ?? 2.5}
47
- />
48
- )
49
- }