@pascal-app/editor 1.1.0 → 1.1.2

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 (262) hide show
  1. package/package.json +9 -15
  2. package/src/components/editor/action-menu-button.tsx +1 -1
  3. package/src/components/editor/alignment-3d-guide-layer.tsx +1 -1
  4. package/src/components/editor/custom-camera-controls.tsx +11 -11
  5. package/src/components/editor/editor-layout-v2.tsx +1 -1
  6. package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
  7. package/src/components/editor/first-person-controls.tsx +20 -1981
  8. package/src/components/editor/floating-action-menu.tsx +1 -1
  9. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  10. package/src/components/editor/floor-region-controls.tsx +6 -2
  11. package/src/components/editor/floorplan-navigation-presentation.test.ts +0 -40
  12. package/src/components/editor/floorplan-navigation-presentation.ts +0 -66
  13. package/src/components/editor/floorplan-panel.tsx +26 -22
  14. package/src/components/editor/floorplan-wall-typing-keys.ts +1 -1
  15. package/src/components/editor/follow-pascal.test.tsx +1 -2
  16. package/src/components/editor/follow-pascal.tsx +1 -1
  17. package/src/components/editor/footprint-height-handle.tsx +1 -1
  18. package/src/components/editor/group-actions.ts +1 -2
  19. package/src/components/editor/group-floating-action-menu.tsx +1 -1
  20. package/src/components/editor/group-move-3d.ts +1 -2
  21. package/src/components/editor/group-rotate-handle.tsx +1 -2
  22. package/src/components/editor/handles/use-handle-drag.ts +1 -2
  23. package/src/components/editor/index.tsx +24 -14
  24. package/src/components/editor/inspector-extension-actions.tsx +1 -1
  25. package/src/components/editor/node-arrow-handles.tsx +1 -1
  26. package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -1
  27. package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
  28. package/src/components/editor/room-controls.tsx +7 -4
  29. package/src/components/editor/room-handles-2d.tsx +2 -3
  30. package/src/components/editor/room-handles.tsx +1 -2
  31. package/src/components/editor/room-highlight.tsx +6 -105
  32. package/src/components/editor/room-labels.test.ts +8 -822
  33. package/src/components/editor/room-labels.tsx +40 -1671
  34. package/src/components/editor/room-webgpu-materials.test.ts +1 -1
  35. package/src/components/editor/selection-manager.tsx +5 -3
  36. package/src/components/editor/site-edge-labels.tsx +1 -1
  37. package/src/components/editor/snapshot-capture-overlay.tsx +2 -5
  38. package/src/components/editor/use-floorplan-background-placement.ts +2 -1
  39. package/src/components/editor/views/plugins-view.tsx +1 -1
  40. package/src/components/editor/views/view-bar.tsx +9 -9
  41. package/src/components/editor/wall-measurement-label.tsx +1 -1
  42. package/src/components/editor/wall-move-side-handles.tsx +1 -1
  43. package/src/components/editor/wall-pull-grip.tsx +2 -2
  44. package/src/components/editor-2d/floorplan-group-move.tsx +2 -3
  45. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1 -1
  46. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +1 -1
  47. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +1 -2
  48. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +0 -37
  49. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +0 -30
  50. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +13 -3
  51. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +1 -2
  52. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +8 -10
  53. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +25 -33
  54. package/src/components/systems/roof/roof-edit-system.tsx +4 -3
  55. package/src/components/tools/distribution/distribution-run-tool.tsx +1 -1
  56. package/src/components/tools/item/placement-history.test.tsx +1 -1
  57. package/src/components/tools/item/use-placement-coordinator.tsx +2 -3
  58. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +6 -2
  59. package/src/components/tools/registry/move-registry-node-tool.tsx +1 -2
  60. package/src/components/tools/shared/cursor-sphere.tsx +1 -1
  61. package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
  62. package/src/components/tools/shared/placement-box.tsx +1 -1
  63. package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
  64. package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
  65. package/src/components/ui/action-menu/camera-actions.tsx +2 -2
  66. package/src/components/ui/action-menu/control-modes.tsx +1 -1
  67. package/src/components/ui/action-menu/follow-pascal-pill.tsx +1 -1
  68. package/src/components/ui/action-menu/index.tsx +3 -3
  69. package/src/components/ui/action-menu/measurement-control.tsx +3 -3
  70. package/src/components/ui/action-menu/placement.ts +0 -18
  71. package/src/components/ui/action-menu/point-ask-button.tsx +1 -1
  72. package/src/components/ui/action-menu/view-toggles.tsx +5 -5
  73. package/src/components/ui/controls/finish-swatch.tsx +1 -1
  74. package/src/components/ui/controls/material-paint-panel.tsx +2 -2
  75. package/src/components/ui/controls/scene-material-list.tsx +2 -2
  76. package/src/components/ui/controls/select-control.tsx +1 -1
  77. package/src/components/ui/controls/terrain-sculpt-panel.tsx +1 -1
  78. package/src/components/ui/floating-level-selector.tsx +3 -3
  79. package/src/components/ui/helpers/contextual-helper-panel.tsx +1 -1
  80. package/src/components/ui/helpers/helper-manager.test.tsx +1 -1
  81. package/src/components/ui/helpers/helper-manager.tsx +1 -1
  82. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  83. package/src/components/ui/node-icon-image.test.tsx +46 -0
  84. package/src/components/ui/node-icon-image.tsx +33 -1
  85. package/src/components/ui/panels/collections/collections-popover.tsx +2 -2
  86. package/src/components/ui/panels/floor-foundation-panel.tsx +2 -2
  87. package/src/components/ui/panels/floor-opening-panel.tsx +4 -4
  88. package/src/components/ui/panels/node-display.ts +1 -1
  89. package/src/components/ui/panels/panel-manager.tsx +1 -1
  90. package/src/components/ui/panels/panel-wrapper.tsx +1 -1
  91. package/src/components/ui/panels/parametric-inspector.tsx +1 -1
  92. package/src/components/ui/panels/room-construction-rows.tsx +6 -6
  93. package/src/components/ui/panels/room-name-options.ts +1 -5
  94. package/src/components/ui/panels/room-panel.tsx +6 -8
  95. package/src/components/ui/primitives/color-dot.tsx +2 -2
  96. package/src/components/ui/primitives/opacity-control.tsx +2 -2
  97. package/src/components/ui/primitives/sidebar.tsx +3 -3
  98. package/src/components/ui/right-stack.tsx +1 -1
  99. package/src/components/ui/sidebar/icon-rail.tsx +1 -1
  100. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  101. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  102. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +1 -1
  103. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +2 -2
  104. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +1 -1
  105. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -1
  106. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  107. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +1 -1
  108. package/src/components/ui/sidebar/panels/site-panel/index.tsx +2 -2
  109. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +1 -1
  110. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  111. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +46 -7
  112. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +1 -1
  113. package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +14 -5
  114. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -2
  115. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +3 -3
  116. package/src/components/ui/sidebar/tab-bar.tsx +1 -1
  117. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +1 -1
  118. package/src/components/ui/wall-type/wall-type-menu.tsx +1 -1
  119. package/src/components/viewer-overlay.tsx +1 -2
  120. package/src/hooks/use-auto-frame.ts +1 -2
  121. package/src/hooks/use-keyboard.ts +1 -2
  122. package/src/hooks/use-selected-room.ts +5 -7
  123. package/src/i18n/floor-footprint-name.ts +1 -1
  124. package/src/i18n/locales/en/messages.po +180 -182
  125. package/src/i18n/locales/es/messages.po +180 -182
  126. package/src/i18n/locales/fr/messages.po +180 -182
  127. package/src/i18n/locales/ja/messages.po +180 -182
  128. package/src/i18n/locales/zh-CN/messages.po +180 -182
  129. package/src/i18n/locales.test.ts +1 -1
  130. package/src/index.tsx +109 -131
  131. package/src/lib/below-grade.test.tsx +4 -4
  132. package/src/lib/camera-facing.test.ts +1 -1
  133. package/src/lib/camera-facing.ts +2 -3
  134. package/src/lib/editor-level-display.ts +1 -1
  135. package/src/lib/floor-footprints.test.ts +2 -2
  136. package/src/lib/floor-opening-draft.ts +1 -1
  137. package/src/lib/floorplan/annotation-visibility.test.ts +1 -1
  138. package/src/lib/floorplan/annotation-visibility.ts +1 -1
  139. package/src/lib/floorplan/floorplan-export.test.ts +1 -1
  140. package/src/lib/floorplan/floorplan-export.tsx +8 -11
  141. package/src/lib/floorplan/floorplan-extension.ts +4 -157
  142. package/src/lib/floorplan/floorplan-mode.ts +2 -1
  143. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +1 -1
  144. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +8 -6
  145. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +3 -4
  146. package/src/lib/floorplan/geometry.ts +2 -112
  147. package/src/lib/floorplan/index.ts +0 -9
  148. package/src/lib/floorplan/items.ts +2 -1
  149. package/src/lib/floorplan/schedules.ts +6 -3
  150. package/src/lib/floorplan/sheet-export.ts +7 -3
  151. package/src/lib/floorplan/site-plan/site-parts.ts +7 -1
  152. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +1 -1
  153. package/src/lib/follow-pascal-presentation.ts +1 -1
  154. package/src/lib/glb-export.ts +1 -1
  155. package/src/lib/material-paint.ts +1 -1
  156. package/src/lib/measurements.test.ts +2 -1
  157. package/src/lib/mezzanine-selection.test.ts +1 -1
  158. package/src/lib/paint-region-mode.ts +1 -1
  159. package/src/lib/point-ask/session.ts +1 -2
  160. package/src/lib/roof-duplication.test.ts +100 -0
  161. package/src/lib/roof-duplication.ts +30 -5
  162. package/src/lib/room-handle-drag.ts +2 -12
  163. package/src/lib/room-name-catalog.test.ts +2 -1
  164. package/src/lib/room-name-messages.test.ts +2 -1
  165. package/src/lib/room-name-messages.ts +1 -118
  166. package/src/lib/room-name-storage.test.ts +2 -1
  167. package/src/lib/room-selection-commands.ts +2 -2
  168. package/src/lib/room-selection.test.ts +6 -6
  169. package/src/lib/room-transform-session.ts +1 -2
  170. package/src/lib/room-zone-routing.ts +2 -2
  171. package/src/lib/selection-routing.test.ts +0 -28
  172. package/src/lib/selection-routing.ts +1 -40
  173. package/src/lib/sfx-bus.ts +8 -32
  174. package/src/lib/sfx-player.test.ts +72 -1
  175. package/src/lib/sfx-player.ts +49 -337
  176. package/src/lib/viewer-selection.test.ts +38 -10
  177. package/src/lib/wall-pull.test.ts +1 -1
  178. package/src/lib/wall-pull.ts +1 -27
  179. package/src/store/use-editor.tsx +17 -26
  180. package/src/store/use-floorplan-annotation-visibility.ts +5 -5
  181. package/src/store/use-wall-region-handles-selection.ts +1 -2
  182. package/src/components/editor/__bench__/room-label-placement.bench.ts +0 -108
  183. package/src/components/editor/first-person/build-collider-world.test.ts +0 -596
  184. package/src/components/editor/first-person/build-collider-world.ts +0 -549
  185. package/src/components/editor/first-person/ground-holes.test.ts +0 -128
  186. package/src/components/editor/first-person/ground-holes.ts +0 -130
  187. package/src/components/editor/first-person/mechanism-targets.test.ts +0 -162
  188. package/src/components/editor/first-person/mechanism-targets.ts +0 -88
  189. package/src/components/editor/first-person/terrain-collider.test.ts +0 -132
  190. package/src/components/editor/first-person/terrain-collider.ts +0 -212
  191. package/src/components/editor/floorplan-camera-sync.test.ts +0 -249
  192. package/src/components/editor/floorplan-camera-sync.ts +0 -213
  193. package/src/components/editor/localized-html.tsx +0 -22
  194. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +0 -236
  195. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +0 -708
  196. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +0 -385
  197. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +0 -670
  198. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +0 -89
  199. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +0 -106
  200. package/src/components/editor-2d/svg-paths.ts +0 -124
  201. package/src/components/ui/action-menu/action-button.tsx +0 -89
  202. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +0 -42
  203. package/src/components/ui/primitives/button.tsx +0 -69
  204. package/src/components/ui/primitives/dropdown-menu.tsx +0 -232
  205. package/src/components/ui/primitives/popover.tsx +0 -42
  206. package/src/components/ui/primitives/switch.tsx +0 -29
  207. package/src/components/ui/primitives/tooltip.tsx +0 -57
  208. package/src/components/ui/scene-loader.tsx +0 -73
  209. package/src/components/viewer/floorplan-compass-button.tsx +0 -67
  210. package/src/components/viewer/floorplan-preview-geometry.test.ts +0 -107
  211. package/src/components/viewer/floorplan-preview-geometry.ts +0 -281
  212. package/src/components/viewer/floorplan-preview-navigation.test.ts +0 -43
  213. package/src/components/viewer/floorplan-preview-navigation.ts +0 -62
  214. package/src/components/viewer/floorplan-preview-visibility.test.ts +0 -35
  215. package/src/components/viewer/floorplan-preview-visibility.ts +0 -19
  216. package/src/components/viewer/floorplan-preview.tsx +0 -1065
  217. package/src/components/viewer/render-settings-menu.tsx +0 -294
  218. package/src/components/viewer/sky-control.tsx +0 -710
  219. package/src/components/viewer/use-viewer-camera-navigation-sync.ts +0 -140
  220. package/src/components/viewer/use-viewer-ui-sounds.test.ts +0 -15
  221. package/src/components/viewer/use-viewer-ui-sounds.ts +0 -46
  222. package/src/components/viewer/viewer-controls-bar.tsx +0 -496
  223. package/src/components/viewer/viewer-hover-label.tsx +0 -42
  224. package/src/components/viewer/viewer-rooms.tsx +0 -248
  225. package/src/components/viewer/viewer-scene-header.tsx +0 -286
  226. package/src/components/viewer/viewer-selection-manager.tsx +0 -124
  227. package/src/components/viewer/viewer-stage-modes.test.ts +0 -38
  228. package/src/components/viewer/viewer-stage-modes.ts +0 -49
  229. package/src/components/viewer/viewer-stage-switcher.tsx +0 -97
  230. package/src/components/viewer/viewer-stage.test.tsx +0 -47
  231. package/src/components/viewer/viewer-stage.tsx +0 -221
  232. package/src/components/walkthrough-hud.tsx +0 -193
  233. package/src/hooks/use-mobile.ts +0 -19
  234. package/src/i18n/level-name.ts +0 -39
  235. package/src/i18n/scene-theme-name.ts +0 -16
  236. package/src/lib/below-grade.ts +0 -133
  237. package/src/lib/camera-pose.test.ts +0 -237
  238. package/src/lib/camera-pose.ts +0 -172
  239. package/src/lib/floorplan/floorplan-extension.test.ts +0 -42
  240. package/src/lib/floorplan/floorplan-readonly.test.ts +0 -47
  241. package/src/lib/floorplan/floorplan-readonly.ts +0 -95
  242. package/src/lib/floorplan/marks.test.ts +0 -213
  243. package/src/lib/floorplan/marks.ts +0 -238
  244. package/src/lib/measurements.ts +0 -8
  245. package/src/lib/node-display-label.ts +0 -37
  246. package/src/lib/room-assembly-overlay.test.ts +0 -275
  247. package/src/lib/room-assembly-overlay.ts +0 -265
  248. package/src/lib/room-selection.ts +0 -417
  249. package/src/lib/scene-bounds.test.ts +0 -327
  250. package/src/lib/scene-bounds.ts +0 -233
  251. package/src/lib/typing-target.ts +0 -9
  252. package/src/lib/viewer-selection.ts +0 -595
  253. package/src/lib/zone-content.ts +0 -133
  254. package/src/store/camera-pose-store.test.ts +0 -29
  255. package/src/store/camera-pose-store.ts +0 -27
  256. package/src/store/navigation-sync-pose-store.test.ts +0 -34
  257. package/src/store/navigation-sync-pose-store.ts +0 -27
  258. package/src/store/use-audio.test.ts +0 -146
  259. package/src/store/use-audio.tsx +0 -169
  260. package/src/store/use-first-person-hud.ts +0 -40
  261. package/src/store/use-follow-pascal.test.ts +0 -62
  262. package/src/store/use-follow-pascal.ts +0 -53
