@expofp/renderer 0.0.0-experimental.cf063fc

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 (280) hide show
  1. package/README.md +145 -0
  2. package/dist/Renderer.d.ts +102 -0
  3. package/dist/Renderer.js +96 -0
  4. package/dist/controls/CameraControls.d.ts +108 -0
  5. package/dist/controls/CameraControls.js +203 -0
  6. package/dist/controls/Controls.d.ts +53 -0
  7. package/dist/controls/Controls.js +54 -0
  8. package/dist/controls/camera-api.d.ts +226 -0
  9. package/dist/controls/camera-api.js +179 -0
  10. package/dist/controls/gestures.d.ts +71 -0
  11. package/dist/controls/gestures.js +98 -0
  12. package/dist/controls/handlers/DoubleTapZoom.d.ts +23 -0
  13. package/dist/controls/handlers/DoubleTapZoom.js +82 -0
  14. package/dist/controls/handlers/Inertia.d.ts +60 -0
  15. package/dist/controls/handlers/Inertia.js +222 -0
  16. package/dist/controls/handlers/Pan.d.ts +23 -0
  17. package/dist/controls/handlers/Pan.js +212 -0
  18. package/dist/controls/handlers/Pitch.d.ts +19 -0
  19. package/dist/controls/handlers/Pitch.js +200 -0
  20. package/dist/controls/handlers/Roll.d.ts +30 -0
  21. package/dist/controls/handlers/Roll.js +196 -0
  22. package/dist/controls/handlers/Zoom.d.ts +28 -0
  23. package/dist/controls/handlers/Zoom.js +34 -0
  24. package/dist/controls/hooks.d.ts +136 -0
  25. package/dist/controls/hooks.js +200 -0
  26. package/dist/controls/index.d.ts +9 -0
  27. package/dist/controls/index.js +7 -0
  28. package/dist/controls/zoom-to.d.ts +89 -0
  29. package/dist/controls/zoom-to.js +91 -0
  30. package/dist/core/InstanceUniforms.d.ts +108 -0
  31. package/dist/core/InstanceUniforms.js +298 -0
  32. package/dist/core/SquareDataTexture.d.ts +277 -0
  33. package/dist/core/SquareDataTexture.js +476 -0
  34. package/dist/core/batch/BatchedMesh.d.ts +74 -0
  35. package/dist/core/batch/BatchedMesh.js +155 -0
  36. package/dist/core/instanced/FrustumCulling.d.ts +141 -0
  37. package/dist/core/instanced/FrustumCulling.js +246 -0
  38. package/dist/core/instanced/InstancedMesh.d.ts +302 -0
  39. package/dist/core/instanced/InstancedMesh.js +497 -0
  40. package/dist/core/instanced/Instances.d.ts +119 -0
  41. package/dist/core/instanced/Instances.js +230 -0
  42. package/dist/core/instanced/Raycasting.d.ts +118 -0
  43. package/dist/core/instanced/Raycasting.js +134 -0
  44. package/dist/core/instanced/instanced-material.d.ts +77 -0
  45. package/dist/core/instanced/instanced-material.js +228 -0
  46. package/dist/core/lines/BatchedLines.d.ts +137 -0
  47. package/dist/core/lines/BatchedLines.js +215 -0
  48. package/dist/core/lines/FrustumCullingLines.d.ts +44 -0
  49. package/dist/core/lines/FrustumCullingLines.js +85 -0
  50. package/dist/core/lines/RaycastingLines.d.ts +45 -0
  51. package/dist/core/lines/RaycastingLines.js +91 -0
  52. package/dist/core/lines/lines-material.d.ts +47 -0
  53. package/dist/core/lines/lines-material.js +124 -0
  54. package/dist/core/shader-source.d.ts +70 -0
  55. package/dist/core/shader-source.js +110 -0
  56. package/dist/core/sprites/BatchedSprites.d.ts +115 -0
  57. package/dist/core/sprites/BatchedSprites.js +173 -0
  58. package/dist/core/sprites/RaycastingSprites.d.ts +41 -0
  59. package/dist/core/sprites/RaycastingSprites.js +71 -0
  60. package/dist/core/sprites/sprite-material.d.ts +59 -0
  61. package/dist/core/sprites/sprite-material.js +192 -0
  62. package/dist/core/text/BatchedText.d.ts +233 -0
  63. package/dist/core/text/BatchedText.js +473 -0
  64. package/dist/core/text/MemberTexture.d.ts +90 -0
  65. package/dist/core/text/MemberTexture.js +289 -0
  66. package/dist/core/text/Text.d.ts +88 -0
  67. package/dist/core/text/Text.js +230 -0
  68. package/dist/core/text/batched-text-material.d.ts +56 -0
  69. package/dist/core/text/batched-text-material.js +123 -0
  70. package/dist/core/text/glyph-packing.d.ts +17 -0
  71. package/dist/core/text/glyph-packing.js +61 -0
  72. package/dist/core/typed-array.d.ts +16 -0
  73. package/dist/core/typed-array.js +20 -0
  74. package/dist/debug/components/DebugPanelProvider.d.ts +20 -0
  75. package/dist/debug/components/DebugPanelProvider.js +50 -0
  76. package/dist/debug/components/DebugTools.d.ts +22 -0
  77. package/dist/debug/components/DebugTools.js +28 -0
  78. package/dist/debug/components/Gui.d.ts +22 -0
  79. package/dist/debug/components/Gui.js +42 -0
  80. package/dist/debug/components/MemoryInfo.d.ts +22 -0
  81. package/dist/debug/components/MemoryInfo.js +42 -0
  82. package/dist/debug/components/Panel.d.ts +31 -0
  83. package/dist/debug/components/Panel.js +104 -0
  84. package/dist/debug/components/Stats.d.ts +9 -0
  85. package/dist/debug/components/Stats.js +24 -0
  86. package/dist/debug/debug-chunk.d.ts +26 -0
  87. package/dist/debug/debug-chunk.js +32 -0
  88. package/dist/debug/debug-context.d.ts +41 -0
  89. package/dist/debug/debug-context.js +59 -0
  90. package/dist/debug/index.d.ts +41 -0
  91. package/dist/debug/index.js +43 -0
  92. package/dist/events/LayoutDriver.d.ts +36 -0
  93. package/dist/events/LayoutDriver.js +192 -0
  94. package/dist/events/PointerMissed.d.ts +23 -0
  95. package/dist/events/PointerMissed.js +58 -0
  96. package/dist/events/def-culling.d.ts +46 -0
  97. package/dist/events/def-culling.js +69 -0
  98. package/dist/events/layout-targets.d.ts +151 -0
  99. package/dist/events/layout-targets.js +397 -0
  100. package/dist/events/placement/box.d.ts +59 -0
  101. package/dist/events/placement/box.js +96 -0
  102. package/dist/events/placement/grid.d.ts +161 -0
  103. package/dist/events/placement/grid.js +235 -0
  104. package/dist/events/placement/index.d.ts +2 -0
  105. package/dist/events/placement/index.js +1 -0
  106. package/dist/events/placement/pass.d.ts +70 -0
  107. package/dist/events/placement/pass.js +617 -0
  108. package/dist/events/pointer.d.ts +194 -0
  109. package/dist/events/pointer.js +356 -0
  110. package/dist/external/ExternalCanvas.d.ts +56 -0
  111. package/dist/external/ExternalCanvas.js +177 -0
  112. package/dist/external/external-camera.d.ts +18 -0
  113. package/dist/external/external-camera.js +103 -0
  114. package/dist/external/external-events.d.ts +14 -0
  115. package/dist/external/external-events.js +33 -0
  116. package/dist/index.d.ts +13 -0
  117. package/dist/index.js +14 -0
  118. package/dist/objects/Background.d.ts +31 -0
  119. package/dist/objects/Background.js +37 -0
  120. package/dist/objects/containers/SlotContainer.d.ts +312 -0
  121. package/dist/objects/containers/SlotContainer.js +311 -0
  122. package/dist/objects/containers/hooks.d.ts +61 -0
  123. package/dist/objects/containers/hooks.js +196 -0
  124. package/dist/objects/containers/index.d.ts +6 -0
  125. package/dist/objects/containers/index.js +4 -0
  126. package/dist/objects/containers/keys.d.ts +10 -0
  127. package/dist/objects/containers/keys.js +18 -0
  128. package/dist/objects/containers/mesh-writes.d.ts +106 -0
  129. package/dist/objects/containers/mesh-writes.js +97 -0
  130. package/dist/objects/containers/props.d.ts +33 -0
  131. package/dist/objects/containers/props.js +1 -0
  132. package/dist/objects/images/ImageInstance.d.ts +29 -0
  133. package/dist/objects/images/ImageInstance.js +152 -0
  134. package/dist/objects/images/Images.d.ts +49 -0
  135. package/dist/objects/images/Images.js +394 -0
  136. package/dist/objects/images/atlas-packer.d.ts +129 -0
  137. package/dist/objects/images/atlas-packer.js +287 -0
  138. package/dist/objects/images/image-container.d.ts +74 -0
  139. package/dist/objects/images/image-container.js +100 -0
  140. package/dist/objects/images/index.d.ts +2 -0
  141. package/dist/objects/images/index.js +1 -0
  142. package/dist/objects/images/pack-sources.d.ts +190 -0
  143. package/dist/objects/images/pack-sources.js +272 -0
  144. package/dist/objects/images/texture-budget.d.ts +120 -0
  145. package/dist/objects/images/texture-budget.js +215 -0
  146. package/dist/objects/images/worker/atlas-worker.d.ts +56 -0
  147. package/dist/objects/images/worker/atlas-worker.js +123 -0
  148. package/dist/objects/images/worker/client.d.ts +62 -0
  149. package/dist/objects/images/worker/client.js +249 -0
  150. package/dist/objects/images/worker/index.d.ts +10 -0
  151. package/dist/objects/images/worker/index.js +1 -0
  152. package/dist/objects/layer/Layer.d.ts +28 -0
  153. package/dist/objects/layer/Layer.js +134 -0
  154. package/dist/objects/layer/index.d.ts +2 -0
  155. package/dist/objects/layer/index.js +1 -0
  156. package/dist/objects/layer/layer-context.d.ts +74 -0
  157. package/dist/objects/layer/layer-context.js +82 -0
  158. package/dist/objects/layer/mount-scheduler.d.ts +131 -0
  159. package/dist/objects/layer/mount-scheduler.js +289 -0
  160. package/dist/objects/layer/visibility-gui.d.ts +19 -0
  161. package/dist/objects/layer/visibility-gui.js +93 -0
  162. package/dist/objects/lighting/Lighting.d.ts +92 -0
  163. package/dist/objects/lighting/Lighting.js +384 -0
  164. package/dist/objects/lighting/OcclusionPasses.d.ts +23 -0
  165. package/dist/objects/lighting/OcclusionPasses.js +271 -0
  166. package/dist/objects/lighting/index.d.ts +11 -0
  167. package/dist/objects/lighting/index.js +10 -0
  168. package/dist/objects/lighting/lighting-gui.d.ts +24 -0
  169. package/dist/objects/lighting/lighting-gui.js +114 -0
  170. package/dist/objects/lighting/occluder.d.ts +34 -0
  171. package/dist/objects/lighting/occluder.js +62 -0
  172. package/dist/objects/lighting/occlusion-targets.d.ts +143 -0
  173. package/dist/objects/lighting/occlusion-targets.js +276 -0
  174. package/dist/objects/lines/LineInstance.d.ts +21 -0
  175. package/dist/objects/lines/LineInstance.js +73 -0
  176. package/dist/objects/lines/Lines.d.ts +17 -0
  177. package/dist/objects/lines/Lines.js +31 -0
  178. package/dist/objects/lines/index.d.ts +2 -0
  179. package/dist/objects/lines/index.js +1 -0
  180. package/dist/objects/lines/line-container.d.ts +35 -0
  181. package/dist/objects/lines/line-container.js +40 -0
  182. package/dist/objects/materials/ColorMaterial.d.ts +92 -0
  183. package/dist/objects/materials/ColorMaterial.js +124 -0
  184. package/dist/objects/materials/LineMaterial.d.ts +44 -0
  185. package/dist/objects/materials/LineMaterial.js +74 -0
  186. package/dist/objects/materials/ModelMaterial.d.ts +53 -0
  187. package/dist/objects/materials/ModelMaterial.js +111 -0
  188. package/dist/objects/materials/TextureMaterial.d.ts +33 -0
  189. package/dist/objects/materials/TextureMaterial.js +48 -0
  190. package/dist/objects/materials/effects/ao/ambient-occlusion-pass.d.ts +77 -0
  191. package/dist/objects/materials/effects/ao/ambient-occlusion-pass.js +296 -0
  192. package/dist/objects/materials/effects/ao/ambient-occlusion-patch.d.ts +29 -0
  193. package/dist/objects/materials/effects/ao/ambient-occlusion-patch.js +115 -0
  194. package/dist/objects/materials/effects/ao/blur-pass.d.ts +61 -0
  195. package/dist/objects/materials/effects/ao/blur-pass.js +101 -0
  196. package/dist/objects/materials/effects/ao/index.d.ts +129 -0
  197. package/dist/objects/materials/effects/ao/index.js +72 -0
  198. package/dist/objects/materials/effects/color-dim.d.ts +115 -0
  199. package/dist/objects/materials/effects/color-dim.js +206 -0
  200. package/dist/objects/materials/effects/contact-shadow.d.ts +100 -0
  201. package/dist/objects/materials/effects/contact-shadow.js +167 -0
  202. package/dist/objects/materials/effects/effects-context.d.ts +53 -0
  203. package/dist/objects/materials/effects/effects-context.js +24 -0
  204. package/dist/objects/materials/shared.d.ts +24 -0
  205. package/dist/objects/materials/shared.js +72 -0
  206. package/dist/objects/models/ModelInstance.d.ts +22 -0
  207. package/dist/objects/models/ModelInstance.js +33 -0
  208. package/dist/objects/models/Models.d.ts +22 -0
  209. package/dist/objects/models/Models.js +186 -0
  210. package/dist/objects/models/gltf-cache.d.ts +17 -0
  211. package/dist/objects/models/gltf-cache.js +50 -0
  212. package/dist/objects/models/index.d.ts +2 -0
  213. package/dist/objects/models/index.js +1 -0
  214. package/dist/objects/models/model-asset.d.ts +48 -0
  215. package/dist/objects/models/model-asset.js +77 -0
  216. package/dist/objects/models/model-container.d.ts +123 -0
  217. package/dist/objects/models/model-container.js +207 -0
  218. package/dist/objects/shapes/ShapeInstance.d.ts +26 -0
  219. package/dist/objects/shapes/ShapeInstance.js +39 -0
  220. package/dist/objects/shapes/Shapes.d.ts +21 -0
  221. package/dist/objects/shapes/Shapes.js +131 -0
  222. package/dist/objects/shapes/index.d.ts +2 -0
  223. package/dist/objects/shapes/index.js +1 -0
  224. package/dist/objects/shapes/shape-container.d.ts +141 -0
  225. package/dist/objects/shapes/shape-container.js +242 -0
  226. package/dist/objects/shapes/shape-geometry.d.ts +27 -0
  227. package/dist/objects/shapes/shape-geometry.js +94 -0
  228. package/dist/objects/text/TextInstance.d.ts +22 -0
  229. package/dist/objects/text/TextInstance.js +95 -0
  230. package/dist/objects/text/Texts.d.ts +30 -0
  231. package/dist/objects/text/Texts.js +43 -0
  232. package/dist/objects/text/index.d.ts +2 -0
  233. package/dist/objects/text/index.js +1 -0
  234. package/dist/objects/text/text-container.d.ts +121 -0
  235. package/dist/objects/text/text-container.js +229 -0
  236. package/dist/objects/text/text-layout.d.ts +92 -0
  237. package/dist/objects/text/text-layout.js +180 -0
  238. package/dist/space/Camera.d.ts +103 -0
  239. package/dist/space/Camera.js +321 -0
  240. package/dist/space/Scene.d.ts +47 -0
  241. package/dist/space/Scene.js +127 -0
  242. package/dist/space/Space.d.ts +325 -0
  243. package/dist/space/Space.js +359 -0
  244. package/dist/space/VisibleRect.d.ts +45 -0
  245. package/dist/space/VisibleRect.js +129 -0
  246. package/dist/space/camera-handle.d.ts +67 -0
  247. package/dist/space/camera-handle.js +63 -0
  248. package/dist/space/camera-kind.d.ts +26 -0
  249. package/dist/space/camera-kind.js +30 -0
  250. package/dist/space/canvas-size.d.ts +24 -0
  251. package/dist/space/canvas-size.js +20 -0
  252. package/dist/space/clipping.d.ts +69 -0
  253. package/dist/space/clipping.js +130 -0
  254. package/dist/space/layout-measure.d.ts +67 -0
  255. package/dist/space/layout-measure.js +175 -0
  256. package/dist/space/viewport.d.ts +112 -0
  257. package/dist/space/viewport.js +67 -0
  258. package/dist/types/defs.d.ts +234 -0
  259. package/dist/types/defs.js +1 -0
  260. package/dist/types/events.d.ts +198 -0
  261. package/dist/types/events.js +1 -0
  262. package/dist/types/guards.d.ts +21 -0
  263. package/dist/types/guards.js +52 -0
  264. package/dist/update/index.d.ts +3 -0
  265. package/dist/update/index.js +2 -0
  266. package/dist/update/slot-writers.d.ts +30 -0
  267. package/dist/update/slot-writers.js +76 -0
  268. package/dist/update/store.d.ts +41 -0
  269. package/dist/update/store.js +104 -0
  270. package/dist/utils/arrays.d.ts +13 -0
  271. package/dist/utils/arrays.js +20 -0
  272. package/dist/utils/colors.d.ts +19 -0
  273. package/dist/utils/colors.js +27 -0
  274. package/dist/utils/hot-state.d.ts +41 -0
  275. package/dist/utils/hot-state.js +37 -0
  276. package/dist/utils/logging.d.ts +29 -0
  277. package/dist/utils/logging.js +55 -0
  278. package/dist/utils/math.d.ts +52 -0
  279. package/dist/utils/math.js +76 -0
  280. package/package.json +79 -0
