@jfk-solutions/demo3d-file-format 1.7.0 → 1.9.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 (86) hide show
  1. package/README.md +289 -1
  2. package/dist/examples/export-embedded-viewer-standalone.html +193 -0
  3. package/dist/examples/three-render-smoke-standalone.html +4997 -0
  4. package/dist/index.d.ts +9 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +8 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/model.d.ts +20 -3
  9. package/dist/model.d.ts.map +1 -1
  10. package/dist/model.js +89 -11
  11. package/dist/model.js.map +1 -1
  12. package/dist/parser.d.ts.map +1 -1
  13. package/dist/parser.js +135 -9
  14. package/dist/parser.js.map +1 -1
  15. package/dist/raw3d-animation.d.ts +63 -0
  16. package/dist/raw3d-animation.d.ts.map +1 -0
  17. package/dist/raw3d-animation.js +200 -0
  18. package/dist/raw3d-animation.js.map +1 -0
  19. package/dist/raw3d-effects.d.ts +66 -0
  20. package/dist/raw3d-effects.d.ts.map +1 -0
  21. package/dist/raw3d-effects.js +172 -0
  22. package/dist/raw3d-effects.js.map +1 -0
  23. package/dist/raw3d-skinning.d.ts +8 -0
  24. package/dist/raw3d-skinning.d.ts.map +1 -0
  25. package/dist/raw3d-skinning.js +37 -0
  26. package/dist/raw3d-skinning.js.map +1 -0
  27. package/dist/raw3d.d.ts +84 -4
  28. package/dist/raw3d.d.ts.map +1 -1
  29. package/dist/raw3d.js +111 -9
  30. package/dist/raw3d.js.map +1 -1
  31. package/dist/rigid-interpolation.d.ts +16 -0
  32. package/dist/rigid-interpolation.d.ts.map +1 -0
  33. package/dist/rigid-interpolation.js +52 -0
  34. package/dist/rigid-interpolation.js.map +1 -0
  35. package/dist/three/backend.d.ts +13 -0
  36. package/dist/three/backend.d.ts.map +1 -0
  37. package/dist/three/backend.js +9 -0
  38. package/dist/three/backend.js.map +1 -0
  39. package/dist/three/effects.d.ts +7 -0
  40. package/dist/three/effects.d.ts.map +1 -0
  41. package/dist/three/effects.js +127 -0
  42. package/dist/three/effects.js.map +1 -0
  43. package/dist/three/index.d.ts +5 -1
  44. package/dist/three/index.d.ts.map +1 -1
  45. package/dist/three/index.js +35 -1
  46. package/dist/three/index.js.map +1 -1
  47. package/dist/three/native-lighting.d.ts +5 -0
  48. package/dist/three/native-lighting.d.ts.map +1 -0
  49. package/dist/three/native-lighting.js +86 -0
  50. package/dist/three/native-lighting.js.map +1 -0
  51. package/dist/three/parallax.d.ts +5 -0
  52. package/dist/three/parallax.d.ts.map +1 -0
  53. package/dist/three/parallax.js +63 -0
  54. package/dist/three/parallax.js.map +1 -0
  55. package/dist/three/presentation.d.ts +31 -0
  56. package/dist/three/presentation.d.ts.map +1 -0
  57. package/dist/three/presentation.js +137 -0
  58. package/dist/three/presentation.js.map +1 -0
  59. package/dist/three/raw3d.d.ts +27 -0
  60. package/dist/three/raw3d.d.ts.map +1 -1
  61. package/dist/three/raw3d.js +536 -25
  62. package/dist/three/raw3d.js.map +1 -1
  63. package/dist/three/renderer.d.ts.map +1 -1
  64. package/dist/three/renderer.js +3 -0
  65. package/dist/three/renderer.js.map +1 -1
  66. package/dist/three/skinning.d.ts +11 -0
  67. package/dist/three/skinning.d.ts.map +1 -0
  68. package/dist/three/skinning.js +92 -0
  69. package/dist/three/skinning.js.map +1 -0
  70. package/dist/three/tsl.d.ts +60 -0
  71. package/dist/three/tsl.d.ts.map +1 -0
  72. package/dist/three/tsl.js +3 -0
  73. package/dist/three/tsl.js.map +1 -0
  74. package/dist/three/webgpu-native.d.ts +3 -0
  75. package/dist/three/webgpu-native.d.ts.map +1 -0
  76. package/dist/three/webgpu-native.js +58 -0
  77. package/dist/three/webgpu-native.js.map +1 -0
  78. package/dist/three/webgpu-relief.d.ts +3 -0
  79. package/dist/three/webgpu-relief.d.ts.map +1 -0
  80. package/dist/three/webgpu-relief.js +61 -0
  81. package/dist/three/webgpu-relief.js.map +1 -0
  82. package/dist/xml.d.ts +4 -1
  83. package/dist/xml.d.ts.map +1 -1
  84. package/dist/xml.js +14 -3
  85. package/dist/xml.js.map +1 -1
  86. package/package.json +6 -2
@@ -1,3 +1,6 @@
1
+ import { bindRaw3DThreeSkin } from "./skinning.js";
2
+ import { applyRaw3DThreeEffects, configureRaw3DDrawable } from "./effects.js";
3
+ import { raw3DNodeBackend } from "./backend.js";
1
4
  import { Raw3DMaterial, Raw3DMesh, Raw3DPackage, Raw3DProject } from "../raw3d.js";