@@ -1,130 +0,0 @@
1
- /**
2
- * Holes in the site ground's collision geometry.
3
- *
4
- * The walkthrough stands on one merged BVH mesh, and the site's ground (a flat slab, or the terrain)
5
- * is a sheet across the whole lot. Where the house stands over a basement the ground is drawn open
6
- * (`coveredBelowGradeFootprints`), but the sheet under the stairwell stayed: the player descending
7
- * the stair met it at grade and could go no deeper. Cutting the collider by the same rings the
8
- * ground is drawn open over lets the player follow the building's floors and stairs there.
9
- *
10
- * Every triangle that reaches a hole is cut against it in plan; the pieces keep the source
11
- * triangle's plane (height and normal interpolated from its corners) and its facing, so the cut
12
- * surface is exactly the one the uncut triangle was.
13
- */
14
-
15
- import { difference, type Ring, union } from '@pascal-app/core'
16
- import { BufferAttribute, BufferGeometry, ShapeUtils, Vector2 } from 'three'
17
-
18
- /** Triangles flatter than this in plan (m²) are vertical faces: they hold nothing up. */
19
- const MIN_PLAN_AREA = 1e-9
20
-
21
- type Corner = { x: number; y: number; z: number; nx: number; ny: number; nz: number }
22
-
23
- /** Twice the signed plan area of `a b c` (x right, z down). */
24
- function orient(a: [number, number], b: [number, number], c: [number, number]): number {
25
- return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
26
- }
27
-
28
- /**
29
- * `geometry` (non-indexed, `position` and `normal`, in the frame the rings are in) without the part
30
- * of it that lies over `holes`. A triangle the clip fails on is kept whole: the ground closed is
31
- * the safe side. The geometry is returned as it is when there are no holes.
32
- */
33
- export function cutGroundCollider(
34
- geometry: BufferGeometry,
35
- holes: readonly Ring[],
36
- ): BufferGeometry {
37
- if (holes.length === 0) return geometry
38
- const cutouts = union(holes)
39
- if (cutouts.length === 0) return geometry
40
- const boxes = cutouts.map(({ outer }) => ({
41
- minX: Math.min(...outer.map(([x]) => x)),
42
- maxX: Math.max(...outer.map(([x]) => x)),
43
- minZ: Math.min(...outer.map(([, z]) => z)),
44
- maxZ: Math.max(...outer.map(([, z]) => z)),
45
- }))
46
-
47
- const position = geometry.getAttribute('position')
48
- const normal = geometry.getAttribute('normal')
49
- const positions: number[] = []
50
- const normals: number[] = []
51
- const push = (corner: Corner) => {
52
- positions.push(corner.x, corner.y, corner.z)
53
- normals.push(corner.nx, corner.ny, corner.nz)
54
- }
55
-
56
- for (let i = 0; i + 2 < position.count; i += 3) {
57
- const corners: Corner[] = [0, 1, 2].map((k) => ({
58
- x: position.getX(i + k),
59
- y: position.getY(i + k),
60
- z: position.getZ(i + k),
61
- nx: normal.getX(i + k),
62
- ny: normal.getY(i + k),
63
- nz: normal.getZ(i + k),
64
- }))
65
- const [a, b, c] = corners as [Corner, Corner, Corner]
66
- const minX = Math.min(a.x, b.x, c.x)
67
- const maxX = Math.max(a.x, b.x, c.x)
68
- const minZ = Math.min(a.z, b.z, c.z)
69
- const maxZ = Math.max(a.z, b.z, c.z)
70
- const reached = boxes.some(
71
- (box) => box.maxX > minX && box.minX < maxX && box.maxZ > minZ && box.minZ < maxZ,
72
- )
73
- const plan: [[number, number], [number, number], [number, number]] = [
74
- [a.x, a.z],
75
- [b.x, b.z],
76
- [c.x, c.z],
77
- ]
78
- const determinant = orient(plan[0], plan[1], plan[2])
79
- if (!reached || Math.abs(determinant) < MIN_PLAN_AREA) {
80
- corners.forEach(push)
81
- continue
82
- }
83
-
84
- let pieces: ReturnType<typeof difference>
85
- try {
86
- pieces = difference(plan, cutouts, { throwOnError: true })
87
- } catch {
88
- corners.forEach(push)
89
- continue
90
- }
91
-
92
- const at = (x: number, z: number): Corner => {
93
- const wa = ((b.z - c.z) * (x - c.x) + (c.x - b.x) * (z - c.z)) / determinant
94
- const wb = ((c.z - a.z) * (x - c.x) + (a.x - c.x) * (z - c.z)) / determinant
95
- const wc = 1 - wa - wb
96
- const nx = a.nx * wa + b.nx * wb + c.nx * wc
97
- const ny = a.ny * wa + b.ny * wb + c.ny * wc
98
- const nz = a.nz * wa + b.nz * wb + c.nz * wc
99
- const length = Math.hypot(nx, ny, nz) || 1
100
- return {
101
- x,
102
- y: a.y * wa + b.y * wb + c.y * wc,
103
- z,
104
- nx: nx / length,
105
- ny: ny / length,
106
- nz: nz / length,
107
- }
108
- }
109
-
110
- for (const piece of pieces) {
111
- const rings = [piece.outer, ...piece.holes]
112
- const points = rings.flat()
113
- const contour = piece.outer.map(([x, z]) => new Vector2(x, z))
114
- const voids = piece.holes.map((ring) => ring.map(([x, z]) => new Vector2(x, z)))
115
- for (const [p, q, r] of ShapeUtils.triangulateShape(contour, voids)) {
116
- const corner = [p, q, r].map((index) => points[index as number] as [number, number])
117
- const swap =
118
- Math.sign(orient(corner[0]!, corner[1]!, corner[2]!)) !== Math.sign(determinant)
119
- const ordered = swap ? [corner[0]!, corner[2]!, corner[1]!] : corner
120
- for (const [x, z] of ordered) push(at(x, z))
121
- }
122
- }
123
- }
124
-
125
- const cut = new BufferGeometry()
126
- cut.setAttribute('position', new BufferAttribute(new Float32Array(positions), 3))
127
- cut.setAttribute('normal', new BufferAttribute(new Float32Array(normals), 3))
128
- geometry.dispose()
129
- return cut
130
- }
@@ -1,162 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
- import {
3
- type AnyNode,
4
- type AnyNodeId,
5
- nodeRegistry,
6
- registerNode,
7
- sceneRegistry,
8
- useInteractive,
9
- useScene,
10
- } from '@pascal-app/core'
11
- import { Object3D } from 'three'
12
- import {
13
- hasNativeLeaf,
14
- mechanismHudInteract,
15
- mechanismTargetIds,
16
- nativeOpeningIds,
17
- watchScriptedDoors,
18
- } from './mechanism-targets'
19
-
20
- const switchMechanism = {
21
- has: (node: AnyNode) => (node as unknown as { joints: number }).joints > 0,
22
- isOn: (node: AnyNode, state: ReturnType<typeof useInteractive.getState>) =>
23
- Boolean(state.mechanisms[node.id]),
24
- set: (node: AnyNode, on: boolean) => useInteractive.getState().setMechanism(node.id, on),
25
- }
26
-
27
- const mounted: [string, string][] = []
28
-
29
- function mount(kind: string, id: string, fields: Record<string, unknown> = {}): AnyNode {
30
- const node = { object: 'node', id, type: kind, ...fields } as unknown as AnyNode
31
- useScene.setState((state) => ({ nodes: { ...state.nodes, [id]: node } }) as never)
32
- sceneRegistry.nodes.set(id, new Object3D())
33
- sceneRegistry.byType[kind]!.add(id)
34
- mounted.push([kind, id])
35
- return node
36
- }
37
-
38
- function registerKind(capabilities: Record<string, unknown>): string {
39
- const kind = `test:walk-${crypto.randomUUID()}`
40
- registerNode({
41
- kind,
42
- schemaVersion: 1,
43
- category: 'furnish',
44
- defaults: () => ({}),
45
- capabilities,
46
- } as never)
47
- return kind
48
- }
49
-
50
- afterEach(() => {
51
- for (const [kind, id] of mounted.splice(0)) {
52
- sceneRegistry.nodes.delete(id)
53
- sceneRegistry.byType[kind]!.delete(id)
54
- useInteractive.getState().removeMechanism(id as AnyNodeId)
55
- }
56
- })
57
-
58
- describe('walkthrough mechanism targets', () => {
59
- test('any mounted node whose kind declares a mechanism is a target, once', () => {
60
- const kind = registerKind({ mechanism: switchMechanism })
61
- mount(kind, 'articulated_1', { joints: 2 })
62
- mount(kind, 'articulated_still', { joints: 0 })
63
- mount(registerKind({}), 'plain_1')
64
- const covered = mount(kind, 'articulated_covered', { joints: 1 })
65
-
66
- expect(mechanismTargetIds(new Set([covered.id]))).toEqual(['articulated_1' as AnyNodeId])
67
- })
68
-
69
- test('the HUD names the node and says what E will do', () => {
70
- const kind = registerKind({ mechanism: switchMechanism })
71
- const node = mount(kind, 'articulated_hud', { joints: 1, name: 'Desk lamp' })
72
- expect(mechanismHudInteract(node)).toEqual({ label: 'Desk lamp', verb: 'turn on' })
73
- useInteractive.getState().setMechanism(node.id, true)
74
- expect(mechanismHudInteract(node)).toEqual({ label: 'Desk lamp', verb: 'turn off' })
75
-
76
- const lid = mount(registerKind({ mechanism: { ...switchMechanism, verb: 'open' } }), 'lid_1', {
77
- joints: 1,
78
- })
79
- expect(mechanismHudInteract(lid)).toEqual({ label: 'item', verb: 'open' })
80
- expect(mechanismHudInteract(mount(registerKind({}), 'plain_hud'))).toBeNull()
81
- })
82
- })
83
-
84
- // A door or window built from a script has no native leaf or sash: the walkthrough's own door and
85
- // window targets only start a native swing on it, which draws nothing. Its `open` clip runs
86
- // through its mechanism, as an authored object's does.
87
- describe('walkthrough doors and windows', () => {
88
- const script = { source: { kind: 'script', script: 'b'.repeat(64), artifact: 'a'.repeat(64) } }
89
-
90
- test('the doors and windows it swings itself are the ones with a native leaf', () => {
91
- const native = mount('door', 'door_native')
92
- const scripted = mount('door', 'door_scripted', script)
93
- mount('window', 'window_native')
94
- mount('window', 'window_scripted', script)
95
-
96
- expect(nativeOpeningIds().sort()).toEqual(['door_native', 'window_native'] as AnyNodeId[])
97
- expect(hasNativeLeaf(native)).toBe(true)
98
- expect(hasNativeLeaf(scripted)).toBe(false)
99
- expect(hasNativeLeaf(undefined)).toBe(true)
100
- })
101
- })
102
-
103
- // The walkthrough's collider world is rebuilt when a native door's tween ends. A door built from a
104
- // script opens by a clip that no tween system reports: its collider follows its mechanism, so
105
- // the world has to be rebuilt when that flips.
106
- describe('walkthrough scripted doors', () => {
107
- const mechanism = {
108
- verb: 'open',
109
- has: (node: AnyNode) => Boolean((node as unknown as { source?: unknown }).source),
110
- isOn: (node: AnyNode, state: ReturnType<typeof useInteractive.getState>) =>
111
- Boolean(state.items[node.id as AnyNodeId]?.controlValues[0]),
112
- set: () => {},
113
- }
114
- let restoreRegistry = () => {}
115
-
116
- beforeEach(() => {
117
- restoreRegistry = nodeRegistry._snapshot()
118
- registerNode({
119
- kind: 'door',
120
- schemaVersion: 1,
121
- category: 'structure',
122
- defaults: () => ({}),
123
- capabilities: { mechanism },
124
- } as never)
125
- })
126
-
127
- afterEach(() => {
128
- useInteractive.setState({ items: {} } as never)
129
- restoreRegistry()
130
- })
131
-
132
- test('a rebuild is asked for when a scripted door opens or closes, and for nothing else', () => {
133
- mount('door', 'door_scripted_watch', { source: { kind: 'script' } })
134
- mount('door', 'door_native_watch')
135
- let rebuilds = 0
136
- const stop = watchScriptedDoors(() => rebuilds++)
137
- const setControls = (scripted: boolean, native: boolean) =>
138
- useInteractive.setState({
139
- items: {
140
- door_scripted_watch: { controlValues: [scripted] },
141
- door_native_watch: { controlValues: [native] },
142
- },
143
- } as never)
144
- try {
145
- // its model initialising the item closed is not a change
146
- setControls(false, false)
147
- expect(rebuilds).toBe(0)
148
- setControls(true, false)
149
- expect(rebuilds).toBe(1)
150
- // the same state again, and a native door's control, are not its business
151
- setControls(true, false)
152
- setControls(true, true)
153
- expect(rebuilds).toBe(1)
154
- setControls(false, true)
155
- expect(rebuilds).toBe(2)
156
- } finally {
157
- stop()
158
- }
159
- setControls(true, true)
160
- expect(rebuilds).toBe(2)
161
- })
162
- })
@@ -1,88 +0,0 @@
1
- import {
2
- type AnyNode,
3
- type AnyNodeId,
4
- isScriptedNode,
5
- nodeMechanism,
6
- nodeRegistry,
7
- sceneRegistry,
8
- useInteractive,
9
- useScene,
10
- } from '@pascal-app/core'
11
- import type { WalkthroughInteract } from '../../../store/use-first-person-hud'
12
-
13
- /**
14
- * Whether the walkthrough swings this door or window itself, through the native `operationState`
15
- * its door and window systems draw. One built from a script has no native leaf or sash: a swing
16
- * would draw nothing, so its clips run through its mechanism, as an authored object's do.
17
- */
18
- export function hasNativeLeaf(node: AnyNode | undefined): boolean {
19
- return !isScriptedNode(node)
20
- }
21
-
22
- /** Mounted doors and windows the walkthrough's own door and window targets cover. */
23
- export function nativeOpeningIds(): AnyNodeId[] {
24
- const nodes = useScene.getState().nodes
25
- const ids: AnyNodeId[] = []
26
- for (const kind of ['door', 'window'] as const) {
27
- for (const id of sceneRegistry.byType[kind] ?? []) {
28
- if (hasNativeLeaf(nodes[id as AnyNodeId])) ids.push(id as AnyNodeId)
29
- }
30
- }
31
- return ids
32
- }
33
-
34
- /**
35
- * Mounted nodes the walkthrough runs through `capabilities.mechanism`, any kind
36
- * included. `covered` holds the nodes a finer target already tested (procedural
37
- * parts, catalog item toggles), so no node is targeted twice.
38
- */
39
- export function mechanismTargetIds(covered: ReadonlySet<string>): AnyNodeId[] {
40
- const nodes = useScene.getState().nodes
41
- const ids: AnyNodeId[] = []
42
- for (const [kind, kindIds] of Object.entries(sceneRegistry.byType)) {
43
- if (!nodeRegistry.get(kind)?.capabilities.mechanism) continue
44
- for (const id of kindIds) {
45
- if (!covered.has(id) && nodeMechanism(nodes[id as AnyNodeId])) ids.push(id as AnyNodeId)
46
- }
47
- }
48
- return ids
49
- }
50
-
51
- /** The walkthrough prompt for a mechanism target: its name and what E does next. */
52
- export function mechanismHudInteract(node: AnyNode): WalkthroughInteract {
53
- const mechanism = nodeMechanism(node)
54
- if (!mechanism) return null
55
- const on = mechanism.isOn(node, useInteractive.getState())
56
- const verb = mechanism.verb === 'open' ? (on ? 'close' : 'open') : on ? 'turn off' : 'turn on'
57
- return { label: node.name ?? 'item', verb }
58
- }
59
-
60
- /** Ids of the mounted doors built from a script that are open, by their mechanism. */
61
- function openScriptedDoors(): string {
62
- const nodes = useScene.getState().nodes
63
- const state = useInteractive.getState()
64
- const open: string[] = []
65
- for (const id of sceneRegistry.byType.door ?? []) {
66
- const node = nodes[id as AnyNodeId]
67
- if (isScriptedNode(node) && nodeMechanism(node)?.isOn(node, state)) open.push(id)
68
- }
69
- return open.sort().join(',')
70
- }
71
-
72
- /**
73
- * Calls `onChange` when a mounted door built from a script opens or closes. The walkthrough's
74
- * collider follows the door's mechanism, and its model's clip is moved by no tween system that
75
- * would report the end, as a native door's does: without this the world keeps the leaf that was
76
- * there when it was last built. It fires at the toggle: the doorway is passable as the leaves
77
- * start to move, and solid again as they start to close.
78
- */
79
- export function watchScriptedDoors(onChange: () => void): () => void {
80
- let known = openScriptedDoors()
81
- return useInteractive.subscribe((state, previous) => {
82
- if (state.items === previous.items) return
83
- const next = openScriptedDoors()
84
- if (next === known) return
85
- known = next
86
- onChange()
87
- })
88
- }
@@ -1,132 +0,0 @@
1
- import { describe, expect, test } from 'bun:test'
2
- import {
3
- applyHeightPatch,
4
- createTerrainField,
5
- flattenPatch,
6
- heightAt,
7
- type TerrainField,
8
- } from '@pascal-app/core'
9
- import { Mesh, MeshBasicMaterial, Raycaster, Vector3 } from 'three'
10
- import { createTerrainColliderGeometry } from './terrain-collider'
11
-
12
- const DOWN = new Vector3(0, -1, 0)
13
- const UP = new Vector3(0, 1, 0)
14
-
15
- /** A 2.5 m plateau over x,z ∈ [2,5] on an otherwise flat field. */
16
- function plateauField(): TerrainField {
17
- const base = createTerrainField({ cols: 17, rows: 17, spacing: 1, origin: [-8, -8] })
18
- const patch = flattenPatch(base, { minX: 2, minZ: 2, maxX: 5, maxZ: 5 }, 2.5)
19
- return applyHeightPatch(base, patch as never)
20
- }
21
-
22
- /** A field sloping 0.1 m per metre along +x, so every sample differs. */
23
- function slopedField(): TerrainField {
24
- const field = createTerrainField({ cols: 9, rows: 9, spacing: 1, origin: [-4, -4] })
25
- const heights = new Int16Array(field.heights)
26
- for (let r = 0; r < field.rows; r++) {
27
- for (let c = 0; c < field.cols; c++) {
28
- heights[r * field.cols + c] = Math.round((c * 0.1) /* m */ / field.step)
29
- }
30
- }
31
- return { ...field, heights }
32
- }
33
-
34
- function colliderMesh(field: TerrainField) {
35
- const geometry = createTerrainColliderGeometry(field)
36
- if (!geometry) throw new Error('no collider geometry')
37
- const mesh = new Mesh(geometry, new MeshBasicMaterial())
38
- mesh.updateMatrixWorld(true)
39
- return mesh
40
- }
41
-
42
- /** The height a downward ray finds — what the walkthrough player stands on. */
43
- function groundUnder(mesh: Mesh, x: number, z: number): number | null {
44
- const raycaster = new Raycaster()
45
- raycaster.set(new Vector3(x, 500, z), DOWN)
46
- const hits = raycaster.intersectObject(mesh, false)
47
- const floor = hits.find((hit) => {
48
- if (!hit.face) return true
49
- return hit.face.normal.clone().transformDirection(mesh.matrixWorld).dot(UP) > 0.2
50
- })
51
- return floor ? floor.point.y : null
52
- }
53
-
54
- describe('createTerrainColliderGeometry', () => {
55
- test('the walkable surface is the field, not a plane', () => {
56
- const field = plateauField()
57
- const mesh = colliderMesh(field)
58
-
59
- // On the plateau, off it, and on the slope between: the collider has to agree
60
- // with `heightAt` at each, or what you see is not what you stand on.
61
- for (const [x, z] of [
62
- [3, 3],
63
- [-3, -3],
64
- [1.5, 3],
65
- [4.5, 4.5],
66
- ] as Array<[number, number]>) {
67
- expect(groundUnder(mesh, x, z)).toBeCloseTo(heightAt(field, x, z), 4)
68
- }
69
- })
70
-
71
- test('a sloped field is sampled per vertex, not averaged', () => {
72
- const field = slopedField()
73
- const mesh = colliderMesh(field)
74
-
75
- // Sample midpoints, where a flat or decimated collider would disagree most.
76
- for (const x of [-3.5, -0.5, 1.5, 3.5]) {
77
- expect(groundUnder(mesh, x, 0)).toBeCloseTo(heightAt(field, x, 0), 4)
78
- }
79
- })
80
-
81
- test('the skirt keeps holding the player far past the field', () => {
82
- const field = slopedField()
83
- const mesh = colliderMesh(field)
84
-
85
- // Past every edge and past a corner: `heightAt` clamps to the border there, so
86
- // the collider must too. Without the skirt these are null and the player falls.
87
- for (const [x, z] of [
88
- [400, 0],
89
- [-400, 0],
90
- [0, 400],
91
- [0, -400],
92
- [400, 400],
93
- [-400, -400],
94
- ] as Array<[number, number]>) {
95
- expect(groundUnder(mesh, x, z)).toBeCloseTo(heightAt(field, x, z), 4)
96
- }
97
- })
98
-
99
- test('every triangle carries a normal, and the surface faces up', () => {
100
- const geometry = createTerrainColliderGeometry(plateauField())
101
- if (!geometry) throw new Error('no collider geometry')
102
-
103
- const position = geometry.getAttribute('position')
104
- const normal = geometry.getAttribute('normal')
105
- expect(normal).toBeDefined()
106
- expect(normal.count).toBe(position.count)
107
- // Non-indexed triangles — `mergeGeometries` needs every contributor to match.
108
- expect(geometry.index).toBeNull()
109
- expect(position.count % 3).toBe(0)
110
-
111
- for (let i = 0; i < normal.count; i++) {
112
- expect(normal.getY(i)).toBeGreaterThan(0)
113
- }
114
- })
115
-
116
- test('decimates rather than exploding on an absurd field', () => {
117
- // 1025² is inside the schema's decode-bomb guard but 1M cells — an exact
118
- // collider would be 2M triangles for a surface nobody can walk that far across.
119
- const field = createTerrainField({ cols: 1025, rows: 1025, spacing: 0.5 })
120
- const geometry = createTerrainColliderGeometry(field)
121
- if (!geometry) throw new Error('no collider geometry')
122
-
123
- const triangles = geometry.getAttribute('position').count / 3
124
- expect(triangles).toBeLessThan(200_000)
125
- // Still a real surface, not an empty one.
126
- expect(triangles).toBeGreaterThan(1000)
127
- })
128
-
129
- test('returns null for a degenerate field', () => {
130
- expect(createTerrainColliderGeometry(createTerrainField({ cols: 1, rows: 1 }))).toBeNull()
131
- })
132
- })