@@ -0,0 +1,152 @@
1
+ import { memo } from 'react';
2
+ import { Matrix4, Vector2, Vector3 } from 'three';
3
+ import { isBatchedSprites } from '../../core/sprites/BatchedSprites';
4
+ import { Y_DOWN_FLIP } from '../../space/Space';
5
+ import { shapeToMatrix, useDefSync, writeMatrixIfChanged } from '../containers';
6
+ /**
7
+ * Keeps one image placement current: claims a slot on mount (where the container writes the fixed
8
+ * uv region), writes the def's transform and visibility on mount and after every `update(def)`, and
9
+ * releases the slot on unmount.
10
+ *
11
+ * Renders no scene node — the mesh belongs to the container. Only the transform lives here:
12
+ * visibility is `useDefSync`'s, the uv region is written once at claim (a def's source cannot
13
+ * change), and color has no analogue for a textured quad. Memoized for the same reason as
14
+ * `ShapeInstance`: a structural change hands the atlas a fresh placement list, rebuilding every
15
+ * instance element, so only prop-equality keeps the unchanged siblings from re-rendering. Both props
16
+ * are identity-stable across a membership's lifetime — a change repacks into a fresh container and
17
+ * fresh placements — so the bailout holds.
18
+ * @param props - The def, its placement, and its container.
19
+ * @returns `null`; the instance lives in the mesh, not the scene graph.
20
+ */
21
+ export const ImageInstance = memo(function ImageInstance(props) {
22
+ const { def, packed, container } = props;
23
+ useDefSync(container, def, () => packed, (slot) => {
24
+ const tile = tileRect(packed);
25
+ writeImageMatrix(container, slot, def, tile);
26
+ writeImageAnchor(container, slot, def, tile);
27
+ container.setOpacityAt(slot, def.opacity);
28
+ // Checked on every sync rather than behind the write, because screenSpace is an ordinary def
29
+ // field. Flipping it reaches this instance and stops there, so this is where a mesh learns that
30
+ // it holds something it should not. The atlas re-filters, which never touches the packing.
31
+ if (!!def.screenSpace !== container.facesCamera)
32
+ container.reportKindMismatch();
33
+ });
34
+ return null;
35
+ });
36
+ /**
37
+ * Composes an image placement's instance matrix, and skips the write when nothing moved.
38
+ *
39
+ * `bounds` is a rect, so the quad's placement composes exactly as a rect shape's does. The two
40
+ * kinds part over the anchor and the tile.
41
+ *
42
+ * A flat quad folds the anchor into the matrix. That shifts the quad within its own unit square so
43
+ * the anchor, not the quad's center, lands on the bounds center, which also makes the anchor the
44
+ * point rotation pivots about. A tile then shrinks the quad onto the fraction of the image it covers.
45
+ *
46
+ * A sprite cannot fold the anchor in. It pivots about its anchor, so the matrix translation has to
47
+ * be the bounds center and stay there for every tile of a split source. Only the tile's scale and
48
+ * the plan-to-view y mirror stay in the matrix here. The anchor itself is
49
+ * {@link writeImageAnchor}'s write, not this one's.
50
+ * @param container - The container holding the instance, which says which kind it draws.
51
+ * @param slot - The instance's slot.
52
+ * @param def - The image def supplying the transform.
53
+ * @param tile - This instance's tile within the source, or `undefined` for an unsplit image.
54
+ */
55
+ function writeImageMatrix(container, slot, def, tile) {
56
+ const matrix = shapeToMatrix(def.bounds);
57
+ if (container.facesCamera) {
58
+ if (tile)
59
+ matrix.scale(_tileScale.set(tile.width, tile.height, 1));
60
+ // Y_DOWN_FLIP is the plan-to-view y mirror for this quad's own 2D linear map. Order does not
61
+ // matter against the tile scale above: the flip lands on the left of the whole linear map
62
+ // either way. Premultiplying also flips the translation, so save it and put it back. The
63
+ // translation is the bounds center the sprite pivots about and must not move.
64
+ const centerY = matrix.elements[13];
65
+ matrix.premultiply(Y_DOWN_FLIP);
66
+ matrix.elements[13] = centerY;
67
+ }
68
+ else {
69
+ const [originX, originY] = def.origin ?? DEFAULT_ORIGIN;
70
+ // The anchor as an offset from the quad's middle, which is the convention both paths use.
71
+ const anchorX = originX - 0.5;
72
+ const anchorY = originY - 0.5;
73
+ // Unit-plane axes run the same way as image axes, x right and y down, matching the atlas uv and
74
+ // the scene's y-down coordinates. So an origin of [0, 0], the top-left corner, shifts the quad
75
+ // by a positive half in both and puts its top-left corner on the bounds center.
76
+ matrix.multiply(_anchor.makeTranslation(-anchorX, -anchorY, 0));
77
+ if (tile) {
78
+ matrix.multiply(_tile.makeScale(tile.width, tile.height, 1).setPosition(tile.x, tile.y, 0));
79
+ }
80
+ }
81
+ writeMatrixIfChanged(container.mesh, slot, matrix);
82
+ }
83
+ /**
84
+ * Writes an image placement's anchor uniform, for the one kind that has one, skipping the write when
85
+ * it did not move.
86
+ *
87
+ * Only a sprite pivots about its anchor. The flat path folds the anchor into the matrix instead (see
88
+ * {@link writeImageMatrix}) and has no anchor uniform to write. The mesh is narrowed here rather
89
+ * than asked through `facesCamera`, which answers the same question but cannot narrow the mesh's
90
+ * type for the two anchor calls. A tile shifts the anchor by the tile's own offset from the image's
91
+ * middle and rescales it into the tile's unit square, so the result still reads as an offset from
92
+ * the tile's own middle.
93
+ *
94
+ * `setAnchorAt` invalidates the mesh's bounds on every call, so the compare-and-skip is what keeps
95
+ * a def whose anchor did not move from paying for that invalidation on every sync, the same way
96
+ * `writeMatrixIfChanged` does for the matrix.
97
+ * @param container - The container holding the instance.
98
+ * @param slot - The instance's slot.
99
+ * @param def - The image def supplying the origin.
100
+ * @param tile - This instance's tile within the source, or `undefined` for an unsplit image.
101
+ */
102
+ function writeImageAnchor(container, slot, def, tile) {
103
+ const mesh = container.mesh;
104
+ if (!isBatchedSprites(mesh))
105
+ return;
106
+ const [originX, originY] = def.origin ?? DEFAULT_ORIGIN;
107
+ // The anchor as an offset from the quad's middle, which is the convention both paths use.
108
+ let x = originX - 0.5;
109
+ let y = originY - 0.5;
110
+ if (tile) {
111
+ x = (x - tile.x) / tile.width;
112
+ y = (y - tile.y) / tile.height;
113
+ }
114
+ mesh.getAnchorAt(slot, _previousAnchor);
115
+ if (_previousAnchor.x === x && _previousAnchor.y === y)
116
+ return;
117
+ mesh.setAnchorAt(slot, x, y);
118
+ }
119
+ /**
120
+ * Where one tile sits inside the whole image, in unit-quad lengths.
121
+ * @param packed - This instance's placement, with the tile's region and the source size to divide
122
+ * by.
123
+ * @returns The tile's size and the offset of its center from the image's middle, or `undefined` for
124
+ * an unsplit placement or a source that has no size to measure against.
125
+ */
126
+ function tileRect(packed) {
127
+ const { source, size } = packed;
128
+ if (!source || size.width <= 0 || size.height <= 0)
129
+ return undefined;
130
+ const fx0 = source.x / size.width;
131
+ const fy0 = source.y / size.height;
132
+ const fx1 = (source.x + source.width) / size.width;
133
+ const fy1 = (source.y + source.height) / size.height;
134
+ return {
135
+ width: fx1 - fx0,
136
+ height: fy1 - fy0,
137
+ x: (fx0 + fx1) / 2 - 0.5,
138
+ y: (fy0 + fy1) / 2 - 0.5,
139
+ };
140
+ }
141
+ /**
142
+ * The anchor a def leaves unset: the image's center, so `bounds.center` is where the middle of the
143
+ * image sits. Matches {@link ImageDef.origin}'s documented default.
144
+ */
145
+ const DEFAULT_ORIGIN = [0.5, 0.5];
146
+ // Scratch shared by every instance and every write. Each is filled and uploaded within a single
147
+ // synchronous statement, so an instance costs a texture write and no allocation. `_previousAnchor`
148
+ // is only ever read back and compared, so a caller still holding the matrix never collides with it.
149
+ const _anchor = new Matrix4();
150
+ const _tile = new Matrix4();
151
+ const _tileScale = new Vector3();
152
+ const _previousAnchor = new Vector2();
@@ -0,0 +1,49 @@
1
+ import { type ImageDef } from '../../types/defs';
2
+ import { type DefsProps } from '../containers';
3
+ import { type CreateAtlasCanvas } from './atlas-packer';
4
+ /** Props for {@link Images}. */
5
+ export interface ImagesProps extends DefsProps<ImageDef> {
6
+ /**
7
+ * Largest atlas edge in pixels, capping how big a source can be before it is split into tiles.
8
+ * Defaults to the GPU's `maxTextureSize`, which is the true hardware ceiling; pass a smaller
9
+ * value to cap atlas memory below it, or to force splitting deterministically in a test.
10
+ */
11
+ maxAtlasSize?: number;
12
+ /**
13
+ * Makes each atlas canvas. Defaults to {@link createAtlasCanvas}, which builds an
14
+ * `OffscreenCanvas` wherever that global exists and a DOM canvas otherwise. The prop exists so
15
+ * that a test can pack without a real 2D context. It is not part of the public scene API.
16
+ */
17
+ createCanvas?: CreateAtlasCanvas;
18
+ }
19
+ /**
20
+ * Draws a collection of images as textured quads, packing their pixels into one or more atlases.
21
+ * Each atlas draws through up to two meshes that share its texture, so a layer of many images still
22
+ * costs a handful of draw calls, not one per image.
23
+ *
24
+ * Unlike a shape layer, an image layer can span **several meshes**: bin-packing splits the images
25
+ * across as many atlases as their pixels need, and each atlas gets its own texture drawn through up
26
+ * to two meshes, a flat `InstancedMesh` and a camera-facing `BatchedSprites` (an
27
+ * {@link ImageAtlas}). An unmixed layer still mounts only one mesh. A def whose source is too big
28
+ * for one atlas is cut into tiles that may land on different atlases, so one def can drive several
29
+ * instances. The atlas iterates its packed placements, not the defs.
30
+ *
31
+ * Packing has no incremental path, so a change to the **set** of images **repacks from scratch**: a
32
+ * fresh atlas set under fresh keys, remounting every mesh and instance. A **reorder** of the same
33
+ * set does not — packing is order-independent, so the atlases stand and each simply re-labels its
34
+ * instances' paint order in place. A per-def `update()` flows straight to the instance's slot, never
35
+ * through here.
36
+ *
37
+ * Paint order holds **within** an atlas (each mesh sorts its instances by the def's membership
38
+ * index) but is best-effort **across** atlases: separate meshes draw in atlas order, which packing
39
+ * decides, not paint order. Overlapping images in one layer that pack onto different atlases is the
40
+ * one case this does not order — rare for the markers, dots, and logos an image layer holds.
41
+ *
42
+ * Packing is asynchronous, so a layer draws nothing until its first pack settles, which is one
43
+ * microtask after the defs arrive. A repack that supersedes a pack still in flight aborts that
44
+ * pack, and closes whatever it produced.
45
+ * @param props - The images to draw.
46
+ * @returns One {@link ImageAtlas} per packed atlas.
47
+ */
48
+ export declare function Images(props: ImagesProps): import("react").JSX.Element;
49
+ //# sourceMappingURL=Images.d.ts.map
@@ -0,0 +1,394 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useThree } from '@react-three/fiber';
3
+ import { use, useEffect, useRef, useState } from 'react';
4
+ import { CanvasTexture, SRGBColorSpace } from 'three';
5
+ import { InstancedMesh } from '../../core/instanced/InstancedMesh';
6
+ import { BatchedSprites } from '../../core/sprites/BatchedSprites';
7
+ import { createLogger } from '../../utils/logging';
8
+ import { defKey, useContainer } from '../containers';
9
+ import { TextureMaterial } from '../materials/TextureMaterial';
10
+ import { packAtlases, } from './atlas-packer';
11
+ import { ImageContainer, OPACITY_UNIFORM, UV_OFFSET_UNIFORM } from './image-container';
12
+ import { ImageInstance } from './ImageInstance';
13
+ import { contextOf, createAtlasCanvas } from './pack-sources';
14
+ import { TextureBudgetContext, useResizeFactor } from './texture-budget';
15
+ const logger = createLogger('images');
16
+ /**
17
+ * Draws a collection of images as textured quads, packing their pixels into one or more atlases.
18
+ * Each atlas draws through up to two meshes that share its texture, so a layer of many images still
19
+ * costs a handful of draw calls, not one per image.
20
+ *
21
+ * Unlike a shape layer, an image layer can span **several meshes**: bin-packing splits the images
22
+ * across as many atlases as their pixels need, and each atlas gets its own texture drawn through up
23
+ * to two meshes, a flat `InstancedMesh` and a camera-facing `BatchedSprites` (an
24
+ * {@link ImageAtlas}). An unmixed layer still mounts only one mesh. A def whose source is too big
25
+ * for one atlas is cut into tiles that may land on different atlases, so one def can drive several
26
+ * instances. The atlas iterates its packed placements, not the defs.
27
+ *
28
+ * Packing has no incremental path, so a change to the **set** of images **repacks from scratch**: a
29
+ * fresh atlas set under fresh keys, remounting every mesh and instance. A **reorder** of the same
30
+ * set does not — packing is order-independent, so the atlases stand and each simply re-labels its
31
+ * instances' paint order in place. A per-def `update()` flows straight to the instance's slot, never
32
+ * through here.
33
+ *
34
+ * Paint order holds **within** an atlas (each mesh sorts its instances by the def's membership
35
+ * index) but is best-effort **across** atlases: separate meshes draw in atlas order, which packing
36
+ * decides, not paint order. Overlapping images in one layer that pack onto different atlases is the
37
+ * one case this does not order — rare for the markers, dots, and logos an image layer holds.
38
+ *
39
+ * Packing is asynchronous, so a layer draws nothing until its first pack settles, which is one
40
+ * microtask after the defs arrive. A repack that supersedes a pack still in flight aborts that
41
+ * pack, and closes whatever it produced.
42
+ * @param props - The images to draw.
43
+ * @returns One {@link ImageAtlas} per packed atlas.
44
+ */
45
+ export function Images(props) {
46
+ const { defs, maxAtlasSize, createCanvas } = props;
47
+ // Handed to each atlas as a prefix of *its* placements rather than apportioned between them, and
48
+ // a split source has several placements per def — so a slice mounts somewhere between one and a
49
+ // few instances per granted child. The scheduler measures what a slice cost and sizes the next
50
+ // one from that, which is what keeps the budget whatever the ratio turns out to be here.
51
+ const mounted = props.mounted ?? defs.length;
52
+ const gl = useThree((state) => state.gl);
53
+ const invalidate = useThree((state) => state.invalidate);
54
+ const maxTextureSize = maxAtlasSize ?? gl.capabilities.maxTextureSize;
55
+ // Repack when the *set* of images changes, through the "adjust state on prop change" pattern. The
56
+ // comparison reads content and not identity. `useStagedChildren` rebuilds its snapshot on *every*
57
+ // `update(layer)`, even one that only flipped the layer's own `hidden`, so identity alone would
58
+ // repack for changes that touched no image. Order is excluded on purpose. Packing reads only
59
+ // pixels, so a reorder produces the same atlases and needs no repack, only the paint-order
60
+ // relabel that each atlas does below. Set equality costs O(n) over a list already in cache, and a
61
+ // repack redraws every canvas.
62
+ const [target, setTarget] = useState(defs);
63
+ if (!sameSet(target, defs))
64
+ setTarget(defs);
65
+ const [packed, setPacked] = useState(NO_ATLASES);
66
+ // Pack the current target, and abandon the pack when the target moves before that pack settles. A
67
+ // pack that already produced atlases when it is abandoned closes them here. Nothing mounted them,
68
+ // so nothing else will.
69
+ //
70
+ // No abort reaches that branch today, on either tier. Everything from the pack's own result down
71
+ // to the handler below is one uninterrupted drain of microtasks, and the only call to `abort` is
72
+ // this effect's cleanup, which runs in a task. So an abort lands before the result, and
73
+ // `packAtlases` rejects instead; or it lands after `adopted` is set, and the cleanup does not
74
+ // abort at all. The branch stays as insurance. One task boundary added anywhere in that chain
75
+ // opens the window, and the cost of finding out late is a leaked set of atlases.
76
+ useEffect(() => {
77
+ const controller = new AbortController();
78
+ let adopted = false;
79
+ packAtlases(target, maxTextureSize, { createCanvas, signal: controller.signal }).then((atlases) => {
80
+ if (controller.signal.aborted) {
81
+ closeAtlases(atlases);
82
+ return;
83
+ }
84
+ adopted = true;
85
+ setPacked((current) => ({
86
+ defs: target,
87
+ atlases,
88
+ generation: current.generation + 1,
89
+ }));
90
+ invalidate();
91
+ }, (error) => {
92
+ // An abort is the normal end of a superseded pack, and not a fault worth reporting.
93
+ if (!controller.signal.aborted)
94
+ logger.error('atlas pack failed %O', error);
95
+ });
96
+ return () => {
97
+ if (!adopted)
98
+ controller.abort();
99
+ };
100
+ }, [target, maxTextureSize, createCanvas, invalidate]);
101
+ // Free the pixels of a pack that a later pack superseded, and of the pack that is mounted when
102
+ // this layer goes away. The effect is keyed on the pack object, so its cleanup holds the pack it
103
+ // closes and never the pack that is mounted now. The initial state holds no atlases, so
104
+ // StrictMode's simulated remount cycles this effect once with that initial value and closes
105
+ // nothing.
106
+ useEffect(() => {
107
+ return () => closeAtlases(packed.atlases);
108
+ }, [packed]);
109
+ return (_jsx(_Fragment, { children: packed.atlases.map((atlas) => (_jsx(ImageAtlas, { atlas: atlas, defs: defs, mounted: mounted }, atlasKey(atlas, packed.generation)))) }));
110
+ }
111
+ /**
112
+ * Owns one atlas's texture and its flat/sprite split, and draws each non-empty side as its own
113
+ * `AtlasMesh`. The atlas's set of images is fixed for its lifetime. A repack remounts the whole
114
+ * component, so the texture is built once from state and released on unmount.
115
+ *
116
+ * The uploaded texture is not fixed. A resizable atlas registers with the scene's texture budget and
117
+ * re-derives a downscaled copy of its canvas whenever the shared resize factor moves (see
118
+ * {@link resizeTexture}). That is a texture re-upload, not a repack. The meshes, their containers,
119
+ * their instances, and their normalized uv regions all stand, because uv does not depend on the
120
+ * canvas's pixel size.
121
+ *
122
+ * The flat/sprite split is a second kind of state, held here rather than derived every render. A
123
+ * def's `screenSpace` is an ordinary slot write that never re-renders this component, so an
124
+ * `AtlasMesh` calls `repartition` when it finds a member that no longer matches its kind.
125
+ * @param props - The atlas and the layer's images in paint order.
126
+ * @returns One `AtlasMesh` per non-empty kind, sharing this atlas's texture.
127
+ */
128
+ function ImageAtlas(props) {
129
+ const { atlas, defs } = props;
130
+ const mounted = props.mounted ?? atlas.images.length;
131
+ const [texture] = useState(() => makeAtlasTexture(atlas.image));
132
+ const invalidate = useThree((state) => state.invalidate);
133
+ const budget = use(TextureBudgetContext);
134
+ const factor = useResizeFactor(budget, atlas);
135
+ const scaledRef = useRef({});
136
+ // Only the texture is ours to free — the mesh, its quad, and its material are all R3F's.
137
+ // StrictMode's simulated unmount disposes then remounts, but three's `dispose` only frees GPU
138
+ // resources — the state-retained texture re-uploads from its canvas on the next render — so the
139
+ // double is a harmless dev-only reupload, not a use-after-free.
140
+ useEffect(() => {
141
+ invalidate();
142
+ return () => texture.dispose();
143
+ }, [texture, invalidate]);
144
+ // Re-derive the uploaded pixels for the current resize factor. This runs on mount and on every
145
+ // factor change. A fixed atlas always reads a factor of `1`, so this settles once for it and
146
+ // never fires again.
147
+ useEffect(() => {
148
+ resizeTexture(texture, atlas.image, scaledRef, factor);
149
+ invalidate();
150
+ }, [texture, atlas, factor, invalidate]);
151
+ // The smaller copy belongs to this component, not to the atlas, so this effect frees it. This is
152
+ // safe under StrictMode's simulated unmount. The next `resizeTexture` call rebuilds the copy from
153
+ // the full atlas, which stays untouched.
154
+ //
155
+ // `scaledRef.current` is read once, here, instead of inside the returned function. The ref holds
156
+ // one stable object for the component's whole life and is never pointed at a new one, so the read
157
+ // is safe either way, but reading it here is what lets the cleanup skip a ref read of its own.
158
+ useEffect(() => {
159
+ const scaled = scaledRef.current;
160
+ return () => closeScaled(scaled);
161
+ }, []);
162
+ // Held in state rather than derived each render, which is the shape `Shapes` uses for its own
163
+ // split. screenSpace is an ordinary slot write that never re-renders this component, so a def
164
+ // crossing the orientation boundary would otherwise stay in a mesh that no longer suits it.
165
+ // repartition is what an instance's reportKindMismatch runs. The atlas's placements never change
166
+ // identity, because a repack remounts this whole component, so nothing else triggers a re-split.
167
+ const [partition, setPartition] = useState(() => partitionImages(atlas.images));
168
+ /** Re-splits the atlas's placements, for a mesh that found a member of the wrong kind. */
169
+ function repartition() {
170
+ setPartition(() => partitionImages(atlas.images));
171
+ }
172
+ // One mesh per kind, keyed so React never reconciles a flat mesh element against a sprite one
173
+ // when the first kind empties out. An empty subset renders nothing, so an unmixed layer costs
174
+ // exactly what it costs today and a mixed layer costs one extra draw call per atlas.
175
+ //
176
+ // renderOrder is the kind's place in IMAGE_KINDS, which makes "flats, then sprites" an explicit
177
+ // rule within a layer. Today the atlas meshes carry no order at all and it falls out of
178
+ // material.id, which is mount order. What that costs is that a flat def and a sprite def in one
179
+ // layer can no longer interleave in array order. `overlay` is there for a caller who needs strict
180
+ // separation, and a camera-facing pin is generally meant to be on top anyway.
181
+ return (_jsx(_Fragment, { children: IMAGE_KINDS.map((kind, renderOrder) => {
182
+ const placements = partition[kind];
183
+ if (placements.length === 0)
184
+ return null;
185
+ return (_jsx(AtlasMesh, { kind: kind, placements: placements, defs: defs, mounted: mounted, capacity: atlas.images.length, texture: texture, renderOrder: renderOrder, onKindMismatch: repartition }, kind));
186
+ }) }));
187
+ }
188
+ /**
189
+ * One atlas mesh and its instances: the atlas's flat images or its camera-facing ones, never both.
190
+ *
191
+ * The material is a child and is genuinely per mesh. `InstancedMeshImpl` wraps whatever it draws with
192
+ * in a closure over its own textures, and two instanced meshes on one material would both render
193
+ * against the first one's. The texture is shared, and that is the whole of the two-mesh split: an
194
+ * atlas is a texture packing and a mesh is a draw call, and nothing forces them to be one thing.
195
+ *
196
+ * The sprite mesh mounts no plane geometry child. `BatchedSpritesImpl` seeds the shared quad
197
+ * itself, and not rendering a replacement is what makes the seeded one stand.
198
+ * @param props - The kind, its placements, and the shared texture.
199
+ * @returns The mesh and one instance component per placement.
200
+ */
201
+ function AtlasMesh(props) {
202
+ const { kind, placements, defs, capacity, texture, renderOrder, onKindMismatch } = props;
203
+ // Handed to this mesh as a prefix of its own placements rather than apportioned between the two,
204
+ // as Shapes does for its batches. A slice can overshoot the scheduler's budget by a factor of two
205
+ // at worst, and the scheduler measures what a slice actually cost and sizes the next one from it.
206
+ const mounted = props.mounted ?? placements.length;
207
+ // Sized from the atlas's whole placement count, not this subset's, so a re-split never resizes a
208
+ // buffer. The container is bookkeeping only, so the copy StrictMode's double-invoked initializer
209
+ // discards is plain garbage.
210
+ const container = useContainer(() => new ImageContainer(capacity), defs, onKindMismatch);
211
+ // args is a frozen snapshot, so R3F never rebuilds the mesh. The ref arrow, rather than
212
+ // ref={container.bind}, keeps the React Compiler from inferring container itself as a ref and
213
+ // rejecting its render reads.
214
+ const mesh = kind === 'sprite' ? (_jsx(BatchedSprites, { args: container.args, ref: (instance) => container.bind(instance), sortObjects: true, customSort: container.customSort, perObjectFrustumCulled: true, renderOrder: renderOrder, children: _jsx(TextureMaterial, { map: texture, uvOffset: UV_OFFSET_UNIFORM, opacityUniform: OPACITY_UNIFORM }) })) : (_jsxs(InstancedMesh, { args: container.args, ref: (instance) => container.bind(instance), sortObjects: true, customSort: container.customSort, perObjectFrustumCulled: true, renderOrder: renderOrder, children: [_jsx("planeGeometry", {}), _jsx(TextureMaterial, { map: texture, uvOffset: UV_OFFSET_UNIFORM, opacityUniform: OPACITY_UNIFORM })] }));
215
+ return (_jsxs(_Fragment, { children: [mesh, placements.slice(0, mounted).map((packed) => (_jsx(ImageInstance, { def: packed.def, packed: packed, container: container }, `${defKey(packed.def)}:${packed.uv.u0}:${packed.uv.v0}`)))] }));
216
+ }
217
+ /**
218
+ * Splits an atlas's placements by orientation.
219
+ *
220
+ * A filter over an array the component already holds. The packing, the canvas, the uv regions and the
221
+ * texture budget are all untouched, so nothing repacks. That is what makes flipping a def's
222
+ * screenSpace cheaper here than a shape's kind change, where a re-split rebuilds partitions.
223
+ * @param images - The atlas's placements, in packing order.
224
+ * @returns The two subsets, each still in packing order.
225
+ */
226
+ function partitionImages(images) {
227
+ const flat = [];
228
+ const sprite = [];
229
+ for (const packed of images)
230
+ (packed.def.screenSpace === true ? sprite : flat).push(packed);
231
+ return { flat, sprite };
232
+ }
233
+ /** The atlas's two meshes, in draw order: flats first, then the camera-facing ones. */
234
+ const IMAGE_KINDS = ['flat', 'sprite'];
235
+ /**
236
+ * Whether two snapshots hold the same *set* of images, order aside — the question "does this need a
237
+ * repack". Order is excluded on purpose: packing reads only pixels, so a reorder produces the same
238
+ * atlases and needs only a paint-order relabel, not a repack. Array identity answers neither
239
+ * question, because the store hands out a fresh snapshot on every layer update.
240
+ * @param a - The packed set.
241
+ * @param b - The incoming set.
242
+ * @returns Whether they hold the same defs.
243
+ */
244
+ function sameSet(a, b) {
245
+ if (a.length !== b.length)
246
+ return false;
247
+ const set = new Set(a);
248
+ return b.every((def) => set.has(def));
249
+ }
250
+ /**
251
+ * A stable React key for an atlas: the repack generation, so a repack remounts every atlas, plus
252
+ * its first placement's def, uv origin, and — for a split source — tile region, which together no
253
+ * other atlas in the same generation can repeat (a placement lands on exactly one atlas). Packing
254
+ * never returns an empty atlas, so the first placement always exists.
255
+ * @param atlas - The atlas to key.
256
+ * @param generation - The repack counter this atlas belongs to.
257
+ * @returns The key.
258
+ */
259
+ function atlasKey(atlas, generation) {
260
+ const first = atlas.images[0];
261
+ // A def keys by identity and uv origin, but a source split into one-tile-per-atlas puts the same
262
+ // def at uv origin (0, 0) on every one of its atlases; the tile's source region is what tells
263
+ // those atlases apart. Absent for an unsplit image, whose uv origin already differs per atlas.
264
+ const region = first.source ? `:${first.source.x},${first.source.y}` : '';
265
+ return `${generation}:${defKey(first.def)}:${first.uv.u0}:${first.uv.v0}${region}`;
266
+ }
267
+ /**
268
+ * Releases an atlas's pixel memory.
269
+ *
270
+ * A worker returns each atlas as an `ImageBitmap`, which holds its pixels outside the JavaScript
271
+ * heap, where garbage collection does not reclaim them promptly. Every path that drops an atlas must
272
+ * therefore close it: a repack that supersedes one, a canceled pack, and unmount. A canvas has no
273
+ * `close`, so the inline path passes through here and nothing happens.
274
+ * @param atlases - The atlases to release.
275
+ */
276
+ function closeAtlases(atlases) {
277
+ for (const atlas of atlases) {
278
+ if ('close' in atlas.image)
279
+ atlas.image.close();
280
+ }
281
+ }
282
+ /**
283
+ * Points an atlas texture at the pixels for the current resize factor.
284
+ *
285
+ * At `factor >= 1`, the texture points at the full atlas. Below that, it points at a downscaled
286
+ * copy.
287
+ *
288
+ * The copy lives in `scaledRef`. The function creates the copy the first time it needs one. After
289
+ * that, it resizes the same copy in place. If the factor later climbs back up because the budget
290
+ * freed up, the next downscale starts from the untouched full atlas. It never upscales pixels
291
+ * already thrown away. The atlas keeps the full atlas regardless, so a downscale costs one extra
292
+ * surface only while it is active. UVs stay the same throughout. They are normalized, so the same
293
+ * regions still address the smaller surface without change.
294
+ *
295
+ * Where `OffscreenCanvas` exists, the downscale draws into one and uploads the bitmap from its
296
+ * `transferToImageBitmap()`. The whole design avoids uploading a DOM canvas. Without
297
+ * `OffscreenCanvas` here, a downscale below a factor of 1 brings that cost back on every atlas.
298
+ * @param texture - The atlas texture to re-point.
299
+ * @param full - The full-resolution atlas pixels. The function never changes them.
300
+ * @param scaledRef - Holds the smaller copy across factor changes. Its object is stable for the
301
+ * caller's whole life; only the fields inside it change.
302
+ * @param factor - The linear scale factor, in `(0, 1]`.
303
+ */
304
+ function resizeTexture(texture, full, scaledRef, factor) {
305
+ const scaled = scaledRef.current;
306
+ if (factor >= 1) {
307
+ // Point back at the full atlas. Close and drop a copy from an earlier, smaller factor.
308
+ if (texture.image !== full) {
309
+ closeScaled(scaled);
310
+ texture.image = full;
311
+ texture.needsUpdate = true;
312
+ }
313
+ return;
314
+ }
315
+ const width = Math.max(1, Math.round(full.width * factor));
316
+ const height = Math.max(1, Math.round(full.height * factor));
317
+ const atlasCanvas = (scaled.canvas ??= createAtlasCanvas(width, height));
318
+ // Setting the size also clears the canvas, so the downscale draws onto a blank surface.
319
+ atlasCanvas.width = width;
320
+ atlasCanvas.height = height;
321
+ const context = contextOf(atlasCanvas);
322
+ // A missing 2D context is a dead end, the same as a blank atlas. Fall back to the full atlas, so
323
+ // the images still draw over budget rather than not at all. Close the last transfer on the way
324
+ // out: the texture points at the full atlas from here on, so the `texture.image !== full` guard
325
+ // above never fires again and nothing else would ever free those pixels.
326
+ if (!context) {
327
+ closeScaled(scaled);
328
+ texture.image = full;
329
+ texture.needsUpdate = true;
330
+ return;
331
+ }
332
+ context.drawImage(full, 0, 0, width, height);
333
+ // The old transfer's pixels are dead the moment a new one replaces them. Garbage collection does
334
+ // not reclaim an `ImageBitmap` right away.
335
+ scaled.bitmap?.close();
336
+ scaled.bitmap =
337
+ 'transferToImageBitmap' in atlasCanvas ? atlasCanvas.transferToImageBitmap() : undefined;
338
+ texture.image = scaled.bitmap ?? atlasCanvas;
339
+ texture.needsUpdate = true;
340
+ }
341
+ /**
342
+ * Releases a smaller copy's transferred pixels, if the copy ever took one.
343
+ * @param scaled - The copy.
344
+ */
345
+ function closeScaled(scaled) {
346
+ scaled.bitmap?.close();
347
+ scaled.bitmap = undefined;
348
+ }
349
+ /**
350
+ * Wraps an atlas image as a texture for `MeshBasicMaterial.map`.
351
+ *
352
+ * Mipmaps stay on. A floor plan is zoomed far out as often as it is zoomed far in, and without
353
+ * mipmaps every image shimmers under minification. The cost is that neighbors can bleed through
354
+ * the deeper mip levels, where the packer's one-pixel gutter no longer separates them.
355
+ * @param image - The packed atlas image: a canvas, or a bitmap a worker transferred out of one.
356
+ * @returns The texture.
357
+ */
358
+ function makeAtlasTexture(image) {
359
+ const texture = new CanvasTexture(image);
360
+ // The atlas holds sRGB pixels; tagging the color space is what makes `MeshBasicMaterial` sample
361
+ // and display them without a gamma shift.
362
+ texture.colorSpace = SRGBColorSpace;
363
+ // Upload the canvas rows in their own order, instead of the vertical flip three applies by
364
+ // default. This way, v runs down the atlas, in the same direction as the packer's uv regions and
365
+ // the scene's y axis. Every axis in the chain then points the same way. If only one end flips,
366
+ // images come out upside down.
367
+ //
368
+ // This governs the inline path, where the texture uploads a canvas. For an `ImageBitmap`, this
369
+ // line does nothing: three skips it, along with two other pixel-store calls, whenever the
370
+ // texture image is an `ImageBitmap` (see `premultiplyAlpha`, below, for the one skip that is not
371
+ // harmless). Orientation then comes from how the bitmap was made. The worker draws into an
372
+ // `OffscreenCanvas` with row 0 at the top and transfers it unflipped, which matches the
373
+ // orientation `UvRect` documents. Do not delete this line: the worker path does not read it, but
374
+ // the inline path still does.
375
+ texture.flipY = false;
376
+ // The same skip covers `UNPACK_PREMULTIPLY_ALPHA_WEBGL` and `UNPACK_COLORSPACE_CONVERSION_WEBGL`.
377
+ // Skipping the second changes nothing: it is a legacy flag for an embedded color profile, and an
378
+ // atlas canvas is drawn, never decoded from a file that could carry one. Skipping the first is
379
+ // not harmless. `transferToImageBitmap()` hands over the canvas's own backing store, which a 2D
380
+ // canvas always keeps premultiplied, and it takes no option to ask for straight alpha instead.
381
+ // With the GL call skipped, that premultiplied data reaches the texture unchanged. Setting
382
+ // `premultiplyAlpha = true` here makes the canvas path premultiply on upload too, so both paths
383
+ // agree by construction rather than by luck. `TextureMaterial` blends on that agreement: it
384
+ // expects a premultiplied source, not a straight one.
385
+ texture.premultiplyAlpha = true;
386
+ texture.generateMipmaps = true;
387
+ return texture;
388
+ }
389
+ /**
390
+ * The pack that a layer starts on: none. Packing is asynchronous, so a layer draws nothing for the
391
+ * first microtask of its life. One value serves every layer, because every layer starts from the
392
+ * same nothing, and nothing writes to it.
393
+ */
394
+ const NO_ATLASES = { defs: [], atlases: [], generation: 0 };