@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
@@ -0,0 +1,31 @@
1
+ // Ephemeral store for a placement tool's 2D floor-plan ghost. A registry
2
+ // placement tool (e.g. column / elevator) publishes a fully-positioned,
3
+ // transient preview node on each `grid:move`; the floor-plan
4
+ // placement-preview layer subscribes and renders the node's `def.floorplan`
5
+ // footprint as a faint ghost that follows the cursor. The 3D view already
6
+ // shows a translucent mesh preview, so this only feeds the 2D layer.
7
+ //
8
+ // Editor-only: the read-only viewer route never places nodes. Lives here
9
+ // rather than in `core` for that reason; node-kind tools (e.g. column) reach
10
+ // it through the `@pascal-app/editor` public surface, the same way they
11
+ // already consume `triggerSFX`. Producers clear on commit, cancel, and
12
+ // unmount.
13
+
14
+ import type { AnyNode } from '@pascal-app/core'
15
+ import { create } from 'zustand'
16
+
17
+ type PlacementPreviewState = {
18
+ /** Transient preview node, already positioned + rotated at the (snapped,
19
+ * aligned) cursor. `null` when no placement is active. */
20
+ node: AnyNode | null
21
+ set(node: AnyNode | null): void
22
+ clear(): void
23
+ }
24
+
25
+ const usePlacementPreview = create<PlacementPreviewState>((set) => ({
26
+ node: null,
27
+ set: (node) => set({ node }),
28
+ clear: () => set({ node: null }),
29
+ }))
30
+
31
+ export default usePlacementPreview
@@ -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
package/tsconfig.json CHANGED
@@ -2,7 +2,8 @@
2
2
  "extends": "@pascal/typescript-config/react-library.json",
3
3
  "compilerOptions": {
4
4
  "rootDir": "src",
5
- "noEmit": true
5
+ "noEmit": true,
6
+ "types": ["bun"]
6
7
  },
7
8
  "include": ["src"],
8
9
  "exclude": ["node_modules"]
