@jfk-solutions/demo3d-file-format 1.6.0 → 1.8.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,7 +1,8 @@
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
3
  export { createRaw3DThreeGroup, createRaw3DThreeScene, decodeRaw3DThreeGeometry } from "./raw3d.js";
4
4
  export { createDemo3DThreeRenderer } from "./renderer.js";
5
+ export { applyDemo3DThreeRenderMode } from "./presentation.js";
5
6
  export async function createDemo3DThreeScene(parsed, options = {}) {
6
7
  const three = await resolveThree(options);
7
8
  const group = await createDemo3DThreeGroup(parsed, { ...options, three });
@@ -45,6 +46,38 @@ export async function createDemo3DThreeGroup(parsed, options = {}) {
45
46
  };
46
47
  return root;
47
48
  }
49
+ /** Dispose geometries, materials, and textures owned by a generated Demo3D or RAW3D group. */
50
+ export function disposeDemo3DThreeGroup(root) {
51
+ const geometries = new Set();
52
+ const materials = new Set();
53
+ const textures = new Set();
54
+ root.traverse((object) => {
55
+ const renderable = object;
56
+ if (renderable.geometry) {
57
+ geometries.add(renderable.geometry);
58
+ }
59
+ const objectMaterials = Array.isArray(renderable.material)
60
+ ? renderable.material
61
+ : renderable.material ? [renderable.material] : [];
62
+ for (const material of objectMaterials) {
63
+ materials.add(material);
64
+ for (const value of Object.values(material)) {
65
+ if (value?.isTexture) {
66
+ textures.add(value);
67
+ }
68
+ }
69
+ }
70
+ });
71
+ for (const geometry of geometries) {
72
+ geometry.dispose();
73
+ }
74
+ for (const material of materials) {
75
+ material.dispose();
76
+ }
77
+ for (const texture of textures) {
78
+ texture.dispose();
79
+ }
80
+ }
48
81
  export function decodeDemo3DThreeGeometry(mesh, three) {
49
82
  if (mesh.meshFormat !== "TriangleList") {
50
83
  throw new Error(`Unsupported Demo3D mesh format: ${mesh.meshFormat ?? "unknown"}`);
@@ -147,6 +180,7 @@ function createState(three, project, options, buffers) {
147
180
  options,
148
181
  warnings: [],
149
182
  meshById: new Map(project.meshes.flatMap((mesh) => (mesh.id ? [[mesh.id, mesh]] : []))),
183
+ pointCloudById: new Map(project.pointClouds.flatMap((pointCloud) => pointCloud.id ? [[pointCloud.id, pointCloud]] : [])),
150
184
  serializedObjectById: indexSerializedObjects(project),
151
185
  textureImageById: indexTextureImages(project),
152
186
  drawingBlockById: indexDrawingBlocks(project),
@@ -182,6 +216,7 @@ function createState(three, project, options, buffers) {
182
216
  unreconstructedProceduralVisuals: 0,
183
217
  imageVisuals: 0,
184
218
  lights: 0,
219
+ pointClouds: 0,
185
220
  missingGeometryPlaceholders: 0,
186
221
  serializedRenderables: 0,
187
222
  unsupported: 0
@@ -404,6 +439,9 @@ function createDirectVisualObject(visual, state, parentVisual) {
404
439
  if (visual.typeName === "e3d:ImportedImageVisual") {
405
440
  return createImportedImageObject(visual, state);
406
441
  }
442
+ if (visual.typeName === "e3d:PointCloudVisual") {
443
+ return createPointCloudObject(visual, state);
444
+ }
407
445
  if (visual.typeName === "e3d:StraightBeltConveyor" ||
408
446
  visual.typeName === "e3d:CurveBeltConveyor" ||
409
447
  visual.typeName === "e3d:InjectorBeltConveyor") {
@@ -446,6 +484,89 @@ function createDirectVisualObject(visual, state, parentVisual) {
446
484
  state.stats.directVisuals += 1;
447
485
  return object;
448
486
  }
487
+ function createPointCloudObject(visual, state) {
488
+ const referenceId = visual.properties?.textOf("PointCloudRef");
489
+ const pointCloud = referenceId ? state.pointCloudById.get(referenceId) : undefined;
490
+ if (!pointCloud) {
491
+ warn(state, {
492
+ code: "DEMO3D_THREE_MISSING_POINT_CLOUD",
493
+ message: `Point cloud reference ${referenceId ?? "<missing>"} could not be resolved.`,
494
+ sourceId: visual.id,
495
+ sourceType: visual.typeName,
496
+ xmlPath: visual.xml.path
497
+ });
498
+ return undefined;
499
+ }
500
+ const group = new state.three.Group();
501
+ group.name = visual.displayName ?? pointCloud.name ?? referenceId ?? visual.typeName;
502
+ const fallbackColor = primaryMaterial(visual)?.diffuse;
503
+ const pointSize = Math.min(128, Math.max(1, numberChild(visual.properties, "PointSize", 1)));
504
+ for (const [primitiveIndex, primitive] of pointCloud.primitives.entries()) {
505
+ const bufferName = primitive.pointsBufferName;
506
+ const buffer = bufferName ? state.bufferByName.get(bufferName.toLowerCase()) : undefined;
507
+ if (!buffer) {
508
+ warn(state, {
509
+ code: "DEMO3D_THREE_MISSING_POINT_CLOUD_BUFFER",
510
+ message: `Point cloud buffer ${bufferName ?? "<missing>"} could not be resolved.`,
511
+ sourceId: visual.id,
512
+ sourceType: visual.typeName,
513
+ xmlPath: primitive.xml.path
514
+ });
515
+ continue;
516
+ }
517
+ if (buffer.byteLength % 12 !== 0) {
518
+ warn(state, {
519
+ code: "DEMO3D_THREE_INVALID_POINT_CLOUD_BUFFER",
520
+ message: `Point cloud buffer ${bufferName} has ${buffer.byteLength} bytes; expected packed Float32 XYZ triples.`,
521
+ sourceId: visual.id,
522
+ sourceType: visual.typeName,
523
+ xmlPath: primitive.xml.path
524
+ });
525
+ continue;
526
+ }
527
+ const positions = new Float32Array(buffer.byteLength / 4);
528
+ const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
529
+ for (let index = 0; index < positions.length; index += 3) {
530
+ positions[index] = view.getFloat32(index * 4, true);
531
+ positions[index + 1] = view.getFloat32((index + 1) * 4, true);
532
+ positions[index + 2] = -view.getFloat32((index + 2) * 4, true);
533
+ }
534
+ const geometry = new state.three.BufferGeometry();
535
+ geometry.setAttribute("position", new state.three.BufferAttribute(positions, 3));
536
+ geometry.computeBoundingBox();
537
+ geometry.computeBoundingSphere();
538
+ const material = new state.three.PointsMaterial({
539
+ color: demo3dColorToHex(primitive.color ?? fallbackColor ?? -1),
540
+ size: pointSize,
541
+ sizeAttenuation: false
542
+ });
543
+ const points = new state.three.Points(geometry, material);
544
+ points.name = pointCloud.primitives.length > 1 ? `${group.name} ${primitiveIndex + 1}` : group.name;
545
+ points.userData.demo3d = {
546
+ kind: "point-cloud-primitive",
547
+ pointCloudId: pointCloud.id,
548
+ bufferName,
549
+ pointCount: positions.length / 3
550
+ };
551
+ group.add(points);
552
+ state.stats.geometries += 1;
553
+ state.stats.materials += 1;
554
+ }
555
+ if (group.children.length === 0) {
556
+ return undefined;
557
+ }
558
+ group.userData.demo3d = {
559
+ kind: "point-cloud",
560
+ id: visual.id,
561
+ pointCloudId: pointCloud.id,
562
+ typeName: visual.typeName,
563
+ xmlPath: visual.xml.path
564
+ };
565
+ state.stats.groups += 1;
566
+ state.stats.directVisuals += 1;
567
+ state.stats.pointClouds += 1;
568
+ return group;
569
+ }
449
570
  function createBeltConveyorObject(visual, state) {
450
571
  if (visual.typeName === "e3d:CurveBeltConveyor") {
451
572
  return createCurveBeltConveyorObject(visual, state);
@@ -629,17 +750,10 @@ function readProceduralBelt(visual, state) {
629
750
  if (!properties || isFalse(properties.textOf("CenterVisible"))) {
630
751
  return undefined;
631
752
  }
632
- const centerProfile = properties.child("CenterProfile");
633
- if (centerProfile && (centerProfile.children.length > 0 || centerProfile.text.length > 0)) {
634
- warn(state, {
635
- code: "DEMO3D_THREE_UNSUPPORTED_BELT_PROFILE",
636
- message: "Straight belt conveyors with a custom CenterProfile are not supported.",
637
- sourceId: visual.id,
638
- sourceType: visual.typeName,
639
- xmlPath: visual.xml.path
640
- });
641
- return undefined;
642
- }
753
+ const centerProfileElement = properties.child("CenterProfile");
754
+ const centerProfile = centerProfileElement && centerProfileElement.children.length > 0
755
+ ? new Demo3DExtrusionProfile(centerProfileElement)
756
+ : undefined;
643
757
  const startPadding = numberChild(properties, "StartPadding", 0);
644
758
  const endPadding = numberChild(properties, "EndPadding", 0);
645
759
  const leftPadding = numberChild(properties, "LeftPadding", 0);
@@ -682,6 +796,7 @@ function readProceduralBelt(visual, state) {
682
796
  zOffset: (rightPadding - leftPadding) / 2,
683
797
  startCap,
684
798
  endCap,
799
+ centerProfile,
685
800
  surfaceMaterial,
686
801
  sideMaterial
687
802
  };
@@ -703,21 +818,55 @@ function beltCap(value, visual, state) {
703
818
  return "Box";
704
819
  }
705
820
  function getProceduralBeltGeometry(belt, state) {
706
- const key = `belt:${belt.length}:${belt.width}:${belt.thickness}:${belt.endRadius}:${belt.xOffset}:${belt.zOffset}:${belt.startCap}:${belt.endCap}`;
821
+ const profileKey = belt.centerProfile?.polygons
822
+ .map((polygon) => polygon.points.map((point) => `${point.x},${point.y}`).join(";"))
823
+ .join("/") ?? "";
824
+ const key = `belt:${belt.length}:${belt.width}:${belt.thickness}:${belt.endRadius}:${belt.xOffset}:${belt.zOffset}:${belt.startCap}:${belt.endCap}:${profileKey}`;
707
825
  const cached = state.primitiveGeometryCache.get(key);
708
826
  if (cached) {
709
827
  return cached;
710
828
  }
711
- const geometry = belt.startCap === "Box" && belt.endCap === "Box"
712
- ? createRectangularBeltGeometry(belt, state)
713
- : createRoundedBeltGeometry(belt, state);
714
- convertTriangleGeometryToThreeCoordinates(geometry);
829
+ const profilePolygon = belt.centerProfile?.polygons.find((polygon) => polygon.points.length >= 3);
830
+ const geometry = belt.centerProfile && profilePolygon
831
+ ? createProfileBeltGeometry(belt, belt.centerProfile, profilePolygon, state)
832
+ : belt.startCap === "Box" && belt.endCap === "Box"
833
+ ? createRectangularBeltGeometry(belt, state)
834
+ : createRoundedBeltGeometry(belt, state);
835
+ if (!profilePolygon) {
836
+ convertTriangleGeometryToThreeCoordinates(geometry);
837
+ }
715
838
  geometry.computeBoundingBox();
716
839
  geometry.computeBoundingSphere();
717
840
  state.primitiveGeometryCache.set(key, geometry);
718
841
  state.stats.geometries += 1;
719
842
  return geometry;
720
843
  }
844
+ function createProfileBeltGeometry(belt, profile, polygon, state) {
845
+ const shape = new state.three.Shape();
846
+ polygon.points.forEach((point, index) => {
847
+ const lateral = -(point.x - profile.anchor.x);
848
+ const vertical = point.y - profile.anchor.y;
849
+ if (index === 0) {
850
+ shape.moveTo(lateral, vertical);
851
+ }
852
+ else {
853
+ shape.lineTo(lateral, vertical);
854
+ }
855
+ });
856
+ shape.closePath();
857
+ const geometry = new state.three.ExtrudeGeometry(shape, {
858
+ bevelEnabled: false,
859
+ curveSegments: 1,
860
+ depth: belt.length,
861
+ steps: 1
862
+ });
863
+ geometry.rotateY(Math.PI / 2);
864
+ geometry.translate(belt.xOffset, 0, belt.zOffset);
865
+ for (const group of geometry.groups) {
866
+ group.materialIndex = group.materialIndex === 0 ? 1 : 0;
867
+ }
868
+ return geometry;
869
+ }
721
870
  function createCurveBeltConveyorObject(visual, state) {
722
871
  const belt = readProceduralCurveBelt(visual, state);
723
872
  if (!belt) {
@@ -894,7 +1043,8 @@ function createRackObject(visual, state) {
894
1043
  }
895
1044
  const frameHeight = numberChild(properties, "FrameHeight", 0);
896
1045
  const uprightWidth = numberChild(properties, "UprightWidth", 0.05);
897
- const uprightDepth = numberChild(properties, "UprightDepth", 0.05);
1046
+ const serializedUprightDepth = numberChild(properties, "UprightDepth", 0.05);
1047
+ const uprightDepth = serializedUprightDepth > 0 ? serializedUprightDepth : uprightWidth;
898
1048
  const frameDepth = numberChildOptional(properties, "FrameDepth") ?? numberChild(properties, "BayDepth", 0);
899
1049
  const strutWidth = numberChild(properties, "StrutWidth", 0.05);
900
1050
  if (frameHeight <= 0 || frameDepth <= 0 || uprightWidth <= 0 || uprightDepth <= 0 || strutWidth <= 0) {
@@ -1644,13 +1794,24 @@ function getDirectVisualGeometry(visual, state) {
1644
1794
  else if (visual.typeName === "e3d:CylinderVisual") {
1645
1795
  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)));
1646
1796
  }
1797
+ else if (visual.typeName === "e3d:SphereVisual") {
1798
+ geometry = new state.three.SphereGeometry(numberChild(properties, "Radius", 0.5), 32, 16);
1799
+ }
1647
1800
  else if (visual.typeName === "e3d:WedgeVisual") {
1648
1801
  geometry = createWedgeGeometry(numberChild(properties, "Width", 1), numberChild(properties, "Height", 1), numberChild(properties, "Length", 1), state);
1649
1802
  }
1650
1803
  if (!geometry) {
1651
1804
  return undefined;
1652
1805
  }
1653
- convertTriangleGeometryToThreeCoordinates(geometry);
1806
+ // BoxGeometry and the box-tube extrusion are already created in Three's
1807
+ // coordinate system. Reflecting these symmetric, Three-native geometries
1808
+ // swaps their front/back UV layouts even though their bounds do not change.
1809
+ // Other direct primitives still need the Demo3D-to-Three Z reflection.
1810
+ if (visual.typeName !== "e3d:BoxVisual" &&
1811
+ visual.typeName !== "e3d:BoxTubeVisual" &&
1812
+ visual.typeName !== "e3d:SphereVisual") {
1813
+ convertTriangleGeometryToThreeCoordinates(geometry);
1814
+ }
1654
1815
  geometry.computeBoundingBox();
1655
1816
  geometry.computeBoundingSphere();
1656
1817
  state.primitiveGeometryCache.set(key, geometry);
@@ -1867,7 +2028,9 @@ function getPrimitiveGeometry(aspect, renderable, state) {
1867
2028
  if (!geometry) {
1868
2029
  return undefined;
1869
2030
  }
1870
- convertTriangleGeometryToThreeCoordinates(geometry);
2031
+ if (type !== "e3d:BoxRendererAspect" && type !== "e3d:ContainerRendererAspect") {
2032
+ convertTriangleGeometryToThreeCoordinates(geometry);
2033
+ }
1871
2034
  geometry.computeBoundingBox();
1872
2035
  geometry.computeBoundingSphere();
1873
2036
  state.primitiveGeometryCache.set(key, geometry);
@@ -2048,7 +2211,9 @@ function getTexture(textureReference, state) {
2048
2211
  texture.colorSpace = state.three.SRGBColorSpace;
2049
2212
  }
2050
2213
  texture.name = image.name ?? image.id;
2051
- texture.flipY = false;
2214
+ // Demo3D's generated primitive UVs use the conventional bottom-left texture
2215
+ // origin expected by Three's TextureLoader.
2216
+ texture.flipY = true;
2052
2217
  state.textureCache.set(textureReference, texture);
2053
2218
  state.stats.textures += 1;
2054
2219
  return texture;