@jfk-solutions/demo3d-file-format 1.0.0 → 1.2.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,4 +1,4 @@
1
- import { Demo3DMaterial, Demo3DMesh, Demo3DPackage, Demo3DProject, Demo3DResource, Demo3DVisual } from "../model.js";
1
+ import { Demo3DConveyorSideProperties, Demo3DDimensionAspect, Demo3DDimensionPoint, Demo3DExtrusionPolygon, Demo3DExtrusionProfile, Demo3DMaterial, Demo3DMesh, Demo3DPackage, Demo3DProject, Demo3DPhotoEye, Demo3DResource, Demo3DSupportStand, Demo3DVisual } from "../model.js";
2
2
  import { Demo3DBinaryBlock, Demo3DXmlElement } from "../xml.js";
3
3
  export { createDemo3DThreeRenderer } from "./renderer.js";
4
4
  export async function createDemo3DThreeScene(parsed, options = {}) {
@@ -24,6 +24,13 @@ export async function createDemo3DThreeGroup(parsed, options = {}) {
24
24
  for (const visual of project.visuals) {
25
25
  root.add(createVisualObject(visual, state));
26
26
  }
27
+ if (options.renderDimensions === true) {
28
+ root.updateWorldMatrix(true, true);
29
+ const dimensions = createDimensionGroup(state);
30
+ if (dimensions.children.length > 0) {
31
+ root.add(dimensions);
32
+ }
33
+ }
27
34
  if (options.includeSerializedRenderables === true) {
28
35
  const serialized = createSerializedRenderableGroup(state);
29
36
  if (serialized.children.length > 0) {
@@ -92,12 +99,14 @@ export function decodeDemo3DThreeGeometry(mesh, three) {
92
99
  export function createDemo3DThreeMaterial(material, three) {
93
100
  const diffuse = material?.diffuse;
94
101
  const color = diffuse === undefined ? 0x9aa0a6 : demo3dColorToHex(diffuse);
95
- const opacity = diffuse === undefined ? 1 : demo3dColorToOpacity(diffuse);
102
+ const opacity = demo3dMaterialOpacity(material);
103
+ const reflectivity = clamp01(material?.reflectivity ?? 0);
96
104
  return new three.MeshStandardMaterial({
97
105
  color,
98
106
  opacity,
99
107
  transparent: opacity < 1,
100
- roughness: 0.72,
108
+ depthWrite: opacity >= 1,
109
+ roughness: 0.9 - reflectivity * 0.75,
101
110
  metalness: 0.05
102
111
  });
103
112
  }
@@ -126,6 +135,9 @@ function createState(three, project, options, buffers) {
126
135
  lineGeometryCache: new Map(),
127
136
  materialCache: new Map(),
128
137
  textureCache: new Map(),
138
+ visualObjectById: new Map(),
139
+ renderableObjectTrees: new WeakSet(),
140
+ warnedGeneratedTypes: new Set(),
129
141
  defaultMaterial: createDemo3DThreeMaterial(undefined, three),
130
142
  stats: {
131
143
  groups: 0,
@@ -138,6 +150,13 @@ function createState(three, project, options, buffers) {
138
150
  lines: 0,
139
151
  directVisuals: 0,
140
152
  proceduralBelts: 0,
153
+ proceduralSupportStands: 0,
154
+ proceduralConveyorSides: 0,
155
+ proceduralPhotoEyes: 0,
156
+ proceduralRollers: 0,
157
+ proceduralMotors: 0,
158
+ dimensions: 0,
159
+ unreconstructedProceduralVisuals: 0,
141
160
  imageVisuals: 0,
142
161
  lights: 0,
143
162
  missingGeometryPlaceholders: 0,
@@ -146,7 +165,7 @@ function createState(three, project, options, buffers) {
146
165
  }
147
166
  };
148
167
  }
149
- function createVisualObject(visual, state, inheritedLayer) {
168
+ function createVisualObject(visual, state, inheritedLayer, parentVisual) {
150
169
  const refs = findMeshReferenceIds(visual.xml);
151
170
  const meshes = [];
152
171
  const textObjects = [];
@@ -163,7 +182,7 @@ function createVisualObject(visual, state, inheritedLayer) {
163
182
  drawingBlockObjects.push(drawingBlockObject);
164
183
  meshes.push(drawingBlockObject);
165
184
  }
166
- const directVisualObject = createDirectVisualObject(visual, state);
185
+ const directVisualObject = createDirectVisualObject(visual, state, parentVisual);
167
186
  if (directVisualObject) {
168
187
  directVisualObjects.push(directVisualObject);
169
188
  meshes.push(directVisualObject);
@@ -211,11 +230,41 @@ function createVisualObject(visual, state, inheritedLayer) {
211
230
  localTransformText: visual.xml.textOf("LR"),
212
231
  layer
213
232
  };
233
+ if (visual.id) {
234
+ state.visualObjectById.set(visual.id, object);
235
+ }
236
+ let containsRenderable = meshes.length > 0;
214
237
  for (const child of visual.children) {
215
- object.add(createVisualObject(child, state, layer));
238
+ const childObject = createVisualObject(child, state, layer, visual);
239
+ object.add(childObject);
240
+ containsRenderable ||= state.renderableObjectTrees.has(childObject);
241
+ }
242
+ if (containsRenderable) {
243
+ state.renderableObjectTrees.add(object);
216
244
  }
245
+ reportUnreconstructedProceduralVisual(visual, containsRenderable, state);
217
246
  return object;
218
247
  }
248
+ function reportUnreconstructedProceduralVisual(visual, containsRenderable, state) {
249
+ if (state.options.includeUnsupported !== true ||
250
+ !visual.properties?.xsiType?.endsWith("ScriptProperties") ||
251
+ visual.typeName === "e3d:PhotoEye" ||
252
+ containsRenderable) {
253
+ return;
254
+ }
255
+ state.stats.unreconstructedProceduralVisuals += 1;
256
+ if (state.warnedGeneratedTypes.has(visual.typeName)) {
257
+ return;
258
+ }
259
+ state.warnedGeneratedTypes.add(visual.typeName);
260
+ warn(state, {
261
+ code: "DEMO3D_THREE_UNRECONSTRUCTABLE_SCRIPT_VISUAL",
262
+ message: `${visual.typeName} has script properties but no serialized geometry or dimensions to reconstruct.`,
263
+ sourceId: visual.id,
264
+ sourceType: visual.typeName,
265
+ xmlPath: visual.xml.path
266
+ });
267
+ }
219
268
  function createAspectRenderableObjects(visual, state) {
220
269
  const objects = [];
221
270
  for (const aspect of findVisualAspects(visual, state)) {
@@ -316,7 +365,7 @@ function createDrawingBlockVisualObject(visual, state) {
316
365
  state.stats.lines += geometry.getAttribute("position").count / 2;
317
366
  return object;
318
367
  }
319
- function createDirectVisualObject(visual, state) {
368
+ function createDirectVisualObject(visual, state, parentVisual) {
320
369
  if (!isRenderableVisualVisible(visual)) {
321
370
  return undefined;
322
371
  }
@@ -331,6 +380,19 @@ function createDirectVisualObject(visual, state) {
331
380
  ? createStraightBeltConveyorObject(visual, state)
332
381
  : undefined;
333
382
  }
383
+ if (visual.typeName === "e3d:SupportStand") {
384
+ return state.options.renderProceduralSupportStands === true
385
+ ? createSupportStandObject(visual, parentVisual, state)
386
+ : undefined;
387
+ }
388
+ if (visual.typeName === "e3d:PhotoEye") {
389
+ return state.options.renderProceduralPhotoEyes === true
390
+ ? createPhotoEyeObject(visual, parentVisual, state)
391
+ : undefined;
392
+ }
393
+ if (isRollerConveyorType(visual.typeName)) {
394
+ return createProceduralRollerConveyorObject(visual, state);
395
+ }
334
396
  const geometry = getDirectVisualGeometry(visual, state);
335
397
  if (!geometry) {
336
398
  return undefined;
@@ -515,6 +577,476 @@ function createRoundedBeltGeometry(belt, state) {
515
577
  }
516
578
  return geometry;
517
579
  }
580
+ function createSupportStandObject(visual, parentVisual, state) {
581
+ const stand = visual instanceof Demo3DSupportStand
582
+ ? visual
583
+ : new Demo3DSupportStand(visual.typeName, visual.xml);
584
+ const properties = stand.supportProperties;
585
+ const legProfile = properties?.legProfile;
586
+ const footProfile = properties?.footProfile;
587
+ const floorPlateProfile = properties?.floorPlateProfile;
588
+ const crossBraceProfile = properties?.crossBraceProfile;
589
+ if (!properties || !legProfile || !crossBraceProfile) {
590
+ warn(state, {
591
+ code: "DEMO3D_THREE_UNSUPPORTED_SUPPORT_STAND_PROFILE",
592
+ message: "Support stand is missing its leg or cross-brace extrusion profile.",
593
+ sourceId: visual.id,
594
+ sourceType: visual.typeName,
595
+ xmlPath: visual.xml.path
596
+ });
597
+ return undefined;
598
+ }
599
+ const braces = [...properties.crossBraceHeights]
600
+ .filter((height) => height > 0)
601
+ .sort((left, right) => left - right);
602
+ const explicitHeight = numberChildOptional(visual.properties, "SupportHeight") ??
603
+ numberChildOptional(visual.properties, "Height") ??
604
+ numberChildOptional(visual.properties, "FloorHeight");
605
+ const finalSpacing = braces.length > 1
606
+ ? braces[braces.length - 1] - braces[braces.length - 2]
607
+ : braces[0];
608
+ const supportHeight = explicitHeight ?? (braces.length > 0
609
+ ? braces[braces.length - 1] + Math.max(finalSpacing ?? 0, 0.05)
610
+ : 1);
611
+ const parentWidth = numberChildOptional(parentVisual?.properties, "RollerWidth") ??
612
+ numberChildOptional(parentVisual?.properties, "BeltWidth") ??
613
+ numberChildOptional(parentVisual?.properties, "Width") ??
614
+ 0.5;
615
+ const conveyorOffset = properties.conveyorOffset;
616
+ const sideExtension = Math.abs(conveyorOffset[2] ?? 0);
617
+ const span = Math.max(parentWidth + sideExtension * 2, 0.1);
618
+ const top = conveyorOffset[1] ?? 0;
619
+ const x = conveyorOffset[0] ?? 0;
620
+ const bottom = top - supportHeight;
621
+ const floorPlateHeight = clamp(properties.floorPlateHeight, 0, supportHeight);
622
+ const footHeight = clamp(properties.footHeight, 0, supportHeight - floorPlateHeight);
623
+ const legHeight = Math.max(supportHeight - floorPlateHeight - footHeight, 0.001);
624
+ const group = new state.three.Group();
625
+ group.name = `${visual.displayName ?? visual.id ?? "Support stand"} geometry`;
626
+ for (const z of [-span / 2, span / 2]) {
627
+ addSupportProfileMeshes(group, legProfile, "vertical", legHeight, [x, top, z], properties.legMaterial, "leg", state);
628
+ if (footProfile && footHeight > 0) {
629
+ addSupportProfileMeshes(group, footProfile, "vertical", footHeight, [x, bottom + floorPlateHeight + footHeight, z], properties.footMaterial, "foot", state);
630
+ }
631
+ if (floorPlateProfile && floorPlateHeight > 0) {
632
+ addSupportProfileMeshes(group, floorPlateProfile, "vertical", floorPlateHeight, [x, bottom + floorPlateHeight, z], properties.floorPlateMaterial, "floor plate", state);
633
+ }
634
+ }
635
+ const braceHeights = [...braces, supportHeight];
636
+ for (const height of braceHeights) {
637
+ addSupportProfileMeshes(group, crossBraceProfile, "horizontal", span, [x, bottom + height, 0], properties.crossBraceMaterial, height === supportHeight ? "top brace" : "cross brace", state);
638
+ }
639
+ if (group.children.length === 0) {
640
+ return undefined;
641
+ }
642
+ group.userData.demo3d = {
643
+ kind: "procedural-support-stand",
644
+ id: visual.id,
645
+ name: visual.displayName,
646
+ typeName: visual.typeName,
647
+ xmlPath: visual.xml.path,
648
+ supportHeight,
649
+ span,
650
+ braceHeights: braces
651
+ };
652
+ state.stats.groups += 1;
653
+ state.stats.directVisuals += 1;
654
+ state.stats.proceduralSupportStands += 1;
655
+ return group;
656
+ }
657
+ function addSupportProfileMeshes(group, profile, axis, length, position, fallbackColor, component, state) {
658
+ profile.polygons.forEach((polygon, index) => {
659
+ if (polygon.points.length < 3) {
660
+ return;
661
+ }
662
+ const geometry = getSupportProfileGeometry(profile, polygon, axis, length, state);
663
+ const material = polygon.materials[0]
664
+ ? getMaterial(polygon.materials[0], state)
665
+ : getColorMaterial(fallbackColor, state);
666
+ const mesh = new state.three.Mesh(geometry, material);
667
+ mesh.name = `${profile.name || component} ${index + 1}`;
668
+ mesh.position.set(position[0], position[1], position[2]);
669
+ mesh.userData.demo3d = { kind: "support-stand-component", component };
670
+ group.add(mesh);
671
+ state.stats.meshes += 1;
672
+ });
673
+ }
674
+ function getSupportProfileGeometry(profile, polygon, axis, length, state) {
675
+ const pointsKey = polygon.points.map((point) => `${point.x},${point.y}`).join(";");
676
+ const key = `support:${axis}:${length}:${profile.anchor.x},${profile.anchor.y}:${pointsKey}`;
677
+ const cached = state.primitiveGeometryCache.get(key);
678
+ if (cached) {
679
+ return cached;
680
+ }
681
+ const shape = new state.three.Shape();
682
+ polygon.points.forEach((point, index) => {
683
+ const x = point.x - profile.anchor.x;
684
+ const y = axis === "vertical"
685
+ ? -(point.y - profile.anchor.y)
686
+ : point.y - profile.anchor.y;
687
+ if (index === 0) {
688
+ shape.moveTo(x, y);
689
+ }
690
+ else {
691
+ shape.lineTo(x, y);
692
+ }
693
+ });
694
+ shape.closePath();
695
+ const geometry = new state.three.ExtrudeGeometry(shape, {
696
+ bevelEnabled: false,
697
+ curveSegments: 1,
698
+ depth: length,
699
+ steps: 1
700
+ });
701
+ if (axis === "vertical") {
702
+ geometry.rotateX(Math.PI / 2);
703
+ }
704
+ else {
705
+ geometry.translate(0, 0, -length / 2);
706
+ }
707
+ geometry.computeBoundingBox();
708
+ geometry.computeBoundingSphere();
709
+ state.primitiveGeometryCache.set(key, geometry);
710
+ state.stats.geometries += 1;
711
+ return geometry;
712
+ }
713
+ function createPhotoEyeObject(visual, parentVisual, state) {
714
+ const photoEye = visual instanceof Demo3DPhotoEye
715
+ ? visual
716
+ : new Demo3DPhotoEye(visual.typeName, visual.xml);
717
+ const properties = photoEye.photoEyeProperties;
718
+ if (!properties) {
719
+ return undefined;
720
+ }
721
+ const parentWidth = numberChildOptional(parentVisual?.properties, "RollerWidth") ??
722
+ numberChildOptional(parentVisual?.properties, "BeltWidth") ??
723
+ numberChildOptional(parentVisual?.properties, "Width") ??
724
+ 0.5;
725
+ const sensorAspect = findVisualAspects(visual, state)
726
+ .find((aspect) => aspect.xsiType === "e3d:SensorSymbolAspect");
727
+ const symbolSide = sensorAspect?.textOf("SymbolSide")?.toLowerCase() ?? "right";
728
+ const side = symbolSide.includes("left") ? -1 : 1;
729
+ const beamThickness = Math.max(parentWidth / 150, 0.003);
730
+ const bodySize = Math.max(parentWidth / 16, 0.025);
731
+ const group = new state.three.Group();
732
+ group.name = `${visual.displayName ?? visual.id ?? "Photo eye"} geometry`;
733
+ const beamGeometry = cachedBoxGeometry(beamThickness, beamThickness, parentWidth, state);
734
+ const beam = new state.three.Mesh(beamGeometry, getColorMaterial(properties.clearedMaterial, state));
735
+ beam.name = "Photo eye beam";
736
+ beam.position.y = properties.beamHeight;
737
+ beam.rotation.x = degreesToRadians(properties.beamAngle);
738
+ beam.userData.demo3d = { kind: "photo-eye-beam" };
739
+ group.add(beam);
740
+ const bodyGeometry = cachedBoxGeometry(bodySize, bodySize, bodySize * 0.75, state);
741
+ const body = new state.three.Mesh(bodyGeometry, getColorMaterial(properties.boxMaterial, state));
742
+ body.name = "Photo eye body";
743
+ body.position.set(0, properties.beamHeight, side * (parentWidth / 2 + bodySize * 0.4));
744
+ body.userData.demo3d = { kind: "photo-eye-body", symbolSide };
745
+ group.add(body);
746
+ group.userData.demo3d = {
747
+ kind: "procedural-photo-eye",
748
+ id: visual.id,
749
+ name: visual.displayName,
750
+ typeName: visual.typeName,
751
+ xmlPath: visual.xml.path,
752
+ beamHeight: properties.beamHeight,
753
+ symbolSide
754
+ };
755
+ state.stats.groups += 1;
756
+ state.stats.meshes += 2;
757
+ state.stats.directVisuals += 1;
758
+ state.stats.proceduralPhotoEyes += 1;
759
+ return group;
760
+ }
761
+ function createProceduralRollerConveyorObject(visual, state) {
762
+ if (state.options.renderProceduralConveyorSides !== true &&
763
+ state.options.renderProceduralRollers !== true &&
764
+ state.options.renderProceduralMotors !== true) {
765
+ return undefined;
766
+ }
767
+ const properties = visual.properties;
768
+ if (!properties) {
769
+ return undefined;
770
+ }
771
+ const group = new state.three.Group();
772
+ group.name = `${visual.displayName ?? visual.id ?? "Roller conveyor"} generated geometry`;
773
+ if (state.options.renderProceduralConveyorSides === true) {
774
+ addConveyorSide(group, visual, properties.child("LeftSide"), "left", state);
775
+ addConveyorSide(group, visual, properties.child("RightSide"), "right", state);
776
+ }
777
+ if (state.options.renderProceduralRollers === true &&
778
+ !hasSerializedRollerGeometry(visual, state)) {
779
+ addProceduralRollers(group, visual, state);
780
+ }
781
+ if (state.options.renderProceduralMotors === true &&
782
+ properties.child("MotorVisual") &&
783
+ !hasRenderableMotorChild(visual, state)) {
784
+ addProceduralMotor(group, visual, state);
785
+ }
786
+ if (group.children.length === 0) {
787
+ return undefined;
788
+ }
789
+ group.userData.demo3d = {
790
+ kind: "procedural-roller-conveyor",
791
+ id: visual.id,
792
+ name: visual.displayName,
793
+ typeName: visual.typeName,
794
+ xmlPath: visual.xml.path
795
+ };
796
+ state.stats.groups += 1;
797
+ state.stats.directVisuals += 1;
798
+ return group;
799
+ }
800
+ function addConveyorSide(group, visual, sideXml, sideName, state) {
801
+ if (!sideXml) {
802
+ return;
803
+ }
804
+ const side = new Demo3DConveyorSideProperties(sideXml.xsiType ?? "e3d:ConveyorSideProperties", sideXml);
805
+ const profile = side.profile;
806
+ if (!side.visible || !profile) {
807
+ return;
808
+ }
809
+ const frames = conveyorSideFrames(visual, sideName);
810
+ if (frames.length < 2) {
811
+ return;
812
+ }
813
+ profile.polygons.forEach((polygon, index) => {
814
+ if (polygon.points.length < 3) {
815
+ return;
816
+ }
817
+ const geometry = getSweptProfileGeometry(profile, polygon, frames, state);
818
+ const materials = polygon.materials.length > 0
819
+ ? polygon.materials.map((material) => getMaterial(material, state))
820
+ : [side.material ? getMaterial(side.material, state) : state.defaultMaterial];
821
+ const mesh = new state.three.Mesh(geometry, materials.length === 1 ? materials[0] : materials);
822
+ mesh.name = `${sideName} conveyor side ${index + 1}`;
823
+ mesh.userData.demo3d = { kind: "procedural-conveyor-side", side: sideName };
824
+ group.add(mesh);
825
+ state.stats.meshes += 1;
826
+ state.stats.proceduralConveyorSides += 1;
827
+ });
828
+ }
829
+ function conveyorSideFrames(visual, side) {
830
+ const properties = visual.properties;
831
+ if (!properties) {
832
+ return [];
833
+ }
834
+ const width = numberChildOptional(properties, "RollerWidth") ??
835
+ numberChildOptional(properties, "Width") ??
836
+ 0.5;
837
+ if (visual.typeName === "e3d:CurveRollerConveyor") {
838
+ const innerRadius = numberChild(properties, "InnerRadius", 0.5);
839
+ const angle = degreesToRadians(numberChild(properties, "Angle", 90));
840
+ const segments = Math.max(4, Math.ceil(Math.abs(angle) / (Math.PI / 24)));
841
+ const pathRadius = side === "left" ? innerRadius : innerRadius + width;
842
+ const frames = [];
843
+ for (let index = 0; index <= segments; index += 1) {
844
+ const theta = angle * (1 - index / segments);
845
+ frames.push({
846
+ center: [
847
+ pathRadius * Math.cos(theta),
848
+ 0,
849
+ pathRadius * Math.sin(theta)
850
+ ],
851
+ lateral: [Math.cos(theta), 0, Math.sin(theta)]
852
+ });
853
+ }
854
+ return frames;
855
+ }
856
+ const length = visual.typeName === "e3d:InjectorRollerConveyor"
857
+ ? Math.max(numberChild(properties, "ShortSideLength", 0), numberChild(properties, "ExtraLength", 0), numberChild(properties, "RollerPitch", 0.1))
858
+ : numberChild(properties, "Length", 1);
859
+ const z = (side === "left" ? -1 : 1) * width / 2;
860
+ return [
861
+ { center: [0, 0, z], lateral: [0, 0, 1] },
862
+ { center: [length, 0, z], lateral: [0, 0, 1] }
863
+ ];
864
+ }
865
+ function getSweptProfileGeometry(profile, polygon, frames, state) {
866
+ const pointsKey = polygon.points.map((point) => `${point.x},${point.y}`).join(";");
867
+ const framesKey = frames
868
+ .map((frame) => `${frame.center.join(",")}/${frame.lateral.join(",")}`)
869
+ .join(";");
870
+ const key = `sweep:${profile.anchor.x},${profile.anchor.y}:${polygon.materials.length}:${pointsKey}:${framesKey}`;
871
+ const cached = state.primitiveGeometryCache.get(key);
872
+ if (cached) {
873
+ return cached;
874
+ }
875
+ const positions = [];
876
+ const indices = [];
877
+ const pointCount = polygon.points.length;
878
+ for (const frame of frames) {
879
+ for (const point of polygon.points) {
880
+ const lateral = point.x - profile.anchor.x;
881
+ const vertical = point.y - profile.anchor.y;
882
+ positions.push(frame.center[0] + frame.lateral[0] * lateral, frame.center[1] + vertical, frame.center[2] + frame.lateral[2] * lateral);
883
+ }
884
+ }
885
+ const groups = [];
886
+ for (let point = 0; point < pointCount; point += 1) {
887
+ const start = indices.length;
888
+ for (let frame = 0; frame < frames.length - 1; frame += 1) {
889
+ const current = frame * pointCount;
890
+ const next = (frame + 1) * pointCount;
891
+ const following = (point + 1) % pointCount;
892
+ indices.push(current + point, next + point, current + following, current + following, next + point, next + following);
893
+ }
894
+ groups.push({
895
+ start,
896
+ count: indices.length - start,
897
+ materialIndex: Math.min(point, Math.max(0, polygon.materials.length - 1))
898
+ });
899
+ }
900
+ const shapePoints = polygon.points.map((point) => new state.three.Vector2(point.x - profile.anchor.x, point.y - profile.anchor.y));
901
+ const capTriangles = state.three.ShapeUtils.triangulateShape(shapePoints, []);
902
+ const finalOffset = (frames.length - 1) * pointCount;
903
+ const capStart = indices.length;
904
+ for (const triangle of capTriangles) {
905
+ indices.push(triangle[2], triangle[1], triangle[0]);
906
+ indices.push(finalOffset + triangle[0], finalOffset + triangle[1], finalOffset + triangle[2]);
907
+ }
908
+ const geometry = new state.three.BufferGeometry();
909
+ geometry.setAttribute("position", new state.three.Float32BufferAttribute(positions, 3));
910
+ geometry.setIndex(indices);
911
+ for (const group of groups) {
912
+ geometry.addGroup(group.start, group.count, group.materialIndex);
913
+ }
914
+ geometry.addGroup(capStart, indices.length - capStart, 0);
915
+ geometry.computeVertexNormals();
916
+ geometry.computeBoundingBox();
917
+ geometry.computeBoundingSphere();
918
+ state.primitiveGeometryCache.set(key, geometry);
919
+ state.stats.geometries += 1;
920
+ return geometry;
921
+ }
922
+ function addProceduralRollers(group, visual, state) {
923
+ const properties = visual.properties;
924
+ if (!properties) {
925
+ return;
926
+ }
927
+ const width = numberChildOptional(properties, "RollerWidth") ??
928
+ numberChildOptional(properties, "Width") ??
929
+ 0.5;
930
+ const diameter = numberChild(properties, "RollerDiameter", 0.05);
931
+ const radius = diameter / 2;
932
+ const material = materialFromContainer(properties.child("SurfaceMaterial") ?? properties, state)
933
+ ?? getColorMaterial(numberChildOptional(properties, "RollerColor"), state);
934
+ const geometry = cachedRollerGeometry(radius, width, state);
935
+ if (visual.typeName === "e3d:CurveRollerConveyor") {
936
+ const count = Math.max(1, Math.round(numberChild(properties, "RollerCount", 1)));
937
+ const innerRadius = numberChild(properties, "InnerRadius", 0.5);
938
+ const angle = degreesToRadians(numberChild(properties, "Angle", 90));
939
+ const centerRadius = innerRadius + width / 2;
940
+ for (let index = 0; index < count; index += 1) {
941
+ const theta = count === 1 ? angle / 2 : angle * (1 - index / (count - 1));
942
+ const roller = new state.three.Mesh(geometry, material);
943
+ roller.name = `Generated roller ${index + 1}`;
944
+ roller.position.set(centerRadius * Math.cos(theta), 0, centerRadius * Math.sin(theta));
945
+ roller.rotation.y = Math.PI / 2 - theta;
946
+ roller.userData.demo3d = { kind: "procedural-roller", index };
947
+ group.add(roller);
948
+ state.stats.meshes += 1;
949
+ state.stats.proceduralRollers += 1;
950
+ }
951
+ return;
952
+ }
953
+ const length = visual.typeName === "e3d:InjectorRollerConveyor"
954
+ ? Math.max(numberChild(properties, "ShortSideLength", 0), numberChild(properties, "RollerPitch", 0.1))
955
+ : numberChild(properties, "Length", 1);
956
+ const pitch = Math.max(numberChild(properties, "RollerPitch", diameter * 1.2), diameter);
957
+ const explicitCount = numberChildOptional(properties, "RollerCount");
958
+ const count = Math.max(1, Math.round(explicitCount ?? length / pitch));
959
+ for (let index = 0; index < count; index += 1) {
960
+ const roller = new state.three.Mesh(geometry, material);
961
+ roller.name = `Generated roller ${index + 1}`;
962
+ roller.position.x = ((index + 0.5) / count) * length;
963
+ roller.userData.demo3d = { kind: "procedural-roller", index };
964
+ group.add(roller);
965
+ state.stats.meshes += 1;
966
+ state.stats.proceduralRollers += 1;
967
+ }
968
+ }
969
+ function cachedRollerGeometry(radius, width, state) {
970
+ const key = `roller:${radius}:${width}`;
971
+ const cached = state.primitiveGeometryCache.get(key);
972
+ if (cached) {
973
+ return cached;
974
+ }
975
+ const geometry = new state.three.CylinderGeometry(radius, radius, width, 16);
976
+ geometry.rotateX(Math.PI / 2);
977
+ geometry.computeBoundingBox();
978
+ geometry.computeBoundingSphere();
979
+ state.primitiveGeometryCache.set(key, geometry);
980
+ state.stats.geometries += 1;
981
+ return geometry;
982
+ }
983
+ function addProceduralMotor(group, visual, state) {
984
+ const properties = visual.properties;
985
+ if (!properties) {
986
+ return;
987
+ }
988
+ const width = numberChildOptional(properties, "RollerWidth") ??
989
+ numberChildOptional(properties, "Width") ??
990
+ 0.5;
991
+ const length = numberChildOptional(properties, "Length") ?? 0.2;
992
+ const diameter = numberChild(properties, "RollerDiameter", 0.05);
993
+ const size = Math.max(diameter * 1.6, 0.06);
994
+ const geometry = cachedBoxGeometry(size * 1.4, size, size, state);
995
+ const motor = new state.three.Mesh(geometry, getColorMaterial(0xff303840 | 0, state));
996
+ motor.name = "Generated conveyor motor";
997
+ motor.position.set(length * 0.75, -size / 2, width / 2 + size / 2);
998
+ motor.userData.demo3d = { kind: "procedural-conveyor-motor", approximate: true };
999
+ group.add(motor);
1000
+ state.stats.meshes += 1;
1001
+ state.stats.proceduralMotors += 1;
1002
+ }
1003
+ function hasSerializedRollerGeometry(visual, state) {
1004
+ const stack = [...visual.children];
1005
+ while (stack.length > 0) {
1006
+ const current = stack.pop();
1007
+ if (!current) {
1008
+ continue;
1009
+ }
1010
+ const displayName = current.displayName?.toLowerCase() ?? "";
1011
+ const propertyType = current.properties?.textOf("Type")?.toLowerCase() ?? "";
1012
+ const isRoller = propertyType === "displayroller" || /^roller(?:\d+)?$/.test(displayName);
1013
+ if (isRoller &&
1014
+ findVisualAspects(current, state).some((aspect) => aspect.xsiType === "e3d:CylinderRendererAspect")) {
1015
+ return true;
1016
+ }
1017
+ stack.push(...current.children);
1018
+ }
1019
+ return false;
1020
+ }
1021
+ function hasRenderableMotorChild(visual, state) {
1022
+ return visual.children.some((child) => {
1023
+ const name = child.displayName?.toLowerCase() ?? "";
1024
+ return (name.includes("motor") || name.includes("transmission")) && findVisualAspects(child, state).length > 0;
1025
+ });
1026
+ }
1027
+ function materialFromContainer(root, state) {
1028
+ if (!root || !state) {
1029
+ return undefined;
1030
+ }
1031
+ const material = firstMaterial(root);
1032
+ return material ? getMaterial(material, state) : undefined;
1033
+ }
1034
+ function cachedBoxGeometry(width, height, depth, state) {
1035
+ const key = `box:${width}:${height}:${depth}`;
1036
+ const cached = state.primitiveGeometryCache.get(key);
1037
+ if (cached) {
1038
+ return cached;
1039
+ }
1040
+ const geometry = new state.three.BoxGeometry(width, height, depth);
1041
+ state.primitiveGeometryCache.set(key, geometry);
1042
+ state.stats.geometries += 1;
1043
+ return geometry;
1044
+ }
1045
+ function isRollerConveyorType(typeName) {
1046
+ return (typeName === "e3d:StraightRollerConveyor" ||
1047
+ typeName === "e3d:CurveRollerConveyor" ||
1048
+ typeName === "e3d:InjectorRollerConveyor");
1049
+ }
518
1050
  function createImportedImageObject(visual, state) {
519
1051
  const properties = visual.properties;
520
1052
  if (!properties) {
@@ -747,13 +1279,16 @@ function getPrimitiveGeometry(aspect, renderable, state) {
747
1279
  let geometry;
748
1280
  if (type === "e3d:CylinderRendererAspect") {
749
1281
  const radius = numberChild(renderable, "Radius", 0.5);
1282
+ const innerRadius = numberChild(renderable, "InnerRadius", 0);
750
1283
  const radiusRatio = numberChild(renderable, "RadiusRatio", 1);
751
1284
  const coneRatio = numberChild(renderable, "ConeRatio", 1);
752
1285
  const length = numberChild(renderable, "Length", 1);
753
1286
  const slices = Math.max(3, Math.round(numberChild(renderable, "Slices", 16)));
754
1287
  const startAngle = degreesToRadians(numberChild(renderable, "StartAngle", 0));
755
1288
  const angle = degreesToRadians(numberChild(renderable, "Angle", 360));
756
- geometry = new state.three.CylinderGeometry(radius * radiusRatio, radius * coneRatio, length, slices, 1, false, startAngle, angle);
1289
+ geometry = innerRadius > 0 && innerRadius < Math.min(radius * radiusRatio, radius * coneRatio)
1290
+ ? createAnnularCylinderGeometry(radius * radiusRatio, radius * coneRatio, innerRadius, length, slices, startAngle, angle, state)
1291
+ : new state.three.CylinderGeometry(radius * radiusRatio, radius * coneRatio, length, slices, 1, false, startAngle, angle);
757
1292
  }
758
1293
  else if (type === "e3d:BoxRendererAspect") {
759
1294
  geometry = new state.three.BoxGeometry(numberChild(renderable, "Width", 1), numberChild(renderable, "Height", 1), numberChild(renderable, "Depth", 1));
@@ -771,12 +1306,71 @@ function getPrimitiveGeometry(aspect, renderable, state) {
771
1306
  state.stats.geometries += 1;
772
1307
  return geometry;
773
1308
  }
1309
+ function createAnnularCylinderGeometry(topRadius, bottomRadius, innerRadius, length, slices, startAngle, angle, state) {
1310
+ const positions = [];
1311
+ const halfLength = length / 2;
1312
+ const fullCircle = Math.abs(angle) >= Math.PI * 2 - 1e-6;
1313
+ const segments = Math.max(1, Math.round(slices * Math.abs(angle) / (Math.PI * 2)));
1314
+ const point = (radius, y, theta) => [
1315
+ radius * Math.sin(theta),
1316
+ y,
1317
+ radius * Math.cos(theta)
1318
+ ];
1319
+ const addTriangle = (a, b, c, normal) => {
1320
+ const abx = b[0] - a[0];
1321
+ const aby = b[1] - a[1];
1322
+ const abz = b[2] - a[2];
1323
+ const acx = c[0] - a[0];
1324
+ const acy = c[1] - a[1];
1325
+ const acz = c[2] - a[2];
1326
+ const crossX = aby * acz - abz * acy;
1327
+ const crossY = abz * acx - abx * acz;
1328
+ const crossZ = abx * acy - aby * acx;
1329
+ const forward = crossX * normal[0] + crossY * normal[1] + crossZ * normal[2] >= 0;
1330
+ const second = forward ? b : c;
1331
+ const third = forward ? c : b;
1332
+ positions.push(...a, ...second, ...third);
1333
+ };
1334
+ const addQuad = (a, b, c, d, normal) => {
1335
+ addTriangle(a, b, c, normal);
1336
+ addTriangle(a, c, d, normal);
1337
+ };
1338
+ for (let segment = 0; segment < segments; segment += 1) {
1339
+ const theta0 = startAngle + angle * (segment / segments);
1340
+ const theta1 = startAngle + angle * ((segment + 1) / segments);
1341
+ const middle = (theta0 + theta1) / 2;
1342
+ const outerBottom0 = point(bottomRadius, -halfLength, theta0);
1343
+ const outerBottom1 = point(bottomRadius, -halfLength, theta1);
1344
+ const outerTop0 = point(topRadius, halfLength, theta0);
1345
+ const outerTop1 = point(topRadius, halfLength, theta1);
1346
+ const innerBottom0 = point(innerRadius, -halfLength, theta0);
1347
+ const innerBottom1 = point(innerRadius, -halfLength, theta1);
1348
+ const innerTop0 = point(innerRadius, halfLength, theta0);
1349
+ const innerTop1 = point(innerRadius, halfLength, theta1);
1350
+ addQuad(outerBottom0, outerBottom1, outerTop1, outerTop0, [Math.sin(middle), 0, Math.cos(middle)]);
1351
+ addQuad(innerBottom0, innerTop0, innerTop1, innerBottom1, [-Math.sin(middle), 0, -Math.cos(middle)]);
1352
+ addQuad(outerTop0, outerTop1, innerTop1, innerTop0, [0, 1, 0]);
1353
+ addQuad(outerBottom0, innerBottom0, innerBottom1, outerBottom1, [0, -1, 0]);
1354
+ }
1355
+ if (!fullCircle) {
1356
+ const direction = Math.sign(angle) || 1;
1357
+ const endAngle = startAngle + angle;
1358
+ addQuad(point(innerRadius, -halfLength, startAngle), point(innerRadius, halfLength, startAngle), point(topRadius, halfLength, startAngle), point(bottomRadius, -halfLength, startAngle), [-direction * Math.cos(startAngle), 0, direction * Math.sin(startAngle)]);
1359
+ addQuad(point(innerRadius, -halfLength, endAngle), point(bottomRadius, -halfLength, endAngle), point(topRadius, halfLength, endAngle), point(innerRadius, halfLength, endAngle), [direction * Math.cos(endAngle), 0, -direction * Math.sin(endAngle)]);
1360
+ }
1361
+ const geometry = new state.three.BufferGeometry();
1362
+ geometry.setAttribute("position", new state.three.Float32BufferAttribute(positions, 3));
1363
+ geometry.computeVertexNormals();
1364
+ return geometry;
1365
+ }
774
1366
  function primitiveGeometryKey(type, renderable) {
775
1367
  const fields = [
776
1368
  "Angle",
777
1369
  "ConeRatio",
1370
+ "CurveRadius",
778
1371
  "Depth",
779
1372
  "Height",
1373
+ "InnerRadius",
780
1374
  "Length",
781
1375
  "Radius",
782
1376
  "RadiusRatio",
@@ -791,7 +1385,13 @@ function getMaterial(material, state) {
791
1385
  return state.defaultMaterial;
792
1386
  }
793
1387
  const textureReference = material.textureReference;
794
- const key = `${String(material.diffuse ?? "default")}:${textureReference ?? ""}`;
1388
+ const key = [
1389
+ "material",
1390
+ String(material.diffuse ?? "default"),
1391
+ String(material.transparency ?? 0),
1392
+ String(material.reflectivity ?? 0),
1393
+ textureReference ?? ""
1394
+ ].join(":");
795
1395
  const cached = state.materialCache.get(key);
796
1396
  if (cached) {
797
1397
  return cached;
@@ -799,26 +1399,55 @@ function getMaterial(material, state) {
799
1399
  const created = createDemo3DThreeMaterial(material, state.three);
800
1400
  const texture = textureReference ? getTexture(textureReference, state) : undefined;
801
1401
  if (texture && "map" in created) {
802
- created.map = texture;
1402
+ const standard = created;
1403
+ standard.map = texture;
1404
+ if (textureReference && state.textureImageById.get(textureReference)?.containsAlpha) {
1405
+ standard.transparent = true;
1406
+ standard.depthWrite = false;
1407
+ }
803
1408
  created.needsUpdate = true;
804
1409
  }
805
1410
  state.materialCache.set(key, created);
806
1411
  state.stats.materials += 1;
807
1412
  return created;
808
1413
  }
1414
+ function getColorMaterial(color, state) {
1415
+ if (color === undefined) {
1416
+ return state.defaultMaterial;
1417
+ }
1418
+ const opacity = demo3dColorToOpacity(color);
1419
+ const key = `color:${color}:${opacity}`;
1420
+ const cached = state.materialCache.get(key);
1421
+ if (cached) {
1422
+ return cached;
1423
+ }
1424
+ const created = new state.three.MeshStandardMaterial({
1425
+ color: demo3dColorToHex(color),
1426
+ opacity,
1427
+ transparent: opacity < 1,
1428
+ depthWrite: opacity >= 1,
1429
+ roughness: 0.9,
1430
+ metalness: 0.05
1431
+ });
1432
+ state.materialCache.set(key, created);
1433
+ state.stats.materials += 1;
1434
+ return created;
1435
+ }
809
1436
  function getImageMaterial(material, state) {
810
1437
  const diffuse = material?.diffuse;
1438
+ const opacity = demo3dMaterialOpacity(material);
811
1439
  const textureReference = material?.textureReference;
812
1440
  const texture = textureReference ? getTexture(textureReference, state) : undefined;
813
- const key = `image:${String(diffuse ?? "default")}:${textureReference ?? ""}`;
1441
+ const key = `image:${String(diffuse ?? "default")}:${String(material?.transparency ?? 0)}:${textureReference ?? ""}`;
814
1442
  const cached = state.materialCache.get(key);
815
1443
  if (cached) {
816
1444
  return cached;
817
1445
  }
818
1446
  const parameters = {
819
1447
  color: diffuse === undefined ? 0xffffff : demo3dColorToHex(diffuse),
820
- transparent: texture !== undefined || demo3dColorToOpacity(diffuse ?? 0xffffffff) < 1,
821
- opacity: diffuse === undefined ? 1 : demo3dColorToOpacity(diffuse),
1448
+ transparent: texture !== undefined || opacity < 1,
1449
+ opacity,
1450
+ depthWrite: texture === undefined && opacity >= 1,
822
1451
  side: state.three.DoubleSide
823
1452
  };
824
1453
  if (texture) {
@@ -901,12 +1530,18 @@ function createFallbackTextMesh(text, lineHeight, material, state) {
901
1530
  }
902
1531
  function getLineMaterial(material, state) {
903
1532
  const color = material?.diffuse === undefined ? 0x202124 : demo3dColorToHex(material.diffuse);
904
- const key = `line:${color}`;
1533
+ const opacity = demo3dMaterialOpacity(material);
1534
+ const key = `line:${color}:${opacity}`;
905
1535
  const cached = state.materialCache.get(key);
906
1536
  if (cached) {
907
1537
  return cached;
908
1538
  }
909
- const created = new state.three.LineBasicMaterial({ color });
1539
+ const created = new state.three.LineBasicMaterial({
1540
+ color,
1541
+ opacity,
1542
+ transparent: opacity < 1,
1543
+ depthWrite: opacity >= 1
1544
+ });
910
1545
  state.materialCache.set(key, created);
911
1546
  state.stats.materials += 1;
912
1547
  return created;
@@ -987,7 +1622,8 @@ function indexTextureImages(project) {
987
1622
  base64: bytes,
988
1623
  mimeType: imageMimeType(bytes),
989
1624
  width: numberChildOptional(value, "Width"),
990
- height: numberChildOptional(value, "Height")
1625
+ height: numberChildOptional(value, "Height"),
1626
+ containsAlpha: isTrue(value?.textOf("ContainsAlpha"))
991
1627
  });
992
1628
  }
993
1629
  return indexed;
@@ -1033,9 +1669,12 @@ function findVisualAspects(visual, state) {
1033
1669
  }
1034
1670
  const aspect = state.serializedObjectById.get(id);
1035
1671
  if (aspect) {
1036
- found.push(aspect);
1672
+ if (!isFalse(aspect.textOf("IsEnabled"))) {
1673
+ found.push(aspect);
1674
+ }
1675
+ continue;
1037
1676
  }
1038
- else if (state.options.includeUnsupported) {
1677
+ if (state.options.includeUnsupported) {
1039
1678
  warn(state, {
1040
1679
  code: "DEMO3D_THREE_MISSING_ASPECT",
1041
1680
  message: `Visual aspect ${id} does not exist in SerializedObjects.`,
@@ -1112,6 +1751,125 @@ function positionSerializedPreviewMesh(mesh, index) {
1112
1751
  mesh.scale.setScalar(scale);
1113
1752
  mesh.position.set(gridX - center.x * scale, -center.y * scale, gridZ - center.z * scale);
1114
1753
  }
1754
+ function createDimensionGroup(state) {
1755
+ const group = new state.three.Group();
1756
+ group.name = "Demo3D dimensions";
1757
+ for (const xml of state.serializedObjectById.values()) {
1758
+ if (xml.xsiType !== "e3d:DimensionAspect") {
1759
+ continue;
1760
+ }
1761
+ const dimension = createDimensionObject(new Demo3DDimensionAspect(xml.xsiType, xml), state);
1762
+ if (dimension) {
1763
+ group.add(dimension);
1764
+ }
1765
+ }
1766
+ if (group.children.length > 0) {
1767
+ group.userData.demo3d = { kind: "dimensions" };
1768
+ state.stats.groups += 1;
1769
+ }
1770
+ return group;
1771
+ }
1772
+ function createDimensionObject(dimension, state) {
1773
+ const start = dimensionPointToWorld(dimension.start, state);
1774
+ const end = dimensionPointToWorld(dimension.end, state);
1775
+ if (!start || !end) {
1776
+ return undefined;
1777
+ }
1778
+ const extensionValues = dimension.extensionDirection;
1779
+ const extension = new state.three.Vector3(extensionValues[0] ?? 0, extensionValues[1] ?? 1, -(extensionValues[2] ?? 0));
1780
+ if (extension.lengthSq() === 0) {
1781
+ extension.set(0, 1, 0);
1782
+ }
1783
+ extension.normalize().multiplyScalar(dimension.height);
1784
+ const lineStart = start.clone().add(extension);
1785
+ const lineEnd = end.clone().add(extension);
1786
+ const positions = [];
1787
+ pushVectorLine(positions, start, lineStart);
1788
+ pushVectorLine(positions, end, lineEnd);
1789
+ pushVectorLine(positions, lineStart, lineEnd);
1790
+ const direction = lineEnd.clone().sub(lineStart);
1791
+ const distance = direction.length();
1792
+ if (distance > 0) {
1793
+ direction.normalize();
1794
+ const normal = extension.clone().normalize();
1795
+ const perpendicular = new state.three.Vector3().crossVectors(direction, normal);
1796
+ if (perpendicular.lengthSq() === 0) {
1797
+ perpendicular.set(0, 0, 1);
1798
+ }
1799
+ perpendicular.normalize();
1800
+ const startArrow = dimensionArrowSize(dimension.startArrow, distance);
1801
+ const endArrow = dimensionArrowSize(dimension.endArrow, distance);
1802
+ const startDirection = dimension.arrowsInside ? direction : direction.clone().negate();
1803
+ const endDirection = dimension.arrowsInside ? direction.clone().negate() : direction;
1804
+ addDimensionArrow(positions, lineStart, startDirection, perpendicular, startArrow.length, startArrow.width);
1805
+ addDimensionArrow(positions, lineEnd, endDirection, perpendicular, endArrow.length, endArrow.width);
1806
+ }
1807
+ const geometry = new state.three.BufferGeometry();
1808
+ geometry.setAttribute("position", new state.three.Float32BufferAttribute(positions, 3));
1809
+ geometry.computeBoundingBox();
1810
+ geometry.computeBoundingSphere();
1811
+ const object = new state.three.Group();
1812
+ const lines = new state.three.LineSegments(geometry, getLineMaterial(dimension.material, state));
1813
+ lines.name = "Dimension lines";
1814
+ object.add(lines);
1815
+ if (distance > 0) {
1816
+ const label = distance.toFixed(3);
1817
+ const color = demo3dColorToHex(dimension.material?.diffuse ?? -16777216);
1818
+ const lineHeight = clamp(distance * 0.035, 0.025, 0.12);
1819
+ const text = canUseCanvas()
1820
+ ? createCanvasTextMesh(label, lineHeight, color, false, state)
1821
+ : createFallbackTextMesh(label, lineHeight, dimension.material, state);
1822
+ text.position.copy(lineStart).lerp(lineEnd, 0.5).add(extension.clone().normalize().multiplyScalar(lineHeight));
1823
+ text.name = `Dimension ${label}`;
1824
+ object.add(text);
1825
+ state.stats.meshes += 1;
1826
+ }
1827
+ object.name = dimension.id ?? "Dimension";
1828
+ object.userData.demo3d = {
1829
+ kind: "dimension",
1830
+ id: dimension.id,
1831
+ distance,
1832
+ unit: dimension.unit,
1833
+ format: dimension.format,
1834
+ arrowsInside: dimension.arrowsInside,
1835
+ flipText: dimension.flipText,
1836
+ lockDirection: dimension.lockDirection,
1837
+ depth: dimension.depth,
1838
+ xmlPath: dimension.xml.path
1839
+ };
1840
+ state.stats.geometries += 1;
1841
+ state.stats.lines += positions.length / 6;
1842
+ state.stats.groups += 1;
1843
+ state.stats.dimensions += 1;
1844
+ return object;
1845
+ }
1846
+ function dimensionArrowSize(profile, distance) {
1847
+ const points = profile?.polygons.flatMap((polygon) => polygon.points) ?? [];
1848
+ const anchorX = profile?.anchor.x ?? 0;
1849
+ const anchorY = profile?.anchor.y ?? 0;
1850
+ const profileLength = points.reduce((largest, point) => Math.max(largest, Math.abs(point.y - anchorY)), 0);
1851
+ const profileWidth = points.reduce((largest, point) => Math.max(largest, Math.abs(point.x - anchorX)), 0);
1852
+ const fallbackLength = clamp(distance * 0.04, 0.02, 0.08);
1853
+ const length = profileLength > 0 ? Math.min(profileLength, distance * 0.25) : fallbackLength;
1854
+ const width = profileWidth > 0 ? Math.min(profileWidth, distance * 0.125) : length * 0.45;
1855
+ return { length, width };
1856
+ }
1857
+ function dimensionPointToWorld(point, state) {
1858
+ if (!point) {
1859
+ return undefined;
1860
+ }
1861
+ const local = new state.three.Vector3(point.point[0] ?? 0, point.point[1] ?? 0, -(point.point[2] ?? 0));
1862
+ const visual = point.visualId ? state.visualObjectById.get(point.visualId) : undefined;
1863
+ return visual ? visual.localToWorld(local) : local;
1864
+ }
1865
+ function pushVectorLine(positions, start, end) {
1866
+ positions.push(start.x, start.y, start.z, end.x, end.y, end.z);
1867
+ }
1868
+ function addDimensionArrow(positions, tip, direction, perpendicular, length, width) {
1869
+ const base = tip.clone().addScaledVector(direction, length);
1870
+ pushVectorLine(positions, tip, base.clone().addScaledVector(perpendicular, width));
1871
+ pushVectorLine(positions, tip, base.clone().addScaledVector(perpendicular, -width));
1872
+ }
1115
1873
  function createPlaceholderMesh(name, meshReferenceId, material, state) {
1116
1874
  const geometry = new state.three.BoxGeometry(0.25, 0.25, 0.25);
1117
1875
  convertTriangleGeometryToThreeCoordinates(geometry);
@@ -1208,7 +1966,20 @@ function demo3dColorToHex(color) {
1208
1966
  }
1209
1967
  function demo3dColorToOpacity(color) {
1210
1968
  const alpha = (color >>> 24) & 0xff;
1211
- return alpha === 0 ? 1 : alpha / 255;
1969
+ return alpha / 255;
1970
+ }
1971
+ function demo3dMaterialOpacity(material) {
1972
+ const colorOpacity = material?.diffuse === undefined
1973
+ ? 1
1974
+ : demo3dColorToOpacity(material.diffuse);
1975
+ const transparency = clamp01(material?.transparency ?? 0);
1976
+ return colorOpacity * (1 - transparency);
1977
+ }
1978
+ function clamp01(value) {
1979
+ return clamp(value, 0, 1);
1980
+ }
1981
+ function clamp(value, minimum, maximum) {
1982
+ return Math.min(maximum, Math.max(minimum, value));
1212
1983
  }
1213
1984
  function applyDemo3DTransform(object, transformText) {
1214
1985
  const values = parsePipeNumbers(transformText);