@@ -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,265 +0,0 @@
1
- 'use client'
2
-
3
- import { useScene } from '@pascal-app/core'
4
- import { ImageIcon, MessageSquare, X } from 'lucide-react'
5
- import { useCallback, useRef, useState } from 'react'
6
- import { Button } from './ui/primitives/button'
7
- import {
8
- Dialog,
9
- DialogContent,
10
- DialogDescription,
11
- DialogHeader,
12
- DialogTitle,
13
- } from './ui/primitives/dialog'
14
-
15
- const MAX_IMAGES = 5
16
- const MAX_IMAGE_SIZE = 5 * 1024 * 1024
17
-
18
- type ImagePreview = { file: File; url: string }
19
-
20
- export function FeedbackDialog({
21
- projectId: projectIdProp,
22
- onSubmit,
23
- }: {
24
- projectId?: string
25
- onSubmit?: (data: {
26
- message: string
27
- projectId?: string
28
- sceneGraph: unknown
29
- images: File[]
30
- }) => Promise<{ success: boolean; error?: string }>
31
- }) {
32
- const projectId = projectIdProp
33
-
34
- const [open, setOpen] = useState(false)
35
- const [message, setMessage] = useState('')
36
- const [images, setImages] = useState<ImagePreview[]>([])
37
- const [isDragging, setIsDragging] = useState(false)
38
- const [isSubmitting, setIsSubmitting] = useState(false)
39
- const [error, setError] = useState<string | null>(null)
40
- const [sent, setSent] = useState(false)
41
- const fileInputRef = useRef<HTMLInputElement>(null)
42
- const dragCounter = useRef(0)
43
-
44
- const handleOpen = () => {
45
- setOpen(true)
46
- setSent(false)
47
- setError(null)
48
- setMessage('')
49
- setImages([])
50
- setIsDragging(false)
51
- dragCounter.current = 0
52
- }
53
-
54
- const handleClose = () => {
55
- if (isSubmitting) return
56
- setOpen(false)
57
- images.forEach((img) => {
58
- URL.revokeObjectURL(img.url)
59
- })
60
- }
61
-
62
- const addFiles = useCallback((files: FileList | File[]) => {
63
- const incoming = Array.from(files).filter(
64
- (f) => f.type.startsWith('image/') && f.size <= MAX_IMAGE_SIZE,
65
- )
66
- setImages((prev) => {
67
- const remaining = MAX_IMAGES - prev.length
68
- const added = incoming.slice(0, remaining).map((file) => ({
69
- file,
70
- url: URL.createObjectURL(file),
71
- }))
72
- return [...prev, ...added]
73
- })
74
- }, [])
75
-
76
- const removeImage = (index: number) => {
77
- setImages((prev) => {
78
- const img = prev[index]
79
- if (img) URL.revokeObjectURL(img.url)
80
- return prev.filter((_, i) => i !== index)
81
- })
82
- }
83
-
84
- // ── Drag handlers (on the entire dialog content) ──
85
- const onDragEnter = (e: React.DragEvent) => {
86
- e.preventDefault()
87
- e.stopPropagation()
88
- dragCounter.current++
89
- if (e.dataTransfer.types.includes('Files')) {
90
- setIsDragging(true)
91
- }
92
- }
93
-
94
- const onDragLeave = (e: React.DragEvent) => {
95
- e.preventDefault()
96
- e.stopPropagation()
97
- dragCounter.current--
98
- if (dragCounter.current === 0) {
99
- setIsDragging(false)
100
- }
101
- }
102
-
103
- const onDragOver = (e: React.DragEvent) => {
104
- e.preventDefault()
105
- e.stopPropagation()
106
- }
107
-
108
- const onDrop = (e: React.DragEvent) => {
109
- e.preventDefault()
110
- e.stopPropagation()
111
- dragCounter.current = 0
112
- setIsDragging(false)
113
- if (e.dataTransfer.files.length > 0) {
114
- addFiles(e.dataTransfer.files)
115
- }
116
- }
117
-
118
- const handleSubmit = async (e: React.FormEvent) => {
119
- e.preventDefault()
120
- setError(null)
121
- setIsSubmitting(true)
122
-
123
- try {
124
- if (!onSubmit) return
125
- const { nodes, rootNodeIds } = useScene.getState()
126
- const sceneGraph = { nodes, rootNodeIds }
127
- const result = await onSubmit({
128
- message,
129
- projectId,
130
- sceneGraph,
131
- images: images.map((img) => img.file),
132
- })
133
- if (result.success) {
134
- setSent(true)
135
- setTimeout(() => setOpen(false), 1500)
136
- } else {
137
- setError(result.error ?? 'Something went wrong')
138
- }
139
- } finally {
140
- setIsSubmitting(false)
141
- }
142
- }
143
-
144
- return (
145
- <>
146
- <button
147
- className="flex items-center gap-2 rounded-lg border border-border bg-background/95 px-3 py-2 font-medium text-sm shadow-lg backdrop-blur-md transition-colors hover:bg-accent/90"
148
- onClick={handleOpen}
149
- >
150
- <MessageSquare className="h-4 w-4" />
151
- Feedback
152
- </button>
153
-
154
- <Dialog onOpenChange={handleClose} open={open}>
155
- <DialogContent
156
- className="sm:max-w-[460px]"
157
- onDragEnter={onDragEnter}
158
- onDragLeave={onDragLeave}
159
- onDragOver={onDragOver}
160
- onDrop={onDrop}
161
- >
162
- {/* Drag overlay — only visible when dragging files over the dialog */}
163
- {isDragging && (
164
- <div className="absolute inset-0 z-50 flex items-center justify-center rounded-lg border-2 border-primary/50 border-dashed bg-primary/5 backdrop-blur-sm transition-all">
165
- <div className="flex flex-col items-center gap-2 text-primary/70">
166
- <ImageIcon className="h-8 w-8" />
167
- <p className="font-medium text-sm">Drop images here</p>
168
- </div>
169
- </div>
170
- )}
171
-
172
- <DialogHeader>
173
- <DialogTitle>Send Feedback</DialogTitle>
174
- <DialogDescription>We&apos;d love to hear your thoughts</DialogDescription>
175
- </DialogHeader>
176
-
177
- {sent ? (
178
- <p className="py-4 text-center text-muted-foreground text-sm">
179
- Thanks for your feedback!
180
- </p>
181
- ) : (
182
- <form className="space-y-4" onSubmit={handleSubmit}>
183
- <div>
184
- <label className="font-medium text-sm" htmlFor="feedback-message">
185
- Your feedback
186
- </label>
187
- <textarea
188
- autoFocus
189
- className="mt-1 w-full resize-none rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
190
- disabled={isSubmitting}
191
- id="feedback-message"
192
- onChange={(e) => setMessage(e.target.value)}
193
- placeholder="Share your thoughts, suggestions, feature requests, or report issues..."
194
- rows={5}
195
- value={message}
196
- />
197
- </div>
198
-
199
- {/* Image thumbnails */}
200
- {images.length > 0 && (
201
- <div className="flex flex-wrap gap-2">
202
- {images.map((img, i) => (
203
- <div
204
- className="group relative h-14 w-14 overflow-hidden rounded-md border border-border"
205
- key={img.url}
206
- >
207
- <img alt="" className="h-full w-full object-cover" src={img.url} />
208
- <button
209
- className="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 transition-opacity group-hover:opacity-100"
210
- onClick={() => removeImage(i)}
211
- type="button"
212
- >
213
- <X className="h-4 w-4 text-white" />
214
- </button>
215
- </div>
216
- ))}
217
- </div>
218
- )}
219
-
220
- {error && <p className="text-destructive text-sm">{error}</p>}
221
-
222
- <div className="flex items-center justify-between">
223
- {/* Subtle attach button */}
224
- <button
225
- className="flex items-center gap-1.5 text-muted-foreground text-xs transition-colors hover:text-foreground disabled:opacity-40"
226
- disabled={isSubmitting || images.length >= MAX_IMAGES}
227
- onClick={() => fileInputRef.current?.click()}
228
- type="button"
229
- >
230
- <ImageIcon className="h-3.5 w-3.5" />
231
- {images.length > 0 ? `${images.length}/${MAX_IMAGES}` : 'Attach'}
232
- </button>
233
- <input
234
- accept="image/*"
235
- className="hidden"
236
- multiple
237
- onChange={(e) => {
238
- if (e.target.files) addFiles(e.target.files)
239
- e.target.value = ''
240
- }}
241
- ref={fileInputRef}
242
- type="file"
243
- />
244
-
245
- <div className="flex gap-2">
246
- <Button
247
- disabled={isSubmitting}
248
- onClick={handleClose}
249
- type="button"
250
- variant="outline"
251
- >
252
- Cancel
253
- </Button>
254
- <Button disabled={isSubmitting || !message.trim() || !onSubmit} type="submit">
255
- {isSubmitting ? 'Sending...' : 'Send Feedback'}
256
- </Button>
257
- </div>
258
- </div>
259
- </form>
260
- )}
261
- </DialogContent>
262
- </Dialog>
263
- </>
264
- )
265
- }