@needle-tools/engine 4.13.1 → 4.14.0-next.31f837e

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 (106) hide show
  1. package/components.needle.json +1 -1
  2. package/dist/generateMeshBVH.worker-DFcS3P04.js +21 -0
  3. package/dist/gltf-progressive-8voIgNp_.js +1528 -0
  4. package/dist/gltf-progressive-BRRBj-nY.umd.cjs +10 -0
  5. package/dist/gltf-progressive-Dkh3tG4-.min.js +10 -0
  6. package/dist/loader.worker-C6cXDgR1.js +23 -0
  7. package/dist/{materialx-BF23AVE8.umd.cjs → materialx-CxlgposR.umd.cjs} +1 -1
  8. package/dist/{materialx-fkoFuRh3.js → materialx-D66rYPqe.js} +2 -2
  9. package/dist/{materialx-B9ddsHcF.min.js → materialx-Dx8st96L.min.js} +1 -1
  10. package/dist/{needle-engine.bundle-tjI5Fq2c.umd.cjs → needle-engine.bundle-BQXG5qbQ.umd.cjs} +138 -145
  11. package/dist/{needle-engine.bundle-DauZUYl7.js → needle-engine.bundle-Byl5i6zJ.js} +6403 -6164
  12. package/dist/needle-engine.bundle-D7w0XD7M.min.js +1646 -0
  13. package/dist/needle-engine.d.ts +251 -23
  14. package/dist/needle-engine.js +416 -415
  15. package/dist/needle-engine.min.js +1 -1
  16. package/dist/needle-engine.umd.cjs +1 -1
  17. package/dist/{postprocessing-DdM-tz1j.js → postprocessing-BkSpxpYB.js} +2 -2
  18. package/dist/{postprocessing-BVNrgYZK.min.js → postprocessing-Ce5-UWiA.min.js} +1 -1
  19. package/dist/{postprocessing-CI2TjWpu.umd.cjs → postprocessing-DFVElmAh.umd.cjs} +1 -1
  20. package/dist/{three-BW2s1Yl-.umd.cjs → three-Bad8p1pf.umd.cjs} +46 -46
  21. package/dist/{three-I__hSXzr.min.js → three-CWn13_u1.min.js} +33 -33
  22. package/dist/{three-VvRoMeIN.js → three-DFV1-P9z.js} +4209 -4209
  23. package/dist/{three-examples-BhfOE7NG.js → three-examples-43yqn3mL.js} +1 -1
  24. package/dist/{three-examples-Bpfu6ke_.umd.cjs → three-examples-CO-tx3Sp.umd.cjs} +1 -1
  25. package/dist/{three-examples-D8zAE_7t.min.js → three-examples-DKuJVGT4.min.js} +1 -1
  26. package/dist/{three-mesh-ui-BU55xDxJ.umd.cjs → three-mesh-ui-ChzVOraf.umd.cjs} +1 -1
  27. package/dist/{three-mesh-ui-C3QbemOV.min.js → three-mesh-ui-DyEA5HQF.min.js} +1 -1
  28. package/dist/{three-mesh-ui-CcMp-FQm.js → three-mesh-ui-fSAQJxdI.js} +1 -1
  29. package/dist/{vendor-COVQl0b8.umd.cjs → vendor-B51YffMU.umd.cjs} +1 -1
  30. package/dist/{vendor-BiyIZ61v.js → vendor-CgpZ5ivC.js} +1 -1
  31. package/dist/{vendor-DW7zqjuT.min.js → vendor-pe19S9r5.min.js} +1 -1
  32. package/lib/engine/api.d.ts +1 -0
  33. package/lib/engine/api.js +1 -0
  34. package/lib/engine/api.js.map +1 -1
  35. package/lib/engine/engine_context.js +12 -2
  36. package/lib/engine/engine_context.js.map +1 -1
  37. package/lib/engine/engine_lightdata.js +8 -6
  38. package/lib/engine/engine_lightdata.js.map +1 -1
  39. package/lib/engine/engine_materialpropertyblock.d.ts +47 -0
  40. package/lib/engine/engine_materialpropertyblock.js +412 -0
  41. package/lib/engine/engine_materialpropertyblock.js.map +1 -0
  42. package/lib/engine/engine_utils.d.ts +1 -0
  43. package/lib/engine/engine_utils.js +1 -0
  44. package/lib/engine/engine_utils.js.map +1 -1
  45. package/lib/engine/engine_utils_screenshot.d.ts +171 -14
  46. package/lib/engine/engine_utils_screenshot.js +65 -0
  47. package/lib/engine/engine_utils_screenshot.js.map +1 -1
  48. package/lib/engine/engine_utils_screenshot.xr.d.ts +1 -1
  49. package/lib/engine/engine_utils_screenshot.xr.js +1 -1
  50. package/lib/engine/extensions/NEEDLE_techniques_webgl.js +3 -0
  51. package/lib/engine/extensions/NEEDLE_techniques_webgl.js.map +1 -1
  52. package/lib/engine/xr/NeedleXRSession.d.ts +5 -0
  53. package/lib/engine/xr/NeedleXRSession.js +5 -0
  54. package/lib/engine/xr/NeedleXRSession.js.map +1 -1
  55. package/lib/engine-components/ReflectionProbe.d.ts +0 -1
  56. package/lib/engine-components/ReflectionProbe.js +15 -76
  57. package/lib/engine-components/ReflectionProbe.js.map +1 -1
  58. package/lib/engine-components/RendererLightmap.d.ts +13 -9
  59. package/lib/engine-components/RendererLightmap.js +68 -81
  60. package/lib/engine-components/RendererLightmap.js.map +1 -1
  61. package/lib/engine-components/SeeThrough.js.map +1 -1
  62. package/lib/engine-components/VideoPlayer.js +6 -0
  63. package/lib/engine-components/VideoPlayer.js.map +1 -1
  64. package/lib/engine-components/utils/OpenURL.d.ts +1 -0
  65. package/lib/engine-components/utils/OpenURL.js +1 -0
  66. package/lib/engine-components/utils/OpenURL.js.map +1 -1
  67. package/lib/engine-components/web/CursorFollow.d.ts +1 -0
  68. package/lib/engine-components/web/CursorFollow.js +1 -0
  69. package/lib/engine-components/web/CursorFollow.js.map +1 -1
  70. package/lib/engine-components/web/ScrollFollow.d.ts +1 -0
  71. package/lib/engine-components/web/ScrollFollow.js +1 -0
  72. package/lib/engine-components/web/ScrollFollow.js.map +1 -1
  73. package/lib/engine-components/webxr/WebARCameraBackground.d.ts +9 -0
  74. package/lib/engine-components/webxr/WebARCameraBackground.js +9 -0
  75. package/lib/engine-components/webxr/WebARCameraBackground.js.map +1 -1
  76. package/lib/engine-components/webxr/WebXR.d.ts +1 -0
  77. package/lib/engine-components/webxr/WebXR.js +1 -0
  78. package/lib/engine-components/webxr/WebXR.js.map +1 -1
  79. package/package.json +4 -4
  80. package/plugins/vite/build-pipeline.js +16 -2
  81. package/src/engine/api.ts +1 -0
  82. package/src/engine/engine_context.ts +17 -3
  83. package/src/engine/engine_lightdata.ts +8 -6
  84. package/src/engine/engine_materialpropertyblock.ts +500 -0
  85. package/src/engine/engine_utils.ts +1 -0
  86. package/src/engine/engine_utils_screenshot.ts +241 -17
  87. package/src/engine/engine_utils_screenshot.xr.ts +1 -1
  88. package/src/engine/extensions/NEEDLE_techniques_webgl.ts +3 -0
  89. package/src/engine/xr/NeedleXRSession.ts +5 -0
  90. package/src/engine-components/ContactShadows.ts +1 -1
  91. package/src/engine-components/ReflectionProbe.ts +17 -89
  92. package/src/engine-components/RendererLightmap.ts +76 -87
  93. package/src/engine-components/SeeThrough.ts +2 -2
  94. package/src/engine-components/VideoPlayer.ts +6 -0
  95. package/src/engine-components/utils/OpenURL.ts +1 -0
  96. package/src/engine-components/web/CursorFollow.ts +1 -0
  97. package/src/engine-components/web/ScrollFollow.ts +1 -0
  98. package/src/engine-components/webxr/WebARCameraBackground.ts +12 -3
  99. package/src/engine-components/webxr/WebXR.ts +1 -0
  100. package/dist/generateMeshBVH.worker-iyfPIK6R.js +0 -21
  101. package/dist/gltf-progressive-BURrJW0U.umd.cjs +0 -8
  102. package/dist/gltf-progressive-DHLDFNvQ.min.js +0 -8
  103. package/dist/gltf-progressive-eiJCrjLb.js +0 -1400
  104. package/dist/loader.worker-C1GG9A7C.js +0 -23
  105. package/dist/needle-engine.bundle-BNIUpreS.min.js +0 -1653
  106. package/src/include/three/DragControls.js +0 -232