2
5
  export async function createRaw3DThreeScene(parsed, options = {}) {
3
6
  const three = await resolveThree(options);
@@ -14,6 +17,9 @@ export async function createRaw3DThreeScene(parsed, options = {}) {
14
17
  };
15
18
  }
16
19
  export async function createRaw3DThreeGroup(parsed, options = {}) {
20
+ return (await buildRaw3DThreeGroup(parsed, options)).group;
21
+ }
22
+ async function buildRaw3DThreeGroup(parsed, options) {
17
23
  const three = await resolveThree(options);
18
24
  const project = parsed instanceof Raw3DPackage ? parsed.model : parsed;
19
25
  const warnings = [];
@@ -36,12 +42,16 @@ export async function createRaw3DThreeGroup(parsed, options = {}) {
36
42
  };
37
43
  const textures = await loadTextures(project, three, stats, warn);
38
44
  const materials = project.materials.map((material) => createMaterial(material, textures, three));
45
+ for (const material of materials)
46
+ material.userData.raw3DHeightChannel = options.parallaxHeightChannel ?? "a";
39
47
  const defaultMaterial = new three.MeshStandardMaterial({ color: 0x9aa0a6, roughness: 0.8 });
40
48
  const geometryCache = new Map();
41
49
  const objects = new Map();
50
+ const sourceNodes = new Map(project.nodes.map((node) => [node.index, node]));
42
51
  const root = new three.Group();
43
52
  root.name = "RAW3D";
44
53
  for (const node of project.nodes) {
54
+ const annotationHidden = !options.showAnnotations && isAnnotationNode(node, sourceNodes);
45
55
  const object = new three.Group();
46
56
  object.name = node.name;
47
57
  applyTransform(object, node);
@@ -61,10 +71,11 @@ export async function createRaw3DThreeGroup(parsed, options = {}) {
61
71
  endPosition: text.endPosition
62
72
  } : undefined,
63
73
  interactionMode: node.interactionMode,
74
+ annotationHidden,
64
75
  customAttributes: Object.fromEntries(node.customAttributes)
65
76
  };
66
77
  objects.set(node.index, object);
67
- if (node.meshIndex !== undefined) {
78
+ if (!annotationHidden && node.meshIndex !== undefined) {
68
79
  const sourceMesh = project.meshes[node.meshIndex];
69
80
  if (!sourceMesh) {
70
81
  warn({
@@ -93,21 +104,36 @@ export async function createRaw3DThreeGroup(parsed, options = {}) {
93
104
  }
94
105
  if (geometry) {
95
106
  const nodeMaterials = resolveNodeMaterials(node, sourceMesh, project, materials, defaultMaterial);
96
- const mesh = new three.Mesh(geometry, nodeMaterials.length === 1 ? nodeMaterials[0] : nodeMaterials);
97
- mesh.name = `${node.name} Mesh`;
98
- mesh.castShadow = node.castsShadow;
99
- mesh.receiveShadow = true;
100
- mesh.userData.raw3d = { kind: "mesh", nodeIndex: node.index, meshIndex: node.meshIndex };
101
- object.add(mesh);
107
+ const drawable = createDrawable(sourceMesh, geometry, nodeMaterials, three);
108
+ drawable.name = `${node.name} ${sourceMesh.meshType}`;
109
+ drawable.castShadow = node.castsShadow && drawable instanceof three.Mesh;
110
+ drawable.visible = node.visible;
111
+ drawable.receiveShadow = drawable instanceof three.Mesh;
112
+ drawable.userData.raw3d = {
113
+ kind: primitiveKind(sourceMesh.meshType),
114
+ nodeIndex: node.index,
115
+ meshIndex: node.meshIndex,
116
+ meshType: sourceMesh.meshType,
117
+ sourceVisible: node.visible
118
+ };
119
+ object.add(drawable);
102
120
  stats.meshes += 1;
103
121
  }
104
122
  }
105
123
  }
106
- }
107
- for (const source of project.lights) {
108
- if (!source.enabled) {
109
- continue;
124
+ if (!annotationHidden && text) {
125
+ const textMaterialIndex = text.materialIndex ?? node.materialIndices[0];
126
+ const sourceMaterial = textMaterialIndex === undefined ? undefined : project.materials[textMaterialIndex];
127
+ const sprite = createTextSprite(text, sourceMaterial, three);
128
+ if (sprite) {
129
+ sprite.name = `${node.name} Text`;
130
+ sprite.visible = node.visible;
131
+ sprite.userData.raw3d = { kind: "text", nodeIndex: node.index, textIndex: node.textIndex, sourceVisible: node.visible };
132
+ object.add(sprite);
133
+ }
110
134
  }
135
+ }
136
+ for (const [sourceIndex, source] of project.lights.entries()) {
111
137
  const parent = source.nodeIndex === undefined ? undefined : objects.get(source.nodeIndex);
112
138
  if (!parent) {
113
139
  warn({
@@ -127,10 +153,12 @@ export async function createRaw3DThreeGroup(parsed, options = {}) {
127
153
  light = new three.AmbientLight(color, 1);
128
154
  break;
129
155
  case "point":
130
- light = new three.PointLight(color, 1, 0, 2);
156
+ light = raw3DNodeBackend(three)?.point() ?? new three.PointLight(color, 1, 0, 2);
157
+ light.color.copy(color);
131
158
  break;
132
159
  case "spot":
133
- light = new three.SpotLight(color, 1);
160
+ light = raw3DNodeBackend(three)?.spot() ?? new three.SpotLight(color, 1);
161
+ light.color.copy(color);
134
162
  break;
135
163
  default: {
136
164
  const directional = new three.DirectionalLight(color, 1);
@@ -141,8 +169,10 @@ export async function createRaw3DThreeGroup(parsed, options = {}) {
141
169
  }
142
170
  }
143
171
  light.name = `${parent.name} ${source.type} light`;
172
+ light.visible = source.enabled;
144
173
  light.userData.raw3d = {
145
174
  kind: "light",
175
+ index: source.index ?? sourceIndex,
146
176
  nodeIndex: source.nodeIndex,
147
177
  materialIndex: source.materialIndex,
148
178
  type: source.type,
@@ -177,7 +207,327 @@ export async function createRaw3DThreeGroup(parsed, options = {}) {
177
207
  // Match the metadata shape used by the Demo3D adapter so generic viewers can
178
208
  // display stats without special-casing the property name.
179
209
  root.userData.demo3d = { kind: "raw3d-package", stats, warnings };
180
- return root;
210
+ const skinBindings = new Map();
211
+ for (const node of project.nodes) {
212
+ const owner = objects.get(node.index);
213
+ const drawable = owner.children.find(child => child instanceof three.Mesh);
214
+ if (drawable && node.meshIndex !== undefined && project.meshes[node.meshIndex]?.bones.length) {
215
+ const binding = bindRaw3DThreeSkin(drawable, node.meshIndex, project, objects, three);
216
+ skinBindings.set(binding.drawable, binding);
217
+ }
218
+ for (const child of owner.children)
219
+ if (["mesh", "lines", "points"].includes(child.userData.raw3d?.kind))
220
+ configureRaw3DDrawable(child, node.isBillboard, node.isDecal, three);
221
+ }
222
+ root.updateMatrixWorld(true);
223
+ for (const binding of skinBindings.values())
224
+ binding.update();
225
+ applyRaw3DThreeEffects(root, project.animation.effects.sample(0), project, three);
226
+ return { group: root, three, project, objects, materials, textures, geometryCache, defaultMaterial, skinBindings };
227
+ }
228
+ /** Optional Three.js binding. Animation data and interpolation live in the core. */
229
+ export async function createRaw3DThreePlayback(parsed, options = {}) {
230
+ const project = parsed instanceof Raw3DPackage ? parsed.model : parsed;
231
+ const animation = project.animation;
232
+ const resources = await buildRaw3DThreeGroup(parsed, options);
233
+ const { group, three, objects, materials, geometryCache, defaultMaterial, skinBindings } = resources;
234
+ let frame = animation.sampleFrame(0);
235
+ for (const warning of animation.warnings) {
236
+ const item = { code: warning.code, message: warning.message };
237
+ group.userData.raw3d.warnings.push(item);
238
+ options.onWarning?.(item);
239
+ }
240
+ const variants = new Map();
241
+ const archivedStreams = new Map([...resources.textures].filter(([key]) => key.startsWith("stream:")));
242
+ const streamRevisions = new Map();
243
+ const streamWarnings = new Set();
244
+ const ownedMaterials = new Set([...materials, defaultMaterial]);
245
+ const ownedTextures = new Set(resources.textures.values());
246
+ const capture = () => group.traverse(object => {
247
+ const drawable = object;
248
+ if (drawable.material)
249
+ for (const material of Array.isArray(drawable.material) ? drawable.material : [drawable.material]) {
250
+ ownedMaterials.add(material);
251
+ const map = material.map;
252
+ if (map)
253
+ ownedTextures.add(map);
254
+ }
255
+ });
256
+ capture();
257
+ let disposed = false;
258
+ const palette = (node, state, mesh) => {
259
+ const layerMaterial = state.layerIndex === undefined ? undefined : project.layers[state.layerIndex]?.materialIndex;
260
+ return Array.from({ length: Math.max(1, mesh.indexBufferIndices.length) }, (_, slot) => {
261
+ const index = node.materialIndices[slot] ?? node.materialIndices[0] ?? layerMaterial;
262
+ const base = (index === undefined ? defaultMaterial : materials[index] ?? defaultMaterial);
263
+ const values = state.materialProperties[slot] ?? {};
264
+ const streamIndex = values.StreamableTexture ?? (index === undefined ? -1 : project.materials[index]?.streamableTextureIndex ?? -1);
265
+ const ordinaryMap = values.Texture === undefined ? base.map : values.Texture < 0 ? null : resources.textures.get(`${values.Texture}:color`);
266
+ const streamMap = streamIndex < 0 ? undefined : resources.textures.get(`stream:${streamIndex}:color`);
267
+ const mapSource = streamMap ?? ordinaryMap;
268
+ if (streamIndex >= 0 && !streamMap && !streamWarnings.has(streamIndex)) {
269
+ streamWarnings.add(streamIndex);
270
+ const warning = { code: "RAW3D_STREAM_TEXTURE_UNAVAILABLE", message: `Streamable texture ${streamIndex} has no archived image or resolved external frame.` };
271
+ group.userData.raw3d.warnings.push(warning);
272
+ options.onWarning?.(warning);
273
+ }
274
+ const key = `${node.index}:${slot}:${index ?? -1}:${values.Texture ?? "base"}:${values.NormalMap ?? values.NormalTexture ?? "base"}:stream:${streamIndex}`;
275
+ let entry = variants.get(key);
276
+ if (!entry) {
277
+ const material = base.clone();
278
+ const normalIndex = values.NormalMap ?? values.NormalTexture;
279
+ const normalSource = normalIndex === undefined ? base.normalMap : normalIndex < 0 ? null : resources.textures.get(`${normalIndex}:normal`);
280
+ if ((values.Texture !== undefined && values.Texture >= 0 && !mapSource) || (normalIndex !== undefined && normalIndex >= 0 && !normalSource))
281
+ throw new Error("An animated RAW3D texture resource could not be loaded.");
282
+ entry = { base, material, map: mapSource?.clone(), normalMap: normalSource?.clone(), mapSource: mapSource ?? undefined };
283
+ variants.set(key, entry);
284
+ ownedMaterials.add(material);
285
+ if (entry.map)
286
+ ownedTextures.add(entry.map);
287
+ if (entry.normalMap)
288
+ ownedTextures.add(entry.normalMap);
289
+ }
290
+ if (entry.mapSource !== (mapSource ?? undefined)) {
291
+ if (entry.map) {
292
+ entry.map.dispose();
293
+ ownedTextures.delete(entry.map);
294
+ }
295
+ entry.map = mapSource?.clone();
296
+ entry.mapSource = mapSource ?? undefined;
297
+ if (entry.map)
298
+ ownedTextures.add(entry.map);
299
+ entry.material.needsUpdate = true;
300
+ }
301
+ if (streamIndex >= 0 && entry.streamRevision !== streamRevisions.get(streamIndex)) {
302
+ if (entry.map)
303
+ entry.map.needsUpdate = true;
304
+ entry.streamRevision = streamRevisions.get(streamIndex);
305
+ }
306
+ const material = entry.material;
307
+ const wasTransparent = material.transparent;
308
+ material.copy(base);
309
+ material.map = entry.map ?? null;
310
+ material.normalMap = entry.normalMap ?? null;
311
+ if (values.Diffuse !== undefined) {
312
+ material.color.setHex(values.Diffuse & 0xffffff, three.SRGBColorSpace);
313
+ material.opacity = (values.Diffuse >>> 24) / 255;
314
+ }
315
+ if (values.Transparency !== undefined)
316
+ material.opacity = 1 - values.Transparency;
317
+ if (values.Reflectivity !== undefined)
318
+ material.roughness = .9 - clamp01(values.Reflectivity) * .75;
319
+ if (values.Luminosity !== undefined && material.emissive)
320
+ material.emissive.copy(material.color).multiplyScalar(values.Luminosity);
321
+ if (values.NormalScale !== undefined && material.normalScale)
322
+ material.normalScale.setScalar(values.NormalScale);
323
+ if (values.NormalDepth !== undefined)
324
+ material.userData.raw3dNormalDepth = values.NormalDepth;
325
+ for (const texture of [material.map, material.normalMap]) {
326
+ if (!texture)
327
+ continue;
328
+ texture.offset.set(values.U ?? base.map?.offset.x ?? 0, values.V ?? base.map?.offset.y ?? 0);
329
+ texture.repeat.set(values.ScaleU ?? base.map?.repeat.x ?? 1, values.ScaleV ?? base.map?.repeat.y ?? 1);
330
+ if (values.U !== undefined)
331
+ texture.offset.x = values.U;
332
+ if (values.V !== undefined)
333
+ texture.offset.y = values.V;
334
+ if (values.ScaleU !== undefined)
335
+ texture.repeat.x = values.ScaleU;
336
+ if (values.ScaleV !== undefined)
337
+ texture.repeat.y = values.ScaleV;
338
+ if (values.U !== undefined || values.V !== undefined)
339
+ texture.wrapS = texture.wrapT = three.RepeatWrapping;
340
+ }
341
+ material.transparent = material.opacity < 1 || base.transparent;
342
+ material.depthWrite = base.depthWrite && !material.transparent;
343
+ if (wasTransparent !== material.transparent)
344
+ material.needsUpdate = true;
345
+ return material;
346
+ });
347
+ };
348
+ let seekRevision = 0, pendingFrames;
349
+ const apply = (time) => {
350
+ if (disposed)
351
+ throw new Error("RAW3D playback has been disposed.");
352
+ frame = animation.sampleFrame(time);
353
+ const states = frame.nodes;
354
+ // Detach first so valid hierarchy changes do not depend on source node order.
355
+ for (const object of objects.values())
356
+ group.add(object);
357
+ for (const node of project.nodes) {
358
+ const object = objects.get(node.index), state = states.get(node.index);
359
+ object.position.set(state.location[0], state.location[1], -state.location[2]);
360
+ object.quaternion.set(-state.quaternion[0], -state.quaternion[1], state.quaternion[2], state.quaternion[3]);
361
+ object.scale.set(...state.scale);
362
+ object.visible = !state.deleted;
363
+ object.userData.raw3d.layerIndex = state.layerIndex;
364
+ object.userData.raw3d.interactionMode = state.interactionMode;
365
+ object.userData.raw3d.highlight = state.highlight;
366
+ object.userData.raw3d.userData = state.userData;
367
+ object.userData.raw3d.clientOwner = state.clientOwner;
368
+ object.userData.raw3d.hasBeenOptimized = state.hasBeenOptimized;
369
+ const parent = state.parentIndex === undefined ? undefined : objects.get(state.parentIndex);
370
+ if (parent && parent !== object && !isDescendantOf(parent, object))
371
+ parent.add(object);
372
+ else if (state.parentIndex !== undefined)
373
+ throw new Error(`Invalid animated parent ${state.parentIndex} for RAW3D node ${node.index}.`);
374
+ let drawable = object.children.find(child => ["mesh", "lines", "points"].includes(child.userData.raw3d?.kind));
375
+ const mesh = state.meshIndex === undefined ? undefined : project.meshes[state.meshIndex];
376
+ if (state.meshIndex !== undefined && !mesh)
377
+ throw new Error(`Missing animated RAW3D mesh ${state.meshIndex}.`);
378
+ if (mesh && !object.userData.raw3d.annotationHidden) {
379
+ if (!geometryCache.has(state.meshIndex))
380
+ geometryCache.set(state.meshIndex, decodeRaw3DThreeGeometry(mesh, project, three));
381
+ if (!drawable || drawable.userData.raw3d.meshType !== mesh.meshType || (drawable.userData.raw3d.meshIndex !== state.meshIndex && (skinBindings.has(drawable) || mesh.bones.length > 0))) {
382
+ if (drawable) {
383
+ object.remove(drawable);
384
+ skinBindings.get(drawable)?.dispose();
385
+ skinBindings.delete(drawable);
386
+ }
387
+ drawable = createDrawable(mesh, geometryCache.get(state.meshIndex), [defaultMaterial], three);
388
+ drawable.userData.raw3d = { kind: primitiveKind(mesh.meshType), nodeIndex: node.index, meshType: mesh.meshType };
389
+ drawable.castShadow = node.castsShadow && drawable instanceof three.Mesh;
390
+ object.add(drawable);
391
+ if (mesh.bones.length && drawable instanceof three.Mesh) {
392
+ const binding = bindRaw3DThreeSkin(drawable, state.meshIndex, project, objects, three);
393
+ drawable = binding.drawable;
394
+ skinBindings.set(drawable, binding);
395
+ }
396
+ // The line/point factory creates topology-specific materials.
397
+ for (const material of Array.isArray(drawable.material) ? drawable.material : [drawable.material])
398
+ ownedMaterials.add(material);
399
+ }
400
+ if (!skinBindings.has(drawable))
401
+ drawable.geometry = geometryCache.get(state.meshIndex);
402
+ drawable.userData.raw3d.meshIndex = state.meshIndex;
403
+ const selected = palette(node, state, mesh);
404
+ if (drawable instanceof three.Mesh)
405
+ drawable.material = selected.length === 1 ? selected[0] : selected;
406
+ else {
407
+ const current = Array.isArray(drawable.material) ? drawable.material : [drawable.material];
408
+ // Keep topology-specific materials instead of assigning mesh shaders to lines/points.
409
+ const adjusted = selected.map((source, slot) => {
410
+ const material = current[slot] ?? (drawable instanceof three.Points ? toPointMaterial(source, three) : toLineMaterial(source, three));
411
+ ownedMaterials.add(material);
412
+ const target = material, original = source;
413
+ target.color.copy(original.color);
414
+ target.opacity = original.opacity;
415
+ target.transparent = original.transparent;
416
+ target.depthWrite = original.depthWrite;
417
+ target.needsUpdate = true;
418
+ if (drawable instanceof three.Points)
419
+ target.size = state.materialProperties[slot]?.PointSize ?? project.materials[node.materialIndices[slot] ?? node.materialIndices[0] ?? (state.layerIndex === undefined ? -1 : project.layers[state.layerIndex]?.materialIndex ?? -1)]?.pointSize ?? 1;
420
+ return material;
421
+ });
422
+ drawable.material = adjusted.length === 1 ? adjusted[0] : adjusted;
423
+ }
424
+ configureRaw3DDrawable(drawable, state.isBillboard, state.isDecal, three);
425
+ }
426
+ if (drawable) {
427
+ drawable.visible = state.visible && !!mesh && !object.userData.raw3d.annotationHidden;
428
+ drawable.userData.raw3d.sourceVisible = drawable.visible;
429
+ }
430
+ for (const child of object.children)
431
+ if (child.userData.raw3d?.kind === "text") {
432
+ child.visible = state.visible && !object.userData.raw3d.annotationHidden;
433
+ child.userData.raw3d.sourceVisible = child.visible;
434
+ }
435
+ }
436
+ // Retain only geometry currently used by nodes; decoded mesh samples are recreatable.
437
+ const used = new Set([...states.values()].map(state => state.meshIndex));
438
+ for (const [index, geometry] of geometryCache)
439
+ if (!used.has(index)) {
440
+ geometry.dispose();
441
+ geometryCache.delete(index);
442
+ }
443
+ group.updateMatrixWorld(true);
444
+ for (const binding of skinBindings.values())
445
+ binding.update();
446
+ applyRaw3DThreeEffects(group, frame, project, three);
447
+ };
448
+ const seek = (time) => { seekRevision++; pendingFrames?.abort(); apply(time); };
449
+ const seekAsync = async (time) => {
450
+ if (disposed)
451
+ throw new Error("RAW3D playback has been disposed.");
452
+ const next = animation.sampleFrame(time), revision = ++seekRevision;
453
+ pendingFrames?.abort();
454
+ const controller = new AbortController();
455
+ pendingFrames = controller;
456
+ const requested = new Set();
457
+ for (const node of project.nodes) {
458
+ const state = next.nodes.get(node.index);
459
+ const mesh = state.meshIndex === undefined ? undefined : project.meshes[state.meshIndex];
460
+ for (let slot = 0; slot < Math.max(1, mesh?.indexBufferIndices.length ?? 1); slot++) {
461
+ const materialIndex = node.materialIndices[slot] ?? node.materialIndices[0] ?? (state.layerIndex === undefined ? undefined : project.layers[state.layerIndex]?.materialIndex);
462
+ const stream = state.materialProperties[slot]?.StreamableTexture ?? (materialIndex === undefined ? -1 : project.materials[materialIndex]?.streamableTextureIndex ?? -1);
463
+ if (stream >= 0)
464
+ requested.add(stream);
465
+ }
466
+ }
467
+ try {
468
+ const resolved = options.resolveStreamTexture ? await Promise.all([...requested].map(async (index) => [index, await options.resolveStreamTexture({ index, time: Math.max(0, time), source: project.streamableTextures.find(source => source.index === index), signal: controller.signal })])) : [];
469
+ if (disposed || revision !== seekRevision)
470
+ return false;
471
+ for (const [index, texture] of resolved) {
472
+ if (texture !== undefined && !texture.isTexture)
473
+ throw new Error("A stream texture resolver must return a Three.js texture or undefined.");
474
+ const key = `stream:${index}:color`, selected = texture ?? archivedStreams.get(key);
475
+ if (selected)
476
+ resources.textures.set(key, selected);
477
+ else
478
+ resources.textures.delete(key);
479
+ streamRevisions.set(index, (streamRevisions.get(index) ?? 0) + 1);
480
+ }
481
+ apply(time);
482
+ return true;
483
+ }
484
+ catch (error) {
485
+ if (disposed || revision !== seekRevision)
486
+ return false;
487
+ throw error;
488
+ }
489
+ finally {
490
+ if (revision === seekRevision)
491
+ pendingFrames = undefined;
492
+ }
493
+ };
494
+ const dispose = () => {
495
+ if (disposed)
496
+ return;
497
+ disposed = true;
498
+ seekRevision++;
499
+ pendingFrames?.abort();
500
+ for (const binding of skinBindings.values())
501
+ binding.dispose();
502
+ skinBindings.clear();
503
+ for (const geometry of geometryCache.values())
504
+ geometry.dispose();
505
+ group.traverse(object => { if (object.userData.raw3d?.kind === "scene-floor")
506
+ object.geometry.dispose(); });
507
+ capture();
508
+ for (const material of ownedMaterials) {
509
+ const source = material;
510
+ if (source.map)
511
+ ownedTextures.add(source.map);
512
+ if (source.normalMap)
513
+ ownedTextures.add(source.normalMap);
514
+ }
515
+ for (const material of ownedMaterials)
516
+ material.dispose();
517
+ for (const texture of ownedTextures)
518
+ texture.dispose();
519
+ group.clear();
520
+ variants.clear();
521
+ geometryCache.clear();
522
+ };
523
+ try {
524
+ await seekAsync(0);
525
+ }
526
+ catch (error) {
527
+ dispose();
528
+ throw error;
529
+ }
530
+ return { group, animation, duration: animation.duration, get state() { return frame; }, seek, seekAsync, dispose };
181
531
  }
182
532
  export function decodeRaw3DThreeGeometry(mesh, project, three) {
183
533
  const vertexBuffer = project.vertexBuffers[mesh.vertexBufferIndex];
@@ -199,10 +549,25 @@ export function decodeRaw3DThreeGeometry(mesh, project, three) {
199
549
  for (let vertex = 0; vertex < vertexCount; vertex += 1) {
200
550
  const base = vertex * vertexBuffer.stride + sourceAttribute.offset;
201
551
  for (let component = 0; component < sourceAttribute.componentCount; component += 1) {
202
- let value = view.getFloat32(base + component * 4, true);
552
+ let value;
553
+ if (/^Color$/i.test(sourceAttribute.type)) {
554
+ const packed = view.getUint32(base, true);
555
+ value = ((packed >>> [16, 8, 0, 24][component]) & 255) / 255;
556
+ }
557
+ else if (/^Byte4$/i.test(sourceAttribute.type))
558
+ value = view.getUint8(base + component);
559
+ else if (/^(?:Normalized)?Short[24]$/i.test(sourceAttribute.type)) {
560
+ value = view.getInt16(base + component * 2, true);
561
+ if (/^Normalized/i.test(sourceAttribute.type))
562
+ value = Math.max(-1, value / 32767);
563
+ }
564
+ else
565
+ value = view.getFloat32(base + component * 4, true);
203
566
  if ((name === "position" || name === "normal" || name === "tangent") && component === 2) {
204
567
  value = -value;
205
568
  }
569
+ if (name === "tangent" && component === 3)
570
+ value = -value;
206
571
  values[vertex * sourceAttribute.componentCount + component] = value;
207
572
  }
208
573
  }
@@ -230,10 +595,12 @@ export function decodeRaw3DThreeGeometry(mesh, project, three) {
230
595
  ? indexView.getUint32(offset * 4, true)
231
596
  : indexView.getUint16(offset * 2, true);
232
597
  }
233
- for (let offset = 0; offset + 2 < indices.length; offset += 3) {
234
- const second = indices[offset + 1];
235
- indices[offset + 1] = indices[offset + 2];
236
- indices[offset + 2] = second;
598
+ if (isTriangleList(mesh.meshType)) {
599
+ for (let offset = 0; offset + 2 < indices.length; offset += 3) {
600
+ const second = indices[offset + 1];
601
+ indices[offset + 1] = indices[offset + 2];
602
+ indices[offset + 2] = second;
603
+ }
237
604
  }
238
605
  indexArrays.push(indices);
239
606
  totalIndexCount += indices.length;
@@ -249,13 +616,117 @@ export function decodeRaw3DThreeGeometry(mesh, project, three) {
249
616
  }
250
617
  geometry.setIndex(new three.BufferAttribute(combined, 1));
251
618
  }
252
- if (!geometry.getAttribute("normal")) {
619
+ if (isTriangleList(mesh.meshType) && !geometry.getAttribute("normal")) {
253
620
  geometry.computeVertexNormals();
254
621
  }
255
622
  geometry.computeBoundingBox();
256
623
  geometry.computeBoundingSphere();
257
624
  return geometry;
258
625
  }
626
+ function createDrawable(mesh, geometry, materials, three) {
627
+ const material = materials[0] ?? new three.MeshStandardMaterial({ color: 0x9aa0a6, roughness: 0.8 });
628
+ switch (mesh.meshType.toLowerCase()) {
629
+ case "linelist": {
630
+ const lineMaterials = materials.map((source) => toLineMaterial(source, three));
631
+ return new three.LineSegments(geometry, lineMaterials.length === 1 ? lineMaterials[0] : lineMaterials);
632
+ }
633
+ case "pointlist": {
634
+ const pointMaterials = materials.map((source) => toPointMaterial(source, three));
635
+ return new three.Points(geometry, pointMaterials.length === 1 ? pointMaterials[0] : pointMaterials);
636
+ }
637
+ default:
638
+ return new three.Mesh(geometry, materials.length === 1 ? material : [...materials]);
639
+ }
640
+ }
641
+ function toLineMaterial(material, three) {
642
+ const source = material;
643
+ return new three.LineBasicMaterial({
644
+ color: source.color,
645
+ opacity: source.opacity,
646
+ transparent: source.transparent,
647
+ depthWrite: source.depthWrite
648
+ });
649
+ }
650
+ function toPointMaterial(material, three) {
651
+ const source = material;
652
+ return new three.PointsMaterial({
653
+ color: source.color,
654
+ opacity: source.opacity,
655
+ transparent: source.transparent,
656
+ depthWrite: source.depthWrite,
657
+ size: source.userData.raw3dPointSize ?? 1,
658
+ vertexColors: /^(pointcloud|1)$/i.test(source.userData.raw3dShader ?? ""),
659
+ sizeAttenuation: true
660
+ });
661
+ }
662
+ function isTriangleList(meshType) {
663
+ return meshType.toLowerCase() === "trianglelist";
664
+ }
665
+ function primitiveKind(meshType) {
666
+ switch (meshType.toLowerCase()) {
667
+ case "linelist": return "lines";
668
+ case "pointlist": return "points";
669
+ default: return "mesh";
670
+ }
671
+ }
672
+ function isAnnotationNode(node, nodes) {
673
+ for (let current = node; current; current = current.parentIndex === undefined
674
+ ? undefined
675
+ : nodes.get(current.parentIndex)) {
676
+ if (/^(DrawingBlock|Dimension)/i.test(current.name)) {
677
+ return true;
678
+ }
679
+ }
680
+ return false;
681
+ }
682
+ function createTextSprite(text, sourceMaterial, three) {
683
+ if (typeof document === "undefined") {
684
+ return undefined;
685
+ }
686
+ const lines = text.value.replace(/\r\n?/g, "\n").split("\n");
687
+ const fontSize = 100;
688
+ const lineHeight = 120;
689
+ const padding = 20;
690
+ const canvas = document.createElement("canvas");
691
+ const context = canvas.getContext("2d");
692
+ if (!context) {
693
+ return undefined;
694
+ }
695
+ context.font = `${fontSize}px ${text.fontFamily || "Arial"}`;
696
+ const width = Math.max(1, ...lines.map((line) => context.measureText(line).width));
697
+ canvas.width = Math.ceil(width + padding * 2);
698
+ canvas.height = Math.max(1, lines.length * lineHeight + padding * 2);
699
+ const drawContext = canvas.getContext("2d");
700
+ if (!drawContext) {
701
+ return undefined;
702
+ }
703
+ drawContext.font = `${fontSize}px ${text.fontFamily || "Arial"}`;
704
+ drawContext.textAlign = "center";
705
+ drawContext.textBaseline = "middle";
706
+ const color = sourceMaterial
707
+ ? `rgb(${Math.round(clamp01(sourceMaterial.red) * 255)}, ${Math.round(clamp01(sourceMaterial.green) * 255)}, ${Math.round(clamp01(sourceMaterial.blue) * 255)})`
708
+ : "rgb(0, 0, 0)";
709
+ drawContext.fillStyle = color;
710
+ lines.forEach((line, index) => {
711
+ drawContext.fillText(line, canvas.width / 2, padding + lineHeight * (index + 0.5));
712
+ });
713
+ const opacity = sourceMaterial ? 1 - clamp01(sourceMaterial.transparency) : 1;
714
+ const texture = new three.CanvasTexture(canvas);
715
+ texture.colorSpace = three.SRGBColorSpace;
716
+ const sprite = new three.Sprite(new three.SpriteMaterial({
717
+ map: texture,
718
+ transparent: true,
719
+ opacity,
720
+ // Labels share planes with their dimension geometry. Draw them as a readable
721
+ // annotation layer instead of letting those lines occlude the canvas.
722
+ depthTest: false,
723
+ depthWrite: false
724
+ }));
725
+ sprite.renderOrder = 1;
726
+ const scale = Math.max(text.size, 0.001) / fontSize;
727
+ sprite.scale.set(canvas.width * scale, canvas.height * scale, 1);
728
+ return sprite;
729
+ }
259
730
  function resolveNodeMaterials(node, mesh, project, materials, fallback) {
260
731
  const layerMaterial = node.layerIndex === undefined
261
732
  ? undefined
@@ -270,6 +741,9 @@ function resolveNodeMaterials(node, mesh, project, materials, fallback) {
270
741
  }
271
742
  async function loadTextures(project, three, stats, warn) {
272
743
  const wanted = new Map();
744
+ for (const stream of project.streamableTextures)
745
+ if (stream.data)
746
+ wanted.set(`stream:${stream.index}:color`, { texture: stream, color: true });
273
747
  for (const material of project.materials) {
274
748
  if (material.textureIndex !== undefined && project.textures[material.textureIndex]) {
275
749
  wanted.set(`${material.textureIndex}:color`, { texture: project.textures[material.textureIndex], color: true });
@@ -278,6 +752,21 @@ async function loadTextures(project, three, stats, warn) {
278
752
  wanted.set(`${material.normalTextureIndex}:normal`, { texture: project.textures[material.normalTextureIndex], color: false });
279
753
  }
280
754
  }
755
+ for (const update of project.root.child("Updates")?.children ?? []) {
756
+ if (update.localName !== "Material")
757
+ continue;
758
+ const name = update.attributes.find(attribute => attribute.localName === "Name")?.value;
759
+ if (!["Texture", "NormalMap", "NormalTexture"].includes(name ?? ""))
760
+ continue;
761
+ const value = update.attributes.find(attribute => attribute.localName === "Value")?.value ?? "";
762
+ for (const token of value.trim().split(/\s+/)) {
763
+ const index = Number(token), texture = project.textures[index];
764
+ if (texture) {
765
+ const color = name === "Texture";
766
+ wanted.set(`${index}:${color ? "color" : "normal"}`, { texture, color });
767
+ }
768
+ }
769
+ }
281
770
  const loaded = new Map();
282
771
  await Promise.all([...wanted].map(async ([key, item]) => {
283
772
  if (!item.texture.data) {
@@ -321,10 +810,13 @@ function createMaterial(source, textures, three) {
321
810
  : textures.get(`${source.normalTextureIndex}:normal`);
322
811
  const map = sharedMap?.clone();
323
812
  const normalMap = sharedNormalMap?.clone();
324
- if (map && (source.scaleU !== 1 || source.scaleV !== 1)) {
325
- map.wrapS = three.RepeatWrapping;
326
- map.wrapT = three.RepeatWrapping;
327
- map.repeat.set(source.scaleU, source.scaleV);
813
+ for (const texture of [map, normalMap]) {
814
+ if (!texture)
815
+ continue;
816
+ if (source.scaleU !== 1 || source.scaleV !== 1 || source.offsetU !== 0 || source.offsetV !== 0)
817
+ texture.wrapS = texture.wrapT = three.RepeatWrapping;
818
+ texture.repeat.set(source.scaleU, source.scaleV);
819
+ texture.offset.set(source.offsetU, source.offsetV);
328
820
  }
329
821
  const parameters = {
330
822
  color: new three.Color(clamp01(source.red), clamp01(source.green), clamp01(source.blue)),
@@ -341,7 +833,26 @@ function createMaterial(source, textures, three) {
341
833
  parameters.normalMap = normalMap;
342
834
  parameters.normalScale = new three.Vector2(source.normalScale, source.normalScale);
343
835
  }
344
- return new three.MeshStandardMaterial(parameters);
836
+ let result;
837
+ switch (source.shader.toLowerCase()) {
838
+ case "flattext":
839
+ case "2":
840
+ result = new three.MeshBasicMaterial({ color: parameters.color, map: map ?? null, opacity, transparent: opacity < 1 || !!map, depthWrite: opacity >= 1, side: three.DoubleSide });
841
+ break;
842
+ case "default":
843
+ case "0":
844
+ case "pointcloud":
845
+ case "1":
846
+ result = raw3DNodeBackend(three)?.standard(parameters) ?? new three.MeshStandardMaterial(parameters);
847
+ break;
848
+ default: throw new Error(`Unsupported RAW3D material shader ${source.shader}.`);
849
+ }
850
+ result.userData.raw3dShader = source.shader;
851
+ result.userData.raw3dPointSize = source.pointSize;
852
+ result.userData.raw3dNormalDepth = source.normalDepth;
853
+ if ("emissive" in result)
854
+ result.emissive.copy(result.color).multiplyScalar(source.luminosity);
855
+ return result;
345
856
  }
346
857
  function applyTransform(object, node) {
347
858
  object.position.set(node.location[0] ?? 0, node.location[1] ?? 0, -(node.location[2] ?? 0));