@pascal-app/viewer 1.0.6 → 1.1.0

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 (238) hide show
  1. package/README.md +54 -21
  2. package/dist/components/viewer/daylight-atmosphere.d.ts +9 -0
  3. package/dist/components/viewer/daylight-atmosphere.d.ts.map +1 -0
  4. package/dist/components/viewer/daylight-atmosphere.js +315 -0
  5. package/dist/components/viewer/drone-controller.d.ts +15 -0
  6. package/dist/components/viewer/drone-controller.d.ts.map +1 -0
  7. package/dist/components/viewer/drone-controller.js +194 -0
  8. package/dist/components/viewer/glb-interactive.d.ts +4 -18
  9. package/dist/components/viewer/glb-interactive.d.ts.map +1 -1
  10. package/dist/components/viewer/glb-interactive.js +32 -268
  11. package/dist/components/viewer/glb-replace-instances.d.ts +6 -3
  12. package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -1
  13. package/dist/components/viewer/glb-replace-instances.js +5 -4
  14. package/dist/components/viewer/glb-scene-motion.d.ts +8 -0
  15. package/dist/components/viewer/glb-scene-motion.d.ts.map +1 -0
  16. package/dist/components/viewer/glb-scene-motion.js +57 -0
  17. package/dist/components/viewer/glb-scene.d.ts +5 -1
  18. package/dist/components/viewer/glb-scene.d.ts.map +1 -1
  19. package/dist/components/viewer/glb-scene.js +290 -49
  20. package/dist/components/viewer/glb-selection-callout.d.ts +17 -0
  21. package/dist/components/viewer/glb-selection-callout.d.ts.map +1 -0
  22. package/dist/components/viewer/glb-selection-callout.js +54 -0
  23. package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
  24. package/dist/components/viewer/glb-walkthrough-controller.js +6 -3
  25. package/dist/components/viewer/index.d.ts +5 -0
  26. package/dist/components/viewer/index.d.ts.map +1 -1
  27. package/dist/components/viewer/index.js +38 -10
  28. package/dist/components/viewer/lights.d.ts.map +1 -1
  29. package/dist/components/viewer/lights.js +192 -5
  30. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  31. package/dist/components/viewer/post-processing.js +150 -94
  32. package/dist/components/viewer/registered-systems.d.ts.map +1 -1
  33. package/dist/components/viewer/registered-systems.js +2 -2
  34. package/dist/components/viewer/scene-environment.js +4 -5
  35. package/dist/components/viewer/selection-callout.d.ts +51 -0
  36. package/dist/components/viewer/selection-callout.d.ts.map +1 -0
  37. package/dist/components/viewer/selection-callout.js +197 -0
  38. package/dist/components/viewer/unsupported-gpu-fallback.d.ts.map +1 -1
  39. package/dist/components/viewer/unsupported-gpu-fallback.js +21 -1
  40. package/dist/components/viewer/use-daylight-rooms.d.ts +13 -0
  41. package/dist/components/viewer/use-daylight-rooms.d.ts.map +1 -0
  42. package/dist/components/viewer/use-daylight-rooms.js +278 -0
  43. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  44. package/dist/components/viewer/viewer-camera.js +12 -1
  45. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  46. package/dist/hooks/use-gltf-ktx2.js +2 -1
  47. package/dist/index.d.ts +26 -8
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +28 -8
  50. package/dist/lib/asset-url.d.ts +0 -1
  51. package/dist/lib/asset-url.d.ts.map +1 -1
  52. package/dist/lib/asset-url.js +3 -3
  53. package/dist/lib/daylight/ground.d.ts +21 -0
  54. package/dist/lib/daylight/ground.d.ts.map +1 -0
  55. package/dist/lib/daylight/ground.js +53 -0
  56. package/dist/lib/daylight/location.d.ts +32 -0
  57. package/dist/lib/daylight/location.d.ts.map +1 -0
  58. package/dist/lib/daylight/location.js +74 -0
  59. package/dist/lib/daylight/look.d.ts +33 -0
  60. package/dist/lib/daylight/look.d.ts.map +1 -0
  61. package/dist/lib/daylight/look.js +13 -0
  62. package/dist/lib/daylight/material-hooks.d.ts +2 -0
  63. package/dist/lib/daylight/material-hooks.d.ts.map +1 -0
  64. package/dist/lib/daylight/material-hooks.js +150 -0
  65. package/dist/lib/daylight/room-context.d.ts +12 -0
  66. package/dist/lib/daylight/room-context.d.ts.map +1 -0
  67. package/dist/lib/daylight/room-context.js +54 -0
  68. package/dist/lib/daylight/room-map.d.ts +124 -0
  69. package/dist/lib/daylight/room-map.d.ts.map +1 -0
  70. package/dist/lib/daylight/room-map.js +630 -0
  71. package/dist/lib/daylight/room-scene.d.ts +56 -0
  72. package/dist/lib/daylight/room-scene.d.ts.map +1 -0
  73. package/dist/lib/daylight/room-scene.js +391 -0
  74. package/dist/lib/daylight/rooms.d.ts +292 -0
  75. package/dist/lib/daylight/rooms.d.ts.map +1 -0
  76. package/dist/lib/daylight/rooms.js +1268 -0
  77. package/dist/lib/daylight/runtime.d.ts +33 -0
  78. package/dist/lib/daylight/runtime.d.ts.map +1 -0
  79. package/dist/lib/daylight/runtime.js +42 -0
  80. package/dist/lib/daylight/sky.d.ts +26 -0
  81. package/dist/lib/daylight/sky.d.ts.map +1 -0
  82. package/dist/lib/daylight/sky.js +217 -0
  83. package/dist/lib/daylight/solar.d.ts +46 -0
  84. package/dist/lib/daylight/solar.d.ts.map +1 -0
  85. package/dist/lib/daylight/solar.js +156 -0
  86. package/dist/lib/device-tier.d.ts +29 -0
  87. package/dist/lib/device-tier.d.ts.map +1 -0
  88. package/dist/lib/device-tier.js +71 -0
  89. package/dist/lib/dispose-object3d.d.ts.map +1 -1
  90. package/dist/lib/dispose-object3d.js +4 -2
  91. package/dist/lib/drone-flight.d.ts +44 -0
  92. package/dist/lib/drone-flight.d.ts.map +1 -0
  93. package/dist/lib/drone-flight.js +121 -0
  94. package/dist/lib/editor-highlight.d.ts +16 -0
  95. package/dist/lib/editor-highlight.d.ts.map +1 -0
  96. package/dist/lib/editor-highlight.js +33 -0
  97. package/dist/lib/foundation-materials.d.ts.map +1 -1
  98. package/dist/lib/foundation-materials.js +2 -4
  99. package/dist/lib/glb-merge.d.ts +23 -0
  100. package/dist/lib/glb-merge.d.ts.map +1 -0
  101. package/dist/lib/glb-merge.js +184 -0
  102. package/dist/lib/hardware-geometry.d.ts +21 -0
  103. package/dist/lib/hardware-geometry.d.ts.map +1 -0
  104. package/dist/lib/hardware-geometry.js +283 -0
  105. package/dist/lib/ktx2-loader.d.ts.map +1 -1
  106. package/dist/lib/ktx2-loader.js +2 -1
  107. package/dist/lib/layer-pass.d.ts +2 -0
  108. package/dist/lib/layer-pass.d.ts.map +1 -1
  109. package/dist/lib/layer-pass.js +20 -0
  110. package/dist/lib/materials.d.ts.map +1 -1
  111. package/dist/lib/materials.js +50 -23
  112. package/dist/lib/measurement-label.d.ts +4 -0
  113. package/dist/lib/measurement-label.d.ts.map +1 -0
  114. package/dist/lib/measurement-label.js +54 -0
  115. package/dist/lib/measurement-presentation.d.ts +5 -0
  116. package/dist/lib/measurement-presentation.d.ts.map +1 -0
  117. package/dist/lib/measurement-presentation.js +8 -0
  118. package/dist/lib/merged-outline-node.d.ts.map +1 -1
  119. package/dist/lib/merged-outline-node.js +7 -1
  120. package/dist/lib/occlusion-culler.d.ts +34 -0
  121. package/dist/lib/occlusion-culler.d.ts.map +1 -0
  122. package/dist/lib/occlusion-culler.js +698 -0
  123. package/dist/lib/pointer-events.d.ts.map +1 -1
  124. package/dist/lib/pointer-events.js +3 -3
  125. package/dist/lib/post-processing-resources.d.ts +4 -0
  126. package/dist/lib/post-processing-resources.d.ts.map +1 -1
  127. package/dist/lib/post-processing-resources.js +5 -0
  128. package/dist/lib/readback-rows.d.ts +14 -0
  129. package/dist/lib/readback-rows.d.ts.map +1 -0
  130. package/dist/lib/readback-rows.js +31 -0
  131. package/dist/lib/reveal-pose.d.ts.map +1 -1
  132. package/dist/lib/reveal-pose.js +3 -0
  133. package/dist/lib/scene-matrices.d.ts +12 -0
  134. package/dist/lib/scene-matrices.d.ts.map +1 -0
  135. package/dist/lib/scene-matrices.js +287 -0
  136. package/dist/lib/scene-pass-mrt.d.ts +5 -2
  137. package/dist/lib/scene-pass-mrt.d.ts.map +1 -1
  138. package/dist/lib/scene-pass-mrt.js +7 -2
  139. package/dist/lib/scene-themes.d.ts +0 -2
  140. package/dist/lib/scene-themes.d.ts.map +1 -1
  141. package/dist/lib/scene-themes.js +0 -2
  142. package/dist/lib/scene-visibility.d.ts +2 -0
  143. package/dist/lib/scene-visibility.d.ts.map +1 -1
  144. package/dist/lib/scene-visibility.js +12 -3
  145. package/dist/lib/site-surfaces.d.ts +1 -8
  146. package/dist/lib/site-surfaces.d.ts.map +1 -1
  147. package/dist/lib/site-surfaces.js +0 -19
  148. package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
  149. package/dist/lib/snapshot-pipeline.js +24 -40
  150. package/dist/lib/texture-reference.d.ts.map +1 -1
  151. package/dist/lib/texture-reference.js +13 -10
  152. package/dist/lib/underground-ambiance.d.ts +0 -2
  153. package/dist/lib/underground-ambiance.d.ts.map +1 -1
  154. package/dist/lib/underground-ambiance.js +0 -2
  155. package/dist/lib/viewer-config.d.ts +21 -0
  156. package/dist/lib/viewer-config.d.ts.map +1 -0
  157. package/dist/lib/viewer-config.js +21 -0
  158. package/dist/lib/viewer-preference-storage.d.ts +7 -0
  159. package/dist/lib/viewer-preference-storage.d.ts.map +1 -0
  160. package/dist/lib/viewer-preference-storage.js +23 -0
  161. package/dist/store/use-item-light-pool.d.ts +3 -6
  162. package/dist/store/use-item-light-pool.d.ts.map +1 -1
  163. package/dist/store/use-item-light-pool.js +12 -0
  164. package/dist/store/use-viewer.d.ts +62 -59
  165. package/dist/store/use-viewer.d.ts.map +1 -1
  166. package/dist/store/use-viewer.js +106 -19
  167. package/dist/systems/ceiling/ceiling-system.d.ts +5 -0
  168. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  169. package/dist/systems/ceiling/ceiling-system.js +11 -1
  170. package/dist/systems/door/door-system.d.ts.map +1 -1
  171. package/dist/systems/door/door-system.js +501 -145
  172. package/dist/systems/door/leaf-profile.d.ts +11 -0
  173. package/dist/systems/door/leaf-profile.d.ts.map +1 -0
  174. package/dist/systems/door/leaf-profile.js +140 -0
  175. package/dist/systems/elevator/elevator-interaction-system.d.ts +16 -2
  176. package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -1
  177. package/dist/systems/elevator/elevator-interaction-system.js +41 -8
  178. package/dist/systems/geometry/geometry-status.d.ts +8 -0
  179. package/dist/systems/geometry/geometry-status.d.ts.map +1 -0
  180. package/dist/systems/geometry/geometry-status.js +24 -0
  181. package/dist/systems/interactive/callout-actions.d.ts +72 -0
  182. package/dist/systems/interactive/callout-actions.d.ts.map +1 -0
  183. package/dist/systems/interactive/callout-actions.js +212 -0
  184. package/dist/systems/interactive/interactive-system.d.ts +5 -1
  185. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  186. package/dist/systems/interactive/interactive-system.js +44 -121
  187. package/dist/systems/interactive/opening-settled.d.ts +16 -0
  188. package/dist/systems/interactive/opening-settled.d.ts.map +1 -0
  189. package/dist/systems/interactive/opening-settled.js +53 -0
  190. package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
  191. package/dist/systems/item-light/item-light-system.js +22 -214
  192. package/dist/systems/item-light/pooled-lights.d.ts +70 -0
  193. package/dist/systems/item-light/pooled-lights.d.ts.map +1 -0
  194. package/dist/systems/item-light/pooled-lights.js +449 -0
  195. package/dist/systems/level/level-system.d.ts.map +1 -1
  196. package/dist/systems/level/level-system.js +19 -1
  197. package/dist/systems/roof/roof-material-slots.d.ts +14 -0
  198. package/dist/systems/roof/roof-material-slots.d.ts.map +1 -0
  199. package/dist/systems/roof/roof-material-slots.js +13 -0
  200. package/dist/systems/roof/roof-materials.d.ts +25 -2
  201. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  202. package/dist/systems/roof/roof-materials.js +85 -26
  203. package/dist/systems/roof/roof-system.d.ts +34 -1
  204. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  205. package/dist/systems/roof/roof-system.js +582 -168
  206. package/dist/systems/wall/opening-casing-geometry.d.ts +14 -0
  207. package/dist/systems/wall/opening-casing-geometry.d.ts.map +1 -0
  208. package/dist/systems/wall/opening-casing-geometry.js +102 -0
  209. package/dist/systems/wall/opening-cutout-geometry.d.ts +9 -0
  210. package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -1
  211. package/dist/systems/wall/opening-cutout-geometry.js +7 -3
  212. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  213. package/dist/systems/wall/wall-materials.js +28 -35
  214. package/dist/systems/wall/wall-system.d.ts +11 -0
  215. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  216. package/dist/systems/wall/wall-system.js +86 -25
  217. package/dist/systems/wall/wall-t-junctions.d.ts +15 -0
  218. package/dist/systems/wall/wall-t-junctions.d.ts.map +1 -0
  219. package/dist/systems/wall/wall-t-junctions.js +149 -0
  220. package/dist/systems/window/window-animation-system.d.ts +3 -12
  221. package/dist/systems/window/window-animation-system.d.ts.map +1 -1
  222. package/dist/systems/window/window-animation-system.js +37 -122
  223. package/dist/systems/window/window-pose.d.ts +31 -0
  224. package/dist/systems/window/window-pose.d.ts.map +1 -0
  225. package/dist/systems/window/window-pose.js +152 -0
  226. package/dist/systems/window/window-roller-shutter.d.ts +8 -0
  227. package/dist/systems/window/window-roller-shutter.d.ts.map +1 -0
  228. package/dist/systems/window/window-roller-shutter.js +64 -0
  229. package/dist/systems/window/window-system.d.ts +2 -11
  230. package/dist/systems/window/window-system.d.ts.map +1 -1
  231. package/dist/systems/window/window-system.js +470 -303
  232. package/package.json +6 -6
  233. package/dist/systems/interactive/control-widget.d.ts +0 -10
  234. package/dist/systems/interactive/control-widget.d.ts.map +0 -1
  235. package/dist/systems/interactive/control-widget.js +0 -35
  236. package/dist/systems/interactive/procedural-controls.d.ts +0 -13
  237. package/dist/systems/interactive/procedural-controls.d.ts.map +0 -1
  238. package/dist/systems/interactive/procedural-controls.js +0 -17
