@pascal-app/viewer 1.0.3 → 1.0.5

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 (276) hide show
  1. package/dist/capture/capture-runtime.js +1 -1
  2. package/dist/components/error-boundary.d.ts.map +1 -1
  3. package/dist/components/renderers/node-renderer.d.ts +1 -1
  4. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  5. package/dist/components/renderers/node-renderer.js +12 -4
  6. package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -1
  7. package/dist/components/viewer/bvh-ecctrl.d.ts.map +1 -1
  8. package/dist/components/viewer/frame-limiter.d.ts +1 -0
  9. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  10. package/dist/components/viewer/frame-limiter.js +22 -3
  11. package/dist/components/viewer/glb-interactive.d.ts +25 -1
  12. package/dist/components/viewer/glb-interactive.d.ts.map +1 -1
  13. package/dist/components/viewer/glb-interactive.js +253 -86
  14. package/dist/components/viewer/glb-mechanisms.d.ts +10 -0
  15. package/dist/components/viewer/glb-mechanisms.d.ts.map +1 -0
  16. package/dist/components/viewer/glb-mechanisms.js +19 -0
  17. package/dist/components/viewer/glb-reference-nodes.js +1 -1
  18. package/dist/components/viewer/glb-replace-instances.js +1 -1
  19. package/dist/components/viewer/glb-scene.d.ts +45 -27
  20. package/dist/components/viewer/glb-scene.d.ts.map +1 -1
  21. package/dist/components/viewer/glb-scene.js +421 -508
  22. package/dist/components/viewer/glb-walkthrough-controller.d.ts +1 -1
  23. package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
  24. package/dist/components/viewer/glb-walkthrough-controller.js +13 -2
  25. package/dist/components/viewer/index.d.ts +24 -1
  26. package/dist/components/viewer/index.d.ts.map +1 -1
  27. package/dist/components/viewer/index.js +111 -37
  28. package/dist/components/viewer/lights.d.ts.map +1 -1
  29. package/dist/components/viewer/lights.js +113 -22
  30. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  31. package/dist/components/viewer/perf-monitor.js +6 -0
  32. package/dist/components/viewer/perf-panel.d.ts.map +1 -1
  33. package/dist/components/viewer/perf-panel.js +1 -1
  34. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  35. package/dist/components/viewer/post-processing.js +104 -36
  36. package/dist/components/viewer/registered-systems.d.ts.map +1 -1
  37. package/dist/components/viewer/registered-systems.js +17 -3
  38. package/dist/components/viewer/render-error.d.ts +18 -0
  39. package/dist/components/viewer/render-error.d.ts.map +1 -0
  40. package/dist/components/viewer/render-error.js +29 -0
  41. package/dist/components/viewer/scene-bvh.d.ts.map +1 -1
  42. package/dist/components/viewer/scene-bvh.js +1 -1
  43. package/dist/components/viewer/scene-environment.d.ts.map +1 -1
  44. package/dist/components/viewer/scene-environment.js +31 -11
  45. package/dist/components/viewer/scene-ground-replacement.d.ts +6 -0
  46. package/dist/components/viewer/scene-ground-replacement.d.ts.map +1 -1
  47. package/dist/components/viewer/scene-ground-replacement.js +15 -0
  48. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  49. package/dist/components/viewer/selection-manager.js +25 -7
  50. package/dist/components/viewer/underground-ambiance.d.ts +9 -0
  51. package/dist/components/viewer/underground-ambiance.d.ts.map +1 -0
  52. package/dist/components/viewer/underground-ambiance.js +105 -0
  53. package/dist/components/viewer/viewer-camera.d.ts +4 -0
  54. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  55. package/dist/components/viewer/viewer-camera.js +12 -2
  56. package/dist/components/viewer/viewer-presentations.d.ts.map +1 -1
  57. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  58. package/dist/hooks/use-gltf-ktx2.js +2 -0
  59. package/dist/hooks/use-node-events.d.ts.map +1 -1
  60. package/dist/index.d.ts +36 -16
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +34 -19
  63. package/dist/lib/asset-url.d.ts.map +1 -1
  64. package/dist/lib/asset-url.js +7 -1
  65. package/dist/lib/backdrop.d.ts +17 -0
  66. package/dist/lib/backdrop.d.ts.map +1 -1
  67. package/dist/lib/backdrop.js +18 -1
  68. package/dist/lib/construction-dust.d.ts +57 -0
  69. package/dist/lib/construction-dust.d.ts.map +1 -0
  70. package/dist/lib/construction-dust.js +187 -0
  71. package/dist/lib/csg-utils.js +1 -1
  72. package/dist/lib/display-state.d.ts +25 -0
  73. package/dist/lib/display-state.d.ts.map +1 -0
  74. package/dist/lib/display-state.js +1 -0
  75. package/dist/lib/foundation-materials.d.ts +26 -0
  76. package/dist/lib/foundation-materials.d.ts.map +1 -0
  77. package/dist/lib/foundation-materials.js +65 -0
  78. package/dist/lib/gltf-unlit.d.ts +10 -0
  79. package/dist/lib/gltf-unlit.d.ts.map +1 -0
  80. package/dist/lib/gltf-unlit.js +23 -0
  81. package/dist/lib/ground-window.d.ts +33 -0
  82. package/dist/lib/ground-window.d.ts.map +1 -0
  83. package/dist/lib/ground-window.js +114 -0
  84. package/dist/lib/hero-pose.d.ts +13 -3
  85. package/dist/lib/hero-pose.d.ts.map +1 -1
  86. package/dist/lib/hero-pose.js +72 -5
  87. package/dist/lib/ink-edges.d.ts +6 -3
  88. package/dist/lib/ink-edges.d.ts.map +1 -1
  89. package/dist/lib/ink-edges.js +17 -7
  90. package/dist/lib/key-light-override.d.ts +11 -0
  91. package/dist/lib/key-light-override.d.ts.map +1 -0
  92. package/dist/lib/key-light-override.js +14 -0
  93. package/dist/lib/ktx2-loader.d.ts +7 -0
  94. package/dist/lib/ktx2-loader.d.ts.map +1 -1
  95. package/dist/lib/ktx2-loader.js +35 -0
  96. package/dist/lib/layer-pass.d.ts.map +1 -1
  97. package/dist/lib/layers.d.ts +11 -7
  98. package/dist/lib/layers.d.ts.map +1 -1
  99. package/dist/lib/layers.js +16 -6
  100. package/dist/lib/live-frame-hold.d.ts +14 -0
  101. package/dist/lib/live-frame-hold.d.ts.map +1 -0
  102. package/dist/lib/live-frame-hold.js +28 -0
  103. package/dist/lib/materials.d.ts +19 -7
  104. package/dist/lib/materials.d.ts.map +1 -1
  105. package/dist/lib/materials.js +65 -9
  106. package/dist/lib/merged-outline-node.d.ts +5 -0
  107. package/dist/lib/merged-outline-node.d.ts.map +1 -1
  108. package/dist/lib/merged-outline-node.js +80 -2
  109. package/dist/lib/node-top-surface-height.d.ts +6 -0
  110. package/dist/lib/node-top-surface-height.d.ts.map +1 -0
  111. package/dist/lib/node-top-surface-height.js +74 -0
  112. package/dist/lib/perf-probe-state.d.ts +39 -0
  113. package/dist/lib/perf-probe-state.d.ts.map +1 -0
  114. package/dist/lib/perf-probe-state.js +17 -0
  115. package/dist/lib/pointer-events.d.ts +26 -2
  116. package/dist/lib/pointer-events.d.ts.map +1 -1
  117. package/dist/lib/pointer-events.js +99 -12
  118. package/dist/lib/post-processing-resources.d.ts.map +1 -1
  119. package/dist/lib/procedural-emission.d.ts +7 -0
  120. package/dist/lib/procedural-emission.d.ts.map +1 -0
  121. package/dist/lib/procedural-emission.js +54 -0
  122. package/dist/lib/reveal-candidates.d.ts +80 -0
  123. package/dist/lib/reveal-candidates.d.ts.map +1 -0
  124. package/dist/lib/reveal-candidates.js +524 -0
  125. package/dist/lib/reveal-ghost.d.ts +9 -0
  126. package/dist/lib/reveal-ghost.d.ts.map +1 -0
  127. package/dist/lib/reveal-ghost.js +62 -0
  128. package/dist/lib/reveal-pending.d.ts +12 -0
  129. package/dist/lib/reveal-pending.d.ts.map +1 -0
  130. package/dist/lib/reveal-pending.js +12 -0
  131. package/dist/lib/reveal-pose.d.ts +70 -0
  132. package/dist/lib/reveal-pose.d.ts.map +1 -0
  133. package/dist/lib/reveal-pose.js +248 -0
  134. package/dist/lib/reveal-schedule.d.ts +208 -0
  135. package/dist/lib/reveal-schedule.d.ts.map +1 -0
  136. package/dist/lib/reveal-schedule.js +410 -0
  137. package/dist/lib/reveal-texture-scale.d.ts +5 -0
  138. package/dist/lib/reveal-texture-scale.d.ts.map +1 -0
  139. package/dist/lib/reveal-texture-scale.js +113 -0
  140. package/dist/lib/reveal-windows.d.ts +23 -0
  141. package/dist/lib/reveal-windows.d.ts.map +1 -0
  142. package/dist/lib/reveal-windows.js +31 -0
  143. package/dist/lib/reveal-wipe.d.ts +71 -0
  144. package/dist/lib/reveal-wipe.d.ts.map +1 -0
  145. package/dist/lib/reveal-wipe.js +105 -0
  146. package/dist/lib/scene-pass-mrt.d.ts +12 -0
  147. package/dist/lib/scene-pass-mrt.d.ts.map +1 -0
  148. package/dist/lib/scene-pass-mrt.js +23 -0
  149. package/dist/lib/scene-support-height.d.ts +4 -0
  150. package/dist/lib/scene-support-height.d.ts.map +1 -0
  151. package/dist/lib/scene-support-height.js +34 -0
  152. package/dist/lib/scene-themes.d.ts +30 -8
  153. package/dist/lib/scene-themes.d.ts.map +1 -1
  154. package/dist/lib/scene-themes.js +36 -0
  155. package/dist/lib/scene-visibility.d.ts +17 -1
  156. package/dist/lib/scene-visibility.d.ts.map +1 -1
  157. package/dist/lib/scene-visibility.js +53 -3
  158. package/dist/lib/scripted-opening.d.ts +11 -0
  159. package/dist/lib/scripted-opening.d.ts.map +1 -0
  160. package/dist/lib/scripted-opening.js +20 -0
  161. package/dist/lib/site-surfaces.d.ts +32 -0
  162. package/dist/lib/site-surfaces.d.ts.map +1 -0
  163. package/dist/lib/site-surfaces.js +70 -0
  164. package/dist/lib/snapshot-pipeline.d.ts +34 -3
  165. package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
  166. package/dist/lib/snapshot-pipeline.js +247 -117
  167. package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -1
  168. package/dist/lib/terrain-perimeter-fill.js +22 -14
  169. package/dist/lib/tsl-compat.d.ts +2 -2
  170. package/dist/lib/tsl-compat.d.ts.map +1 -1
  171. package/dist/lib/underground-ambiance.d.ts +168 -0
  172. package/dist/lib/underground-ambiance.d.ts.map +1 -0
  173. package/dist/lib/underground-ambiance.js +334 -0
  174. package/dist/lib/zone-geometry.d.ts +11 -0
  175. package/dist/lib/zone-geometry.d.ts.map +1 -0
  176. package/dist/lib/zone-geometry.js +46 -0
  177. package/dist/store/use-item-light-pool.d.ts +14 -10
  178. package/dist/store/use-item-light-pool.d.ts.map +1 -1
  179. package/dist/store/use-item-light-pool.js +52 -26
  180. package/dist/store/use-viewer.d.ts +194 -26
  181. package/dist/store/use-viewer.d.ts.map +1 -1
  182. package/dist/store/use-viewer.js +164 -21
  183. package/dist/systems/ceiling/ceiling-system.d.ts +15 -1
  184. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  185. package/dist/systems/ceiling/ceiling-system.js +88 -6
  186. package/dist/systems/construction-reveal/construction-dust.d.ts +16 -0
  187. package/dist/systems/construction-reveal/construction-dust.d.ts.map +1 -0
  188. package/dist/systems/construction-reveal/construction-dust.js +92 -0
  189. package/dist/systems/construction-reveal/construction-reveal.d.ts +276 -0
  190. package/dist/systems/construction-reveal/construction-reveal.d.ts.map +1 -0
  191. package/dist/systems/construction-reveal/construction-reveal.js +2383 -0
  192. package/dist/systems/construction-reveal/dust-material.d.ts +10 -0
  193. package/dist/systems/construction-reveal/dust-material.d.ts.map +1 -0
  194. package/dist/systems/construction-reveal/dust-material.js +17 -0
  195. package/dist/systems/construction-reveal/ghost-dress.d.ts +10 -0
  196. package/dist/systems/construction-reveal/ghost-dress.d.ts.map +1 -0
  197. package/dist/systems/construction-reveal/ghost-dress.js +24 -0
  198. package/dist/systems/door/door-system.d.ts.map +1 -1
  199. package/dist/systems/door/door-system.js +55 -17
  200. package/dist/systems/elevator/elevator-interaction-system.d.ts +7 -0
  201. package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -1
  202. package/dist/systems/elevator/elevator-interaction-system.js +18 -1
  203. package/dist/systems/floor-elevation/floor-elevation-system.d.ts +3 -1
  204. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
  205. package/dist/systems/floor-elevation/floor-elevation-system.js +33 -5
  206. package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
  207. package/dist/systems/geometry/geometry-system.js +71 -11
  208. package/dist/systems/interactive/control-widget.d.ts.map +1 -1
  209. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  210. package/dist/systems/interactive/interactive-system.js +52 -17
  211. package/dist/systems/interactive/procedural-controls.d.ts +13 -0
  212. package/dist/systems/interactive/procedural-controls.d.ts.map +1 -0
  213. package/dist/systems/interactive/procedural-controls.js +17 -0
  214. package/dist/systems/interactive/scripted-clips.d.ts +19 -0
  215. package/dist/systems/interactive/scripted-clips.d.ts.map +1 -0
  216. package/dist/systems/interactive/scripted-clips.js +134 -0
  217. package/dist/systems/item/item-system.d.ts.map +1 -1
  218. package/dist/systems/item/item-system.js +9 -3
  219. package/dist/systems/item/object-cut-invalidation.d.ts +3 -0
  220. package/dist/systems/item/object-cut-invalidation.d.ts.map +1 -0
  221. package/dist/systems/item/object-cut-invalidation.js +68 -0
  222. package/dist/systems/item-light/item-light-system.d.ts +1 -1
  223. package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
  224. package/dist/systems/item-light/item-light-system.js +48 -53
  225. package/dist/systems/level/level-hold.d.ts +4 -0
  226. package/dist/systems/level/level-hold.d.ts.map +1 -0
  227. package/dist/systems/level/level-hold.js +14 -0
  228. package/dist/systems/level/level-system.d.ts.map +1 -1
  229. package/dist/systems/level/level-system.js +19 -6
  230. package/dist/systems/level/level-utils.d.ts +19 -6
  231. package/dist/systems/level/level-utils.d.ts.map +1 -1
  232. package/dist/systems/level/level-utils.js +68 -9
  233. package/dist/systems/level/shadow-only-levels.d.ts +9 -0
  234. package/dist/systems/level/shadow-only-levels.d.ts.map +1 -0
  235. package/dist/systems/level/shadow-only-levels.js +7 -0
  236. package/dist/systems/roof/__fixtures__/roof-plane-cases.d.ts +103 -0
  237. package/dist/systems/roof/__fixtures__/roof-plane-cases.d.ts.map +1 -0
  238. package/dist/systems/roof/__fixtures__/roof-plane-cases.js +317 -0
  239. package/dist/systems/roof/roof-materials.d.ts +16 -2
  240. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  241. package/dist/systems/roof/roof-materials.js +64 -31
  242. package/dist/systems/roof/roof-system.d.ts +21 -4
  243. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  244. package/dist/systems/roof/roof-system.js +374 -49
  245. package/dist/systems/slab/slab-system.d.ts +1 -1
  246. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  247. package/dist/systems/slab/slab-system.js +49 -19
  248. package/dist/systems/stair/stair-system.d.ts.map +1 -1
  249. package/dist/systems/stair/stair-system.js +133 -354
  250. package/dist/systems/wall/wall-cutout-cache.d.ts +39 -3
  251. package/dist/systems/wall/wall-cutout-cache.d.ts.map +1 -1
  252. package/dist/systems/wall/wall-cutout-cache.js +141 -19
  253. package/dist/systems/wall/wall-cutout.d.ts +6 -0
  254. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  255. package/dist/systems/wall/wall-cutout.js +42 -28
  256. package/dist/systems/wall/wall-finish-data.d.ts +22 -0
  257. package/dist/systems/wall/wall-finish-data.d.ts.map +1 -0
  258. package/dist/systems/wall/wall-finish-data.js +17 -0
  259. package/dist/systems/wall/wall-material-variant.d.ts.map +1 -1
  260. package/dist/systems/wall/wall-materials.d.ts +9 -4
  261. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  262. package/dist/systems/wall/wall-materials.js +135 -153
  263. package/dist/systems/wall/wall-placeholder-sweep.d.ts.map +1 -1
  264. package/dist/systems/wall/wall-system.d.ts +33 -2
  265. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  266. package/dist/systems/wall/wall-system.js +621 -159
  267. package/dist/systems/window/corner-frame.d.ts +10 -0
  268. package/dist/systems/window/corner-frame.d.ts.map +1 -0
  269. package/dist/systems/window/corner-frame.js +17 -0
  270. package/dist/systems/window/window-system.d.ts +1 -1
  271. package/dist/systems/window/window-system.d.ts.map +1 -1
  272. package/dist/systems/window/window-system.js +71 -27
  273. package/package.json +10 -10
  274. package/dist/systems/fence/fence-system.d.ts +0 -22
  275. package/dist/systems/fence/fence-system.d.ts.map +0 -1
  276. package/dist/systems/fence/fence-system.js +0 -357
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { create } from 'zustand';
3
3
  import { persist } from 'zustand/middleware';
