@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
@@ -15,6 +15,13 @@ export type SegmentAngleReference = {
15
15
  orientation: 'directed' | 'axis'
16
16
  }
17
17
 
18
+ export type SegmentAngleArc = {
19
+ angle: number
20
+ startAngle: number
21
+ endAngle: number
22
+ midAngle: number
23
+ }
24
+
18
25
  const POINT_MATCH_TOLERANCE = 1e-5
19
26
  const SEGMENT_POINT_TOLERANCE = 0.15
20
27
  const CURVE_TANGENT_SAMPLE_SPACING = 0.08
@@ -92,6 +99,36 @@ export function getAngleBetweenVectors(first: PlanPoint, second: PlanPoint): num
92
99
  return Math.acos(cosine)
93
100
  }
94
101
 
102
+ function normalizeSignedAngle(angle: number) {
103
+ let nextAngle = angle
104
+
105
+ while (nextAngle <= -Math.PI) {
106
+ nextAngle += Math.PI * 2
107
+ }
108
+
109
+ while (nextAngle > Math.PI) {
110
+ nextAngle -= Math.PI * 2
111
+ }
112
+
113
+ return nextAngle
114
+ }
115
+
116
+ function getSignedAngleArc(vector: PlanPoint, referenceVector: PlanPoint): SegmentAngleArc | null {
117
+ const angle = getAngleBetweenVectors(vector, referenceVector)
118
+ if (angle === null) return null
119
+
120
+ const startAngle = Math.atan2(referenceVector[1], referenceVector[0])
121
+ const vectorAngle = Math.atan2(vector[1], vector[0])
122
+ const signedDelta = normalizeSignedAngle(vectorAngle - startAngle)
123
+
124
+ return {
125
+ angle: Math.abs(signedDelta),
126
+ startAngle,
127
+ endAngle: startAngle + signedDelta,
128
+ midAngle: startAngle + signedDelta / 2,
129
+ }
130
+ }
131
+
95
132
  export function getAngleToSegmentReference(
96
133
  vector: PlanPoint,
97
134
  reference: SegmentAngleReference,
@@ -111,6 +148,25 @@ export function getAngleToSegmentReference(
111
148
  return Math.min(angle, reverseAngle)
112
149
  }
113
150
 
151
+ export function getAngleArcToSegmentReference(
152
+ vector: PlanPoint,
153
+ reference: SegmentAngleReference,
154
+ ): SegmentAngleArc | null {
155
+ const directArc = getSignedAngleArc(vector, reference.vector)
156
+
157
+ if (!directArc || reference.orientation === 'directed') {
158
+ return directArc
159
+ }
160
+
161
+ const reverseArc = getSignedAngleArc(vector, [-reference.vector[0], -reference.vector[1]])
162
+
163
+ if (!reverseArc) {
164
+ return directArc
165
+ }
166
+
167
+ return reverseArc.angle < directArc.angle ? reverseArc : directArc
168
+ }
169
+
114
170
  export function getSegmentAngleReferenceAtPoint(
115
171
  point: PlanPoint,
116
172
  segment: SegmentAngleLike,
@@ -145,7 +201,7 @@ export function getSegmentAngleReferenceAtPoint(
145
201
  }
146
202
 
147
203
  const projected = getProjectedPointOnSegment(point, segment)
148
- if (!projected || !pointsMatch(point, projected, SEGMENT_POINT_TOLERANCE)) {
204
+ if (!(projected && pointsMatch(point, projected, SEGMENT_POINT_TOLERANCE))) {
149
205
  return null
150
206
  }
151
207
 
@@ -1,19 +1,319 @@
1
- import { type SiteNode, useScene } from '@pascal-app/core'
2
- import { useCallback } from 'react'
3
- import { PolygonEditor } from '../shared/polygon-editor'
4
-
5
- /**
6
- * Site boundary editor - allows editing site polygon when in site phase
7
- * Uses the generic PolygonEditor component
8
- */
1
+ import { emitter, type SiteNode, useLiveNodeOverrides, useScene } from '@pascal-app/core'
2
+ import { SCENE_LAYER } from '@pascal-app/viewer'
3
+ import { useGLTF } from '@react-three/drei/core/Gltf'
4
+ import { useFrame } from '@react-three/fiber'
5
+ import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react'
6
+ import { Color, CylinderGeometry, DoubleSide, type Mesh, type Object3D, RingGeometry } from 'three'
7
+ import { MeshBasicNodeMaterial } from 'three/webgpu'
8
+ import { EDITOR_LAYER } from '../../../lib/constants'
9
+ import { sfxEmitter } from '../../../lib/sfx-bus'
10
+ import { SITE_BOUNDARY_DRAG_LABEL } from '../../../lib/site-boundary'
11
+ import useEditor, { selectSiteFloorplanContext } from '../../../store/use-editor'
12
+ import {
13
+ ARROW_COLOR,
14
+ ARROW_HOVER_COLOR,
15
+ NO_RAYCAST,
16
+ useInvisibleHitAreaMaterial,
17
+ } from '../../editor/node-arrow-handles'
18
+ import {
19
+ PolygonEditor,
20
+ type PolygonHandleHandlers,
21
+ type PolygonMidpointHandleRenderProps,
22
+ type PolygonVertexHandleRenderProps,
23
+ } from '../shared/polygon-editor'
24
+ import { SITE_FLAG_MODEL_URL } from './site-flag-model'
25
+
26
+ const SITE_FLAG_BASE_Y = 0
27
+ const SITE_FLAG_HIT_HEIGHT = 0.5
28
+ const SITE_FLAG_HIT_RADIUS = 0.24
29
+ const SITE_FLAG_HIT_Y = SITE_FLAG_BASE_Y + SITE_FLAG_HIT_HEIGHT / 2
30
+ const SITE_FLAG_MODEL_MIN_Y = -0.034798760316334665
31
+ const SITE_FLAG_SCALE = 0.5
32
+ const SITE_FLAG_ACTIVE_LIFT = 0.08
33
+ const SITE_FLAG_MODEL_Y = SITE_FLAG_BASE_Y - SITE_FLAG_MODEL_MIN_Y * SITE_FLAG_SCALE
34
+ const SITE_FLAG_HALO_INNER_RADIUS = 0.08
35
+ const SITE_FLAG_HALO_OUTER_RADIUS = SITE_FLAG_HIT_RADIUS * 0.95
36
+ const SITE_FLAG_HALO_Y = SITE_FLAG_BASE_Y + 0.002
37
+ const SITE_FLAG_HALO_COLOR = '#6366f1'
38
+
39
+ type TintableMaterial = {
40
+ color?: Color
41
+ depthWrite: boolean
42
+ opacity: number
43
+ needsUpdate: boolean
44
+ transparent: boolean
45
+ }
46
+
47
+ function SiteFlagModel({
48
+ active,
49
+ hovered,
50
+ opacity = 1,
51
+ scale = SITE_FLAG_SCALE,
52
+ }: {
53
+ active: boolean
54
+ hovered: boolean
55
+ opacity?: number
56
+ scale?: number
57
+ }) {
58
+ const { scene } = useGLTF(SITE_FLAG_MODEL_URL, true)
59
+ const modelRef = useRef<Object3D>(null)
60
+ const flagScene = useMemo(() => {
61
+ const cloned = scene.clone(true)
62
+
63
+ cloned.traverse((object) => {
64
+ object.layers.set(SCENE_LAYER)
65
+
66
+ if ((object as Mesh).isMesh) {
67
+ const mesh = object as Mesh
68
+ mesh.castShadow = false
69
+ mesh.frustumCulled = false
70
+ mesh.raycast = NO_RAYCAST
71
+ mesh.receiveShadow = false
72
+ mesh.renderOrder = 1010
73
+ mesh.material = new MeshBasicNodeMaterial({
74
+ color: new Color(ARROW_COLOR),
75
+ depthTest: false,
76
+ depthWrite: opacity >= 0.999,
77
+ opacity,
78
+ transparent: opacity < 0.999,
79
+ })
80
+ }
81
+ })
82
+
83
+ return cloned
84
+ }, [opacity, scene])
85
+
86
+ useEffect(() => {
87
+ const color = new Color(active || hovered ? ARROW_HOVER_COLOR : ARROW_COLOR)
88
+
89
+ flagScene.traverse((object) => {
90
+ if ((object as Mesh).isMesh) {
91
+ const mesh = object as Mesh
92
+ const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
93
+
94
+ for (const material of materials as Array<MeshBasicNodeMaterial & TintableMaterial>) {
95
+ material.color?.copy(color)
96
+ material.opacity = opacity
97
+ material.transparent = opacity < 0.999
98
+ material.depthWrite = opacity >= 0.999
99
+ material.needsUpdate = true
100
+ }
101
+ }
102
+ })
103
+ }, [active, flagScene, hovered, opacity])
104
+
105
+ useFrame((_, delta) => {
106
+ const model = modelRef.current
107
+ if (!model) return
108
+
109
+ const smoothing = 1 - Math.exp(-delta * 18)
110
+ const targetY = SITE_FLAG_MODEL_Y + (active ? SITE_FLAG_ACTIVE_LIFT : 0)
111
+ model.position.y += (targetY - model.position.y) * smoothing
112
+ })
113
+
114
+ useEffect(
115
+ () => () => {
116
+ flagScene.traverse((object) => {
117
+ if ((object as Mesh).isMesh) {
118
+ const materials = (object as Mesh).material
119
+ if (Array.isArray(materials)) {
120
+ materials.forEach((material) => {
121
+ material.dispose()
122
+ })
123
+ } else {
124
+ materials.dispose()
125
+ }
126
+ }
127
+ })
128
+ },
129
+ [flagScene],
130
+ )
131
+
132
+ return (
133
+ <primitive
134
+ object={flagScene}
135
+ position={[0, SITE_FLAG_MODEL_Y, 0]}
136
+ ref={modelRef}
137
+ scale={scale}
138
+ />
139
+ )
140
+ }
141
+
142
+ function SiteFlagHoverHalo({ visible }: { visible: boolean }) {
143
+ const haloRef = useRef<Mesh>(null)
144
+ const geometry = useMemo(() => {
145
+ const nextGeometry = new RingGeometry(
146
+ SITE_FLAG_HALO_INNER_RADIUS,
147
+ SITE_FLAG_HALO_OUTER_RADIUS,
148
+ 56,
149
+ )
150
+ nextGeometry.rotateX(-Math.PI / 2)
151
+ return nextGeometry
152
+ }, [])
153
+ const material = useMemo(
154
+ () =>
155
+ new MeshBasicNodeMaterial({
156
+ color: new Color(SITE_FLAG_HALO_COLOR),
157
+ depthTest: true,
158
+ depthWrite: false,
159
+ opacity: 0,
160
+ side: DoubleSide,
161
+ transparent: true,
162
+ }),
163
+ [],
164
+ )
165
+
166
+ useFrame((_, delta) => {
167
+ const mesh = haloRef.current
168
+ if (!mesh) return
169
+
170
+ const smoothing = 1 - Math.exp(-delta * 18)
171
+ const targetOpacity = visible ? 0.52 : 0
172
+ const targetScale = visible ? 1 : 0.08
173
+ const nextOpacity = material.opacity + (targetOpacity - material.opacity) * smoothing
174
+ const nextScale = mesh.scale.x + (targetScale - mesh.scale.x) * smoothing
175
+
176
+ material.opacity = nextOpacity
177
+ mesh.scale.setScalar(nextScale)
178
+ mesh.visible = visible || nextOpacity > 0.01
179
+ })
180
+
181
+ useEffect(
182
+ () => () => {
183
+ geometry.dispose()
184
+ material.dispose()
185
+ },
186
+ [geometry, material],
187
+ )
188
+
189
+ return (
190
+ <mesh
191
+ frustumCulled={false}
192
+ geometry={geometry}
193
+ layers={SCENE_LAYER}
194
+ material={material}
195
+ position={[0, SITE_FLAG_HALO_Y, 0]}
196
+ raycast={NO_RAYCAST}
197
+ ref={haloRef}
198
+ renderOrder={1009}
199
+ scale={0.08}
200
+ visible={false}
201
+ />
202
+ )
203
+ }
204
+
205
+ function SiteFlagFallback({
206
+ active,
207
+ hovered,
208
+ opacity = 1,
209
+ }: {
210
+ active: boolean
211
+ hovered: boolean
212
+ opacity?: number
213
+ }) {
214
+ const color = active || hovered ? ARROW_HOVER_COLOR : ARROW_COLOR
215
+
216
+ return (
217
+ <group position={[0, SITE_FLAG_BASE_Y + (active ? SITE_FLAG_ACTIVE_LIFT : 0), 0]}>
218
+ <mesh layers={SCENE_LAYER} position={[0, 0.16, 0]} raycast={NO_RAYCAST}>
219
+ <cylinderGeometry args={[0.11, 0.16, 0.32, 24]} />
220
+ <meshBasicMaterial color={color} depthTest={false} opacity={opacity} transparent />
221
+ </mesh>
222
+ <mesh layers={SCENE_LAYER} position={[0, 0.34, 0]} raycast={NO_RAYCAST}>
223
+ <cylinderGeometry args={[0.04, 0.11, 0.14, 24]} />
224
+ <meshBasicMaterial color={color} depthTest={false} opacity={opacity} transparent />
225
+ </mesh>
226
+ </group>
227
+ )
228
+ }
229
+
230
+ function SiteBoundaryFlagHandle({
231
+ active = false,
232
+ baseY,
233
+ handleProps,
234
+ hovered,
235
+ modelOpacity = 1,
236
+ modelScale = SITE_FLAG_SCALE,
237
+ modelVisible = true,
238
+ point,
239
+ }: {
240
+ active?: boolean
241
+ baseY: number
242
+ handleProps: PolygonHandleHandlers
243
+ hovered: boolean
244
+ modelOpacity?: number
245
+ modelScale?: number
246
+ modelVisible?: boolean
247
+ point: [number, number]
248
+ }) {
249
+ const hitGeometry = useMemo(
250
+ () =>
251
+ new CylinderGeometry(SITE_FLAG_HIT_RADIUS, SITE_FLAG_HIT_RADIUS, SITE_FLAG_HIT_HEIGHT, 24),
252
+ [],
253
+ )
254
+ const hitMaterial = useInvisibleHitAreaMaterial()
255
+
256
+ useEffect(() => () => hitGeometry.dispose(), [hitGeometry])
257
+
258
+ return (
259
+ <group position={[point[0], baseY, point[1]]}>
260
+ <SiteFlagHoverHalo visible={active || hovered} />
261
+ {(modelVisible || active || hovered) && (
262
+ <Suspense
263
+ fallback={<SiteFlagFallback active={active} hovered={hovered} opacity={modelOpacity} />}
264
+ >
265
+ <SiteFlagModel
266
+ active={active}
267
+ hovered={hovered}
268
+ opacity={modelOpacity}
269
+ scale={modelScale}
270
+ />
271
+ </Suspense>
272
+ )}
273
+ <mesh
274
+ frustumCulled={false}
275
+ geometry={hitGeometry}
276
+ layers={EDITOR_LAYER}
277
+ material={hitMaterial}
278
+ position={[0, SITE_FLAG_HIT_Y, 0]}
279
+ renderOrder={1012}
280
+ {...handleProps}
281
+ />
282
+ </group>
283
+ )
284
+ }
285
+
9
286
  export const SiteBoundaryEditor: React.FC = () => {
10
287
  const nodes = useScene((state) => state.nodes)
11
288
  const rootNodeIds = useScene((state) => state.rootNodeIds)
12
289
  const updateNode = useScene((state) => state.updateNode)
290
+ const phase = useEditor((state) => state.phase)
291
+ const mode = useEditor((state) => state.mode)
292
+ const [hoveredVertex, setHoveredVertex] = useState<number | null>(null)
293
+ const [hoveredMidpoint, setHoveredMidpoint] = useState<number | null>(null)
13
294
 
14
- // Get the site node (first root node)
15
295
  const siteNode = rootNodeIds[0] ? nodes[rootNodeIds[0]] : null
16
296
  const site = siteNode?.type === 'site' ? (siteNode as SiteNode) : null
297
+ const siteId = site?.id
298
+ const isSiteEditing = phase === 'site'
299
+ const showSiteHandles = isSiteEditing || mode === 'select'
300
+ const isSiteBoundaryHighlighted =
301
+ isSiteEditing || hoveredVertex !== null || hoveredMidpoint !== null
302
+ const [isDraggingSiteBoundary, setIsDraggingSiteBoundary] = useState(false)
303
+ const isDraggingSiteBoundaryRef = useRef(false)
304
+ const livePolygon = useLiveNodeOverrides(
305
+ useCallback(
306
+ (state) => {
307
+ if (!siteId) return null
308
+ return (state.overrides.get(siteId)?.polygon as SiteNode['polygon'] | undefined) ?? null
309
+ },
310
+ [siteId],
311
+ ),
312
+ )
313
+ const displayPolygon =
314
+ !(isDraggingSiteBoundary || isDraggingSiteBoundaryRef.current) && livePolygon?.points
315
+ ? livePolygon.points
316
+ : site?.polygon?.points
17
317
 
18
318
  const handlePolygonChange = useCallback(
19
319
  (newPolygon: Array<[number, number]>) => {
@@ -29,14 +329,166 @@ export const SiteBoundaryEditor: React.FC = () => {
29
329
  [site, updateNode],
30
330
  )
31
331
 
32
- if (!site?.polygon?.points || site.polygon.points.length < 3) return null
332
+ const handlePolygonPreview = useCallback(
333
+ (previewPolygon: ReadonlyArray<readonly [number, number]> | null) => {
334
+ if (!siteId) return
335
+
336
+ if (!previewPolygon) {
337
+ useLiveNodeOverrides.getState().clearFields(siteId, ['polygon'])
338
+ return
339
+ }
340
+
341
+ useLiveNodeOverrides.getState().set(siteId, {
342
+ polygon: {
343
+ type: 'polygon',
344
+ points: previewPolygon.map(([x, z]) => [x, z] as [number, number]),
345
+ },
346
+ })
347
+ },
348
+ [siteId],
349
+ )
350
+
351
+ const handleSiteBoundaryDragChange = useCallback(
352
+ (isDragging: boolean) => {
353
+ isDraggingSiteBoundaryRef.current = isDragging
354
+ setIsDraggingSiteBoundary(isDragging)
355
+
356
+ if (!siteId) return
357
+
358
+ const editor = useEditor.getState()
359
+ if (isDragging) {
360
+ editor.setActiveHandleDrag({ nodeId: siteId, label: SITE_BOUNDARY_DRAG_LABEL })
361
+ } else if (
362
+ editor.activeHandleDrag?.nodeId === siteId &&
363
+ editor.activeHandleDrag.label === SITE_BOUNDARY_DRAG_LABEL
364
+ ) {
365
+ editor.setActiveHandleDrag(null)
366
+ }
367
+
368
+ if (!isDragging) {
369
+ useLiveNodeOverrides.getState().clearFields(siteId, ['polygon'])
370
+ }
371
+ },
372
+ [siteId],
373
+ )
374
+
375
+ useEffect(
376
+ () => () => {
377
+ if (!siteId) return
378
+ const editor = useEditor.getState()
379
+ if (
380
+ editor.activeHandleDrag?.nodeId === siteId &&
381
+ editor.activeHandleDrag.label === SITE_BOUNDARY_DRAG_LABEL
382
+ ) {
383
+ editor.setActiveHandleDrag(null)
384
+ }
385
+ useLiveNodeOverrides.getState().clearFields(siteId, ['polygon'])
386
+ isDraggingSiteBoundaryRef.current = false
387
+ },
388
+ [siteId],
389
+ )
390
+
391
+ useEffect(() => {
392
+ if (isSiteEditing && siteId) {
393
+ selectSiteFloorplanContext()
394
+ }
395
+ }, [isSiteEditing, siteId])
396
+
397
+ const activateSiteEditing = useCallback(() => {
398
+ isDraggingSiteBoundaryRef.current = true
399
+ setIsDraggingSiteBoundary(true)
400
+ if (useEditor.getState().phase !== 'site') {
401
+ useEditor.setState({ catalogCategory: null, mode: 'select', phase: 'site', tool: null })
402
+ }
403
+ selectSiteFloorplanContext()
404
+ }, [])
405
+
406
+ const exitSiteEditing = useCallback(() => {
407
+ const editor = useEditor.getState()
408
+ editor.setPhase('structure')
409
+ editor.setStructureLayer('elements')
410
+ editor.setMode('select')
411
+ }, [])
412
+
413
+ useEffect(() => {
414
+ if (!isSiteEditing) return
415
+
416
+ const onGridClick = () => {
417
+ if (useEditor.getState().phase !== 'site') return
418
+ exitSiteEditing()
419
+ }
420
+
421
+ emitter.on('grid:click', onGridClick)
422
+ return () => {
423
+ emitter.off('grid:click', onGridClick)
424
+ }
425
+ }, [exitSiteEditing, isSiteEditing])
426
+
427
+ const renderSiteFlagVertex = useCallback(
428
+ ({
429
+ handleProps,
430
+ height,
431
+ isDragging,
432
+ isHovered,
433
+ point,
434
+ position,
435
+ }: PolygonVertexHandleRenderProps) => {
436
+ const baseY = position[1] - height / 2
437
+
438
+ return (
439
+ <SiteBoundaryFlagHandle
440
+ active={isDragging}
441
+ baseY={baseY}
442
+ handleProps={handleProps}
443
+ hovered={isHovered}
444
+ point={point}
445
+ />
446
+ )
447
+ },
448
+ [],
449
+ )
450
+ const renderSiteFlagMidpoint = useCallback(
451
+ ({ handleProps, height, isHovered, point, position }: PolygonMidpointHandleRenderProps) => {
452
+ const baseY = position[1] - height / 2
453
+
454
+ return (
455
+ <SiteBoundaryFlagHandle
456
+ baseY={baseY}
457
+ handleProps={handleProps}
458
+ hovered={isHovered}
459
+ modelOpacity={0.48}
460
+ modelScale={SITE_FLAG_SCALE * 0.92}
461
+ modelVisible={isHovered}
462
+ point={point}
463
+ />
464
+ )
465
+ },
466
+ [],
467
+ )
468
+
469
+ if (!displayPolygon || displayPolygon.length < 3) return null
470
+ if (!showSiteHandles) return null
33
471
 
34
472
  return (
35
473
  <PolygonEditor
36
- color="#10b981"
474
+ color={isSiteBoundaryHighlighted ? ARROW_COLOR : '#10b981'}
37
475
  minVertices={3}
476
+ onBeforeVertexDrag={activateSiteEditing}
477
+ onDragCommit={() => {
478
+ sfxEmitter.emit('sfx:item-place')
479
+ exitSiteEditing()
480
+ }}
481
+ onDragStart={() => sfxEmitter.emit('sfx:item-pick')}
482
+ onDragStateChange={handleSiteBoundaryDragChange}
483
+ onMidpointHoverChange={setHoveredMidpoint}
38
484
  onPolygonChange={handlePolygonChange}
39
- polygon={site.polygon.points}
485
+ onPolygonPreview={handlePolygonPreview}
486
+ onVertexHoverChange={setHoveredVertex}
487
+ polygon={displayPolygon}
488
+ renderMidpointHandle={renderSiteFlagMidpoint}
489
+ renderVertexHandle={renderSiteFlagVertex}
490
+ showBorderLine={isSiteBoundaryHighlighted}
491
+ showMidpointHandles={showSiteHandles}
40
492
  />
41
493
  )
42
494
  }
@@ -0,0 +1,18 @@
1
+ const SITE_FLAG_MODEL_BASE64 = [
2
+ 'Z2xURgIAAADgAwAAAAMAAEpTT057ImFzc2V0Ijp7ImdlbmVyYXRvciI6ImdsVEYtVHJhbnNmb3JtIHY0LjMuMCIsInZlcnNpb24i',
3
+ 'OiIyLjAifSwiYWNjZXNzb3JzIjpbeyJ0eXBlIjoiU0NBTEFSIiwiY29tcG9uZW50VHlwZSI6NTEyMywiY291bnQiOjE2OH0seyJ0',
4
+ 'eXBlIjoiVkVDMyIsImNvbXBvbmVudFR5cGUiOjUxMjYsImNvdW50IjozMCwibWF4IjpbMC4wMDY3MzM3MTM2NzE1NjUwNTYsMC4w',
5
+ 'MDYyNDAyODE3NDIwNjYxNDUsMC4wMDAzNDc5ODc2MDMxNjMzNDY2NV0sIm1pbiI6Wy0wLjAwMDMwMTM2NjExODk0NzA0NCwtMC4w',
6
+ 'MDAzNDc5ODc2MDMxNjMzNDY2NSwtMC4wMDAzNDc5ODc4NjUwOTc4MjA3Nl19XSwiYnVmZmVyVmlld3MiOlt7ImJ1ZmZlciI6MCwi',
7
+ 'Ynl0ZU9mZnNldCI6MCwiYnl0ZUxlbmd0aCI6MTkzfV0sImJ1ZmZlcnMiOlt7ImJ5dGVMZW5ndGgiOjE5M31dLCJtZXNoZXMiOlt7',
8
+ 'InByaW1pdGl2ZXMiOlt7ImF0dHJpYnV0ZXMiOnsiUE9TSVRJT04iOjF9LCJtb2RlIjo0LCJpbmRpY2VzIjowLCJleHRlbnNpb25z',
9
+ 'Ijp7IktIUl9kcmFjb19tZXNoX2NvbXByZXNzaW9uIjp7ImJ1ZmZlclZpZXciOjAsImF0dHJpYnV0ZXMiOnsiUE9TSVRJT04iOjB9',
10
+ 'fX19XX1dLCJub2RlcyI6W3sic2NhbGUiOlsxMDAsMTAwLDEwMF0sIm1lc2giOjB9XSwic2NlbmVzIjpbeyJub2RlcyI6WzBdfV0s',
11
+ 'InNjZW5lIjowLCJleHRlbnNpb25zVXNlZCI6WyJLSFJfZHJhY29fbWVzaF9jb21wcmVzc2lvbiJdLCJleHRlbnNpb25zUmVxdWly',
12
+ 'ZWQiOlsiS0hSX2RyYWNvX21lc2hfY29tcHJlc3Npb24iXX0gICDEAAAAQklOAERSQUNPAgIBAQAAAB44ADcDAA7f6qkv1VVRZfUo',
13
+ '1qKKAgEBEAH/AAABAAkDAAACAQEBAA0DEREXaQaJCEUEiQg1EwsuVMV2v4nwGqm5Tl7xigAAAFMDACwAZQAcaFlXAMoAAJS7Umpl',
14
+ 'CAIAyqteasnAqAAAAEt9IJaxqKVWGb4CAIBXSq0MAKDUyQAA/CcAQABAmVEBAADA1KdWGV4BKPOK/1QGgAWQDAAAAAD/DwAArQCe',
15
+ 'uRtytrkkcra5h4bmOwwAAAA=',
16
+ ].join('')
17
+
18
+ export const SITE_FLAG_MODEL_URL = `data:model/gltf-binary;base64,${SITE_FLAG_MODEL_BASE64}`
@@ -13,5 +13,5 @@ export const DEFAULT_SPIRAL_TOP_LANDING_MODE = 'none' as const
13
13
  export const DEFAULT_SPIRAL_TOP_LANDING_DEPTH = 0.9
14
14
  export const DEFAULT_SPIRAL_SHOW_CENTER_COLUMN = true
15
15
  export const DEFAULT_SPIRAL_SHOW_STEP_SUPPORTS = true
16
- export const DEFAULT_STAIR_RAILING_MODE = 'right' as const
16
+ export const DEFAULT_STAIR_RAILING_MODE = 'both' as const
17
17
  export const DEFAULT_STAIR_RAILING_HEIGHT = 0.92