@expofp/renderer 3.2.4 → 3.27.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 (228) hide show
  1. package/README.md +111 -0
  2. package/dist/Renderer.d.ts +88 -0
  3. package/dist/Renderer.js +53 -0
  4. package/dist/controls/CameraControls.d.ts +102 -0
  5. package/dist/controls/CameraControls.js +239 -0
  6. package/dist/controls/camera-api.d.ts +206 -0
  7. package/dist/controls/camera-api.js +181 -0
  8. package/dist/controls/camera-move.d.ts +13 -0
  9. package/dist/controls/camera-move.js +18 -0
  10. package/dist/controls/camera-state.d.ts +40 -0
  11. package/dist/controls/camera-state.js +65 -0
  12. package/dist/controls/gestures.d.ts +70 -0
  13. package/dist/controls/gestures.js +84 -0
  14. package/dist/controls/handlers/Inertia.d.ts +111 -0
  15. package/dist/controls/handlers/Inertia.js +215 -0
  16. package/dist/controls/handlers/Pan.d.ts +23 -0
  17. package/dist/controls/handlers/Pan.js +32 -0
  18. package/dist/controls/handlers/Pitch.d.ts +94 -0
  19. package/dist/controls/handlers/Pitch.js +191 -0
  20. package/dist/controls/handlers/Roll.d.ts +103 -0
  21. package/dist/controls/handlers/Roll.js +182 -0
  22. package/dist/controls/handlers/Zoom.d.ts +15 -0
  23. package/dist/controls/handlers/Zoom.js +26 -0
  24. package/dist/controls/index.d.ts +56 -0
  25. package/dist/controls/index.js +58 -0
  26. package/dist/controls/zoom-to.d.ts +89 -0
  27. package/dist/controls/zoom-to.js +91 -0
  28. package/dist/core/InstanceUniforms.d.ts +93 -0
  29. package/dist/core/InstanceUniforms.js +266 -0
  30. package/dist/core/SquareDataTexture.d.ts +277 -0
  31. package/dist/core/SquareDataTexture.js +476 -0
  32. package/dist/core/batch/BatchedMesh.d.ts +74 -0
  33. package/dist/core/batch/BatchedMesh.js +149 -0
  34. package/dist/core/instanced/FrustumCulling.d.ts +141 -0
  35. package/dist/core/instanced/FrustumCulling.js +246 -0
  36. package/dist/core/instanced/InstancedMesh.d.ts +302 -0
  37. package/dist/core/instanced/InstancedMesh.js +497 -0
  38. package/dist/core/instanced/Instances.d.ts +119 -0
  39. package/dist/core/instanced/Instances.js +230 -0
  40. package/dist/core/instanced/Raycasting.d.ts +118 -0
  41. package/dist/core/instanced/Raycasting.js +134 -0
  42. package/dist/core/instanced/instanced-material.d.ts +77 -0
  43. package/dist/core/instanced/instanced-material.js +224 -0
  44. package/dist/core/lines/BatchedLines.d.ts +135 -0
  45. package/dist/core/lines/BatchedLines.js +213 -0
  46. package/dist/core/lines/FrustumCulling.d.ts +43 -0
  47. package/dist/core/lines/FrustumCulling.js +85 -0
  48. package/dist/core/lines/Raycasting.d.ts +44 -0
  49. package/dist/core/lines/Raycasting.js +91 -0
  50. package/dist/core/lines/lines-material.d.ts +47 -0
  51. package/dist/core/lines/lines-material.js +115 -0
  52. package/dist/core/shader-source.d.ts +50 -0
  53. package/dist/core/shader-source.js +84 -0
  54. package/dist/core/text/BatchedText.d.ts +227 -0
  55. package/dist/core/text/BatchedText.js +467 -0
  56. package/dist/core/text/MemberTexture.d.ts +90 -0
  57. package/dist/core/text/MemberTexture.js +194 -0
  58. package/dist/core/text/Text.d.ts +73 -0
  59. package/dist/core/text/Text.js +215 -0
  60. package/dist/core/text/batched-text-material.d.ts +56 -0
  61. package/dist/core/text/batched-text-material.js +121 -0
  62. package/dist/core/text/glyph-packing.d.ts +17 -0
  63. package/dist/core/text/glyph-packing.js +61 -0
  64. package/dist/core/typed-array.d.ts +16 -0
  65. package/dist/core/typed-array.js +20 -0
  66. package/dist/debug/components/DebugPanelProvider.d.ts +20 -0
  67. package/dist/debug/components/DebugPanelProvider.js +38 -0
  68. package/dist/debug/components/DebugTools.d.ts +22 -0
  69. package/dist/debug/components/DebugTools.js +28 -0
  70. package/dist/debug/components/Gui.d.ts +20 -0
  71. package/dist/debug/components/Gui.js +32 -0
  72. package/dist/debug/components/MemoryInfo.d.ts +22 -0
  73. package/dist/debug/components/MemoryInfo.js +42 -0
  74. package/dist/debug/components/Panel.d.ts +31 -0
  75. package/dist/debug/components/Panel.js +98 -0
  76. package/dist/debug/components/Stats.d.ts +9 -0
  77. package/dist/debug/components/Stats.js +24 -0
  78. package/dist/debug/debug-context.d.ts +41 -0
  79. package/dist/debug/debug-context.js +59 -0
  80. package/dist/debug/index.d.ts +51 -0
  81. package/dist/debug/index.js +64 -0
  82. package/dist/events/LayoutDriver.d.ts +31 -0
  83. package/dist/events/LayoutDriver.js +88 -0
  84. package/dist/events/PointerMissed.d.ts +23 -0
  85. package/dist/events/PointerMissed.js +58 -0
  86. package/dist/events/def-culling.d.ts +46 -0
  87. package/dist/events/def-culling.js +69 -0
  88. package/dist/events/layout-targets.d.ts +104 -0
  89. package/dist/events/layout-targets.js +245 -0
  90. package/dist/events/pointer.d.ts +118 -0
  91. package/dist/events/pointer.js +168 -0
  92. package/dist/external/ExternalCanvas.d.ts +56 -0
  93. package/dist/external/ExternalCanvas.js +178 -0
  94. package/dist/external/external-camera.d.ts +17 -0
  95. package/dist/external/external-camera.js +82 -0
  96. package/dist/external/external-events.d.ts +14 -0
  97. package/dist/external/external-events.js +33 -0
  98. package/dist/index.d.ts +11 -783
  99. package/dist/index.js +12 -8028
  100. package/dist/objects/Background.d.ts +31 -0
  101. package/dist/objects/Background.js +37 -0
  102. package/dist/objects/Lighting.d.ts +20 -0
  103. package/dist/objects/Lighting.js +85 -0
  104. package/dist/objects/containers/SlotContainer.d.ts +266 -0
  105. package/dist/objects/containers/SlotContainer.js +251 -0
  106. package/dist/objects/containers/hooks.d.ts +46 -0
  107. package/dist/objects/containers/hooks.js +118 -0
  108. package/dist/objects/containers/index.d.ts +6 -0
  109. package/dist/objects/containers/index.js +4 -0
  110. package/dist/objects/containers/keys.d.ts +10 -0
  111. package/dist/objects/containers/keys.js +18 -0
  112. package/dist/objects/containers/mesh-writes.d.ts +106 -0
  113. package/dist/objects/containers/mesh-writes.js +97 -0
  114. package/dist/objects/containers/props.d.ts +23 -0
  115. package/dist/objects/containers/props.js +1 -0
  116. package/dist/objects/images/ImageInstance.d.ts +29 -0
  117. package/dist/objects/images/ImageInstance.js +83 -0
  118. package/dist/objects/images/Images.d.ts +37 -0
  119. package/dist/objects/images/Images.js +204 -0
  120. package/dist/objects/images/atlas-packer.d.ts +128 -0
  121. package/dist/objects/images/atlas-packer.js +315 -0
  122. package/dist/objects/images/image-container.d.ts +51 -0
  123. package/dist/objects/images/image-container.js +70 -0
  124. package/dist/objects/images/index.d.ts +2 -0
  125. package/dist/objects/images/index.js +1 -0
  126. package/dist/objects/images/texture-budget.d.ts +120 -0
  127. package/dist/objects/images/texture-budget.js +215 -0
  128. package/dist/objects/layer/Layer.d.ts +27 -0
  129. package/dist/objects/layer/Layer.js +114 -0
  130. package/dist/objects/layer/index.d.ts +2 -0
  131. package/dist/objects/layer/index.js +1 -0
  132. package/dist/objects/layer/mount-scheduler.d.ts +102 -0
  133. package/dist/objects/layer/mount-scheduler.js +145 -0
  134. package/dist/objects/layer/visibility-gui.d.ts +19 -0
  135. package/dist/objects/layer/visibility-gui.js +93 -0
  136. package/dist/objects/lines/LineInstance.d.ts +21 -0
  137. package/dist/objects/lines/LineInstance.js +73 -0
  138. package/dist/objects/lines/Lines.d.ts +17 -0
  139. package/dist/objects/lines/Lines.js +29 -0
  140. package/dist/objects/lines/index.d.ts +2 -0
  141. package/dist/objects/lines/index.js +1 -0
  142. package/dist/objects/lines/line-container.d.ts +35 -0
  143. package/dist/objects/lines/line-container.js +40 -0
  144. package/dist/objects/materials/ColorMaterial.d.ts +85 -0
  145. package/dist/objects/materials/ColorMaterial.js +89 -0
  146. package/dist/objects/materials/LineMaterial.d.ts +44 -0
  147. package/dist/objects/materials/LineMaterial.js +72 -0
  148. package/dist/objects/materials/ModelMaterial.d.ts +41 -0
  149. package/dist/objects/materials/ModelMaterial.js +84 -0
  150. package/dist/objects/materials/TextureMaterial.d.ts +30 -0
  151. package/dist/objects/materials/TextureMaterial.js +29 -0
  152. package/dist/objects/materials/dim.d.ts +123 -0
  153. package/dist/objects/materials/dim.js +213 -0
  154. package/dist/objects/materials/shared.d.ts +6 -0
  155. package/dist/objects/materials/shared.js +51 -0
  156. package/dist/objects/models/ModelInstance.d.ts +22 -0
  157. package/dist/objects/models/ModelInstance.js +33 -0
  158. package/dist/objects/models/Models.d.ts +22 -0
  159. package/dist/objects/models/Models.js +177 -0
  160. package/dist/objects/models/gltf-cache.d.ts +17 -0
  161. package/dist/objects/models/gltf-cache.js +50 -0
  162. package/dist/objects/models/index.d.ts +2 -0
  163. package/dist/objects/models/index.js +1 -0
  164. package/dist/objects/models/model-asset.d.ts +48 -0
  165. package/dist/objects/models/model-asset.js +77 -0
  166. package/dist/objects/models/model-container.d.ts +139 -0
  167. package/dist/objects/models/model-container.js +227 -0
  168. package/dist/objects/shapes/ShapeInstance.d.ts +26 -0
  169. package/dist/objects/shapes/ShapeInstance.js +40 -0
  170. package/dist/objects/shapes/Shapes.d.ts +21 -0
  171. package/dist/objects/shapes/Shapes.js +120 -0
  172. package/dist/objects/shapes/index.d.ts +2 -0
  173. package/dist/objects/shapes/index.js +1 -0
  174. package/dist/objects/shapes/shape-container.d.ts +165 -0
  175. package/dist/objects/shapes/shape-container.js +272 -0
  176. package/dist/objects/shapes/shape-geometry.d.ts +27 -0
  177. package/dist/objects/shapes/shape-geometry.js +94 -0
  178. package/dist/objects/text/TextInstance.d.ts +22 -0
  179. package/dist/objects/text/TextInstance.js +95 -0
  180. package/dist/objects/text/Texts.d.ts +30 -0
  181. package/dist/objects/text/Texts.js +41 -0
  182. package/dist/objects/text/index.d.ts +2 -0
  183. package/dist/objects/text/index.js +1 -0
  184. package/dist/objects/text/text-container.d.ts +105 -0
  185. package/dist/objects/text/text-container.js +206 -0
  186. package/dist/objects/text/text-layout.d.ts +83 -0
  187. package/dist/objects/text/text-layout.js +154 -0
  188. package/dist/space/Camera.d.ts +103 -0
  189. package/dist/space/Camera.js +321 -0
  190. package/dist/space/Scene.d.ts +44 -0
  191. package/dist/space/Scene.js +112 -0
  192. package/dist/space/Space.d.ts +288 -0
  193. package/dist/space/Space.js +329 -0
  194. package/dist/space/VisibleRect.d.ts +72 -0
  195. package/dist/space/VisibleRect.js +129 -0
  196. package/dist/space/camera-handle.d.ts +67 -0
  197. package/dist/space/camera-handle.js +63 -0
  198. package/dist/space/camera-kind.d.ts +26 -0
  199. package/dist/space/camera-kind.js +30 -0
  200. package/dist/space/canvas-size.d.ts +24 -0
  201. package/dist/space/canvas-size.js +20 -0
  202. package/dist/space/clipping.d.ts +55 -0
  203. package/dist/space/clipping.js +108 -0
  204. package/dist/space/layout-measure.d.ts +65 -0
  205. package/dist/space/layout-measure.js +173 -0
  206. package/dist/space/viewport.d.ts +112 -0
  207. package/dist/space/viewport.js +67 -0
  208. package/dist/types/defs.d.ts +169 -0
  209. package/dist/types/defs.js +1 -0
  210. package/dist/types/events.d.ts +183 -0
  211. package/dist/types/events.js +1 -0
  212. package/dist/types/guards.d.ts +14 -0
  213. package/dist/types/guards.js +42 -0
  214. package/dist/update/index.d.ts +3 -0
  215. package/dist/update/index.js +2 -0
  216. package/dist/update/slot-writers.d.ts +30 -0
  217. package/dist/update/slot-writers.js +72 -0
  218. package/dist/update/store.d.ts +41 -0
  219. package/dist/update/store.js +98 -0
  220. package/dist/utils/arrays.d.ts +13 -0
  221. package/dist/utils/arrays.js +20 -0
  222. package/dist/utils/colors.d.ts +19 -0
  223. package/dist/utils/colors.js +27 -0
  224. package/dist/utils/logging.d.ts +26 -0
  225. package/dist/utils/logging.js +47 -0
  226. package/dist/utils/math.d.ts +24 -0
  227. package/dist/utils/math.js +27 -0
  228. package/package.json +67 -19