@@ -0,0 +1,630 @@
1
+ import { DataTexture, FloatType, NearestFilter, RGBAFormat, RGFormat, UnsignedByteType, Vector2, } from 'three';
2
+ import { Fn, floor, If, int, ivec2, Loop, round, texture, uniform } from 'three/tsl';
3
+ export const CELL = 0.1;
4
+ /** Cells per tile side. */
5
+ export const TILE = 16;
6
+ const TILE_M = CELL * TILE;
7
+ /** Margin around a room: the 2-cell dilation plus polygon slop. */
8
+ const APRON = 0.35;
9
+ /** Cells built around a tile so its 2-pass dilation and its edges' neighbours are exact. */
10
+ const MARGIN = 3;
11
+ /** Rooms of a level closer than this (in tiles) share a cluster. */
12
+ const CLUSTER_GAP = 2;
13
+ const LEVEL_W = 256;
14
+ /** Directory cells, at most (they grow coarser on vast sites). */
15
+ const MAX_DIR_CELLS = 65536;
16
+ /** Room ids are stored as two bytes (id + 1). */
17
+ export const MAX_ROOMS = 65535;
18
+ /** WebGPU guarantees 8192 texels a side; the renderer asks for no more. */
19
+ export const DEFAULT_TEXTURE_LIMIT = 8192;
20
+ export class RoomMap {
21
+ /** The device's largest 2D texture side; set before `build`. */
22
+ textureLimit = DEFAULT_TEXTURE_LIMIT;
23
+ levels = [];
24
+ /** Level indices sorted by building, then height: the order the GPU searches in. */
25
+ order = [];
26
+ position = [];
27
+ /** Per building: its range in `order`. */
28
+ buildingRange = [];
29
+ originX = 0;
30
+ originZ = 0;
31
+ dirTiles = 8;
32
+ dirW = 0;
33
+ dirH = 0;
34
+ /** Per directory cell: the buildings whose rooms reach it. */
35
+ dirLists = [];
36
+ atlas = new Uint8Array(2);
37
+ atlasTilesW = 1;
38
+ atlasTiles = 0;
39
+ indexBytes = 0;
40
+ warnedOverflow = false;
41
+ u = {
42
+ origin: uniform(new Vector2()),
43
+ dirTiles: uniform(8, 'int'),
44
+ dirW: uniform(0, 'int'),
45
+ dirH: uniform(0, 'int'),
46
+ /** One building: its level range is the only candidate (no directory reads). */
47
+ single: uniform(1, 'int'),
48
+ singleCount: uniform(0, 'int'),
49
+ /** Binary-search steps for the tallest building. */
50
+ searchSteps: uniform(0, 'int'),
51
+ atlasTilesW: uniform(1, 'int'),
52
+ indexW: uniform(1, 'int'),
53
+ index: texture(floatTexture(new Float32Array(4), 1, 1)),
54
+ levels: texture(floatTexture(new Float32Array(8), 2, 1)),
55
+ atlas: texture(byteTexture(new Uint8Array(2), 1, 1)),
56
+ };
57
+ levelData = new Float32Array(8);
58
+ /** Rebuild from rooms. `onTile` sees every built tile (the doorway search walks room edges). */
59
+ build(levels, rooms, onTile) {
60
+ this.levels = levels.map((l) => ({ y: l.y, building: l.building, clusters: [] }));
61
+ this.order = levels
62
+ .map((_, i) => i)
63
+ .sort((a, b) => levels[a].building - levels[b].building || levels[a].y - levels[b].y);
64
+ this.position = new Array(levels.length);
65
+ this.order.forEach((level, pos) => {
66
+ this.position[level] = pos;
67
+ });
68
+ const buildings = levels.reduce((n, l) => Math.max(n, l.building + 1), 0);
69
+ this.buildingRange = Array.from({ length: buildings }, () => [0, 0]);
70
+ this.order.forEach((level, pos) => {
71
+ const range = this.buildingRange[levels[level].building];
72
+ if (range[1] === 0)
73
+ range[0] = pos;
74
+ range[1]++;
75
+ });
76
+ if (rooms.length > MAX_ROOMS)
77
+ console.warn(`[daylight] ${rooms.length} rooms: only the first ${MAX_ROOMS} get room light`);
78
+ const used = rooms.slice(0, MAX_ROOMS);
79
+ // Site origin on the tile grid; every rect below is in whole tiles from it.
80
+ let minX = Number.POSITIVE_INFINITY;
81
+ let minZ = Number.POSITIVE_INFINITY;
82
+ for (const room of used)
83
+ for (const [x, z] of room.polygon) {
84
+ minX = Math.min(minX, x);
85
+ minZ = Math.min(minZ, z);
86
+ }
87
+ if (!Number.isFinite(minX))
88
+ minX = minZ = 0;
89
+ const originTx = Math.floor((minX - APRON) / TILE_M);
90
+ const originTz = Math.floor((minZ - APRON) / TILE_M);
91
+ this.originX = originTx * TILE_M;
92
+ this.originZ = originTz * TILE_M;
93
+ // Each room's tile rect (padded by the apron).
94
+ const rects = used.map((room) => {
95
+ let x0 = Number.POSITIVE_INFINITY;
96
+ let z0 = Number.POSITIVE_INFINITY;
97
+ let x1 = Number.NEGATIVE_INFINITY;
98
+ let z1 = Number.NEGATIVE_INFINITY;
99
+ for (const [x, z] of room.polygon) {
100
+ x0 = Math.min(x0, x);
101
+ z0 = Math.min(z0, z);
102
+ x1 = Math.max(x1, x);
103
+ z1 = Math.max(z1, z);
104
+ }
105
+ return {
106
+ tx0: Math.floor((x0 - APRON) / TILE_M) - originTx,
107
+ tz0: Math.floor((z0 - APRON) / TILE_M) - originTz,
108
+ tx1: Math.floor((x1 + APRON) / TILE_M) - originTx,
109
+ tz1: Math.floor((z1 + APRON) / TILE_M) - originTz,
110
+ };
111
+ });
112
+ const atlasTilesW = Math.max(1, Math.floor(Math.min(this.textureLimit, 4096) / TILE));
113
+ const atlasCapacity = atlasTilesW * Math.max(1, Math.floor(this.textureLimit / TILE));
114
+ const detailTiles = [];
115
+ let buffer = new Uint16Array(0);
116
+ let scratch = new Uint16Array(0);
117
+ // Where rooms overlap (an automatic room under drawn ones, a unit around its rooms), the
118
+ // smallest wins: rasterized last, so each level's rooms go largest first.
119
+ const area = used.map((room) => Math.abs(ringArea(room.polygon)));
120
+ const byLevel = levels.map(() => []);
121
+ used.forEach((room, ri) => {
122
+ byLevel[room.level]?.push(ri);
123
+ });
124
+ for (const list of byLevel)
125
+ list.sort((a, b) => area[b] - area[a] || a - b);
126
+ for (let li = 0; li < levels.length; li++) {
127
+ const level = this.levels[li];
128
+ for (const members of clusterRooms(byLevel[li], rects)) {
129
+ let tx0 = Number.POSITIVE_INFINITY;
130
+ let tz0 = Number.POSITIVE_INFINITY;
131
+ let tx1 = Number.NEGATIVE_INFINITY;
132
+ let tz1 = Number.NEGATIVE_INFINITY;
133
+ for (const ri of members) {
134
+ const r = rects[ri];
135
+ tx0 = Math.min(tx0, r.tx0);
136
+ tz0 = Math.min(tz0, r.tz0);
137
+ tx1 = Math.max(tx1, r.tx1);
138
+ tz1 = Math.max(tz1, r.tz1);
139
+ }
140
+ const cluster = {
141
+ tx0,
142
+ tz0,
143
+ tw: tx1 - tx0 + 1,
144
+ th: tz1 - tz0 + 1,
145
+ entries: new Float32Array((tx1 - tx0 + 1) * (tz1 - tz0 + 1)),
146
+ };
147
+ level.clusters.push(cluster);
148
+ // Rooms per tile, from each room's own rect.
149
+ const buckets = Array.from({ length: cluster.entries.length }, () => []);
150
+ for (const ri of members) {
151
+ const r = rects[ri];
152
+ for (let tz = r.tz0; tz <= r.tz1; tz++)
153
+ for (let tx = r.tx0; tx <= r.tx1; tx++)
154
+ buckets[(tz - tz0) * cluster.tw + (tx - tx0)].push(ri);
155
+ }
156
+ // One row of tiles at a time (plus a margin all round): each room rasterizes once per row.
157
+ const stride = cluster.tw * TILE + 2 * MARGIN;
158
+ const rowH = TILE + 2 * MARGIN;
159
+ if (buffer.length < stride * rowH) {
160
+ buffer = new Uint16Array(stride * rowH);
161
+ scratch = new Uint16Array(stride * rowH);
162
+ }
163
+ const strip = buffer.subarray(0, stride * rowH);
164
+ const inRow = new Set();
165
+ for (let tz = tz0; tz <= tz1; tz++) {
166
+ inRow.clear();
167
+ for (let tx = tx0; tx <= tx1; tx++)
168
+ for (const ri of buckets[(tz - tz0) * cluster.tw + (tx - tx0)])
169
+ inRow.add(ri);
170
+ if (!inRow.size)
171
+ continue;
172
+ const ci0 = tx0 * TILE - MARGIN;
173
+ const cj0 = tz * TILE - MARGIN;
174
+ const x0 = this.originX + ci0 * CELL;
175
+ const z0 = this.originZ + cj0 * CELL;
176
+ strip.fill(0);
177
+ for (const ri of [...inRow].sort((a, b) => area[b] - area[a] || a - b))
178
+ rasterize(used[ri], ri + 1, strip, stride, rowH, x0, z0);
179
+ // Growth only fills empty cells: inside a big room there are none.
180
+ if (strip.includes(0))
181
+ dilate(strip, scratch, stride, rowH);
182
+ for (let tx = tx0; tx <= tx1; tx++) {
183
+ const t = (tz - tz0) * cluster.tw + (tx - tx0);
184
+ if (!buckets[t].length)
185
+ continue;
186
+ const i0 = MARGIN + (tx - tx0) * TILE;
187
+ const first = strip[MARGIN * stride + i0];
188
+ let same = true;
189
+ for (let j = MARGIN; j < MARGIN + TILE && same; j++)
190
+ for (let i = i0; i < i0 + TILE; i++)
191
+ if (strip[j * stride + i] !== first) {
192
+ same = false;
193
+ break;
194
+ }
195
+ if (same)
196
+ cluster.entries[t] = first;
197
+ else if (detailTiles.length >= atlasCapacity) {
198
+ // Out of atlas: the tile's most common room stands for all of it.
199
+ if (!this.warnedOverflow) {
200
+ console.warn('[daylight] room map atlas full: some room edges are coarse');
201
+ this.warnedOverflow = true;
202
+ }
203
+ cluster.entries[t] = majority(strip, stride, i0);
204
+ }
205
+ else {
206
+ const tile = new Uint16Array(TILE * TILE);
207
+ for (let j = 0; j < TILE; j++)
208
+ tile.set(strip.subarray((MARGIN + j) * stride + i0, (MARGIN + j) * stride + i0 + TILE), j * TILE);
209
+ detailTiles.push(tile);
210
+ cluster.entries[t] = -detailTiles.length;
211
+ }
212
+ onTile?.({
213
+ level: li,
214
+ ci0,
215
+ cj0,
216
+ x0,
217
+ z0,
218
+ stride,
219
+ i0,
220
+ j0: MARGIN,
221
+ cells: strip,
222
+ uniform: same,
223
+ });
224
+ }
225
+ }
226
+ }
227
+ }
228
+ // Atlas: detailed tiles in rows, two bytes (room + 1) per cell.
229
+ this.atlasTilesW = atlasTilesW;
230
+ this.atlasTiles = detailTiles.length;
231
+ const atlasRows = Math.max(1, Math.ceil(detailTiles.length / atlasTilesW));
232
+ const aw = atlasTilesW * TILE;
233
+ this.atlas = new Uint8Array(aw * atlasRows * TILE * 2);
234
+ detailTiles.forEach((tile, a) => {
235
+ const ox = (a % atlasTilesW) * TILE;
236
+ const oz = Math.floor(a / atlasTilesW) * TILE;
237
+ for (let j = 0; j < TILE; j++)
238
+ for (let i = 0; i < TILE; i++) {
239
+ const v = tile[j * TILE + i];
240
+ const k = ((oz + j) * aw + ox + i) * 2;
241
+ this.atlas[k] = v & 255;
242
+ this.atlas[k + 1] = v >> 8;
243
+ }
244
+ });
245
+ // Directory: per cell, the buildings whose clusters reach it (coarser cells on vast sites).
246
+ let maxTx = 0;
247
+ let maxTz = 0;
248
+ for (const l of this.levels)
249
+ for (const c of l.clusters) {
250
+ maxTx = Math.max(maxTx, c.tx0 + c.tw);
251
+ maxTz = Math.max(maxTz, c.tz0 + c.th);
252
+ }
253
+ this.dirTiles = Math.max(8, Math.ceil(Math.sqrt((maxTx * maxTz) / MAX_DIR_CELLS)));
254
+ this.dirW = Math.max(1, Math.ceil(maxTx / this.dirTiles));
255
+ this.dirH = Math.max(1, Math.ceil(maxTz / this.dirTiles));
256
+ this.dirLists = Array.from({ length: this.dirW * this.dirH }, () => []);
257
+ for (const l of this.levels)
258
+ for (const c of l.clusters)
259
+ for (let dz = Math.floor(c.tz0 / this.dirTiles); dz <= Math.floor((c.tz0 + c.th - 1) / this.dirTiles); dz++)
260
+ for (let dx = Math.floor(c.tx0 / this.dirTiles); dx <= Math.floor((c.tx0 + c.tw - 1) / this.dirTiles); dx++) {
261
+ const list = this.dirLists[dz * this.dirW + dx];
262
+ if (!list.includes(l.building))
263
+ list.push(l.building);
264
+ }
265
+ this.upload();
266
+ }
267
+ /** CPU lookup in assembled space (same rule as the shader). */
268
+ roomAt(x, y, z) {
269
+ const tx = Math.floor((x - this.originX) / TILE_M);
270
+ const tz = Math.floor((z - this.originZ) / TILE_M);
271
+ const dx = Math.floor(tx / this.dirTiles);
272
+ const dz = Math.floor(tz / this.dirTiles);
273
+ if (dx < 0 || dz < 0 || dx >= this.dirW || dz >= this.dirH)
274
+ return -1;
275
+ for (const b of this.dirLists[dz * this.dirW + dx]) {
276
+ const [start, count] = this.buildingRange[b];
277
+ let found = start;
278
+ for (let pos = start; pos < start + count; pos++)
279
+ if (this.levels[this.order[pos]].y <= y - 0.12)
280
+ found = pos;
281
+ const room = this.levelRoomAt(this.order[found], x, z, tx, tz);
282
+ if (room >= 0)
283
+ return room;
284
+ }
285
+ return -1;
286
+ }
287
+ /** A level's room at a point; clusters' rects can overlap (a wing around a courtyard pavilion). */
288
+ levelRoomAt(li, x, z, tx, tz) {
289
+ for (const c of this.levels[li].clusters) {
290
+ const lx = tx - c.tx0;
291
+ const lz = tz - c.tz0;
292
+ if (lx < 0 || lz < 0 || lx >= c.tw || lz >= c.th)
293
+ continue;
294
+ const e = c.entries[lz * c.tw + lx];
295
+ if (e > 0)
296
+ return e - 1;
297
+ if (e === 0)
298
+ continue;
299
+ const a = -e - 1;
300
+ const ci = Math.floor((x - this.originX) / CELL) - tx * TILE;
301
+ const cj = Math.floor((z - this.originZ) / CELL) - tz * TILE;
302
+ const aw = this.atlasTilesW * TILE;
303
+ const k = ((Math.floor(a / this.atlasTilesW) * TILE + cj) * aw + (a % this.atlasTilesW) * TILE + ci) *
304
+ 2;
305
+ const room = this.atlas[k] + this.atlas[k + 1] * 256 - 1;
306
+ if (room >= 0)
307
+ return room;
308
+ }
309
+ return -1;
310
+ }
311
+ /** The level stacked right above `level` in its building, or −1. */
312
+ levelAbove(level) {
313
+ const pos = this.position[level];
314
+ if (pos === undefined)
315
+ return -1;
316
+ const next = this.order[pos + 1];
317
+ return next !== undefined && this.levels[next].building === this.levels[level].building
318
+ ? next
319
+ : -1;
320
+ }
321
+ /** Index the GPU uses for `level` (rooms store it). */
322
+ gpuLevel(level) {
323
+ return this.position[level] ?? 0;
324
+ }
325
+ /**
326
+ * Live presentation per level (input order): explode offset, whether it is shown, and whether
327
+ * the levels over it are hidden (its rooms open to the sky from above).
328
+ */
329
+ setPresentation(offsets, shown, focused) {
330
+ let changed = false;
331
+ this.order.forEach((level, pos) => {
332
+ const off = offsets[level] ?? 0;
333
+ const flags = (focused[level] ? 1 : 0) + (shown[level] !== false ? 2 : 0);
334
+ const t = pos * 8;
335
+ const liveY = this.levels[level].y + off;
336
+ if (this.levelData[t] !== liveY) {
337
+ this.levelData[t] = liveY;
338
+ changed = true;
339
+ }
340
+ if (this.levelData[t + 6] !== off || this.levelData[t + 7] !== flags) {
341
+ this.levelData[t + 6] = off;
342
+ this.levelData[t + 7] = flags;
343
+ changed = true;
344
+ }
345
+ });
346
+ if (changed)
347
+ this.u.levels.value.needsUpdate = true;
348
+ }
349
+ memoryBytes() {
350
+ return this.atlas.byteLength + this.indexBytes + this.levelData.byteLength;
351
+ }
352
+ get stats() {
353
+ return { atlasTiles: this.atlasTiles, dirW: this.dirW, dirH: this.dirH };
354
+ }
355
+ upload() {
356
+ // index: [directory cells] [directory lists] [clusters, 2 texels] [entries]
357
+ const dirCells = this.dirW * this.dirH;
358
+ const listTexels = this.dirLists.reduce((s, l) => s + l.length, 0);
359
+ const clusterCount = this.levels.reduce((s, l) => s + l.clusters.length, 0);
360
+ const entryTexels = this.levels.reduce((s, l) => s + l.clusters.reduce((t, c) => t + c.entries.length, 0), 0);
361
+ const texels = dirCells + listTexels + clusterCount * 2 + entryTexels + 1;
362
+ // As wide as the device allows, so a vast site's directory still fits.
363
+ const indexW = Math.min(this.textureLimit, 4096);
364
+ const rows = Math.max(1, Math.ceil(texels / indexW));
365
+ if (rows > this.textureLimit)
366
+ console.warn('[daylight] room map index exceeds the device texture limit');
367
+ const index = new Float32Array(indexW * rows * 4);
368
+ let list = dirCells;
369
+ this.dirLists.forEach((buildings, c) => {
370
+ index[c * 4] = list;
371
+ index[c * 4 + 1] = buildings.length;
372
+ for (const b of buildings) {
373
+ const [start, count] = this.buildingRange[b];
374
+ index[list * 4] = start;
375
+ index[list * 4 + 1] = count;
376
+ list++;
377
+ }
378
+ });
379
+ const clusterStart = new Array(this.levels.length).fill(0);
380
+ let cluster = list;
381
+ let entry = list + clusterCount * 2;
382
+ this.levels.forEach((l, li) => {
383
+ clusterStart[li] = cluster;
384
+ for (const c of l.clusters) {
385
+ index.set([c.tx0, c.tz0, c.tw, c.th, entry, 0, 0, 0], cluster * 4);
386
+ for (let k = 0; k < c.entries.length; k++)
387
+ index[(entry + k) * 4] = c.entries[k];
388
+ entry += c.entries.length;
389
+ cluster += 2;
390
+ }
391
+ });
392
+ this.indexBytes = index.byteLength;
393
+ // levels (GPU order): [liveY, clusterStart, clusterCount, 0] [0, 0, offset, flags]
394
+ const levelRows = Math.max(1, Math.ceil((this.order.length * 2) / LEVEL_W));
395
+ this.levelData = new Float32Array(LEVEL_W * levelRows * 4);
396
+ this.order.forEach((level, pos) => {
397
+ const l = this.levels[level];
398
+ this.levelData.set([l.y, clusterStart[level], l.clusters.length, 0, 0, 0, 0, 2], pos * 8);
399
+ });
400
+ const aw = this.atlasTilesW * TILE;
401
+ replace(this.u.index, floatTexture(index, indexW, rows));
402
+ replace(this.u.levels, floatTexture(this.levelData, LEVEL_W, levelRows));
403
+ replace(this.u.atlas, byteTexture(this.atlas, aw, this.atlas.length / 2 / aw));
404
+ this.u.origin.value.set(this.originX, this.originZ);
405
+ this.u.indexW.value = indexW;
406
+ this.u.dirTiles.value = this.dirTiles;
407
+ this.u.dirW.value = this.dirW;
408
+ this.u.dirH.value = this.dirH;
409
+ this.u.atlasTilesW.value = this.atlasTilesW;
410
+ this.u.single.value = this.buildingRange.length <= 1 ? 1 : 0;
411
+ this.u.singleCount.value = this.buildingRange[0]?.[1] ?? 0;
412
+ this.u.searchSteps.value = this.buildingRange.reduce((steps, [, count]) => Math.max(steps, Math.ceil(Math.log2(count + 1))), 0);
413
+ }
414
+ // ---- TSL ---------------------------------------------------------------------------------
415
+ indexTexel(i) {
416
+ const k = int(i);
417
+ return this.u.index.load(ivec2(k.mod(this.u.indexW), k.div(this.u.indexW)));
418
+ }
419
+ /** [liveY, clusterStart, clusterCount, 0] of the level at GPU position `pos`. */
420
+ levelTexel0(pos) {
421
+ const k = int(pos).mul(2);
422
+ return this.u.levels.load(ivec2(k.mod(LEVEL_W), k.div(LEVEL_W)));
423
+ }
424
+ /** [0, 0, offset, flags] of the level at GPU position `pos`. */
425
+ levelTexel1(pos) {
426
+ const k = int(pos).mul(2).add(1);
427
+ return this.u.levels.load(ivec2(k.mod(LEVEL_W), k.div(LEVEL_W)));
428
+ }
429
+ /** Room index (−1 outside) at a world-space position (live heights). */
430
+ roomAtNode = Fn(([p]) => {
431
+ const u = this.u;
432
+ const room = int(-1).toVar();
433
+ const tileF = p.xz.sub(u.origin).div(TILE_M);
434
+ const tile = ivec2(floor(tileF));
435
+ // Candidate buildings: the only one, or the directory cell's list.
436
+ const count = int(0).toVar();
437
+ const listStart = int(0).toVar();
438
+ If(u.single.equal(1), () => {
439
+ count.assign(1);
440
+ }).Else(() => {
441
+ const dir = ivec2(floor(tileF.div(u.dirTiles)));
442
+ If(dir.x
443
+ .greaterThanEqual(0)
444
+ .and(dir.y.greaterThanEqual(0))
445
+ .and(dir.x.lessThan(u.dirW))
446
+ .and(dir.y.lessThan(u.dirH)), () => {
447
+ const d = this.indexTexel(dir.y.mul(u.dirW).add(dir.x));
448
+ listStart.assign(int(d.x));
449
+ count.assign(int(d.y));
450
+ });
451
+ });
452
+ const y = p.y.sub(0.12);
453
+ Loop({ start: int(0), end: count, type: 'int', condition: '<' }, ({ i }) => {
454
+ If(room.lessThan(0), () => {
455
+ const start = int(0).toVar();
456
+ const levels = u.singleCount.toVar();
457
+ If(u.single.equal(0), () => {
458
+ const range = this.indexTexel(listStart.add(i));
459
+ start.assign(int(range.x));
460
+ levels.assign(int(range.y));
461
+ });
462
+ // Last level of the building whose live floor is at or below the point (else its lowest).
463
+ const lo = start.toVar();
464
+ const hi = start.add(levels).sub(1).toVar();
465
+ const found = start.toVar();
466
+ Loop({ start: int(0), end: u.searchSteps, type: 'int', condition: '<' }, () => {
467
+ If(lo.lessThanEqual(hi), () => {
468
+ const mid = lo.add(hi).div(2);
469
+ If(this.levelTexel0(mid).x.lessThanEqual(y), () => {
470
+ found.assign(mid);
471
+ lo.assign(mid.add(1));
472
+ }).Else(() => {
473
+ hi.assign(mid.sub(1));
474
+ });
475
+ });
476
+ });
477
+ const level = this.levelTexel0(found);
478
+ // Clusters' rects can overlap: the first one holding a room at the point wins.
479
+ Loop({ start: int(0), end: int(level.z), type: 'int', condition: '<' }, ({ i: c }) => {
480
+ If(room.lessThan(0), () => {
481
+ const base = int(level.y).add(c.mul(2));
482
+ const rect = this.indexTexel(base);
483
+ const local = tile.sub(ivec2(int(rect.x), int(rect.y)));
484
+ If(local.x
485
+ .greaterThanEqual(0)
486
+ .and(local.y.greaterThanEqual(0))
487
+ .and(local.x.lessThan(int(rect.z)))
488
+ .and(local.y.lessThan(int(rect.w))), () => {
489
+ const entryStart = int(this.indexTexel(base.add(1)).x);
490
+ const e = this.indexTexel(entryStart.add(local.y.mul(int(rect.z))).add(local.x)).x;
491
+ If(e.greaterThan(0.5), () => {
492
+ room.assign(int(round(e)).sub(1));
493
+ }).ElseIf(e.lessThan(-0.5), () => {
494
+ const a = int(round(e.negate())).sub(1);
495
+ const cell = ivec2(floor(p.xz.sub(u.origin).div(CELL))).sub(tile.mul(TILE));
496
+ const at = ivec2(a.mod(u.atlasTilesW).mul(TILE).add(cell.x), a.div(u.atlasTilesW).mul(TILE).add(cell.y));
497
+ const texel = u.atlas.load(at);
498
+ const id = int(round(texel.x.mul(255))).add(int(round(texel.y.mul(255))).mul(256));
499
+ room.assign(id.sub(1));
500
+ });
501
+ });
502
+ });
503
+ });
504
+ });
505
+ });
506
+ return room;
507
+ });
508
+ }
509
+ // ---- build helpers --------------------------------------------------------------------------
510
+ /** Groups a level's rooms whose padded tile rects lie within `CLUSTER_GAP` tiles of each other. */
511
+ function clusterRooms(members, rects) {
512
+ const parent = members.map((_, i) => i);
513
+ const find = (i) => {
514
+ while (parent[i] !== i) {
515
+ parent[i] = parent[parent[i]];
516
+ i = parent[i];
517
+ }
518
+ return i;
519
+ };
520
+ const order = members
521
+ .map((_, i) => i)
522
+ .sort((a, b) => rects[members[a]].tx0 - rects[members[b]].tx0);
523
+ for (let a = 0; a < order.length; a++) {
524
+ const ra = rects[members[order[a]]];
525
+ for (let b = a + 1; b < order.length; b++) {
526
+ const rb = rects[members[order[b]]];
527
+ if (rb.tx0 > ra.tx1 + CLUSTER_GAP)
528
+ break;
529
+ if (rb.tz0 <= ra.tz1 + CLUSTER_GAP && ra.tz0 <= rb.tz1 + CLUSTER_GAP)
530
+ parent[find(order[a])] = find(order[b]);
531
+ }
532
+ }
533
+ const groups = new Map();
534
+ members.forEach((ri, i) => {
535
+ const root = find(i);
536
+ const group = groups.get(root);
537
+ if (group)
538
+ group.push(ri);
539
+ else
540
+ groups.set(root, [ri]);
541
+ });
542
+ return [...groups.values()];
543
+ }
544
+ function ringArea(ring) {
545
+ let a = 0;
546
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++)
547
+ a += ring[j][0] * ring[i][1] - ring[i][0] * ring[j][1];
548
+ return a / 2;
549
+ }
550
+ /** Scanline fill of a room (holes subtracted, even-odd) at cell centres. */
551
+ function rasterize(room, value, cells, width, height, x0, z0) {
552
+ const rings = [room.polygon, ...(room.holes ?? [])];
553
+ let minZ = Number.POSITIVE_INFINITY;
554
+ let maxZ = Number.NEGATIVE_INFINITY;
555
+ for (const [, z] of room.polygon) {
556
+ minZ = Math.min(minZ, z);
557
+ maxZ = Math.max(maxZ, z);
558
+ }
559
+ const j0 = Math.max(0, Math.floor((minZ - z0) / CELL));
560
+ const j1 = Math.min(height - 1, Math.ceil((maxZ - z0) / CELL));
561
+ const xs = [];
562
+ for (let j = j0; j <= j1; j++) {
563
+ const z = z0 + (j + 0.5) * CELL;
564
+ xs.length = 0;
565
+ for (const ring of rings)
566
+ for (let a = 0, b = ring.length - 1; a < ring.length; b = a++) {
567
+ const [xa, za] = ring[a];
568
+ const [xb, zb] = ring[b];
569
+ if (za > z !== zb > z)
570
+ xs.push(((xb - xa) * (z - za)) / (zb - za) + xa);
571
+ }
572
+ xs.sort((p, q) => p - q);
573
+ for (let k = 0; k + 1 < xs.length; k += 2) {
574
+ // Cells whose centre lies in (xs[k], xs[k+1]].
575
+ const i0 = Math.max(0, Math.floor((xs[k] - x0) / CELL - 0.5) + 1);
576
+ const i1 = Math.min(width - 1, Math.floor((xs[k + 1] - x0) / CELL - 0.5));
577
+ if (i1 >= i0)
578
+ cells.fill(value, j * width + i0, j * width + i1 + 1);
579
+ }
580
+ }
581
+ }
582
+ /** Two passes of 4-neighbour growth into empty cells: wall thickness and polygon slop resolve. */
583
+ function dilate(cells, scratch, width, height) {
584
+ for (let pass = 0; pass < 2; pass++) {
585
+ scratch.set(cells);
586
+ for (let j = 1; j < height - 1; j++)
587
+ for (let i = 1; i < width - 1; i++) {
588
+ const k = j * width + i;
589
+ if (scratch[k])
590
+ continue;
591
+ const n = scratch[k - 1] || scratch[k + 1] || scratch[k - width] || scratch[k + width];
592
+ if (n)
593
+ cells[k] = n;
594
+ }
595
+ }
596
+ }
597
+ /** The most common value among a tile's own cells. */
598
+ function majority(cells, stride, i0) {
599
+ const counts = new Map();
600
+ for (let j = MARGIN; j < MARGIN + TILE; j++)
601
+ for (let i = i0; i < i0 + TILE; i++) {
602
+ const v = cells[j * stride + i];
603
+ counts.set(v, (counts.get(v) ?? 0) + 1);
604
+ }
605
+ let best = 0;
606
+ let bestCount = -1;
607
+ for (const [v, c] of counts)
608
+ if (c > bestCount) {
609
+ best = v;
610
+ bestCount = c;
611
+ }
612
+ return best;
613
+ }
614
+ function floatTexture(data, width, height) {
615
+ const tex = new DataTexture(data, width, height, RGBAFormat, FloatType);
616
+ tex.magFilter = tex.minFilter = NearestFilter;
617
+ tex.needsUpdate = true;
618
+ return tex;
619
+ }
620
+ function byteTexture(data, width, height) {
621
+ const tex = new DataTexture(data, width, height, RGFormat, UnsignedByteType);
622
+ tex.magFilter = tex.minFilter = NearestFilter;
623
+ tex.needsUpdate = true;
624
+ return tex;
625
+ }
626
+ function replace(node, tex) {
627
+ const previous = node.value;
628
+ node.value = tex;
629
+ previous.dispose();
630
+ }
@@ -0,0 +1,56 @@
1
+ import { type Object3D, Vector3 } from 'three';
2
+ import type { RoomLamp, RoomSceneInput } from './rooms';
3
+ /**
4
+ * Builds the room system's input from a scene: the live parametric graph (scene registry +
5
+ * node data) or a baked artifact (identity `userData` stamped by the GLB export).
6
+ */
7
+ /** A lamp as the item-light pools see it (live catalog/procedural items and baked items alike). */
8
+ export type LampFeedLight = {
9
+ key: string;
10
+ color: string;
11
+ distance: number;
12
+ /** Fully-on intensity. */
13
+ peak?: number;
14
+ getWorldPosition: (out: Vector3) => boolean;
15
+ /** The lamp's item: its node id (live) or object (baked). */
16
+ nodeId?: string;
17
+ object?: Object3D;
18
+ /** A spotlight's cone (angle in degrees, full cone; softness = penumbra) and its aim. */
19
+ spot?: {
20
+ direction?: readonly [number, number, number];
21
+ angle?: number;
22
+ softness?: number;
23
+ };
24
+ getWorldDirection?: (out: Vector3) => void;
25
+ };
26
+ type LampFeed = {
27
+ lights: () => Iterable<LampFeedLight>;
28
+ intensity: (light: LampFeedLight) => number;
29
+ };
30
+ /** Item-light pools hand their lamps to the room system while the daylight look is active. */
31
+ export declare const lampFeeds: Set<LampFeed>;
32
+ /** Lamps from every registered feed, in the room system's units. */
33
+ export declare function feedLamps(): RoomLamp[];
34
+ /** Current on/off of a fed lamp (by key). */
35
+ export declare function feedLampOn(key: string): boolean | undefined;
36
+ /** The live parametric scene (registry + node data), or null while it has no zones. */
37
+ export declare function liveRoomScene(virtualLampsOn: boolean): RoomSceneInput | null;
38
+ /** Live level objects in the room system's level order (explode offsets, visibility, focus). */
39
+ export declare function liveLevelObjects(): (Object3D | undefined)[];
40
+ /** A baked artifact's level objects in the room system's level order. */
41
+ export declare function bakedLevelObjects(root: Object3D | null): Object3D[];
42
+ /** How open a live door is (0 closed … 1 fully open), the transient state first. */
43
+ export declare function liveDoorOpenness(id: string): number;
44
+ /** A baked artifact's identity tree (levels, zones, covers, walls, openings), or null. */
45
+ export declare function bakedRoomScene(root: Object3D, virtualLampsOn: boolean): RoomSceneInput | null;
46
+ /**
47
+ * Lamp shades and bulbs follow their own lamp (the enhanced viewer's isolated emitters): each
48
+ * lamp's emissive meshes get private materials whose emission follows that lamp's switch. A light
49
+ * can sit on a child item while the glowing mesh belongs to its parent item: search the item, then
50
+ * its nearest item ancestor. Re-installed if the item swaps its materials.
51
+ */
52
+ export declare function syncLampEmitters(): void;
53
+ /** The lamps' own materials back (the daylight look left). */
54
+ export declare function restoreLampEmitters(): void;
55
+ export {};
56
+ //# sourceMappingURL=room-scene.d.ts.map