4
- import { SCENE_THEME_IDS } from '../lib/scene-themes';
4
+ import { getSceneTheme, projectSceneTheme, SCENE_THEME_IDS, } from '../lib/scene-themes';
5
5
  const CAMERA_MODES = ['perspective', 'orthographic'];
6
6
  const RENDER_SHADINGS = ['solid', 'rendered'];
7
7
  const COLOR_PRESETS = ['clay', 'white', 'mono', 'blueprint'];
@@ -89,7 +89,24 @@ function normalizeProjectPreferences(value) {
89
89
  if (!preferences || typeof preferences !== 'object' || Array.isArray(preferences))
90
90
  continue;
91
91
  const record = preferences;
92
+ const look = record.look && typeof record.look === 'object'
93
+ ? record.look
94
+ : undefined;
92
95
  next[projectId] = {
96
+ ...(look
97
+ ? {
98
+ look: {
99
+ ...(typeof look.theme === 'string' && SCENE_THEME_IDS.includes(look.theme)
100
+ ? { theme: look.theme }
101
+ : {}),
102
+ ...(typeof look.edges === 'string' && EDGE_MODES.includes(look.edges)
103
+ ? { edges: look.edges }
104
+ : {}),
105
+ ...(typeof look.shadows === 'boolean' ? { shadows: look.shadows } : {}),
106
+ shadingByContext: normalizeShadingByContext(look.shadingByContext),
107
+ },
108
+ }
109
+ : {}),
93
110
  ...(typeof record.showScans === 'boolean' ? { showScans: record.showScans } : {}),
94
111
  ...(typeof record.showGuides === 'boolean' ? { showGuides: record.showGuides } : {}),
95
112
  ...(typeof record.showGrid === 'boolean' ? { showGrid: record.showGrid } : {}),
@@ -120,6 +137,47 @@ function normalizePersistedViewerState(value) {
120
137
  projectPreferences: normalizeProjectPreferences(state.projectPreferences),
121
138
  };
122
139
  }
140
+ /** The owner's pick (16 Oct): the section cut's clean edge over earth walls, and the simple fade. */
141
+ export const DEFAULT_UNDERGROUND_LOOK = {
142
+ edge: 'section-earth',
143
+ transition: 'fade',
144
+ };
145
+ function resolveLook(state, patch) {
146
+ const next = { ...state, ...patch };
147
+ const local = next.projectPreferences[next.projectId ?? '']?.look;
148
+ const savedTheme = local?.theme ?? next.projectPresentation?.theme ?? next.lookDefaults.theme;
149
+ const theme = getSceneTheme(next.previewSceneTheme ?? savedTheme);
150
+ return {
151
+ ...patch,
152
+ savedSceneTheme: savedTheme,
153
+ sceneTheme: theme.id,
154
+ sceneAppearance: projectSceneTheme(theme, next.previewSceneTheme === null && !local?.theme ? next.projectPresentation : undefined),
155
+ shading: local?.shadingByContext?.[next.renderContext] ??
156
+ next.projectPresentation?.shading ??
157
+ next.shadingByContext[next.renderContext] ??
158
+ next.contextDefaultShading,
159
+ edges: local?.edges ?? next.projectPresentation?.edges ?? next.lookDefaults.edges,
160
+ shadows: local?.shadows ?? next.projectPresentation?.shadows ?? next.lookDefaults.shadows,
161
+ };
162
+ }
163
+ function chooseLook(state, look) {
164
+ const key = state.projectId ?? '';
165
+ const previous = state.projectPreferences[key];
166
+ const { shadingByContext, ...defaults } = look;
167
+ return resolveLook(state, {
168
+ ...(!state.projectId
169
+ ? {
170
+ lookDefaults: { ...state.lookDefaults, ...defaults },
171
+ shadingByContext: { ...state.shadingByContext, ...shadingByContext },
172
+ }
173
+ : {}),
174
+ projectPreferences: {
175
+ ...state.projectPreferences,
176
+ [key]: { ...previous, look: { ...previous?.look, ...look } },
177
+ },
178
+ ...('theme' in look ? { previewSceneTheme: null } : {}),
179
+ });
180
+ }
123
181
  const useViewer = create()(persist((set) => ({
124
182
  selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [] },
125
183
  focusedUnitId: null,
@@ -134,6 +192,15 @@ const useViewer = create()(persist((set) => ({
134
192
  }
135
193
  return { externalSelectedIds: ids };
136
194
  }),
195
+ pointedIds: [],
196
+ setPointedIds: (ids) => set((state) => {
197
+ const next = ids ?? [];
198
+ if (state.pointedIds.length === next.length &&
199
+ state.pointedIds.every((id, index) => id === next[index])) {
200
+ return state;
201
+ }
202
+ return { pointedIds: next };
203
+ }),
137
204
  hoverHighlightMode: 'default',
138
205
  setHoverHighlightMode: (mode) => set((state) => (state.hoverHighlightMode === mode ? state : { hoverHighlightMode: mode })),
139
206
  hoveredId: null,
@@ -141,9 +208,24 @@ const useViewer = create()(persist((set) => ({
141
208
  cameraMode: 'perspective',
142
209
  setCameraMode: (mode) => set({ cameraMode: mode }),
143
210
  sceneTheme: 'studio',
144
- setSceneTheme: (id) => set({ sceneTheme: id }),
211
+ sceneAppearance: getSceneTheme('studio'),
212
+ lookDefaults: { theme: 'studio', edges: 'soft', shadows: true },
213
+ projectPresentation: undefined,
214
+ setProjectPresentation: (projectPresentation) => set((state) => resolveLook(state, { projectPresentation })),
215
+ resetProjectLook: () => set((state) => {
216
+ const projectPreferences = { ...state.projectPreferences };
217
+ const key = state.projectId ?? '';
218
+ const { look: _look, ...rest } = projectPreferences[key] ?? {};
219
+ projectPreferences[key] = rest;
220
+ return resolveLook(state, { projectPreferences, previewSceneTheme: null });
221
+ }),
222
+ previewSceneTheme: null,
223
+ contextDefaultShading: 'rendered',
224
+ savedSceneTheme: 'studio',
225
+ setSceneTheme: (id) => set((state) => chooseLook(state, { theme: id })),
226
+ showSceneTheme: (id) => set((state) => resolveLook(state, { previewSceneTheme: id })),
145
227
  renderContext: 'editor',
146
- setRenderContext: (context) => set({ renderContext: context }),
228
+ setRenderContext: (context, defaultShading = 'rendered') => set((state) => resolveLook(state, { renderContext: context, contextDefaultShading: defaultShading })),
147
229
  isExporting: false,
148
230
  setExporting: (value) => set({ isExporting: value }),
149
231
  itemLoadFailures: {},
@@ -161,18 +243,20 @@ const useViewer = create()(persist((set) => ({
161
243
  setRenderPaused: (value) => set({ renderPaused: value }),
162
244
  shading: 'rendered',
163
245
  shadingByContext: {},
164
- setShading: (shading) => set((state) => ({
165
- shading,
166
- shadingByContext: { ...state.shadingByContext, [state.renderContext]: shading },
246
+ setShading: (shading) => set((state) => chooseLook(state, {
247
+ shadingByContext: {
248
+ ...state.projectPreferences[state.projectId ?? '']?.look?.shadingByContext,
249
+ [state.renderContext]: shading,
250
+ },
167
251
  })),
168
252
  textures: true,
169
253
  setTextures: (textures) => set({ textures }),
170
254
  colorPreset: 'clay',
171
255
  setColorPreset: (preset) => set({ colorPreset: preset }),
172
256
  edges: 'soft',
173
- setEdges: (edges) => set({ edges }),
257
+ setEdges: (edges) => set((state) => chooseLook(state, { edges })),
174
258
  shadows: true,
175
- setShadows: (shadows) => set({ shadows }),
259
+ setShadows: (shadows) => set((state) => chooseLook(state, { shadows })),
176
260
  unit: detectDefaultUnit(),
177
261
  metricNotation: 'meters',
178
262
  unitExplicit: false,
@@ -180,6 +264,22 @@ const useViewer = create()(persist((set) => ({
180
264
  setMetricNotation: (metricNotation) => set({ unit: 'metric', metricNotation, unitExplicit: true }),
181
265
  levelMode: 'stacked',
182
266
  setLevelMode: (mode) => set({ levelMode: mode }),
267
+ hideLevelsAboveSelection: false,
268
+ levelsRevealing: [],
269
+ nodesRevealing: [],
270
+ belowGrade: false,
271
+ gradeStanding: 'at-grade',
272
+ groundWindowBox: null,
273
+ groundWindowVeiled: false,
274
+ groundMode: 'auto',
275
+ setGroundMode: (mode) => set({ groundMode: mode }),
276
+ hasLevelBelowGrade: false,
277
+ diveAvailable: false,
278
+ eyeUnderGround: false,
279
+ pluginFoundationLevelIds: [],
280
+ ambianceActive: false,
281
+ undergroundLook: DEFAULT_UNDERGROUND_LOOK,
282
+ setUndergroundLook: (look) => set((state) => ({ undergroundLook: { ...state.undergroundLook, ...look } })),
183
283
  wallMode: 'up',
184
284
  setWallMode: (mode) => set({ wallMode: mode }),
185
285
  showScans: true,
@@ -234,16 +334,20 @@ const useViewer = create()(persist((set) => ({
234
334
  setInkOpacity: (opacity) => set({ inkOpacity: opacity }),
235
335
  projectId: null,
236
336
  setProjectId: (id) => set((state) => {
237
- if (!id)
238
- return { projectId: id };
239
- const prefs = state.projectPreferences?.[id] || {};
240
- return {
337
+ if (id === state.projectId)
338
+ return state;
339
+ const prefs = (id && state.projectPreferences?.[id]) || {};
340
+ const { '': _unscoped, ...projectPreferences } = state.projectPreferences;
341
+ return resolveLook(state, {
241
342
  projectId: id,
343
+ projectPreferences,
344
+ projectPresentation: undefined,
345
+ previewSceneTheme: null,
242
346
  showScans: prefs.showScans ?? true,
243
347
  showGuides: prefs.showGuides ?? true,
244
348
  showGrid: prefs.showGrid ?? true,
245
349
  showMeasurements: prefs.showMeasurements ?? true,
246
- };
350
+ });
247
351
  }),
248
352
  projectPreferences: {},
249
353
  setSelection: (updates) => set((state) => {
@@ -295,25 +399,64 @@ const useViewer = create()(persist((set) => ({
295
399
  setInputDragging: (dragging) => set({ inputDragging: dragging }),
296
400
  }), {
297
401
  name: 'viewer-preferences',
298
- merge: (persistedState, currentState) => ({
299
- ...currentState,
300
- ...normalizePersistedViewerState(persistedState),
301
- }),
402
+ merge: (persistedState, currentState) => {
403
+ const persisted = normalizePersistedViewerState(persistedState);
404
+ return {
405
+ ...currentState,
406
+ ...persisted,
407
+ ...resolveLook(currentState, {
408
+ ...persisted,
409
+ lookDefaults: {
410
+ theme: persisted.sceneTheme ?? currentState.lookDefaults.theme,
411
+ edges: persisted.edges ?? currentState.lookDefaults.edges,
412
+ shadows: persisted.shadows ?? currentState.lookDefaults.shadows,
413
+ },
414
+ }),
415
+ };
416
+ },
302
417
  partialize: (state) => ({
303
418
  cameraMode: state.cameraMode,
304
- sceneTheme: state.sceneTheme,
419
+ sceneTheme: state.lookDefaults.theme,
305
420
  shadingByContext: state.shadingByContext,
306
421
  textures: state.textures,
307
422
  colorPreset: state.colorPreset,
308
- edges: state.edges,
309
- shadows: state.shadows,
423
+ edges: state.lookDefaults.edges,
424
+ shadows: state.lookDefaults.shadows,
310
425
  ...(state.unitExplicit ? { unit: state.unit } : {}),
311
426
  metricNotation: state.metricNotation,
312
427
  levelMode: state.levelMode,
313
428
  wallMode: state.wallMode,
314
- projectPreferences: state.projectPreferences,
429
+ projectPreferences: Object.fromEntries(Object.entries(state.projectPreferences).filter(([key]) => key !== '')),
315
430
  }),
316
431
  }));
432
+ /**
433
+ * Whether the site's ground is drawn as a veil wholesale: a level below grade is shown, the
434
+ * ground on auto. A level only partly below grade is the exception — there the ground window
435
+ * veils just the ground in the way, and the rest stays solid. The walkthrough walks on its
436
+ * floors: its ground is always solid, whatever level the overview had shown.
437
+ */
438
+ export function groundVeiled(state) {
439
+ return (state.belowGrade &&
440
+ state.groundMode === 'auto' &&
441
+ state.gradeStanding !== 'partly-below-grade' &&
442
+ state.walkthroughMode !== true);
443
+ }
444
+ /** Whether any ground is veiled now, wholesale or through the eye's window. */
445
+ export function anyGroundVeiled(state) {
446
+ return (groundVeiled(state) ||
447
+ (state.groundMode === 'auto' && state.groundWindowVeiled && state.walkthroughMode !== true));
448
+ }
449
+ /**
450
+ * Whether the site draws its own ground. `showGround: false` says other geometry supplies the
451
+ * ground (an authored landscape), which stands on a level above a basement; while a level below
452
+ * grade is shown, the level display hides the storeys above it (`belowGrade`) and the landscape
453
+ * with them. The site's ground stands in then, with its veil, hole and pit, so the basement is
454
+ * seen through an opening and not through nothing. With a level at or above grade shown, the
455
+ * landscape is shown and `showGround` is the author's.
456
+ */
457
+ export function siteGroundShown(site, state) {
458
+ return site.showGround !== false || state.belowGrade;
459
+ }
317
460
  /** Apply an authoritative country code (e.g. IP-derived by the host app) as
318
461
  * the unit default. Stronger signal than the timezone heuristic used at store
319
462
  * creation, but still a default: it never overrides an explicit user choice
@@ -1,6 +1,19 @@
1
- import { type CeilingNode } from '@pascal-app/core';
1
+ import { type CeilingNode, type CeilingSurfaceCell, useScene } from '@pascal-app/core';
2
2
  import * as THREE from 'three';
3
+ /** Name of the meshes a ceiling draws its paint regions with, one per region. */
4
+ export declare const CEILING_REGION_MESH = "ceiling-region";
5
+ /**
6
+ * The material a ceiling draws a region finish with. The ceiling's renderer
7
+ * owns materials (theme, textures, scene materials) and leaves this on the
8
+ * registered mesh; the system builds geometry and asks it.
9
+ */
10
+ export type CeilingRegionMaterial = (finish: CeilingSurfaceCell['finish']) => THREE.Material;
11
+ type SceneNodes = ReturnType<typeof useScene.getState>['nodes'];
3
12
  export declare const CeilingSystem: () => null;
13
+ /**
14
+ * Updates the geometry for a single ceiling
15
+ */
16
+ export declare function updateCeilingGeometry(node: CeilingNode, mesh: THREE.Mesh, extraHoles?: Array<Array<[number, number]>>, nodes?: SceneNodes): void;
4
17
  /**
5
18
  * Generates flat ceiling geometry from polygon (no extrusion).
6
19
  *
@@ -9,4 +22,5 @@ export declare const CeilingSystem: () => null;
9
22
  * stored on the node — they are recomputed on every rebuild.
10
23
  */
11
24
  export declare function generateCeilingGeometry(ceilingNode: CeilingNode, extraHoles?: Array<Array<[number, number]>>): THREE.BufferGeometry;
25
+ export {};
12
26
  //# sourceMappingURL=ceiling-system.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ceiling-system.d.ts","sourceRoot":"","sources":["../../../src/systems/ceiling/ceiling-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,WAAW,EAOjB,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAgB9B,eAAO,MAAM,aAAa,YA4BzB,CAAA;AAgED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,WAAW,EACxB,UAAU,GAAE,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAM,GAC9C,KAAK,CAAC,cAAc,CA6DtB"}
1
+ {"version":3,"file":"ceiling-system.d.ts","sourceRoot":"","sources":["../../../src/systems/ceiling/ceiling-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,WAAW,EAChB,KAAK,kBAAkB,EAYvB,QAAQ,EACT,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,iFAAiF;AACjF,eAAO,MAAM,mBAAmB,mBAAmB,CAAA;AAEnD;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC,QAAQ,CAAC,KAAK,KAAK,CAAC,QAAQ,CAAA;AAE5F,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAA;AAa/D,eAAO,MAAM,aAAa,YA4BzB,CAAA;AAsCD;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,WAAW,EACjB,IAAI,EAAE,KAAK,CAAC,IAAI,EAChB,UAAU,GAAE,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAM,EAC/C,KAAK,GAAE,UAAsC,QA8C9C;AAuDD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,WAAW,EACxB,UAAU,GAAE,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAM,GAC9C,KAAK,CAAC,cAAc,CA6DtB"}
@@ -1,7 +1,10 @@
1
- import { getEffectiveNode, nodeRegistry, resolveCeilingHeight, sceneRegistry, useLiveTransforms, useScene, } from '@pascal-app/core';
1
+ import { CEILING_DRAW_OFFSET, CEILING_SURFACE_ROLE, ceilingPaintRegions, computeCeilingSurfaceCells, getEffectiveCutterNode, getEffectiveNode, hostedCutterHoles, isScriptedNode, nodeRegistry, resolveCeilingHeight, sceneRegistry, useLiveTransforms, useScene, } from '@pascal-app/core';
2
2
  import { useFrame } from '@react-three/fiber';
3
3
  import * as THREE from 'three';
4
+ import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
4
5
  import { mergeSurfaceHolePolygons } from '../surface-hole-geometry';
6
+ /** Name of the meshes a ceiling draws its paint regions with, one per region. */
7
+ export const CEILING_REGION_MESH = 'ceiling-region';
5
8
  function ensureUv2Attribute(geometry) {
6
9
  const uv = geometry.getAttribute('uv');
7
10
  if (!uv)
@@ -49,29 +52,53 @@ export const CeilingSystem = () => {
49
52
  * participate just by declaring `capabilities.ceilingCut` on its definition.
50
53
  */
51
54
  function collectCeilingHoles(ceiling, nodes) {
52
- const holes = [];
55
+ // Only the ceiling's own children can bind to it.
56
+ const hosted = { [ceiling.id]: ceiling };
57
+ for (const childId of ceiling.children ?? []) {
58
+ const child = nodes[childId];
59
+ if (child)
60
+ hosted[child.id] = getEffectiveCutterNode(child);
61
+ }
62
+ const holes = hostedCutterHoles(ceiling, hosted);
53
63
  for (const childId of ceiling.children ?? []) {
54
64
  const child = nodes[childId];
55
65
  if (!child)
56
66
  continue;
67
+ if (isScriptedNode(child) && child.source.manifest.cutters?.length)
68
+ continue;
57
69
  const def = nodeRegistry.get(child.type);
58
70
  const hole = def?.capabilities?.ceilingCut?.buildCeilingHole(child);
59
71
  if (hole)
60
72
  holes.push(hole);
61
73
  }
74
+ for (const [, def] of nodeRegistry.entries())
75
+ holes.push(...(def.capabilities?.ceilingCut?.holesFor?.(ceiling) ?? []));
62
76
  return holes;
63
77
  }
64
78
  /**
65
79
  * Updates the geometry for a single ceiling
66
80
  */
67
- function updateCeilingGeometry(node, mesh, extraHoles = [], nodes = useScene.getState().nodes) {
81
+ export function updateCeilingGeometry(node, mesh, extraHoles = [], nodes = useScene.getState().nodes) {
68
82
  const newGeo = generateCeilingGeometry(node, extraHoles);
83
+ // Painted regions partition the underside: the ceiling's own mesh keeps what
84
+ // no region covers, and each region draws as its own single-material mesh,
85
+ // so batching buckets them by material like any other surface.
86
+ const regions = node.polygon.length >= 3 ? ceilingPaintRegions(node, nodes) : [];
87
+ const cells = regions.length
88
+ ? computeCeilingSurfaceCells(node.polygon, [...mergeSurfaceHolePolygons(node.holes || []), ...extraHoles], regions)
89
+ : null;
90
+ const surface = cells?.find((cell) => cell.role === CEILING_SURFACE_ROLE);
69
91
  mesh.geometry.dispose();
70
- mesh.geometry = newGeo;
92
+ mesh.geometry = cells ? flatCeilingGeometry(surface?.polygons ?? []) : newGeo;
93
+ mesh.userData.paintRole = CEILING_SURFACE_ROLE;
94
+ syncCeilingRegionMeshes(mesh, (cells ?? []).filter((cell) => cell.role !== CEILING_SURFACE_ROLE));
71
95
  const gridMesh = mesh.getObjectByName('ceiling-grid');
72
96
  if (gridMesh) {
73
97
  gridMesh.geometry.dispose();
74
- gridMesh.geometry = newGeo.clone();
98
+ gridMesh.geometry = cells ? newGeo : newGeo.clone();
99
+ }
100
+ else if (cells) {
101
+ newGeo.dispose();
75
102
  }
76
103
  // Position at the ceiling height and reset X/Z so live-drag mesh
77
104
  // offsets (set by move tools during the drag) don't leak into the
@@ -85,7 +112,62 @@ function updateCeilingGeometry(node, mesh, extraHoles = [], nodes = useScene.get
85
112
  // follows-mode ceiling re-parks under the current plane on every rebuild
86
113
  // (level-height edits / covering-slab changes dirty-mark ceilings).
87
114
  // Slight offset to avoid z-fighting with upper-level slabs.
88
- mesh.position.y = resolveCeilingHeight(node, nodes) - 0.01 + (liveTransform?.position[1] ?? 0);
115
+ mesh.position.y =
116
+ resolveCeilingHeight(node, nodes) - CEILING_DRAW_OFFSET + (liveTransform?.position[1] ?? 0);
117
+ }
118
+ /**
119
+ * One child mesh per region cell, reused by position so a repaint keeps its
120
+ * mesh; surplus meshes go. The material comes from the renderer's resolver.
121
+ */
122
+ function syncCeilingRegionMeshes(mesh, cells) {
123
+ const existing = mesh.children.filter((child) => child.isMesh && child.name === CEILING_REGION_MESH);
124
+ const resolve = mesh.userData.ceilingRegionMaterial;
125
+ cells.forEach((cell, index) => {
126
+ let region = existing[index];
127
+ if (!region) {
128
+ region = new THREE.Mesh();
129
+ region.name = CEILING_REGION_MESH;
130
+ region.castShadow = true;
131
+ region.receiveShadow = true;
132
+ mesh.add(region);
133
+ }
134
+ else
135
+ region.geometry.dispose();
136
+ region.geometry = flatCeilingGeometry(cell.polygons);
137
+ region.userData.paintRole = cell.role;
138
+ region.userData.finish = cell.finish;
139
+ region.userData.__fromGeometry = true;
140
+ region.material = resolve?.(cell.finish) ?? mesh.material;
141
+ });
142
+ for (const surplus of existing.slice(cells.length)) {
143
+ surplus.geometry.dispose();
144
+ surplus.removeFromParent();
145
+ }
146
+ }
147
+ /** Flat geometry for level-XZ pieces, laid out the way `generateCeilingGeometry` lays a ceiling. */
148
+ function flatCeilingGeometry(pieces) {
149
+ const parts = pieces
150
+ .filter((piece) => piece.outer.length >= 3)
151
+ .map((piece) => {
152
+ const shape = new THREE.Shape(piece.outer.map(([x, z]) => new THREE.Vector2(x, -z)));
153
+ for (const hole of piece.holes)
154
+ if (hole.length >= 3)
155
+ shape.holes.push(new THREE.Path(hole.map(([x, z]) => new THREE.Vector2(x, -z))));
156
+ const geometry = new THREE.ShapeGeometry(shape);
157
+ geometry.rotateX(-Math.PI / 2);
158
+ geometry.computeVertexNormals();
159
+ ensureUv2Attribute(geometry);
160
+ return geometry;
161
+ });
162
+ if (parts.length === 0)
163
+ return generateCeilingGeometry({ polygon: [] });
164
+ if (parts.length === 1)
165
+ return parts[0];
166
+ const merged = mergeGeometries(parts) ?? parts[0];
167
+ for (const part of parts)
168
+ if (part !== merged)
169
+ part.dispose();
170
+ return merged;
89
171
  }
90
172
  /**
91
173
  * Generates flat ceiling geometry from polygon (no extrusion).
@@ -0,0 +1,16 @@
1
+ import { type DustPool } from '../../lib/construction-dust';
2
+ /**
3
+ * Draws a reveal's dust: one instanced mesh of camera-facing soft discs, as
4
+ * many as the pool holds, on the grid layer: inside the scene pass, so walls
5
+ * in front hide it (on the overlay layer it showed through them, Victor run
6
+ * 12), with no depth written, so the ink never outlines it; thumbnails leave
7
+ * it out. Renders nothing itself; it owns the mesh, adds it to the scene and
8
+ * disposes it on unmount.
9
+ */
10
+ export declare function ConstructionDust({ pool, clock }: {
11
+ pool: DustPool;
12
+ clock: {
13
+ current: number;
14
+ };
15
+ }): null;
16
+ //# sourceMappingURL=construction-dust.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"construction-dust.d.ts","sourceRoot":"","sources":["../../../src/systems/construction-reveal/construction-dust.tsx"],"names":[],"mappings":"AAcA,OAAO,EAAE,KAAK,QAAQ,EAAkB,MAAM,6BAA6B,CAAA;AAI3E;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,QA8E/F"}
@@ -0,0 +1,92 @@
1
+ 'use client';
2
+ import { emitter } from '@pascal-app/core';
3
+ import { useFrame, useThree } from '@react-three/fiber';
4
+ import { useEffect, useMemo } from 'react';
5
+ import { DynamicDrawUsage, InstancedBufferAttribute, InstancedMesh, Matrix4, PlaneGeometry, Quaternion, Vector3, } from 'three';
6
+ import { dustSpriteSize } from '../../lib/construction-dust';
7
+ import { GRID_LAYER } from '../../lib/layers';
8
+ import { createDustMaterial } from './dust-material';
9
+ /**
10
+ * Draws a reveal's dust: one instanced mesh of camera-facing soft discs, as
11
+ * many as the pool holds, on the grid layer: inside the scene pass, so walls
12
+ * in front hide it (on the overlay layer it showed through them, Victor run
13
+ * 12), with no depth written, so the ink never outlines it; thumbnails leave
14
+ * it out. Renders nothing itself; it owns the mesh, adds it to the scene and
15
+ * disposes it on unmount.
16
+ */
17
+ export function ConstructionDust({ pool, clock }) {
18
+ const scene = useThree((state) => state.scene);
19
+ const camera = useThree((state) => state.camera);
20
+ const height = useThree((state) => state.size.height);
21
+ const dust = useMemo(() => {
22
+ const geometry = new PlaneGeometry(1, 1);
23
+ const alpha = new InstancedBufferAttribute(new Float32Array(pool.capacity), 1);
24
+ alpha.setUsage(DynamicDrawUsage);
25
+ const material = createDustMaterial(alpha);
26
+ const mesh = new InstancedMesh(geometry, material, pool.capacity);
27
+ mesh.name = 'construction-dust';
28
+ mesh.instanceMatrix.setUsage(DynamicDrawUsage);
29
+ mesh.layers.set(GRID_LAYER);
30
+ mesh.frustumCulled = false;
31
+ mesh.raycast = () => { };
32
+ mesh.count = 0;
33
+ mesh.visible = false;
34
+ return { mesh, alpha, geometry, material };
35
+ }, [pool]);
36
+ useEffect(() => {
37
+ scene.add(dust.mesh);
38
+ // A capture clones the scene in the same tick: no dust in it, whatever the camera layers.
39
+ const hide = () => {
40
+ dust.mesh.visible = false;
41
+ dust.mesh.count = 0;
42
+ };
43
+ emitter.on('thumbnail:before-capture', hide);
44
+ return () => {
45
+ emitter.off('thumbnail:before-capture', hide);
46
+ scene.remove(dust.mesh);
47
+ dust.geometry.dispose();
48
+ dust.material.dispose();
49
+ dust.mesh.dispose();
50
+ };
51
+ }, [dust, scene]);
52
+ const write = useMemo(() => {
53
+ const matrix = new Matrix4();
54
+ const position = new Vector3();
55
+ const size = new Vector3();
56
+ const facing = new Quaternion();
57
+ return {
58
+ facing,
59
+ sprite: (slot, x, y, z, scale, alpha) => {
60
+ position.set(x, y, z);
61
+ const drawn = dustSpriteSize(scale, camera, height, position);
62
+ size.set(drawn, drawn, drawn);
63
+ dust.mesh.setMatrixAt(slot, matrix.compose(position, facing, size));
64
+ dust.alpha.setX(slot, alpha);
65
+ },
66
+ };
67
+ }, [dust, camera, height]);
68
+ useFrame(() => {
69
+ const { mesh, alpha } = dust;
70
+ if (pool.takeWarm()) {
71
+ // One sprite with no size and no alpha: drawn, so its pipeline is built, and nothing to see.
72
+ write.facing.copy(camera.quaternion);
73
+ write.sprite(0, 0, 0, 0, 0, 0);
74
+ mesh.count = 1;
75
+ mesh.visible = true;
76
+ mesh.instanceMatrix.needsUpdate = true;
77
+ alpha.needsUpdate = true;
78
+ return;
79
+ }
80
+ if (!mesh.visible && pool.quietAt(clock.current))
81
+ return;
82
+ write.facing.copy(camera.quaternion);
83
+ const count = pool.step(clock.current, write.sprite);
84
+ mesh.count = count;
85
+ mesh.visible = count > 0;
86
+ if (count === 0)
87
+ return;
88
+ mesh.instanceMatrix.needsUpdate = true;
89
+ alpha.needsUpdate = true;
90
+ });
91
+ return null;
92
+ }