@expofp/renderer 3.2.5 → 3.27.1

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 -8031
  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,22 @@
1
+ import { type ModelDef } from '../../types/defs';
2
+ import { type DefsProps } from '../containers';
3
+ /** Props for {@link Models}. */
4
+ export type ModelsProps = DefsProps<ModelDef>;
5
+ /**
6
+ * Draws a layer's models. Every def sharing a URL draws from the same loaded asset, and every part of
7
+ * that asset — one per source material, merged at load — becomes a single `InstancedMesh`. A layer
8
+ * holding twenty copies of a two-material chair is therefore **two draw calls**, not forty.
9
+ *
10
+ * `InstancedMesh` rather than `BatchedMesh` on purpose. Nothing here ever addresses a sub-mesh
11
+ * individually — the addressable unit is the def — so merging at load is strictly better than
12
+ * carrying per-sub-mesh bookkeeping, and it avoids the `WEBGL_multi_draw` path a batched mesh needs,
13
+ * which is slow in Firefox.
14
+ *
15
+ * **Loading is an effect, not Suspense.** A suspended leaf would never run the effect that reports
16
+ * back to the mount scheduler, so the scene's admission queue would stall permanently on the first
17
+ * model layer. Instead nothing renders until the assets resolve.
18
+ * @param props - The models to draw.
19
+ * @returns One mesh per part of every loaded model, with an instance component per def.
20
+ */
21
+ export declare function Models(props: ModelsProps): import("react").JSX.Element;
22
+ //# sourceMappingURL=Models.d.ts.map
@@ -0,0 +1,177 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useThree } from '@react-three/fiber';
3
+ import { Fragment, useEffect, useState } from 'react';
4
+ import { InstancedMesh } from '../../core/instanced/InstancedMesh';
5
+ import { sameSequence } from '../../utils/arrays';
6
+ import { createLogger } from '../../utils/logging';
7
+ import { defKey, useContainer } from '../containers';
8
+ import { ModelMaterial } from '../materials/ModelMaterial';
9
+ import { loadModel } from './gltf-cache';
10
+ import { MODEL_KINDS, ModelContainer, modelKindOf, OPACITY_UNIFORM, } from './model-container';
11
+ import { ModelInstance } from './ModelInstance';
12
+ const logger = createLogger('models');
13
+ /**
14
+ * Draws a layer's models. Every def sharing a URL draws from the same loaded asset, and every part of
15
+ * that asset — one per source material, merged at load — becomes a single `InstancedMesh`. A layer
16
+ * holding twenty copies of a two-material chair is therefore **two draw calls**, not forty.
17
+ *
18
+ * `InstancedMesh` rather than `BatchedMesh` on purpose. Nothing here ever addresses a sub-mesh
19
+ * individually — the addressable unit is the def — so merging at load is strictly better than
20
+ * carrying per-sub-mesh bookkeeping, and it avoids the `WEBGL_multi_draw` path a batched mesh needs,
21
+ * which is slow in Firefox.
22
+ *
23
+ * **Loading is an effect, not Suspense.** A suspended leaf would never run the effect that reports
24
+ * back to the mount scheduler, so the scene's admission queue would stall permanently on the first
25
+ * model layer. Instead nothing renders until the assets resolve.
26
+ * @param props - The models to draw.
27
+ * @returns One mesh per part of every loaded model, with an instance component per def.
28
+ */
29
+ export function Models(props) {
30
+ const { defs } = props;
31
+ // The layer split two ways at once: by url, because that is what a mesh's geometry comes from,
32
+ // and by kind, because `transparent` is a material property and a translucent copy of a model
33
+ // therefore cannot share a mesh with an opaque one (see `modelKindOf`).
34
+ //
35
+ // Held in state rather than derived each render, the same "adjust on prop change" shape `Images`
36
+ // uses for its pack — because the split has a second trigger besides `defs`. A def's `opacity` is
37
+ // an ordinary slot write that never re-renders this component, so one crossing 1 would otherwise
38
+ // leave the def in a batch whose material describes the wrong render list. `repartition` is what
39
+ // the containers call when they notice.
40
+ const [partition, setPartition] = useState(() => partitionModels(defs, undefined));
41
+ if (partition.defs !== defs)
42
+ setPartition((current) => partitionModels(defs, current.batches));
43
+ /** Re-splits the current membership, for a batch that found a member of the wrong kind. */
44
+ function repartition() {
45
+ setPartition((current) => partitionModels(defs, current.batches));
46
+ }
47
+ // The distinct urls the layer draws, held in state so the load effect depends on a value that
48
+ // changes exactly when the *set* does: the snapshot from `useStagedChildren` is rebuilt on every
49
+ // `update(layer)`, including one that only flipped the layer's own `hidden`, so an effect
50
+ // depending on `defs` would reload for changes that touched no model at all. Derived as a value
51
+ // rather than suppressed in the dependency list: a `react-hooks/exhaustive-deps` suppression makes
52
+ // the React Compiler bail on the whole module, which fails every spec that transitively imports
53
+ // it. Sorted, so comparing two of them position by position answers the question actually being
54
+ // asked — same *set*? — and a layer whose models were merely reordered loads nothing.
55
+ const wanted = distinctUrls(defs);
56
+ const [urls, setUrls] = useState(wanted);
57
+ if (!sameSequence(urls, wanted))
58
+ setUrls(wanted);
59
+ const [assets, setAssets] = useState([]);
60
+ const invalidate = useThree((state) => state.invalidate);
61
+ useEffect(() => {
62
+ let live = true;
63
+ void Promise.all(urls.map(loadOrWarn)).then((loaded) => {
64
+ if (!live)
65
+ return;
66
+ // `filter(Boolean)` does not narrow a union, so the guard is spelled out.
67
+ setAssets(loaded.filter((asset) => asset !== undefined));
68
+ invalidate();
69
+ });
70
+ return () => {
71
+ live = false;
72
+ };
73
+ }, [urls, invalidate]);
74
+ // Nested rather than flattened because React scopes keys to their parent, so each level only has
75
+ // to be unique among its siblings — and `ModelPart.id` is, where `material.uuid` is not.
76
+ //
77
+ // `renderOrder` is the kind's place in `MODEL_KINDS` rather than mount order, for the reason
78
+ // layers carry one: these meshes tie on `groupOrder`, so the tiebreak would otherwise fall to
79
+ // `material.id`, which ascends in the order materials were built. It barely matters for the
80
+ // translucent batch on its own — `transparent: true` puts it in three's transparent list, drawn
81
+ // after every opaque object whatever the number says — but it is numbered anyway so the intent
82
+ // reads correctly and so it still orders against another translucent batch tying with it.
83
+ return (_jsx(_Fragment, { children: assets.map((asset) => (_jsx(Fragment, { children: MODEL_KINDS.map((kind, renderOrder) => {
84
+ const members = partition.batches[kind].get(asset.url);
85
+ if (!members)
86
+ return null;
87
+ return (_jsx(Fragment, { children: asset.parts.map((part) => (_jsx(ModelPartMesh, { asset: asset, part: part, defs: members, kind: kind, renderOrder: renderOrder, onKindMismatch: repartition }, part.id))) }, kind));
88
+ }) }, asset.url))) }));
89
+ }
90
+ /**
91
+ * Splits a layer's models by kind and url, keeping every batch that is already mounted.
92
+ * @param defs - The layer's models, in paint order.
93
+ * @param mounted - The batches already on screen, whose keys are carried forward emptied.
94
+ * @returns The split.
95
+ */
96
+ function partitionModels(defs, mounted) {
97
+ const batches = {
98
+ opaque: emptied(mounted?.opaque),
99
+ translucent: emptied(mounted?.translucent),
100
+ };
101
+ for (const def of defs) {
102
+ const byUrl = batches[modelKindOf(def)];
103
+ const members = byUrl.get(def.url);
104
+ if (members)
105
+ members.push(def);
106
+ else
107
+ byUrl.set(def.url, [def]);
108
+ }
109
+ return { defs, batches };
110
+ }
111
+ /**
112
+ * The same urls, each with an empty membership — how a mounted batch survives losing its last
113
+ * member. See {@link ModelBatches}.
114
+ * @param previous - The batch to carry forward, if it has been mounted.
115
+ * @returns The emptied batch.
116
+ */
117
+ function emptied(previous) {
118
+ const next = new Map();
119
+ if (previous)
120
+ for (const url of previous.keys())
121
+ next.set(url, []);
122
+ return next;
123
+ }
124
+ /**
125
+ * One model part as a single instanced draw, with an instance per def placing it.
126
+ * @param props - The part and the defs drawing it.
127
+ * @returns The mesh and one instance component per def.
128
+ */
129
+ function ModelPartMesh(props) {
130
+ const { asset, part, defs, kind, renderOrder, onKindMismatch } = props;
131
+ // Ordering is a no-op for models — their container declares it so, and the mesh depth-sorts itself
132
+ // instead — but the hit resolution is not: `InstancedMeshExt.raycast` walks the live slots,
133
+ // skipping deleted and hidden ones, and raycasts the real merged geometry, so a hit names the
134
+ // model's actual triangles and the slot that drew them.
135
+ const container = useContainer(() => new ModelContainer(asset.extents, defs.length, kind), defs);
136
+ // Assigned after mount rather than passed to the constructor: the container is built once and
137
+ // outlives every render, so it cannot close over a current callback. Nothing can fire before this
138
+ // runs — the split that placed these defs was computed from the same opacities they mount with.
139
+ useEffect(() => {
140
+ container.setKindMismatchHandler(onKindMismatch);
141
+ return () => container.setKindMismatchHandler(undefined);
142
+ }, [container, onKindMismatch]);
143
+ // The geometry belongs to the module-level cache and may be drawing in another layer, so it is
144
+ // mounted as a `primitive` with `dispose={null}` — R3F must not free it on unmount. Sharing it is
145
+ // safe because `InstancedMeshExt` never writes to the geometry it is handed: its draw list lives in
146
+ // a texture of its own, addressed by `gl_InstanceID`. The material cannot be shared the same way —
147
+ // the mesh wraps it in a closure over its own textures — so `ModelMaterial` clones it per mesh.
148
+ //
149
+ // The mesh's own matrix is glTF → plan, which is where three can see the mirror that mapping
150
+ // needs (see `ModelContainer.meshMatrix`); `matrixAutoUpdate` off because it is composed, not
151
+ // decomposed from position/rotation/scale. `sortObjects` is what a depth-writing draw wants in
152
+ // place of paint order — nearest first while opaque, farthest first once the material blends.
153
+ return (_jsxs(_Fragment, { children: [_jsxs(InstancedMesh, { args: container.args, ref: (mesh) => container.bind(mesh), matrixAutoUpdate: false, matrix: container.meshMatrix, sortObjects: true, renderOrder: renderOrder, children: [_jsx("primitive", { object: part.geometry, attach: "geometry", dispose: null }), _jsx(ModelMaterial, { source: part.material, opacityUniform: OPACITY_UNIFORM, translucent: kind === 'translucent' })] }), defs.map((def) => (_jsx(ModelInstance, { def: def, container: container }, defKey(def))))] }));
154
+ }
155
+ /**
156
+ * Loads one model, reporting a failure rather than rejecting — one missing model should not cost a
157
+ * layer the rest of them.
158
+ * @param url - The model to load.
159
+ * @returns The asset, or `undefined` if it could not be loaded.
160
+ */
161
+ async function loadOrWarn(url) {
162
+ try {
163
+ return await loadModel(url);
164
+ }
165
+ catch (error) {
166
+ logger.warn('failed to load model %s: %O', url, error);
167
+ return undefined;
168
+ }
169
+ }
170
+ /**
171
+ * The distinct urls a layer draws, sorted so that an equal set is an equal sequence.
172
+ * @param defs - The layer's models.
173
+ * @returns The urls to load.
174
+ */
175
+ function distinctUrls(defs) {
176
+ return [...new Set(defs.map((def) => def.url))].sort();
177
+ }
@@ -0,0 +1,17 @@
1
+ import { type ModelAsset } from './model-asset';
2
+ /**
3
+ * Loads and parses a GLB, or returns the in-flight promise for one already being loaded.
4
+ *
5
+ * Keyed by URL and never evicted: a plan holds a handful of distinct models, often placed many times
6
+ * over, and the whole point of the cache is that the twentieth copy of a chair costs nothing. The
7
+ * entry is dropped on failure so a later attempt can retry.
8
+ *
9
+ * Ours rather than R3F's `useLoader` because what is cached is the *baked* asset, not the parsed
10
+ * glTF, and because `useLoader` is a Suspense hook: a suspended leaf never runs the effect that
11
+ * reports to the mount scheduler, so the scene's admission queue would stall on the first model
12
+ * layer (see `Models`). A hook could not be called per url in a loop either.
13
+ * @param url - The model's URL.
14
+ * @returns The parsed asset.
15
+ */
16
+ export declare function loadModel(url: string): Promise<ModelAsset>;
17
+ //# sourceMappingURL=gltf-cache.d.ts.map
@@ -0,0 +1,50 @@
1
+ import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';
2
+ import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
3
+ import { bakeGltf } from './model-asset';
4
+ /**
5
+ * Loads and parses a GLB, or returns the in-flight promise for one already being loaded.
6
+ *
7
+ * Keyed by URL and never evicted: a plan holds a handful of distinct models, often placed many times
8
+ * over, and the whole point of the cache is that the twentieth copy of a chair costs nothing. The
9
+ * entry is dropped on failure so a later attempt can retry.
10
+ *
11
+ * Ours rather than R3F's `useLoader` because what is cached is the *baked* asset, not the parsed
12
+ * glTF, and because `useLoader` is a Suspense hook: a suspended leaf never runs the effect that
13
+ * reports to the mount scheduler, so the scene's admission queue would stall on the first model
14
+ * layer (see `Models`). A hook could not be called per url in a loop either.
15
+ * @param url - The model's URL.
16
+ * @returns The parsed asset.
17
+ */
18
+ export function loadModel(url) {
19
+ const pending = cache.get(url);
20
+ if (pending)
21
+ return pending;
22
+ const promise = parse(url).catch((error) => {
23
+ cache.delete(url);
24
+ throw error;
25
+ });
26
+ cache.set(url, promise);
27
+ return promise;
28
+ }
29
+ /**
30
+ * Fetches and bakes a GLB.
31
+ * @param url - The model's URL.
32
+ * @returns The parsed asset.
33
+ */
34
+ async function parse(url) {
35
+ const gltf = await loader().loadAsync(url);
36
+ return bakeGltf(gltf.scene, url);
37
+ }
38
+ /**
39
+ * The loader, built once. Meshopt is wired in because its decoder ships inside three's addons and
40
+ * costs nothing at rest; Draco deliberately is not, since its decoder is fetched from a CDN at
41
+ * runtime and an offline plan copy could not satisfy that.
42
+ * @returns The shared loader.
43
+ */
44
+ function loader() {
45
+ _loader ??= new GLTFLoader().setMeshoptDecoder(MeshoptDecoder);
46
+ return _loader;
47
+ }
48
+ /** In-flight and settled loads, keyed by URL. */
49
+ const cache = new Map();
50
+ let _loader;
@@ -0,0 +1,2 @@
1
+ export { Models, type ModelsProps } from './Models';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ export { Models } from './Models';
@@ -0,0 +1,48 @@
1
+ import { Box3, type BufferGeometry, type Group, type Material } from 'three';
2
+ /** One drawable piece of a loaded model: everything in it that shares a source material. */
3
+ export interface ModelPart {
4
+ /**
5
+ * Identifies the part within its asset, and so keys the mesh drawing it. The material alone will
6
+ * not do: parts are bucketed by material **and** merge signature, so one material can yield more
7
+ * than one part and two parts can share a `material.uuid`.
8
+ */
9
+ readonly id: string;
10
+ /** Every mesh in the GLB using {@link material}, flattened and merged into one. */
11
+ readonly geometry: BufferGeometry;
12
+ /** The material the GLB authored for those meshes. Shared; clone before mutating. */
13
+ readonly material: Material;
14
+ }
15
+ /** A parsed GLB, ready to be instanced. */
16
+ export interface ModelAsset {
17
+ /** Where it was loaded from; also what picks out the defs drawing it. */
18
+ readonly url: string;
19
+ /** One entry per distinct source material, so a whole layer costs one draw call per entry. */
20
+ readonly parts: readonly ModelPart[];
21
+ /**
22
+ * The union of every baked part's bounds, in the model's own y-up space. `ModelContainer` maps
23
+ * them into the plan's once, which is the only place the two frames meet.
24
+ */
25
+ readonly extents: Box3;
26
+ }
27
+ /**
28
+ * Flattens a loaded glTF scene and merges it down to one geometry per source material.
29
+ *
30
+ * Per mesh node, its geometry is cloned and transformed by the node's world matrix, so every vertex
31
+ * lands relative to the model's root and the node hierarchy is no longer needed — which is what lets
32
+ * the merge happen at all. Then it is filed under its material, sub-keyed by {@link mergeSignature},
33
+ * because `mergeGeometries` requires its inputs to agree about their attributes.
34
+ *
35
+ * **Nothing here knows about the plan.** The geometry stays in the model's own space, glTF's y-up,
36
+ * and the mapping onto the plan is a transform the *mesh* carries — see `ModelContainer`. That is
37
+ * not a tidiness argument: a mapping baked into vertices is one three cannot see when it decides
38
+ * face culling from `matrixWorld`, and the map is a mirror.
39
+ *
40
+ * Merging is what replaces legacy's `BatchedMesh`: we never address a sub-mesh individually — the
41
+ * addressable unit is the def — so one geometry per material is strictly better, and it costs no
42
+ * `WEBGL_multi_draw` path.
43
+ * @param scene - The glTF scene to bake.
44
+ * @param url - Where it came from; carried on the asset, and names the model in diagnostics.
45
+ * @returns The parsed asset.
46
+ */
47
+ export declare function bakeGltf(scene: Group, url: string): ModelAsset;
48
+ //# sourceMappingURL=model-asset.d.ts.map
@@ -0,0 +1,77 @@
1
+ import { Box3, Mesh } from 'three';
2
+ import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
3
+ import { createLogger } from '../../utils/logging';
4
+ const logger = createLogger('model');
5
+ /**
6
+ * Flattens a loaded glTF scene and merges it down to one geometry per source material.
7
+ *
8
+ * Per mesh node, its geometry is cloned and transformed by the node's world matrix, so every vertex
9
+ * lands relative to the model's root and the node hierarchy is no longer needed — which is what lets
10
+ * the merge happen at all. Then it is filed under its material, sub-keyed by {@link mergeSignature},
11
+ * because `mergeGeometries` requires its inputs to agree about their attributes.
12
+ *
13
+ * **Nothing here knows about the plan.** The geometry stays in the model's own space, glTF's y-up,
14
+ * and the mapping onto the plan is a transform the *mesh* carries — see `ModelContainer`. That is
15
+ * not a tidiness argument: a mapping baked into vertices is one three cannot see when it decides
16
+ * face culling from `matrixWorld`, and the map is a mirror.
17
+ *
18
+ * Merging is what replaces legacy's `BatchedMesh`: we never address a sub-mesh individually — the
19
+ * addressable unit is the def — so one geometry per material is strictly better, and it costs no
20
+ * `WEBGL_multi_draw` path.
21
+ * @param scene - The glTF scene to bake.
22
+ * @param url - Where it came from; carried on the asset, and names the model in diagnostics.
23
+ * @returns The parsed asset.
24
+ */
25
+ export function bakeGltf(scene, url) {
26
+ const buckets = new Map();
27
+ const extents = new Box3();
28
+ scene.updateMatrixWorld(true);
29
+ scene.traverse((node) => {
30
+ // `instanceof` on a generic class instantiates its parameters as `any`, so the narrowed node is
31
+ // re-typed rather than used directly.
32
+ if (!(node instanceof Mesh))
33
+ return;
34
+ const mesh = node;
35
+ if (!mesh.geometry || !mesh.material)
36
+ return;
37
+ // A multi-material mesh splits its draws by geometry group; we take the first, as legacy did.
38
+ const material = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
39
+ if (!material)
40
+ return;
41
+ const baked = mesh.geometry.clone();
42
+ baked.applyMatrix4(mesh.matrixWorld);
43
+ baked.computeBoundingBox();
44
+ if (baked.boundingBox)
45
+ extents.union(baked.boundingBox);
46
+ const signature = mergeSignature(baked);
47
+ const id = `${material.uuid}|${signature}`;
48
+ const bucket = buckets.get(id) ?? { material, signature, geometries: [] };
49
+ bucket.geometries.push(baked);
50
+ buckets.set(id, bucket);
51
+ });
52
+ const parts = [];
53
+ for (const [id, { material, signature, geometries }] of buckets) {
54
+ const merged = geometries.length === 1 ? geometries[0] : mergeGeometries(geometries);
55
+ // `mergeGeometries` still returns null when its inputs disagree about something the signature
56
+ // does not cover — an attribute's item size or array type, a morph target. Reported rather than
57
+ // thrown: one unmergeable material should not cost the whole model. The message has to name
58
+ // enough of the asset to find the offending meshes inside it, since nothing else will.
59
+ if (!merged) {
60
+ logger.warn('model %s: dropped a part, %d geometries would not merge — material "%s" (%s), %s', url, geometries.length, material.name, material.uuid, signature);
61
+ continue;
62
+ }
63
+ parts.push({ id, geometry: merged, material });
64
+ }
65
+ return { url, parts, extents };
66
+ }
67
+ /**
68
+ * A stable description of what `mergeGeometries` insists its inputs agree about: whether they are
69
+ * indexed, and which attributes they carry. Geometries sharing a material but not this are bucketed
70
+ * apart and drawn as separate parts, rather than handed to a merge that would reject the lot.
71
+ * @param geometry - The geometry to describe.
72
+ * @returns The description.
73
+ */
74
+ function mergeSignature(geometry) {
75
+ const indexed = geometry.getIndex() ? 'indexed' : 'non-indexed';
76
+ return `${indexed} [${Object.keys(geometry.attributes).sort().join(', ')}]`;
77
+ }
@@ -0,0 +1,139 @@
1
+ import { type Rect } from '@expofp/geometry';
2
+ import { type Box3, Matrix4 } from 'three';
3
+ import { type InstancedMeshExt, type InstancedMeshExtParams } from '../../core/instanced/InstancedMesh';
4
+ import { type ModelDef } from '../../types/defs';
5
+ import { SlotContainer } from '../containers';
6
+ /**
7
+ * The per-instance uniform a model mesh reads its opacity from. The container owns the name: it
8
+ * declares the schema on bind and writes each slot's value, and hands the name to `ModelMaterial`,
9
+ * which patches its shader to multiply by it.
10
+ */
11
+ export declare const OPACITY_UNIFORM = "instanceOpacity";
12
+ /** The constructor tuple R3F builds one model mesh from. Frozen, so R3F never rebuilds the mesh. */
13
+ export type ModelMeshArgs = [params: InstancedMeshExtParams];
14
+ /**
15
+ * Every kind of model batch, **in the order they draw within a layer** — which is what `Models`
16
+ * numbers them by.
17
+ */
18
+ export declare const MODEL_KINDS: readonly ['opaque', 'translucent'];
19
+ /** Which batch a model draws from; see {@link modelKindOf}. */
20
+ export type ModelKind = (typeof MODEL_KINDS)[number];
21
+ /**
22
+ * Which batch a model belongs in: `translucent` exactly when its `opacity` is below 1.
23
+ *
24
+ * A partition rather than a per-instance flag, because `transparent` is a **material** property —
25
+ * three decides which render list an *object* belongs to, never which instance — so two copies of
26
+ * one model differing only in opacity cannot share a mesh. `opacity` is updatable, so a def can
27
+ * cross the boundary at any time; that is what `reportKindMismatch` is for.
28
+ * @param def - The model to classify.
29
+ * @returns The kind of batch it draws from.
30
+ */
31
+ export declare function modelKindOf(def: ModelDef): ModelKind;
32
+ /**
33
+ * One model part's slot bookkeeping: every def in the layer drawing that part gets an instance, so
34
+ * twenty copies of a chair are one draw call rather than twenty.
35
+ *
36
+ * Much simpler than a shape container. The geometry is the part's — shared, never per instance — so
37
+ * there is no geometry cache and no capacity pre-scan, and `InstancedMeshExt` grows its own buffers,
38
+ * so nothing here has to size anything. What is left is a slot per def and the transform each one
39
+ * draws with.
40
+ */
41
+ export declare class ModelContainer extends SlotContainer<null, InstancedMeshExt, ModelDef> {
42
+ /** The constructor args R3F builds the mesh from; a frozen snapshot, never mutated. */
43
+ readonly args: ModelMeshArgs;
44
+ /** Which kind of member this container holds, and so which render list its mesh draws from. */
45
+ readonly kind: ModelKind;
46
+ /**
47
+ * What the mesh drawing this part must carry, so that three sees the mirror the plan's frame
48
+ * requires and {@link place}'s plan-space answers land where they say. See {@link PLAN_FROM_GLTF}.
49
+ */
50
+ readonly meshMatrix: Matrix4;
51
+ /** The model's bounds in plan units; every instance is fitted against them. */
52
+ private readonly extents;
53
+ private kindMismatchHandler;
54
+ /**
55
+ * Builds the bookkeeping for one model part.
56
+ * @param extents - The model's own bounds, y-up, as `bakeGltf` left them.
57
+ * @param count - How many defs the layer holds for this part, used as the mesh's initial capacity.
58
+ * @param kind - Which of the layer's batches this is; its material is built for that one alone.
59
+ */
60
+ constructor(extents: Box3, count: number, kind?: ModelKind);
61
+ /**
62
+ * Paint order is not implemented for models, so this does nothing.
63
+ *
64
+ * Not an omission, and not a limitation either — the mesh takes a `customSort` like any other, and
65
+ * wiring the inherited pass to it would take a line. Def order is simply the wrong rule here. It
66
+ * is the 2D painter's rule, and it earns its keep only where nothing writes depth; a model does
67
+ * write depth, so which of two overlapping models is in front is settled by the depth buffer
68
+ * whatever order they draw in, and a *translucent* one wants farthest-first — depth order, which
69
+ * def order cannot express and which the mesh already applies for itself (`sortObjects`, see
70
+ * `Models`). Nor would def order reach across a layer: a model layer is one mesh per source
71
+ * material, and no sort inside a mesh orders a def against one drawn from another part.
72
+ *
73
+ * A no-op rather than the inherited pass, so a caller that orders every other layer kind does not
74
+ * quietly appear to order this one.
75
+ */
76
+ setPaintOrder(): void;
77
+ /**
78
+ * Registers what to do when a member turns out to be the wrong kind. `Models` supplies a re-split;
79
+ * `undefined` clears it.
80
+ *
81
+ * A method rather than an assignable field because the container is held in `useState`, and the
82
+ * React Compiler rejects writing to a property of a state value.
83
+ * @param handler - The re-split to run, or `undefined` to clear it.
84
+ */
85
+ setKindMismatchHandler(handler: (() => void) | undefined): void;
86
+ /**
87
+ * Reports that a member no longer matches {@link kind} — its `opacity` crossed 1 — so it is
88
+ * drawing from a mesh whose material describes the wrong render list. Runs the registered handler,
89
+ * if any; a container mounted outside `Models` has none and the report is simply dropped.
90
+ */
91
+ reportKindMismatch(): void;
92
+ /**
93
+ * Writes a def's placement into its slot: the model's extents fitted into `bounds`.
94
+ *
95
+ * The mesh carries {@link PLAN_FROM_GLTF}, so its instances are placed in the model's own space,
96
+ * while {@link fitToBounds} answers in the plan's. Conjugating moves the placement across — undo
97
+ * the mesh's map, place, redo it — and is unavoidable: the instance matrix is innermost, so
98
+ * anything the mesh applies outside it has to be taken back inside.
99
+ * @param slot - The instance's slot.
100
+ * @param def - The def supplying the bounds.
101
+ */
102
+ place(slot: number, def: ModelDef): void;
103
+ /**
104
+ * Writes a def's opacity into its slot — **clamped to opaque unless this batch blends**.
105
+ *
106
+ * An opaque batch's material is `transparent: false`, which three answers by dropping
107
+ * `NormalBlending` to `NoBlending` (`WebGLState.setMaterial`). With blending off a fragment alpha
108
+ * below 1 is not composited but *written*, into a framebuffer the page shows through — so the
109
+ * model does not fade, it turns white.
110
+ *
111
+ * That is not a hypothetical: an opacity leaving 1 re-splits the layer, and until React commits it
112
+ * the def is still in this batch with its new opacity already written. Clamping is what makes the
113
+ * gap invisible — the model simply stays solid until the batch that can blend it takes over —
114
+ * without anything having to be fast enough.
115
+ * @param slot - The instance's slot.
116
+ * @param def - The def supplying the opacity.
117
+ */
118
+ setOpacity(slot: number, def: ModelDef): void;
119
+ protected addInstance(): number;
120
+ protected removeInstance(slot: number): void;
121
+ }
122
+ /**
123
+ * Composes the transform that lands a baked model inside a def's bounds.
124
+ *
125
+ * The model is scaled uniformly — by the *smaller* of the two axis ratios, so it fits inside the
126
+ * rect rather than being stretched to it — then spun about z by the bounds' rotation and moved so its
127
+ * center sits at the bounds' center with its base at the bounds' elevation.
128
+ *
129
+ * Uniform scale is what keeps a model's proportions: a chair placed in a rect twice as wide as it is
130
+ * deep should not become a wide chair.
131
+ *
132
+ * Plan units throughout, in and out — {@link ModelContainer.place} is what carries the answer into
133
+ * the frame the mesh draws in.
134
+ * @param extents - The model's bounds, in plan units.
135
+ * @param bounds - Where the def wants it, in plan space.
136
+ * @returns The shared scratch matrix, composed. Valid until the next call.
137
+ */
138
+ export declare function fitToBounds(extents: Box3, bounds: Rect): Matrix4;
139
+ //# sourceMappingURL=model-container.d.ts.map