@@ -0,0 +1,500 @@
1
+ import { Material, Object3D, Color, Texture, Vector2, Vector3, Vector4, WebGLRenderer, Scene, Camera, BufferGeometry, Group } from "three";
2
+
3
+ type MaterialPropertyType = number | number[] | Color | Texture | Vector2 | Vector3 | Vector4 | null;
4
+
5
+ export interface TextureTransform {
6
+ offset?: Vector2;
7
+ repeat?: Vector2;
8
+ }
9
+
10
+ export interface PropertyBlockOverride {
11
+ name: string;
12
+ value: MaterialPropertyType;
13
+ textureTransform?: TextureTransform;
14
+ }
15
+
16
+ type NonFunctionPropertyNames<T> = {
17
+ [K in keyof T]: T[K] extends Function ? never : K
18
+ }[keyof T];
19
+
20
+ // Centralized registry for all property block related data
21
+ class PropertyBlockRegistry {
22
+ // Map from object to its property block
23
+ private objectToBlock = new WeakMap<Object3D, MaterialPropertyBlock>();
24
+
25
+ // Track which materials belong to which property block (to prevent applying to wrong materials)
26
+ // Use WeakSet for automatic cleanup when materials are garbage collected
27
+ private objectToMaterials = new WeakMap<Object3D, WeakSet<Material>>();
28
+
29
+ // Track which meshes have callbacks for which property block owners
30
+ private meshToOwners = new WeakMap<Object3D, Set<Object3D>>();
31
+
32
+ // Track original callback functions for cleanup (reserved for future use)
33
+ private meshToOriginalCallbacks = new WeakMap<Object3D, {
34
+ onBeforeRender?: ObjectRenderCallback;
35
+ onAfterRender?: ObjectRenderCallback;
36
+ }>();
37
+
38
+ getBlock(object: Object3D): MaterialPropertyBlock | undefined {
39
+ return this.objectToBlock.get(object);
40
+ }
41
+
42
+ setBlock(object: Object3D, block: MaterialPropertyBlock): void {
43
+ this.objectToBlock.set(object, block);
44
+ }
45
+
46
+ deleteBlock(object: Object3D): void {
47
+ this.objectToBlock.delete(object);
48
+ this.objectToMaterials.delete(object);
49
+ }
50
+
51
+ addMaterial(object: Object3D, material: Material): void {
52
+ let materials = this.objectToMaterials.get(object);
53
+ if (!materials) {
54
+ materials = new WeakSet();
55
+ this.objectToMaterials.set(object, materials);
56
+ }
57
+ materials.add(material);
58
+ }
59
+
60
+ hasMaterial(object: Object3D, material: Material): boolean {
61
+ return this.objectToMaterials.get(object)?.has(material) ?? false;
62
+ }
63
+
64
+ isHooked(mesh: Object3D, owner: Object3D): boolean {
65
+ return this.meshToOwners.get(mesh)?.has(owner) ?? false;
66
+ }
67
+
68
+ addHook(mesh: Object3D, owner: Object3D): void {
69
+ let owners = this.meshToOwners.get(mesh);
70
+ if (!owners) {
71
+ owners = new Set();
72
+ this.meshToOwners.set(mesh, owners);
73
+ }
74
+ owners.add(owner);
75
+ }
76
+
77
+ removeHook(mesh: Object3D, owner: Object3D): void {
78
+ const owners = this.meshToOwners.get(mesh);
79
+ if (owners) {
80
+ owners.delete(owner);
81
+ if (owners.size === 0) {
82
+ this.meshToOwners.delete(mesh);
83
+ }
84
+ }
85
+ }
86
+
87
+ getOriginalCallbacks(mesh: Object3D): { onBeforeRender?: ObjectRenderCallback; onAfterRender?: ObjectRenderCallback } | undefined {
88
+ return this.meshToOriginalCallbacks.get(mesh);
89
+ }
90
+
91
+ setOriginalCallbacks(mesh: Object3D, callbacks: { onBeforeRender?: ObjectRenderCallback; onAfterRender?: ObjectRenderCallback }): void {
92
+ this.meshToOriginalCallbacks.set(mesh, callbacks);
93
+ }
94
+ }
95
+
96
+ const registry = new PropertyBlockRegistry();
97
+
98
+ export class MaterialPropertyBlock<T extends Material = Material> {
99
+ private _overrides: PropertyBlockOverride[] = [];
100
+ private _defines: Record<string, string | number | boolean> = {};
101
+ private _object: Object3D | null = null;
102
+
103
+ get object(): Object3D | null { return this._object; }
104
+
105
+ constructor(object: Object3D | null = null) {
106
+ this._object = object;
107
+ }
108
+
109
+ static get<T extends Material = Material>(object: Object3D): MaterialPropertyBlock<T> {
110
+ let block = registry.getBlock(object);
111
+ if (!block) {
112
+ block = new MaterialPropertyBlock(object);
113
+ registry.setBlock(object, block);
114
+ attachPropertyBlockToObject(object, block);
115
+ }
116
+ return block as MaterialPropertyBlock<T>;
117
+ }
118
+
119
+ dispose() {
120
+ if (this._object) {
121
+ registry.deleteBlock(this._object);
122
+ // TODO: Add cleanup for hooked meshes
123
+ }
124
+ this._overrides = [];
125
+ this._object = null;
126
+ }
127
+
128
+ setOverride<K extends NonFunctionPropertyNames<T>>(name: K, value: T[K], textureTransform?: TextureTransform): void;
129
+ setOverride(name: string, value: MaterialPropertyType, textureTransform?: TextureTransform): void;
130
+ setOverride(name: string, value: MaterialPropertyType, textureTransform?: TextureTransform): void {
131
+ const existing = this._overrides.find(o => o.name === name);
132
+ if (existing) {
133
+ existing.value = value;
134
+ existing.textureTransform = textureTransform;
135
+ } else {
136
+ this._overrides.push({ name, value, textureTransform });
137
+ }
138
+ }
139
+
140
+ getOverride(name: string): PropertyBlockOverride | undefined {
141
+ return this._overrides.find(o => o.name === name);
142
+ }
143
+
144
+ clearOverride(name: string): void {
145
+ const index = this._overrides.findIndex(o => o.name === name);
146
+ if (index >= 0) {
147
+ this._overrides.splice(index, 1);
148
+ }
149
+ }
150
+
151
+ clearAllOverrides(): void {
152
+ this._overrides = [];
153
+ }
154
+
155
+ removeOverride(name: string): void {
156
+ this.clearOverride(name);
157
+ }
158
+
159
+ get overrides(): PropertyBlockOverride[] {
160
+ return this._overrides;
161
+ }
162
+
163
+ hasOverrides(): boolean {
164
+ return this._overrides.length > 0;
165
+ }
166
+
167
+ /**
168
+ * Set a shader define that will be included in the program cache key
169
+ * This allows different objects sharing the same material to have different shader programs
170
+ */
171
+ setDefine(name: string, value: string | number | boolean): void {
172
+ this._defines[name] = value;
173
+ }
174
+
175
+ /**
176
+ * Remove a shader define
177
+ */
178
+ clearDefine(name: string): void {
179
+ this._defines[name] = undefined as any;
180
+ }
181
+
182
+ /**
183
+ * Get all defines set on this property block
184
+ */
185
+ getDefines(): Readonly<Record<string, string | number | boolean>> {
186
+ return this._defines;
187
+ }
188
+
189
+ getCacheKey(): string {
190
+ const parts: string[] = [];
191
+
192
+ // Add defines to cache key
193
+ const defineKeys = Object.keys(this._defines).sort();
194
+ for (const key of defineKeys) {
195
+ const value = this._defines[key];
196
+ if (value !== undefined) {
197
+ parts.push(`d:${key}=${value}`);
198
+ }
199
+ }
200
+
201
+ // Add overrides to cache key
202
+ for (const o of this._overrides) {
203
+ if (o.value === null) continue;
204
+ let val = "";
205
+ if (o.value instanceof Texture) {
206
+ val = (o.value as any).uuid || "texture";
207
+ if (o.textureTransform) {
208
+ const t = o.textureTransform;
209
+ if (t.offset) val += `;to:${t.offset.x},${t.offset.y}`;
210
+ if (t.repeat) val += `;tr:${t.repeat.x},${t.repeat.y}`;
211
+ }
212
+ } else if (Array.isArray(o.value)) {
213
+ val = o.value.join(",");
214
+ } else if (o.value && typeof o.value === "object" && "r" in o.value) {
215
+ const c = o.value as any;
216
+ val = `${c.r},${c.g},${c.b},${c.a !== undefined ? c.a : ""}`;
217
+ } else if (o.value && typeof o.value === "object" && "x" in o.value) {
218
+ // Vector2, Vector3, Vector4
219
+ const v = o.value as any;
220
+ val = `${v.x},${v.y}${v.z !== undefined ? `,${v.z}` : ""}${v.w !== undefined ? `,${v.w}` : ""}`;
221
+ } else {
222
+ val = String(o.value);
223
+ }
224
+ parts.push(`${o.name}=${val}`);
225
+ }
226
+ return parts.join(";");
227
+ }
228
+ }
229
+
230
+ const $originalValues = Symbol("originalValues");
231
+
232
+ interface OriginalValue {
233
+ name: string;
234
+ value: unknown;
235
+ }
236
+
237
+ interface SavedTextureTransform {
238
+ name: string;
239
+ offsetX: number;
240
+ offsetY: number;
241
+ repeatX: number;
242
+ repeatY: number;
243
+ }
244
+
245
+ const $savedTextureTransforms = Symbol("savedTextureTransforms");
246
+
247
+ type ObjectRenderCallback = (this: Object3D, renderer: WebGLRenderer, scene: Scene, camera: Camera, geometry: BufferGeometry, material: Material, group: Group) => void;
248
+
249
+ // Collect all materials from an object and its children
250
+ function collectMaterials(object: Object3D, materials: Set<Material>): void {
251
+ const obj = object as Object3D & { material?: Material | Material[] };
252
+ if (obj.material) {
253
+ if (Array.isArray(obj.material)) {
254
+ obj.material.forEach(mat => materials.add(mat));
255
+ } else {
256
+ materials.add(obj.material);
257
+ }
258
+ }
259
+
260
+ // For Groups, collect materials from children too
261
+ if (object.type === "Group") {
262
+ object.children.forEach(child => collectMaterials(child, materials));
263
+ }
264
+ }
265
+
266
+ // Find property block by checking this object and parent (if parent is a Group)
267
+ // Returns both the block and the owner object
268
+ function findPropertyBlockAndOwner(obj: Object3D): { block: MaterialPropertyBlock; owner: Object3D } | undefined {
269
+ // First check if this object itself has a property block
270
+ let block = registry.getBlock(obj);
271
+ if (block) return { block, owner: obj };
272
+
273
+ // If not, check if parent is a Group and has a property block
274
+ if (obj.parent && obj.parent.type === "Group") {
275
+ block = registry.getBlock(obj.parent);
276
+ if (block) return { block, owner: obj.parent };
277
+ }
278
+
279
+ return undefined;
280
+ }
281
+
282
+ // #region OnBeforeRender
283
+ const onBeforeRender_MaterialBlock: ObjectRenderCallback = function (this: Object3D, _renderer: WebGLRenderer, _scene: Scene, _camera: Camera, _geometry: BufferGeometry, material: Material, _group: Group) {
284
+ const result = findPropertyBlockAndOwner(this);
285
+ if (!result) {
286
+ return;
287
+ }
288
+
289
+ const { block: propertyBlock, owner } = result;
290
+
291
+ // Only apply if this material was registered with this property block
292
+ if (!registry.hasMaterial(owner, material)) {
293
+ return;
294
+ }
295
+
296
+ const overrides = propertyBlock.overrides;
297
+ const mat = material as any;
298
+
299
+ // Apply defines to material - this affects shader compilation
300
+ const defines = propertyBlock.getDefines();
301
+ const defineKeys = Object.keys(defines);
302
+ if (defineKeys.length > 0) {
303
+ if (!mat.defines) mat.defines = {};
304
+ for (const key of defineKeys) {
305
+ const value = defines[key];
306
+ if (value !== undefined) {
307
+ mat.defines[key] = value;
308
+ }
309
+ }
310
+ }
311
+
312
+ // Still set up cache key even if no overrides (defines affect it)
313
+ if (overrides.length === 0 && defineKeys.length === 0) {
314
+ return;
315
+ }
316
+
317
+ // Defines always affect shader compilation → need program change
318
+ let needsProgramChange = defineKeys.length > 0;
319
+
320
+ if (!mat[$originalValues]) {
321
+ mat[$originalValues] = [];
322
+ }
323
+ const originalValues = mat[$originalValues] as OriginalValue[];
324
+
325
+ for (const override of overrides) {
326
+ if (override.value === null) continue;
327
+
328
+ const currentValue = mat[override.name];
329
+
330
+ const existingOriginal = originalValues.find((o: OriginalValue) => o.name === override.name);
331
+ if (existingOriginal) {
332
+ // Update to current value each frame so animations/external changes are preserved
333
+ existingOriginal.value = currentValue;
334
+ } else {
335
+ originalValues.push({ name: override.name, value: currentValue });
336
+ }
337
+
338
+ // Check if this override changes shader features (truthiness change).
339
+ // E.g. null → Texture enables USE_LIGHTMAP, Texture → null disables it.
340
+ // Pure uniform changes (red → blue, textureA → textureB) don't need program switch.
341
+ if (!needsProgramChange && !!currentValue !== !!override.value) {
342
+ needsProgramChange = true;
343
+ }
344
+
345
+ // Set all material properties including lightMap -
346
+ // three.js reads material.lightMap to determine shader parameters and upload uniforms
347
+ mat[override.name] = override.value;
348
+
349
+ // Apply per-object texture transform (offset/repeat) if specified
350
+ if (override.textureTransform && override.value instanceof Texture) {
351
+ const tex = override.value;
352
+ if (!mat[$savedTextureTransforms]) mat[$savedTextureTransforms] = [];
353
+ (mat[$savedTextureTransforms] as SavedTextureTransform[]).push({
354
+ name: override.name,
355
+ offsetX: tex.offset.x, offsetY: tex.offset.y,
356
+ repeatX: tex.repeat.x, repeatY: tex.repeat.y
357
+ });
358
+ const t = override.textureTransform;
359
+ if (t.offset) tex.offset.copy(t.offset);
360
+ if (t.repeat) tex.repeat.copy(t.repeat);
361
+ }
362
+ }
363
+
364
+ // Only set needsUpdate when overrides change shader features (truthiness changes
365
+ // like null↔texture, or defines added). This triggers getProgram() for program switches.
366
+ // Pure uniform overrides (color, roughness) skip this — no version increment needed.
367
+ if (needsProgramChange) {
368
+ mat.needsUpdate = true;
369
+ }
370
+ // _forceRefresh triggers uniform re-upload for consecutive objects sharing
371
+ // the same program and material (without it three.js skips the upload).
372
+ mat._forceRefresh = true;
373
+ };
374
+
375
+ // #region OnAfterRender
376
+ const onAfterRender_MaterialBlock: ObjectRenderCallback = function (this: Object3D, _renderer: WebGLRenderer, _scene: Scene, _camera: Camera, _geometry: BufferGeometry, material: Material, _group: Group) {
377
+ const result = findPropertyBlockAndOwner(this);
378
+ if (!result) {
379
+ return;
380
+ }
381
+
382
+ const { block: propertyBlock, owner } = result;
383
+
384
+ // Only restore if this material was registered with this property block
385
+ if (!registry.hasMaterial(owner, material)) {
386
+ return;
387
+ }
388
+
389
+ const overrides = propertyBlock.overrides;
390
+
391
+ const mat = material as any;
392
+ const originalValues = mat[$originalValues] as OriginalValue[] | undefined;
393
+
394
+ // Clean up defines — this affects shader compilation
395
+ const defines = propertyBlock.getDefines();
396
+ const defineKeys = Object.keys(defines);
397
+ let needsProgramChange = false;
398
+ if (defineKeys.length > 0 && mat.defines) {
399
+ for (const key of defineKeys) {
400
+ delete mat.defines[key];
401
+ }
402
+ needsProgramChange = true;
403
+ }
404
+
405
+ if (overrides.length === 0) {
406
+ if (needsProgramChange) {
407
+ mat.needsUpdate = true;
408
+ mat._forceRefresh = true;
409
+ }
410
+ return;
411
+ }
412
+
413
+ if (!originalValues) return;
414
+
415
+ // Restore texture transforms before restoring material properties
416
+ const savedTransforms = mat[$savedTextureTransforms] as SavedTextureTransform[] | undefined;
417
+ if (savedTransforms && savedTransforms.length > 0) {
418
+ for (const saved of savedTransforms) {
419
+ const override = overrides.find(o => o.name === saved.name);
420
+ if (override?.value instanceof Texture) {
421
+ override.value.offset.set(saved.offsetX, saved.offsetY);
422
+ override.value.repeat.set(saved.repeatX, saved.repeatY);
423
+ }
424
+ }
425
+ savedTransforms.length = 0;
426
+ }
427
+
428
+ for (const override of overrides) {
429
+ const original = originalValues.find(o => o.name === override.name);
430
+ if (original) {
431
+ // Check if restoring changes shader features (truthiness change)
432
+ if (!needsProgramChange && !!override.value !== !!original.value) {
433
+ needsProgramChange = true;
434
+ }
435
+ mat[override.name] = original.value;
436
+ }
437
+ }
438
+
439
+ // Only set needsUpdate when restoring affects shader features
440
+ if (needsProgramChange) {
441
+ mat.needsUpdate = true;
442
+ }
443
+ // Always force uniform refresh so the next object gets correct values
444
+ mat._forceRefresh = true;
445
+ };
446
+
447
+
448
+ // #region Attach Callbacks
449
+ function attachPropertyBlockToObject(object: Object3D, _propertyBlock: MaterialPropertyBlock): void {
450
+ // Collect and register all materials that belong to this property block
451
+ const materials = new Set<Material>();
452
+ collectMaterials(object, materials);
453
+ materials.forEach(mat => registry.addMaterial(object, mat));
454
+
455
+ // Attach callbacks to renderable objects (Mesh, SkinnedMesh)
456
+ // Groups don't render themselves but we still need to handle child meshes
457
+ if (object.type === "Group") {
458
+ object.children.forEach(child => {
459
+ if (child.type === "Mesh" || child.type === "SkinnedMesh") {
460
+ attachCallbacksToMesh(child, object);
461
+ }
462
+ });
463
+ } else if (object.type === "Mesh" || object.type === "SkinnedMesh") {
464
+ attachCallbacksToMesh(object, object);
465
+ }
466
+ }
467
+ function attachCallbacksToMesh(mesh: Object3D, propertyBlockOwner: Object3D): void {
468
+ // Check if this specific mesh already has our callbacks attached for this property block owner
469
+ if (registry.isHooked(mesh, propertyBlockOwner)) {
470
+ // Already hooked for this property block owner
471
+ return;
472
+ }
473
+
474
+ registry.addHook(mesh, propertyBlockOwner);
475
+
476
+ if (!mesh.onBeforeRender) {
477
+ mesh.onBeforeRender = onBeforeRender_MaterialBlock;
478
+ } else {
479
+ const original = mesh.onBeforeRender;
480
+ mesh.onBeforeRender = function (renderer, scene, camera, geometry, material, group) {
481
+ original.call(this, renderer, scene, camera, geometry, material, group);
482
+ onBeforeRender_MaterialBlock.call(this, renderer, scene, camera, geometry, material, group);
483
+ };
484
+ }
485
+
486
+ if (!mesh.onAfterRender) {
487
+ mesh.onAfterRender = onAfterRender_MaterialBlock;
488
+ } else {
489
+ const original = mesh.onAfterRender;
490
+ mesh.onAfterRender = function (renderer, scene, camera, geometry, material, group) {
491
+ onAfterRender_MaterialBlock.call(this, renderer, scene, camera, geometry, material, group);
492
+ original.call(this, renderer, scene, camera, geometry, material, group);
493
+ };
494
+ }
495
+ }
496
+ //#endregion
497
+
498
+ export function objectHasPropertyBlock(object: Object3D): boolean {
499
+ return registry.getBlock(object) !== undefined;
500
+ }
@@ -589,6 +589,7 @@ declare global {
589
589
 
590
590
  /**
591
591
  * Utility functions to detect certain device types (mobile, desktop), browsers, or capabilities.
592
+ * @category Utilities
592
593
  */
593
594
  export namespace DeviceUtilities {
594
595