@jfk-solutions/demo3d-file-format 1.5.0 → 1.7.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.
@@ -1,5 +1,6 @@
1
- import { Demo3DConveyorSideProperties, Demo3DDimensionAspect, Demo3DDimensionPoint, Demo3DExtrusionPolygon, Demo3DExtrusionProfile, Demo3DMaterial, Demo3DMesh, Demo3DPackage, Demo3DProject, Demo3DPhotoEye, Demo3DResource, Demo3DSupportStand, Demo3DVisual } from "../model.js";
1
+ import { Demo3DConveyorSideProperties, Demo3DDimensionAspect, Demo3DDimensionPoint, Demo3DExtrusionPolygon, Demo3DExtrusionProfile, Demo3DMaterial, Demo3DMesh, Demo3DPackage, Demo3DPointCloud, Demo3DProject, Demo3DPhotoEye, Demo3DResource, Demo3DSupportStand, Demo3DVisual } from "../model.js";
2
2
  import { Demo3DBinaryBlock, Demo3DXmlElement } from "../xml.js";
3
+ export { createRaw3DThreeGroup, createRaw3DThreeScene, decodeRaw3DThreeGeometry } from "./raw3d.js";
3
4
  export { createDemo3DThreeRenderer } from "./renderer.js";
4
5
  export async function createDemo3DThreeScene(parsed, options = {}) {
5
6
  const three = await resolveThree(options);
@@ -88,6 +89,7 @@ export function decodeDemo3DThreeGeometry(mesh, three) {
88
89
  if (mesh.indices) {
89
90
  geometry.setIndex(new three.BufferAttribute(readIndexBuffer(mesh.indices, mesh.indexFormat), 1));
90
91
  }
92
+ addMeshSubsetGroups(geometry, mesh);
91
93
  convertTriangleGeometryToThreeCoordinates(geometry);
92
94
  if (!normals) {
93
95
  geometry.computeVertexNormals();
@@ -96,6 +98,25 @@ export function decodeDemo3DThreeGeometry(mesh, three) {
96
98
  geometry.computeBoundingSphere();
97
99
  return geometry;
98
100
  }
101
+ function addMeshSubsetGroups(geometry, mesh) {
102
+ const subsets = mesh.auxiliary?.toUint8Array();
103
+ const drawCount = geometry.getIndex()?.count ?? geometry.getAttribute("position").count;
104
+ const faceCount = Math.floor(drawCount / 3);
105
+ if (!subsets || subsets.length !== faceCount || faceCount === 0) {
106
+ return;
107
+ }
108
+ let runStart = 0;
109
+ let materialIndex = subsets[0] ?? 0;
110
+ for (let face = 1; face <= faceCount; face += 1) {
111
+ const nextMaterialIndex = face < faceCount ? subsets[face] : undefined;
112
+ if (face < faceCount && nextMaterialIndex === materialIndex) {
113
+ continue;
114
+ }
115
+ geometry.addGroup(runStart * 3, (face - runStart) * 3, materialIndex);
116
+ runStart = face;
117
+ materialIndex = nextMaterialIndex ?? 0;
118
+ }
119
+ }
99
120
  export function createDemo3DThreeMaterial(material, three) {
100
121
  const diffuse = material?.diffuse;
101
122
  const color = diffuse === undefined ? 0x9aa0a6 : demo3dColorToHex(diffuse);
@@ -126,6 +147,7 @@ function createState(three, project, options, buffers) {
126
147
  options,
127
148
  warnings: [],
128
149
  meshById: new Map(project.meshes.flatMap((mesh) => (mesh.id ? [[mesh.id, mesh]] : []))),
150
+ pointCloudById: new Map(project.pointClouds.flatMap((pointCloud) => pointCloud.id ? [[pointCloud.id, pointCloud]] : [])),
129
151
  serializedObjectById: indexSerializedObjects(project),
130
152
  textureImageById: indexTextureImages(project),
131
153
  drawingBlockById: indexDrawingBlocks(project),
@@ -161,6 +183,7 @@ function createState(three, project, options, buffers) {
161
183
  unreconstructedProceduralVisuals: 0,
162
184
  imageVisuals: 0,
163
185
  lights: 0,
186
+ pointClouds: 0,
164
187
  missingGeometryPlaceholders: 0,
165
188
  serializedRenderables: 0,
166
189
  unsupported: 0
@@ -192,7 +215,7 @@ function createVisualObject(visual, state, inheritedLayer, parentVisual) {
192
215
  meshes.push(directVisualObject);
193
216
  }
194
217
  for (const ref of refs) {
195
- const mesh = createMeshObject(ref, visual, visual.materials[0], state);
218
+ const mesh = createMeshObject(ref, visual, visual.materials, state);
196
219
  if (mesh) {
197
220
  if (mesh.userData.demo3d?.kind === "missing-geometry-placeholder") {
198
221
  placeholderObjects.push(mesh);
@@ -281,11 +304,13 @@ function createAspectRenderableObjects(visual, state) {
281
304
  for (const renderable of renderables.children) {
282
305
  const meshIds = findMeshReferenceIds(renderable);
283
306
  for (const meshId of meshIds) {
284
- const material = firstMaterial(renderable) ?? visual.materials[0];
307
+ const serializedMaterials = renderableMaterials(renderable);
308
+ const materials = serializedMaterials.length > 0 ? serializedMaterials : visual.materials;
309
+ const primaryMaterial = materials[0];
285
310
  const mesh = state.meshById.has(meshId)
286
- ? createMeshObject(meshId, renderable, material, state)
287
- : createPrimitiveRenderableObject(aspect, renderable, meshId, material, state) ??
288
- createMeshObject(meshId, renderable, material, state);
311
+ ? createMeshObject(meshId, renderable, materials, state)
312
+ : createPrimitiveRenderableObject(aspect, renderable, meshId, primaryMaterial, state) ??
313
+ createMeshObject(meshId, renderable, materials, state);
289
314
  if (!mesh) {
290
315
  continue;
291
316
  }
@@ -381,6 +406,9 @@ function createDirectVisualObject(visual, state, parentVisual) {
381
406
  if (visual.typeName === "e3d:ImportedImageVisual") {
382
407
  return createImportedImageObject(visual, state);
383
408
  }
409
+ if (visual.typeName === "e3d:PointCloudVisual") {
410
+ return createPointCloudObject(visual, state);
411
+ }
384
412
  if (visual.typeName === "e3d:StraightBeltConveyor" ||
385
413
  visual.typeName === "e3d:CurveBeltConveyor" ||
386
414
  visual.typeName === "e3d:InjectorBeltConveyor") {
@@ -423,6 +451,89 @@ function createDirectVisualObject(visual, state, parentVisual) {
423
451
  state.stats.directVisuals += 1;
424
452
  return object;
425
453
  }
454
+ function createPointCloudObject(visual, state) {
455
+ const referenceId = visual.properties?.textOf("PointCloudRef");
456
+ const pointCloud = referenceId ? state.pointCloudById.get(referenceId) : undefined;
457
+ if (!pointCloud) {
458
+ warn(state, {
459
+ code: "DEMO3D_THREE_MISSING_POINT_CLOUD",
460
+ message: `Point cloud reference ${referenceId ?? "<missing>"} could not be resolved.`,
461
+ sourceId: visual.id,
462
+ sourceType: visual.typeName,
463
+ xmlPath: visual.xml.path
464
+ });
465
+ return undefined;
466
+ }
467
+ const group = new state.three.Group();
468
+ group.name = visual.displayName ?? pointCloud.name ?? referenceId ?? visual.typeName;
469
+ const fallbackColor = primaryMaterial(visual)?.diffuse;
470
+ const pointSize = Math.min(128, Math.max(1, numberChild(visual.properties, "PointSize", 1)));
471
+ for (const [primitiveIndex, primitive] of pointCloud.primitives.entries()) {
472
+ const bufferName = primitive.pointsBufferName;
473
+ const buffer = bufferName ? state.bufferByName.get(bufferName.toLowerCase()) : undefined;
474
+ if (!buffer) {
475
+ warn(state, {
476
+ code: "DEMO3D_THREE_MISSING_POINT_CLOUD_BUFFER",
477
+ message: `Point cloud buffer ${bufferName ?? "<missing>"} could not be resolved.`,
478
+ sourceId: visual.id,
479
+ sourceType: visual.typeName,
480
+ xmlPath: primitive.xml.path
481
+ });
482
+ continue;
483
+ }
484
+ if (buffer.byteLength % 12 !== 0) {
485
+ warn(state, {
486
+ code: "DEMO3D_THREE_INVALID_POINT_CLOUD_BUFFER",
487
+ message: `Point cloud buffer ${bufferName} has ${buffer.byteLength} bytes; expected packed Float32 XYZ triples.`,
488
+ sourceId: visual.id,
489
+ sourceType: visual.typeName,
490
+ xmlPath: primitive.xml.path
491
+ });
492
+ continue;
493
+ }
494
+ const positions = new Float32Array(buffer.byteLength / 4);
495
+ const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
496
+ for (let index = 0; index < positions.length; index += 3) {
497
+ positions[index] = view.getFloat32(index * 4, true);
498
+ positions[index + 1] = view.getFloat32((index + 1) * 4, true);
499
+ positions[index + 2] = -view.getFloat32((index + 2) * 4, true);
500
+ }
501
+ const geometry = new state.three.BufferGeometry();
502
+ geometry.setAttribute("position", new state.three.BufferAttribute(positions, 3));
503
+ geometry.computeBoundingBox();
504
+ geometry.computeBoundingSphere();
505
+ const material = new state.three.PointsMaterial({
506
+ color: demo3dColorToHex(primitive.color ?? fallbackColor ?? -1),
507
+ size: pointSize,
508
+ sizeAttenuation: false
509
+ });
510
+ const points = new state.three.Points(geometry, material);
511
+ points.name = pointCloud.primitives.length > 1 ? `${group.name} ${primitiveIndex + 1}` : group.name;
512
+ points.userData.demo3d = {
513
+ kind: "point-cloud-primitive",
514
+ pointCloudId: pointCloud.id,
515
+ bufferName,
516
+ pointCount: positions.length / 3
517
+ };
518
+ group.add(points);
519
+ state.stats.geometries += 1;
520
+ state.stats.materials += 1;
521
+ }
522
+ if (group.children.length === 0) {
523
+ return undefined;
524
+ }
525
+ group.userData.demo3d = {
526
+ kind: "point-cloud",
527
+ id: visual.id,
528
+ pointCloudId: pointCloud.id,
529
+ typeName: visual.typeName,
530
+ xmlPath: visual.xml.path
531
+ };
532
+ state.stats.groups += 1;
533
+ state.stats.directVisuals += 1;
534
+ state.stats.pointClouds += 1;
535
+ return group;
536
+ }
426
537
  function createBeltConveyorObject(visual, state) {
427
538
  if (visual.typeName === "e3d:CurveBeltConveyor") {
428
539
  return createCurveBeltConveyorObject(visual, state);
@@ -606,17 +717,10 @@ function readProceduralBelt(visual, state) {
606
717
  if (!properties || isFalse(properties.textOf("CenterVisible"))) {
607
718
  return undefined;
608
719
  }
609
- const centerProfile = properties.child("CenterProfile");
610
- if (centerProfile && (centerProfile.children.length > 0 || centerProfile.text.length > 0)) {
611
- warn(state, {
612
- code: "DEMO3D_THREE_UNSUPPORTED_BELT_PROFILE",
613
- message: "Straight belt conveyors with a custom CenterProfile are not supported.",
614
- sourceId: visual.id,
615
- sourceType: visual.typeName,
616
- xmlPath: visual.xml.path
617
- });
618
- return undefined;
619
- }
720
+ const centerProfileElement = properties.child("CenterProfile");
721
+ const centerProfile = centerProfileElement && centerProfileElement.children.length > 0
722
+ ? new Demo3DExtrusionProfile(centerProfileElement)
723
+ : undefined;
620
724
  const startPadding = numberChild(properties, "StartPadding", 0);
621
725
  const endPadding = numberChild(properties, "EndPadding", 0);
622
726
  const leftPadding = numberChild(properties, "LeftPadding", 0);
@@ -659,6 +763,7 @@ function readProceduralBelt(visual, state) {
659
763
  zOffset: (rightPadding - leftPadding) / 2,
660
764
  startCap,
661
765
  endCap,
766
+ centerProfile,
662
767
  surfaceMaterial,
663
768
  sideMaterial
664
769
  };
@@ -680,21 +785,55 @@ function beltCap(value, visual, state) {
680
785
  return "Box";
681
786
  }
682
787
  function getProceduralBeltGeometry(belt, state) {
683
- const key = `belt:${belt.length}:${belt.width}:${belt.thickness}:${belt.endRadius}:${belt.xOffset}:${belt.zOffset}:${belt.startCap}:${belt.endCap}`;
788
+ const profileKey = belt.centerProfile?.polygons
789
+ .map((polygon) => polygon.points.map((point) => `${point.x},${point.y}`).join(";"))
790
+ .join("/") ?? "";
791
+ const key = `belt:${belt.length}:${belt.width}:${belt.thickness}:${belt.endRadius}:${belt.xOffset}:${belt.zOffset}:${belt.startCap}:${belt.endCap}:${profileKey}`;
684
792
  const cached = state.primitiveGeometryCache.get(key);
685
793
  if (cached) {
686
794
  return cached;
687
795
  }
688
- const geometry = belt.startCap === "Box" && belt.endCap === "Box"
689
- ? createRectangularBeltGeometry(belt, state)
690
- : createRoundedBeltGeometry(belt, state);
691
- convertTriangleGeometryToThreeCoordinates(geometry);
796
+ const profilePolygon = belt.centerProfile?.polygons.find((polygon) => polygon.points.length >= 3);
797
+ const geometry = belt.centerProfile && profilePolygon
798
+ ? createProfileBeltGeometry(belt, belt.centerProfile, profilePolygon, state)
799
+ : belt.startCap === "Box" && belt.endCap === "Box"
800
+ ? createRectangularBeltGeometry(belt, state)
801
+ : createRoundedBeltGeometry(belt, state);
802
+ if (!profilePolygon) {
803
+ convertTriangleGeometryToThreeCoordinates(geometry);
804
+ }
692
805
  geometry.computeBoundingBox();
693
806
  geometry.computeBoundingSphere();
694
807
  state.primitiveGeometryCache.set(key, geometry);
695
808
  state.stats.geometries += 1;
696
809
  return geometry;
697
810
  }
811
+ function createProfileBeltGeometry(belt, profile, polygon, state) {
812
+ const shape = new state.three.Shape();
813
+ polygon.points.forEach((point, index) => {
814
+ const lateral = -(point.x - profile.anchor.x);
815
+ const vertical = point.y - profile.anchor.y;
816
+ if (index === 0) {
817
+ shape.moveTo(lateral, vertical);
818
+ }
819
+ else {
820
+ shape.lineTo(lateral, vertical);
821
+ }
822
+ });
823
+ shape.closePath();
824
+ const geometry = new state.three.ExtrudeGeometry(shape, {
825
+ bevelEnabled: false,
826
+ curveSegments: 1,
827
+ depth: belt.length,
828
+ steps: 1
829
+ });
830
+ geometry.rotateY(Math.PI / 2);
831
+ geometry.translate(belt.xOffset, 0, belt.zOffset);
832
+ for (const group of geometry.groups) {
833
+ group.materialIndex = group.materialIndex === 0 ? 1 : 0;
834
+ }
835
+ return geometry;
836
+ }
698
837
  function createCurveBeltConveyorObject(visual, state) {
699
838
  const belt = readProceduralCurveBelt(visual, state);
700
839
  if (!belt) {
@@ -871,7 +1010,8 @@ function createRackObject(visual, state) {
871
1010
  }
872
1011
  const frameHeight = numberChild(properties, "FrameHeight", 0);
873
1012
  const uprightWidth = numberChild(properties, "UprightWidth", 0.05);
874
- const uprightDepth = numberChild(properties, "UprightDepth", 0.05);
1013
+ const serializedUprightDepth = numberChild(properties, "UprightDepth", 0.05);
1014
+ const uprightDepth = serializedUprightDepth > 0 ? serializedUprightDepth : uprightWidth;
875
1015
  const frameDepth = numberChildOptional(properties, "FrameDepth") ?? numberChild(properties, "BayDepth", 0);
876
1016
  const strutWidth = numberChild(properties, "StrutWidth", 0.05);
877
1017
  if (frameHeight <= 0 || frameDepth <= 0 || uprightWidth <= 0 || uprightDepth <= 0 || strutWidth <= 0) {
@@ -1604,6 +1744,7 @@ function getDirectVisualGeometry(visual, state) {
1604
1744
  properties.textOf("Length") ?? "",
1605
1745
  properties.textOf("Radius") ?? "",
1606
1746
  properties.textOf("StartAngle") ?? "",
1747
+ properties.textOf("Thickness") ?? "",
1607
1748
  properties.textOf("Width") ?? ""
1608
1749
  ].join("|")}`;
1609
1750
  const cached = state.primitiveGeometryCache.get(key);
@@ -1611,25 +1752,70 @@ function getDirectVisualGeometry(visual, state) {
1611
1752
  return cached;
1612
1753
  }
1613
1754
  let geometry;
1614
- if (visual.typeName === "e3d:BoxVisual" || visual.typeName === "e3d:BoxTubeVisual") {
1755
+ if (visual.typeName === "e3d:BoxVisual") {
1615
1756
  geometry = new state.three.BoxGeometry(numberChild(properties, "Width", 1), numberChild(properties, "Height", 1), numberChild(properties, "Depth", 1));
1616
1757
  }
1758
+ else if (visual.typeName === "e3d:BoxTubeVisual") {
1759
+ geometry = createBoxTubeGeometry(numberChild(properties, "Width", 0.25), numberChild(properties, "Height", 1), numberChild(properties, "Depth", 0.25), numberChild(properties, "Thickness", 0.015), state);
1760
+ }
1617
1761
  else if (visual.typeName === "e3d:CylinderVisual") {
1618
1762
  geometry = new state.three.CylinderGeometry(numberChild(properties, "Radius", 0.5), numberChild(properties, "Radius", 0.5), numberChild(properties, "Length", 1), 32, 1, false, degreesToRadians(numberChild(properties, "StartAngle", 0)), degreesToRadians(numberChild(properties, "Angle", 360)));
1619
1763
  }
1764
+ else if (visual.typeName === "e3d:SphereVisual") {
1765
+ geometry = new state.three.SphereGeometry(numberChild(properties, "Radius", 0.5), 32, 16);
1766
+ }
1620
1767
  else if (visual.typeName === "e3d:WedgeVisual") {
1621
1768
  geometry = createWedgeGeometry(numberChild(properties, "Width", 1), numberChild(properties, "Height", 1), numberChild(properties, "Length", 1), state);
1622
1769
  }
1623
1770
  if (!geometry) {
1624
1771
  return undefined;
1625
1772
  }
1626
- convertTriangleGeometryToThreeCoordinates(geometry);
1773
+ // BoxGeometry and the box-tube extrusion are already created in Three's
1774
+ // coordinate system. Reflecting these symmetric, Three-native geometries
1775
+ // swaps their front/back UV layouts even though their bounds do not change.
1776
+ // Other direct primitives still need the Demo3D-to-Three Z reflection.
1777
+ if (visual.typeName !== "e3d:BoxVisual" &&
1778
+ visual.typeName !== "e3d:BoxTubeVisual" &&
1779
+ visual.typeName !== "e3d:SphereVisual") {
1780
+ convertTriangleGeometryToThreeCoordinates(geometry);
1781
+ }
1627
1782
  geometry.computeBoundingBox();
1628
1783
  geometry.computeBoundingSphere();
1629
1784
  state.primitiveGeometryCache.set(key, geometry);
1630
1785
  state.stats.geometries += 1;
1631
1786
  return geometry;
1632
1787
  }
1788
+ function createBoxTubeGeometry(width, height, depth, thickness, state) {
1789
+ const halfWidth = Math.max(width, 0) / 2;
1790
+ const halfDepth = Math.max(depth, 0) / 2;
1791
+ const edge = clamp(thickness, 0, Math.min(halfWidth, halfDepth));
1792
+ const shape = new state.three.Shape();
1793
+ shape.moveTo(-halfWidth, -halfDepth);
1794
+ shape.lineTo(-halfWidth, halfDepth);
1795
+ shape.lineTo(halfWidth, halfDepth);
1796
+ shape.lineTo(halfWidth, -halfDepth);
1797
+ shape.closePath();
1798
+ const innerHalfWidth = halfWidth - edge;
1799
+ const innerHalfDepth = halfDepth - edge;
1800
+ if (innerHalfWidth > 0 && innerHalfDepth > 0) {
1801
+ const hole = new state.three.Path();
1802
+ hole.moveTo(-innerHalfWidth, -innerHalfDepth);
1803
+ hole.lineTo(innerHalfWidth, -innerHalfDepth);
1804
+ hole.lineTo(innerHalfWidth, innerHalfDepth);
1805
+ hole.lineTo(-innerHalfWidth, innerHalfDepth);
1806
+ hole.closePath();
1807
+ shape.holes.push(hole);
1808
+ }
1809
+ const geometry = new state.three.ExtrudeGeometry(shape, {
1810
+ depth: Math.max(height, 0),
1811
+ steps: 1,
1812
+ bevelEnabled: false
1813
+ });
1814
+ // ExtrudeGeometry uses Z for its extrusion. Demo3D box tubes extrude along Y.
1815
+ geometry.rotateX(-Math.PI / 2);
1816
+ geometry.translate(0, -Math.max(height, 0) / 2, 0);
1817
+ return geometry;
1818
+ }
1633
1819
  function createWedgeGeometry(width, height, length, state) {
1634
1820
  const x = width / 2;
1635
1821
  const y = height / 2;
@@ -1696,8 +1882,12 @@ function getDrawingBlockGeometry(blockId, state) {
1696
1882
  state.stats.geometries += 1;
1697
1883
  return geometry;
1698
1884
  }
1699
- function createMeshObject(meshId, source, material, state) {
1885
+ function createMeshObject(meshId, source, materialSelection, state) {
1700
1886
  const mesh = state.meshById.get(meshId);
1887
+ const materials = Array.isArray(materialSelection)
1888
+ ? materialSelection
1889
+ : materialSelection ? [materialSelection] : [];
1890
+ const primaryMaterial = materials[0];
1701
1891
  const sourceId = source instanceof Demo3DVisual ? source.id : source.textOf("Id");
1702
1892
  const sourceType = source instanceof Demo3DVisual ? source.typeName : source.xsiType ?? source.localName;
1703
1893
  const xmlPath = source instanceof Demo3DVisual ? source.xml.path : source.path;
@@ -1710,20 +1900,24 @@ function createMeshObject(meshId, source, material, state) {
1710
1900
  xmlPath
1711
1901
  });
1712
1902
  return state.options.showPlaceholders === true
1713
- ? createPlaceholderMesh(sourceId ?? meshId, meshId, material, state)
1903
+ ? createPlaceholderMesh(sourceId ?? meshId, meshId, primaryMaterial, state)
1714
1904
  : undefined;
1715
1905
  }
1716
1906
  const geometry = getGeometry(mesh, state);
1717
1907
  if (!geometry) {
1718
1908
  return state.options.showPlaceholders === true
1719
- ? createPlaceholderMesh(sourceId ?? meshId, meshId, material, state)
1909
+ ? createPlaceholderMesh(sourceId ?? meshId, meshId, primaryMaterial, state)
1720
1910
  : undefined;
1721
1911
  }
1722
- const object = new state.three.Mesh(geometry, getMaterial(material, state));
1912
+ const threeMaterial = materials.length > 1
1913
+ ? materials.map((material) => getMaterial(material, state))
1914
+ : getMaterial(primaryMaterial, state);
1915
+ const object = new state.three.Mesh(geometry, threeMaterial);
1723
1916
  object.name = source instanceof Demo3DVisual ? source.displayName ?? meshId : meshId;
1724
1917
  object.userData.demo3d = {
1725
1918
  kind: "mesh",
1726
1919
  meshId,
1920
+ materialSlots: materials.length,
1727
1921
  sourceId,
1728
1922
  sourceType,
1729
1923
  xmlPath
@@ -1801,7 +1995,9 @@ function getPrimitiveGeometry(aspect, renderable, state) {
1801
1995
  if (!geometry) {
1802
1996
  return undefined;
1803
1997
  }
1804
- convertTriangleGeometryToThreeCoordinates(geometry);
1998
+ if (type !== "e3d:BoxRendererAspect" && type !== "e3d:ContainerRendererAspect") {
1999
+ convertTriangleGeometryToThreeCoordinates(geometry);
2000
+ }
1805
2001
  geometry.computeBoundingBox();
1806
2002
  geometry.computeBoundingSphere();
1807
2003
  state.primitiveGeometryCache.set(key, geometry);
@@ -1982,7 +2178,9 @@ function getTexture(textureReference, state) {
1982
2178
  texture.colorSpace = state.three.SRGBColorSpace;
1983
2179
  }
1984
2180
  texture.name = image.name ?? image.id;
1985
- texture.flipY = false;
2181
+ // Demo3D's generated primitive UVs use the conventional bottom-left texture
2182
+ // origin expected by Three's TextureLoader.
2183
+ texture.flipY = true;
1986
2184
  state.textureCache.set(textureReference, texture);
1987
2185
  state.stats.textures += 1;
1988
2186
  return texture;
@@ -2217,8 +2415,7 @@ function createSerializedRenderableGroup(state) {
2217
2415
  }
2218
2416
  const meshIds = findMeshReferenceIds(renderable);
2219
2417
  for (const meshId of meshIds) {
2220
- const material = firstMaterial(renderable);
2221
- const mesh = createMeshObject(meshId, renderable, material, state);
2418
+ const mesh = createMeshObject(meshId, renderable, renderableMaterials(renderable), state);
2222
2419
  if (!mesh) {
2223
2420
  continue;
2224
2421
  }
@@ -2418,6 +2615,23 @@ function findVisualMeshReferenceIds(root) {
2418
2615
  }
2419
2616
  return [...ids];
2420
2617
  }
2618
+ function renderableMaterials(renderable) {
2619
+ const materials = [];
2620
+ const materialProperties = renderable.child("MaterialProperties");
2621
+ for (const entry of materialProperties?.children ?? []) {
2622
+ const material = firstMaterial(entry);
2623
+ if (material) {
2624
+ materials.push(material);
2625
+ }
2626
+ }
2627
+ if (materials.length === 0) {
2628
+ const material = firstMaterial(renderable);
2629
+ if (material) {
2630
+ materials.push(material);
2631
+ }
2632
+ }
2633
+ return materials;
2634
+ }
2421
2635
  function firstMaterial(root) {
2422
2636
  let found;
2423
2637
  visit(root, (node) => {
@@ -2501,7 +2715,9 @@ function applyDemo3DTransform(object, transformText) {
2501
2715
  return;
2502
2716
  }
2503
2717
  object.position.set(values[0] ?? 0, values[1] ?? 0, -(values[2] ?? 0));
2504
- object.rotation.set(-(values[3] ?? 0), -(values[4] ?? 0), values[5] ?? 0);
2718
+ // Demo3D rotation triples are pitch (X), yaw (Y), and roll (Z). Its
2719
+ // RotationYawPitchRoll matrix corresponds to Three's intrinsic YXZ order.
2720
+ object.rotation.set(-(values[3] ?? 0), -(values[4] ?? 0), values[5] ?? 0, "YXZ");
2505
2721
  }
2506
2722
  function convertTriangleGeometryToThreeCoordinates(geometry) {
2507
2723
  geometry.scale(1, 1, -1);