@@ -0,0 +1,315 @@
1
+ import { MaxRectsPacker } from 'maxrects-packer';
2
+ import { createLogger } from '../../utils/logging';
3
+ const logger = createLogger('atlas-packer');
4
+ // Need to reduce blocking time on the main thread
5
+ // Naive approach of offloading to web worker is not feasible:
6
+ // HTMLImageElement can't be easily transferred, and createImageBitmap() on sources takes more time
7
+ // than actual atlas packing. Needs to be investigated further
8
+ /**
9
+ * Gap kept between neighboring images in an atlas, so linear filtering at one image's edge can
10
+ * never reach into the next one's pixels. The packer applies it *between* rects only — atlas edges
11
+ * stay flush against the pixels — which is also what lets a tile exactly `maxDimension` across
12
+ * still fit a bin.
13
+ */
14
+ const PADDING = 1;
15
+ /**
16
+ * How many source pixels per square unit of `bounds` a def may carry before it is called
17
+ * over-sampled. Past this the extra resolution cannot reach the screen at any sane zoom, so the
18
+ * atlas space and texture memory are simply wasted — usually a full-size logo standing in for a
19
+ * marker icon.
20
+ */
21
+ const MAX_AREA_RATIO = 1000;
22
+ /**
23
+ * Whether a source's pixels may be downscaled to fit a memory budget. `true` for decoded images and
24
+ * bitmaps, `false` for canvases — a canvas's pixels are the caller's, counted at full size but never
25
+ * compressed (see the scene's `memoryLimit`).
26
+ *
27
+ * Detected by the absence of `getContext`: `HTMLCanvasElement` and `OffscreenCanvas` both have it,
28
+ * `HTMLImageElement` and `ImageBitmap` do not. A property test rather than `instanceof`, because the
29
+ * SDK runs in WebViews where `OffscreenCanvas`/`ImageBitmap` may be missing as globals and an
30
+ * `instanceof` against an absent one throws.
31
+ * @param source - The source to classify.
32
+ * @returns Whether the source can be downscaled.
33
+ */
34
+ export function isResizableSource(source) {
35
+ return !('getContext' in source);
36
+ }
37
+ /**
38
+ * Packs image defs into atlas canvases and reports where every def's pixels landed.
39
+ *
40
+ * Sources are split by kind first — resizable (image/bitmap) from fixed (canvas) — and each kind is
41
+ * packed on its own, so no atlas ever mixes the two and a memory budget can scale a resizable atlas
42
+ * whole without touching a canvas's pixels. Within a kind, sources are deduped — two defs pointing
43
+ * at the same URL, or holding the same canvas object, are drawn once and share one region — and then
44
+ * offered to the packer whole. Anything too big for an atlas comes back parked in a natural-size bin
45
+ * of its own, which the packer flags; those are the only sources cut into tiles, and the tiles go
46
+ * back through the same packer. Letting the packer decide is what keeps the guarantee that no atlas
47
+ * exceeds `maxDimension` without measuring every image up front.
48
+ *
49
+ * Packing reads only the sources' pixel dimensions, never `bounds`, so a def that later moves or
50
+ * resizes keeps its region: the quad's matrix scales it, and nothing needs repacking. Membership
51
+ * changes do, though — this is a batch call with no incremental path.
52
+ * @param defs - The images to pack.
53
+ * @param maxDimension - Largest atlas edge in pixels, floored to a positive integer.
54
+ * @param options - Overrides for how atlas canvases are made.
55
+ * @returns One entry per atlas, in packing order — resizable atlases first, then fixed.
56
+ */
57
+ export function packAtlases(defs, maxDimension, options = {}) {
58
+ const startedAt = performance.now();
59
+ const { createCanvas = createDomCanvas } = options;
60
+ const limit = Math.max(1, Math.floor(maxDimension));
61
+ const resizable = [];
62
+ const fixed = [];
63
+ for (const def of defs)
64
+ (isResizableSource(def.source) ? resizable : fixed).push(def);
65
+ // One packer for both groups, `reset()` between them — the config (size, padding, options) is the
66
+ // same for each, so reusing it spares an allocation per pack. It cannot be shared across pack calls
67
+ // though: `limit` is baked in at construction and varies call to call.
68
+ // `pot: false` because WebGL 2 samples non-power-of-two textures without restriction, so rounding
69
+ // each atlas up would only waste memory. `smart` stays on (the default), shrinking every bin to
70
+ // its content — hence reading the size back off the bin rather than assuming `limit`.
71
+ const packer = new MaxRectsPacker(limit, limit, PADDING, { pot: false });
72
+ const packedResizable = packGroup(packer, resizable, limit, createCanvas, true);
73
+ const packedFixed = packGroup(packer, fixed, limit, createCanvas, false);
74
+ const atlases = [...packedResizable.atlases, ...packedFixed.atlases];
75
+ const elapsed = (performance.now() - startedAt).toFixed(1);
76
+ logger.debug('packed defs: %d (%d resizable), sources: %d, tiles: %d, atlases: %d (%sms)', packedResizable.defs + packedFixed.defs, packedResizable.defs, packedResizable.sources + packedFixed.sources, packedResizable.tiles + packedFixed.tiles, atlases.length, elapsed);
77
+ return atlases;
78
+ }
79
+ /**
80
+ * Packs one kind of source — all resizable, or all fixed — into atlases, tagging each with the
81
+ * group's kind. Same pipeline for both: dedup, offer whole, cut whatever the packer parks oversized.
82
+ * @param packer - The shared packer, reset to empty before this group is added.
83
+ * @param defs - The defs of this kind; may be empty, which packs to nothing.
84
+ * @param limit - Largest atlas edge in pixels.
85
+ * @param createCanvas - Makes each atlas canvas.
86
+ * @param resizable - The kind, stamped onto every atlas produced.
87
+ * @returns The atlases and their counts.
88
+ */
89
+ function packGroup(packer, defs, limit, createCanvas, resizable) {
90
+ const sources = collectSources(defs);
91
+ const { rects, skipped } = sourceRects(sources.values());
92
+ // Clear whatever the previous group left in the packer, keeping its configuration.
93
+ packer.reset();
94
+ // `addArray` sorts by longest edge before placing, which packs far tighter than insertion order.
95
+ packer.addArray(rects);
96
+ // The packer tags whatever it had to park in an oversized bin, so it — not a measurement of our
97
+ // own — decides what needs cutting. The tiles are ordinary rects, and pack into the bins the
98
+ // first pass already opened.
99
+ const oversized = rects.filter((rect) => rect.oversized);
100
+ if (oversized.length > 0)
101
+ packer.addArray(oversized.flatMap((rect) => cutTiles(rect, limit)));
102
+ // Every oversized bin is now represented by its tiles, so it is dropped rather than returned:
103
+ // handing back a canvas larger than the caller's limit is the thing this all exists to prevent.
104
+ const bins = packer.bins.filter((bin) => !bin.rects.some((rect) => rect.oversized));
105
+ const atlases = bins.map((bin) => ({ ...renderAtlas(bin, createCanvas), resizable }));
106
+ return {
107
+ atlases,
108
+ defs: defs.length - skipped.defs,
109
+ sources: sources.size - skipped.sources,
110
+ tiles: bins.reduce((count, bin) => count + bin.rects.length, 0),
111
+ };
112
+ }
113
+ /**
114
+ * The pixel size of an image source.
115
+ *
116
+ * Duck-typed rather than `instanceof`: the SDK runs in WebViews where `OffscreenCanvas` and
117
+ * `ImageBitmap` may be missing as globals, and an `instanceof` against an absent global throws.
118
+ * @param source - The source to measure.
119
+ * @returns Its intrinsic size, `0 × 0` for an image element that has not decoded yet.
120
+ */
121
+ export function imageSourceSize(source) {
122
+ // `naturalWidth` is the decoded size; an `<img>`'s `width` is its layout size, which is 0 for an
123
+ // element that was never in the document — the usual case for images loaded purely as textures.
124
+ const image = source;
125
+ return typeof image.naturalWidth === 'number'
126
+ ? { width: image.naturalWidth, height: image.naturalHeight }
127
+ : { width: source.width, height: source.height };
128
+ }
129
+ /**
130
+ * Groups defs by the source they draw, so identical images are packed once.
131
+ *
132
+ * Identity is the URL for an image element and the object itself for a canvas or bitmap — a `Map`
133
+ * never confuses the two, since a string key cannot collide with an object one. An `<img>` with no
134
+ * `src` keys on itself instead, so a set of blank images is not collapsed into a single region.
135
+ * @param defs - The defs to group.
136
+ * @returns One entry per unique source, in first-seen order.
137
+ */
138
+ function collectSources(defs) {
139
+ const sources = new Map();
140
+ for (const def of defs) {
141
+ const src = def.source.src;
142
+ const key = typeof src === 'string' && src !== '' ? src : def.source;
143
+ let entry = sources.get(key);
144
+ if (!entry) {
145
+ entry = {
146
+ source: def.source,
147
+ ...imageSourceSize(def.source),
148
+ defs: [],
149
+ bounds: { width: 0, height: 0 },
150
+ };
151
+ sources.set(key, entry);
152
+ }
153
+ entry.defs.push(def);
154
+ // Copied rather than referenced: `Rect` exposes an in-place `set`, so holding onto its size
155
+ // vector would let a later bounds change rewrite what was measured here.
156
+ const { width, height } = def.bounds.size;
157
+ if (width * height > entry.bounds.width * entry.bounds.height) {
158
+ entry.bounds = { width, height };
159
+ }
160
+ }
161
+ return sources;
162
+ }
163
+ /**
164
+ * Turns each unique source into the one rect that offers it to the packer whole, dropping the ones
165
+ * with no pixels to draw and flagging the ones carrying more resolution than they can show.
166
+ * @param sources - The unique sources to offer.
167
+ * @returns The rects to pack, and what was dropped for having no pixels.
168
+ */
169
+ function sourceRects(sources) {
170
+ const rects = [];
171
+ const skipped = { defs: 0, sources: 0 };
172
+ for (const entry of sources) {
173
+ // A `drawImage` with a zero-width or zero-height source region throws in some browsers, so an
174
+ // image that has not decoded (or a canvas sized to nothing) is dropped rather than drawn.
175
+ if (entry.width <= 0 || entry.height <= 0) {
176
+ skipped.sources++;
177
+ skipped.defs += entry.defs.length;
178
+ logger.warn('skipping source with no pixels (%dx%d) %O', entry.width, entry.height, entry.source);
179
+ continue;
180
+ }
181
+ warnIfOverSampled(entry);
182
+ rects.push({
183
+ width: entry.width,
184
+ height: entry.height,
185
+ x: 0,
186
+ y: 0,
187
+ hash: `${rects.length}`,
188
+ data: { entry },
189
+ });
190
+ }
191
+ return { rects, skipped };
192
+ }
193
+ /**
194
+ * Reports a source carrying far more pixels than the bounds it is drawn into can ever show. Once
195
+ * per source rather than once per def, since deduped defs all share the one texture region.
196
+ * @param entry - The source to check.
197
+ */
198
+ function warnIfOverSampled(entry) {
199
+ const boundsArea = entry.bounds.width * entry.bounds.height;
200
+ // Degenerate bounds would divide out to Infinity and warn about every such def, which says
201
+ // nothing about resolution — that is a bounds bug, and not this function's to report.
202
+ if (boundsArea <= 0)
203
+ return;
204
+ const ratio = (entry.width * entry.height) / boundsArea;
205
+ if (ratio <= MAX_AREA_RATIO)
206
+ return;
207
+ logger.warn('over-sampled source: %dx%d px drawn into %sx%s units — %d px per unit², limit %d', entry.width, entry.height,
208
+ // At most two decimals, trailing zeros dropped: 4 → 4, 4.1 → 4.1, 4.2999 → 4.3.
209
+ Number(entry.bounds.width.toFixed(2)), Number(entry.bounds.height.toFixed(2)), Math.round(ratio), MAX_AREA_RATIO);
210
+ }
211
+ /**
212
+ * Cuts a source the packer could not fit into a grid of tiles no larger than one atlas. Tiles are
213
+ * exact and non-overlapping — each ends up alone (or beside its own siblings) in an atlas, so
214
+ * nothing can bleed across the cut and no overlap column is needed to hide it.
215
+ * @param rect - The oversized rect, holding the source to cut.
216
+ * @param limit - Largest tile edge in pixels.
217
+ * @returns The tiles to pack in its place.
218
+ */
219
+ function cutTiles(rect, limit) {
220
+ const { entry } = rect.data;
221
+ const columns = Math.ceil(entry.width / limit);
222
+ const rows = Math.ceil(entry.height / limit);
223
+ logger.warn('source %dx%d exceeds the %dpx atlas limit, splitting into %d tiles', entry.width, entry.height, limit, columns * rows);
224
+ const tiles = [];
225
+ for (let row = 0; row < rows; row++) {
226
+ const y = row * limit;
227
+ const height = Math.min(limit, entry.height - y);
228
+ for (let column = 0; column < columns; column++) {
229
+ const x = column * limit;
230
+ const width = Math.min(limit, entry.width - x);
231
+ tiles.push({
232
+ width,
233
+ height,
234
+ x: 0,
235
+ y: 0,
236
+ hash: `${rect.hash}:${column},${row}`,
237
+ data: { entry, source: { x, y, width, height } },
238
+ });
239
+ }
240
+ }
241
+ return tiles;
242
+ }
243
+ /**
244
+ * Draws one packed bin onto a canvas and maps its rects back onto the defs that wanted them. The
245
+ * `resizable` flag is not this function's to set — {@link packGroup} stamps it from the group's kind.
246
+ * @param bin - The packed bin, already sized to its content.
247
+ * @param createCanvas - Makes the canvas to draw on.
248
+ * @returns The atlas, less its `resizable` flag.
249
+ */
250
+ function renderAtlas(bin, createCanvas) {
251
+ const canvas = createCanvas(bin.width, bin.height);
252
+ const context = contextOf(canvas);
253
+ // Nothing downstream can recover from this: every atlas would upload blank, so the images would
254
+ // silently draw as nothing at all. Better to fail where the cause is still visible.
255
+ if (!context)
256
+ throw new Error(`atlas-packer: no 2d context for a ${bin.width}x${bin.height} atlas`);
257
+ const images = [];
258
+ for (const rect of bin.rects) {
259
+ const { entry, source } = rect.data;
260
+ // The copied region is the tile when there is one and the whole image otherwise — both read
261
+ // from the source's own axes. It happens to equal the packed rect's size today (tiles are
262
+ // placed 1:1, unrotated), but taking it from the destination would only be right by accident.
263
+ // Kept apart from `source` itself, which stays undefined for an unsplit image so the placement
264
+ // below does not report a region the caller has no use for.
265
+ const region = source ?? { x: 0, y: 0, width: entry.width, height: entry.height };
266
+ context.drawImage(entry.source, region.x, region.y, region.width, region.height, rect.x, rect.y, rect.width, rect.height);
267
+ // The rect is in canvas pixels and uv keeps the canvas's axes, so this is a plain normalization
268
+ // with no vertical inversion (see {@link UvRect}). Shared by every def on this rect — dedup is
269
+ // the whole point.
270
+ const uv = {
271
+ u0: rect.x / bin.width,
272
+ v0: rect.y / bin.height,
273
+ u1: (rect.x + rect.width) / bin.width,
274
+ v1: (rect.y + rect.height) / bin.height,
275
+ };
276
+ for (const def of entry.defs)
277
+ images.push(source ? { def, uv, source } : { def, uv });
278
+ }
279
+ return { canvas, images };
280
+ }
281
+ /**
282
+ * Makes an atlas canvas in the document, the default when the caller supplies no factory.
283
+ * @param width - The atlas width in pixels.
284
+ * @param height - The atlas height in pixels.
285
+ * @returns The canvas.
286
+ */
287
+ function createDomCanvas(width, height) {
288
+ const canvas = document.createElement('canvas');
289
+ canvas.width = width;
290
+ canvas.height = height;
291
+ return canvas;
292
+ }
293
+ /**
294
+ * Resolves an atlas canvas's 2D context, asking one canvas kind at a time.
295
+ *
296
+ * Deliberately not `canvas.getContext('2d')` on the union. The two kinds' overload lists disagree
297
+ * at the end — `HTMLCanvasElement` closes with `getContext(id: string): RenderingContext | null`,
298
+ * `OffscreenCanvas` with an `OffscreenRenderingContextId` one — and resolving across the union can
299
+ * combine one kind's `"2d"` overload with the other's catch-all, yielding
300
+ * `OffscreenCanvasRenderingContext2D | RenderingContext`: a type covering bitmap and WebGL contexts
301
+ * that have no `drawImage`. It surfaces only on incremental rebuilds (any compile error in this
302
+ * file leaves the bad resolution cached in `.tsbuildinfo`, where it outlives the error that caused
303
+ * it), so a clean build hides it and a working tree quietly breaks. Narrowed first, each branch has
304
+ * one concrete type whose `"2d"` overload is the only candidate, and the result is stable either
305
+ * way.
306
+ * @param canvas - The atlas canvas.
307
+ * @returns Its 2D context, or `null` when the canvas cannot provide one.
308
+ */
309
+ function contextOf(canvas) {
310
+ // `in` rather than `instanceof`: in a worker there is no `HTMLCanvasElement` global to test
311
+ // against, and an `instanceof` against a missing global throws.
312
+ if ('convertToBlob' in canvas)
313
+ return canvas.getContext('2d');
314
+ return canvas.getContext('2d');
315
+ }
@@ -0,0 +1,51 @@
1
+ import { type InstancedMeshExt, type InstancedMeshExtParams } from '../../core/instanced/InstancedMesh';
2
+ import { type ImageDef } from '../../types/defs';
3
+ import { SlotContainer } from '../containers';
4
+ import { type Atlas, type PackedImage } from './atlas-packer';
5
+ /**
6
+ * The per-instance uniform each atlas mesh reads its texture region from. The container owns the
7
+ * name: it writes each slot's region here (`setUniformAt`) and hands it to the mesh's
8
+ * `TextureMaterial` as the `uvOffset` prop, so the material patches its shader to sample the region
9
+ * without knowing the name itself. `xy` is the region's origin in uv space and `zw` its size, so
10
+ * `uv * zw + xy` maps the quad's unit uv onto the region.
11
+ */
12
+ export declare const UV_OFFSET_UNIFORM = "uvOffset";
13
+ /**
14
+ * The constructor tuple R3F builds one atlas's mesh from, passed as `<instancedMesh args={…}>`.
15
+ * R3F diffs `args` element-wise and rebuilds the object when any element changes, so this is
16
+ * snapshotted once and never mutated.
17
+ *
18
+ * Only the options: `InstancedMeshExt` takes no geometry or material, seeding throwaways that the
19
+ * element's `<planeGeometry>` and `<TextureMaterial>` children replace (R3F creates, attaches, and
20
+ * disposes those).
21
+ */
22
+ export type ImageMeshArgs = [params: InstancedMeshExtParams];
23
+ /**
24
+ * One atlas's mesh bookkeeping.
25
+ *
26
+ * Simpler than a shape container, and deliberately so: an atlas draws one shared quad, so there is
27
+ * no geometry cache and no geometry-id juggling, and `InstancedMeshExt` grows its own buffers, so
28
+ * there is no capacity pre-scan — the mesh is merely sized to the atlas's packed-image count and
29
+ * left to resize itself if a claim ever overflows it. What remains is a slot per packed image and
30
+ * the uv region each one samples, written once at claim since a def's source never changes.
31
+ *
32
+ * Paint order is incremental, like the shape container's, because packing is order-independent: a
33
+ * reorder of the same images keeps this container and only re-labels its slots, never repacking. A
34
+ * change to the *set* of images is the harder case and still repacks into a fresh container —
35
+ * packing has no incremental path for that — but a pure reorder does not.
36
+ */
37
+ export declare class ImageContainer extends SlotContainer<PackedImage, InstancedMeshExt, ImageDef> {
38
+ /** The constructor args R3F builds the mesh from; a frozen snapshot, never mutated. */
39
+ readonly args: ImageMeshArgs;
40
+ /**
41
+ * Builds one atlas's bookkeeping. The mesh, its quad, and its material are all R3F's — the atlas
42
+ * component renders `<instancedMesh args={container.args}>` with the geometry and material as
43
+ * children, and hands the mesh back through {@link SlotContainer.bind} — so R3F owns their
44
+ * lifetimes, including disposal on unmount and survival across StrictMode's simulated remount.
45
+ * @param atlas - The packed atlas this container draws; only its image count sizes the mesh.
46
+ */
47
+ constructor(atlas: Atlas);
48
+ protected addInstance(packed: PackedImage): number;
49
+ protected removeInstance(slot: number): void;
50
+ }
51
+ //# sourceMappingURL=image-container.d.ts.map
@@ -0,0 +1,70 @@
1
+ import { Vector4 } from 'three';
2
+ import { SlotContainer } from '../containers';
3
+ import { DIM_SCHEMA } from '../materials/dim';
4
+ /**
5
+ * The per-instance uniform each atlas mesh reads its texture region from. The container owns the
6
+ * name: it writes each slot's region here (`setUniformAt`) and hands it to the mesh's
7
+ * `TextureMaterial` as the `uvOffset` prop, so the material patches its shader to sample the region
8
+ * without knowing the name itself. `xy` is the region's origin in uv space and `zw` its size, so
9
+ * `uv * zw + xy` maps the quad's unit uv onto the region.
10
+ */
11
+ export const UV_OFFSET_UNIFORM = 'uvOffset';
12
+ /**
13
+ * One atlas's mesh bookkeeping.
14
+ *
15
+ * Simpler than a shape container, and deliberately so: an atlas draws one shared quad, so there is
16
+ * no geometry cache and no geometry-id juggling, and `InstancedMeshExt` grows its own buffers, so
17
+ * there is no capacity pre-scan — the mesh is merely sized to the atlas's packed-image count and
18
+ * left to resize itself if a claim ever overflows it. What remains is a slot per packed image and
19
+ * the uv region each one samples, written once at claim since a def's source never changes.
20
+ *
21
+ * Paint order is incremental, like the shape container's, because packing is order-independent: a
22
+ * reorder of the same images keeps this container and only re-labels its slots, never repacking. A
23
+ * change to the *set* of images is the harder case and still repacks into a fresh container —
24
+ * packing has no incremental path for that — but a pure reorder does not.
25
+ */
26
+ export class ImageContainer extends SlotContainer {
27
+ /** The constructor args R3F builds the mesh from; a frozen snapshot, never mutated. */
28
+ args;
29
+ /**
30
+ * Builds one atlas's bookkeeping. The mesh, its quad, and its material are all R3F's — the atlas
31
+ * component renders `<instancedMesh args={container.args}>` with the geometry and material as
32
+ * children, and hands the mesh back through {@link SlotContainer.bind} — so R3F owns their
33
+ * lifetimes, including disposal on unmount and survival across StrictMode's simulated remount.
34
+ * @param atlas - The packed atlas this container draws; only its image count sizes the mesh.
35
+ */
36
+ constructor(atlas) {
37
+ super({ schema: IMAGE_SCHEMA });
38
+ this.args = [{ capacity: Math.max(1, atlas.images.length) }];
39
+ }
40
+ addInstance(packed) {
41
+ // `addInstances` grows the buffers itself if the atlas ever overflows its sized capacity, so
42
+ // there is nothing to pre-scan; the id is captured from the creation callback.
43
+ let slot = -1;
44
+ this.mesh.addInstances(1, (id) => {
45
+ slot = id;
46
+ });
47
+ // The uv region is fixed for the instance's life — a def's source never changes, and a set
48
+ // change repacks into a fresh container — so it is written once, here, rather than on every
49
+ // sync.
50
+ const { u0, v0, u1, v1 } = packed.uv;
51
+ this.mesh.setUniformAt(slot, UV_OFFSET_UNIFORM, _uvOffset.set(u0, v0, u1 - u0, v1 - v0));
52
+ return slot;
53
+ }
54
+ removeInstance(slot) {
55
+ this.mesh.removeInstances(slot);
56
+ }
57
+ }
58
+ /**
59
+ * The schema the mesh declares (via `addUniforms`) so it can hold a {@link UV_OFFSET_UNIFORM} and a
60
+ * dim per slot.
61
+ *
62
+ * Both in one call although the two come from different places: `InstanceUniforms.add` merges and
63
+ * rebuilds the backing texture, so declaring them separately would build it twice per bind.
64
+ */
65
+ const IMAGE_SCHEMA = {
66
+ vertex: { [UV_OFFSET_UNIFORM]: 'vec4', ...DIM_SCHEMA.vertex },
67
+ };
68
+ // Scratch for the one uv-region upload a claim performs, filled and read within a single
69
+ // synchronous statement.
70
+ const _uvOffset = new Vector4();
@@ -0,0 +1,2 @@
1
+ export { Images, type ImagesProps } from './Images';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ export { Images } from './Images';
@@ -0,0 +1,120 @@
1
+ import { type Atlas } from './atlas-packer';
2
+ /**
3
+ * The GPU footprint of an atlas of the given pixel size: RGBA, with the mipmap chain counted. Reads
4
+ * the atlas's full (packed) dimensions — the resize factor scales this down, so budgeting against
5
+ * the full size is what the factor is solved from.
6
+ * @param width - Atlas width in pixels.
7
+ * @param height - Atlas height in pixels.
8
+ * @returns The size in bytes.
9
+ */
10
+ export declare function getAtlasSizeBytes(width: number, height: number): number;
11
+ /**
12
+ * One atlas's contribution to the budget: its full size, and whether it may be scaled to fit. The
13
+ * object doubles as its own registration handle — {@link TextureBudget.unregister} takes the very
14
+ * object {@link TextureBudget.register} was given, so identity alone distinguishes two atlases of
15
+ * equal size and no separate key is needed.
16
+ */
17
+ interface AtlasBudgetEntry {
18
+ /** Full atlas width in pixels. */
19
+ width: number;
20
+ /** Full atlas height in pixels. */
21
+ height: number;
22
+ /** Whether this atlas's bytes shrink with the resize factor, or count at full size always. */
23
+ resizable: boolean;
24
+ }
25
+ /**
26
+ * The scene's shared texture-memory budget. Every image atlas registers its full size here, and the
27
+ * store solves for one linear resize factor that brings the resizable atlases within `memoryLimit` —
28
+ * shared by all of them, so their relative sizes are preserved with no priority.
29
+ *
30
+ * The factor is exposed through `useSyncExternalStore`: an atlas that mounts, unmounts, or a limit
31
+ * that changes recomputes it, and a material move re-derives only the resizable atlases' textures —
32
+ * never a repack, since uv regions are normalized and so invariant to the canvas's pixel size.
33
+ * Recompute runs synchronously per registration; the render churn that would cause is left to
34
+ * React's automatic batching to coalesce, and the `FACTOR_EPSILON` gate keeps sub-pixel nudges from
35
+ * notifying at all. When no cap is set the whole apparatus stands down: {@link TextureBudget.isActive}
36
+ * is false, atlases skip registration entirely (see {@link useResizeFactor}), and the factor is 1.
37
+ */
38
+ export declare class TextureBudget {
39
+ /** The cap in megabytes, or undefined/`0` for no cap — a falsy value scales nothing. */
40
+ private memoryLimitMb;
41
+ /** Every registered atlas; the entry object is its own handle. */
42
+ private readonly atlases;
43
+ /** Subscribers to the resize factor — the mounted resizable atlases. */
44
+ private readonly listeners;
45
+ /** The current shared linear resize factor; `1` means no scaling. */
46
+ private factor;
47
+ /**
48
+ * @param memoryLimitMb - The cap in megabytes, or undefined/`0` for no cap.
49
+ */
50
+ constructor(memoryLimitMb?: number);
51
+ /**
52
+ * Whether a cap is in force. False for an absent or zero limit — nothing can be compressed, so
53
+ * consumers skip the budget entirely rather than do work that resolves to "no scaling".
54
+ * @returns Whether a positive limit is set.
55
+ */
56
+ isActive(): boolean;
57
+ /**
58
+ * Changes the cap. Called when the renderer's `memoryLimit` prop changes, so a caller can raise,
59
+ * lower, or clear it at runtime and the resizable atlases re-derive their textures live.
60
+ * @param memoryLimitMb - The new cap, or undefined/`0` for no cap.
61
+ */
62
+ setMemoryLimit(memoryLimitMb: number | undefined): void;
63
+ /**
64
+ * Registers one atlas's full size, recomputing the factor. Only called while {@link isActive}, so
65
+ * an uncapped scene never populates this set.
66
+ * @param entry - The atlas's dimensions and kind; pass the same object back to unregister.
67
+ */
68
+ register(entry: AtlasBudgetEntry): void;
69
+ /**
70
+ * Drops an atlas's contribution, recomputing the factor.
71
+ * @param entry - The object {@link register} was given.
72
+ */
73
+ unregister(entry: AtlasBudgetEntry): void;
74
+ /**
75
+ * The current shared resize factor.
76
+ * @returns The factor in `(0, 1]`.
77
+ */
78
+ getResizeFactor(): number;
79
+ /**
80
+ * Subscribes to factor and active-state changes.
81
+ * @param listener - Called when the factor moves past {@link FACTOR_EPSILON} or the cap toggles.
82
+ * @returns The unsubscribe function.
83
+ */
84
+ subscribe(listener: () => void): () => void;
85
+ /** Recomputes the factor and, if it moved materially, notifies subscribers. */
86
+ private recompute;
87
+ /** Notifies every subscriber. */
88
+ private notify;
89
+ /**
90
+ * Solves for the linear factor that fits the resizable atlases in the budget the fixed ones leave.
91
+ *
92
+ * Every degenerate case falls back to `1` — no scaling — rather than forcing a fit: no cap, nothing
93
+ * resizable to scale, or a cap the fixed atlases alone already exceed (canvases cannot be
94
+ * compressed, so there is nothing to give). The factor is only ever clamped up to `1`, never down
95
+ * to a floor: an image is scaled by however much the budget demands.
96
+ * @returns The factor in `(0, 1]`.
97
+ */
98
+ private computeResizeFactor;
99
+ }
100
+ /**
101
+ * The scene's texture budget, provided by `Renderer`. The default is an uncapped budget, so an image
102
+ * layer rendered outside a `Renderer` (a test, a story) behaves exactly as before this existed —
103
+ * everything uploads at full size.
104
+ */
105
+ export declare const TextureBudgetContext: import("react").Context<TextureBudget>;
106
+ /**
107
+ * Registers an atlas with the scene budget for its mounted lifetime and returns the resize factor it
108
+ * should upload at: the shared factor for a resizable atlas, always `1` for a fixed one (its pixels
109
+ * still count toward the budget, they just never scale).
110
+ *
111
+ * When the budget is inactive — no `memoryLimit` — the atlas is not registered at all: there is
112
+ * nothing to solve, so an uncapped scene does no per-atlas budget work. Turning a cap on at runtime
113
+ * flips `isActive`, which re-runs the registration effect.
114
+ * @param budget - The scene budget from context.
115
+ * @param atlas - The atlas to register and read a factor for.
116
+ * @returns The linear factor this atlas's canvas should be scaled by before upload, in `(0, 1]`.
117
+ */
118
+ export declare function useResizeFactor(budget: TextureBudget, atlas: Atlas): number;
119
+ export {};
120
+ //# sourceMappingURL=texture-budget.d.ts.map