@jfk-solutions/demo3d-file-format 1.3.0 → 1.5.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.
@@ -136,6 +136,7 @@ function createState(three, project, options, buffers) {
136
136
  materialCache: new Map(),
137
137
  textureCache: new Map(),
138
138
  visualObjectById: new Map(),
139
+ layerVisibilityByName: new Map(project.layers.map((layer) => [layer.name, layer.visible])),
139
140
  renderableObjectTrees: new WeakSet(),
140
141
  warnedGeneratedTypes: new Set(),
141
142
  defaultMaterial: createDemo3DThreeMaterial(undefined, three),
@@ -150,6 +151,7 @@ function createState(three, project, options, buffers) {
150
151
  lines: 0,
151
152
  directVisuals: 0,
152
153
  proceduralBelts: 0,
154
+ proceduralRacks: 0,
153
155
  proceduralSupportStands: 0,
154
156
  proceduralConveyorSides: 0,
155
157
  proceduralPhotoEyes: 0,
@@ -166,23 +168,25 @@ function createState(three, project, options, buffers) {
166
168
  };
167
169
  }
168
170
  function createVisualObject(visual, state, inheritedLayer, parentVisual) {
169
- const refs = findMeshReferenceIds(visual.xml);
171
+ const layer = visual.layer ?? inheritedLayer;
172
+ const renderCurrent = isRenderableVisualVisible(visual) && isLayerVisible(layer, state);
173
+ const refs = renderCurrent ? findVisualMeshReferenceIds(visual.xml) : [];
170
174
  const meshes = [];
171
175
  const textObjects = [];
172
176
  const drawingBlockObjects = [];
173
177
  const directVisualObjects = [];
174
178
  const placeholderObjects = [];
175
- const textObject = createTextVisualObject(visual, state);
179
+ const textObject = renderCurrent ? createTextVisualObject(visual, state) : undefined;
176
180
  if (textObject) {
177
181
  textObjects.push(textObject);
178
182
  meshes.push(textObject);
179
183
  }
180
- const drawingBlockObject = createDrawingBlockVisualObject(visual, state);
184
+ const drawingBlockObject = renderCurrent ? createDrawingBlockVisualObject(visual, state) : undefined;
181
185
  if (drawingBlockObject) {
182
186
  drawingBlockObjects.push(drawingBlockObject);
183
187
  meshes.push(drawingBlockObject);
184
188
  }
185
- const directVisualObject = createDirectVisualObject(visual, state, parentVisual);
189
+ const directVisualObject = renderCurrent ? createDirectVisualObject(visual, state, parentVisual) : undefined;
186
190
  if (directVisualObject) {
187
191
  directVisualObjects.push(directVisualObject);
188
192
  meshes.push(directVisualObject);
@@ -196,7 +200,7 @@ function createVisualObject(visual, state, inheritedLayer, parentVisual) {
196
200
  meshes.push(mesh);
197
201
  }
198
202
  }
199
- const aspectRenderableObjects = createAspectRenderableObjects(visual, state);
203
+ const aspectRenderableObjects = renderCurrent ? createAspectRenderableObjects(visual, state) : [];
200
204
  meshes.push(...aspectRenderableObjects);
201
205
  let object;
202
206
  if (meshes.length === 1 &&
@@ -219,7 +223,6 @@ function createVisualObject(visual, state, inheritedLayer, parentVisual) {
219
223
  object.name = visual.displayName ?? visual.id ?? visual.typeName;
220
224
  applyDemo3DTransform(object, visual.xml.textOf("LR"));
221
225
  applyDemo3DScale(object, visual.properties?.textOf("Scale"));
222
- const layer = visual.layer ?? inheritedLayer;
223
226
  object.userData.demo3d = {
224
227
  kind: "visual",
225
228
  id: visual.id,
@@ -228,7 +231,8 @@ function createVisualObject(visual, state, inheritedLayer, parentVisual) {
228
231
  xmlPath: visual.xml.path,
229
232
  localTransform: visual.localTransform,
230
233
  localTransformText: visual.xml.textOf("LR"),
231
- layer
234
+ layer,
235
+ layerVisible: isLayerVisible(layer, state)
232
236
  };
233
237
  if (visual.id) {
234
238
  state.visualObjectById.set(visual.id, object);
@@ -242,7 +246,9 @@ function createVisualObject(visual, state, inheritedLayer, parentVisual) {
242
246
  if (containsRenderable) {
243
247
  state.renderableObjectTrees.add(object);
244
248
  }
245
- reportUnreconstructedProceduralVisual(visual, containsRenderable, state);
249
+ if (renderCurrent) {
250
+ reportUnreconstructedProceduralVisual(visual, containsRenderable, state);
251
+ }
246
252
  return object;
247
253
  }
248
254
  function reportUnreconstructedProceduralVisual(visual, containsRenderable, state) {
@@ -375,9 +381,16 @@ function createDirectVisualObject(visual, state, parentVisual) {
375
381
  if (visual.typeName === "e3d:ImportedImageVisual") {
376
382
  return createImportedImageObject(visual, state);
377
383
  }
378
- if (visual.typeName === "e3d:StraightBeltConveyor") {
384
+ if (visual.typeName === "e3d:StraightBeltConveyor" ||
385
+ visual.typeName === "e3d:CurveBeltConveyor" ||
386
+ visual.typeName === "e3d:InjectorBeltConveyor") {
379
387
  return state.options.renderProceduralBelts === true
380
- ? createStraightBeltConveyorObject(visual, state)
388
+ ? createBeltConveyorObject(visual, state)
389
+ : undefined;
390
+ }
391
+ if (visual.typeName === "e3d:RackVisual") {
392
+ return state.options.renderProceduralRacks === true
393
+ ? createRackObject(visual, state)
381
394
  : undefined;
382
395
  }
383
396
  if (visual.typeName === "e3d:SupportStand") {
@@ -410,7 +423,16 @@ function createDirectVisualObject(visual, state, parentVisual) {
410
423
  state.stats.directVisuals += 1;
411
424
  return object;
412
425
  }
413
- function createStraightBeltConveyorObject(visual, state) {
426
+ function createBeltConveyorObject(visual, state) {
427
+ if (visual.typeName === "e3d:CurveBeltConveyor") {
428
+ return createCurveBeltConveyorObject(visual, state);
429
+ }
430
+ if (visual.typeName === "e3d:InjectorBeltConveyor") {
431
+ const injector = createInjectorBeltConveyorObject(visual, state);
432
+ if (injector) {
433
+ return injector;
434
+ }
435
+ }
414
436
  const belt = readProceduralBelt(visual, state);
415
437
  if (!belt) {
416
438
  return undefined;
@@ -439,6 +461,146 @@ function createStraightBeltConveyorObject(visual, state) {
439
461
  state.stats.proceduralBelts += 1;
440
462
  return object;
441
463
  }
464
+ const DEFAULT_BELT_WIDTH = 0.5;
465
+ const DEFAULT_BELT_DIAMETER = 0.06;
466
+ function appendOrientedTriangle(target, a, b, c, normal) {
467
+ const abx = b[0] - a[0];
468
+ const aby = b[1] - a[1];
469
+ const abz = b[2] - a[2];
470
+ const acx = c[0] - a[0];
471
+ const acy = c[1] - a[1];
472
+ const acz = c[2] - a[2];
473
+ const crossX = aby * acz - abz * acy;
474
+ const crossY = abz * acx - abx * acz;
475
+ const crossZ = abx * acy - aby * acx;
476
+ const forward = crossX * normal[0] + crossY * normal[1] + crossZ * normal[2] >= 0;
477
+ target.push(...a, ...(forward ? b : c), ...(forward ? c : b));
478
+ }
479
+ function appendOrientedQuad(target, a, b, c, d, normal) {
480
+ appendOrientedTriangle(target, a, b, c, normal);
481
+ appendOrientedTriangle(target, a, c, d, normal);
482
+ }
483
+ function createInjectorBeltConveyorObject(visual, state) {
484
+ const belt = readProceduralInjectorBelt(visual);
485
+ if (!belt) {
486
+ return undefined;
487
+ }
488
+ const geometry = getProceduralInjectorBeltGeometry(belt, state);
489
+ const object = new state.three.Mesh(geometry, [
490
+ getMaterial(belt.surfaceMaterial, state),
491
+ getMaterial(belt.sideMaterial, state)
492
+ ]);
493
+ object.name = visual.displayName ?? visual.id ?? visual.typeName;
494
+ object.userData.demo3d = {
495
+ kind: "procedural-belt",
496
+ id: visual.id,
497
+ name: visual.displayName,
498
+ typeName: visual.typeName,
499
+ xmlPath: visual.xml.path,
500
+ loopHeight: belt.thickness,
501
+ startEdge: belt.startEdge,
502
+ endEdge: belt.endEdge,
503
+ connectorShaped: true
504
+ };
505
+ state.stats.meshes += 1;
506
+ state.stats.directVisuals += 1;
507
+ state.stats.proceduralBelts += 1;
508
+ return object;
509
+ }
510
+ function readProceduralInjectorBelt(visual) {
511
+ const properties = visual.properties;
512
+ if (!properties || isFalse(properties.textOf("CenterVisible"))) {
513
+ return undefined;
514
+ }
515
+ const startEdge = conveyorConnectorEdge(visual, "Start");
516
+ const serializedEndEdge = conveyorConnectorEdge(visual, "End");
517
+ if (!startEdge || !serializedEndEdge) {
518
+ return undefined;
519
+ }
520
+ const sameDirection = pointDistanceSquared(startEdge[0], serializedEndEdge[0]) +
521
+ pointDistanceSquared(startEdge[1], serializedEndEdge[1]);
522
+ const reversedDirection = pointDistanceSquared(startEdge[0], serializedEndEdge[1]) +
523
+ pointDistanceSquared(startEdge[1], serializedEndEdge[0]);
524
+ const endEdge = sameDirection <= reversedDirection
525
+ ? serializedEndEdge
526
+ : [serializedEndEdge[1], serializedEndEdge[0]];
527
+ const beltDiameter = numberChildOptional(properties, "CenterDiameter") ??
528
+ numberChildOptional(properties, "BeltDiameter") ??
529
+ DEFAULT_BELT_DIAMETER;
530
+ const thickness = isTrue(properties.textOf("UseBeltCenterHeight"))
531
+ ? numberChild(properties, "BeltCenterHeight", beltDiameter)
532
+ : beltDiameter;
533
+ if (thickness <= 0) {
534
+ return undefined;
535
+ }
536
+ const surfaceMaterial = firstMaterial(properties.child("SurfaceMaterial") ?? properties);
537
+ const sideMaterial = firstMaterial(properties.child("SurfaceSideMaterial") ?? properties) ?? surfaceMaterial;
538
+ return { startEdge, endEdge, thickness, surfaceMaterial, sideMaterial };
539
+ }
540
+ function conveyorConnectorEdge(visual, name) {
541
+ const connector = visual.xml.child("CN")?.children.find((candidate) => candidate.xsiType === "e3d:ConveyorConnector" && candidate.textOf("Name") === name);
542
+ if (!connector) {
543
+ return undefined;
544
+ }
545
+ const start = pipePoint(connector.textOf("Start"));
546
+ const end = pipePoint(connector.textOf("End"));
547
+ return start && end ? [start, end] : undefined;
548
+ }
549
+ function pipePoint(value) {
550
+ const values = parsePipeNumbers(value);
551
+ if (values.length === 0) {
552
+ return undefined;
553
+ }
554
+ return [values[0] ?? 0, values[1] ?? 0, -(values[2] ?? 0)];
555
+ }
556
+ function pointDistanceSquared(left, right) {
557
+ return ((left[0] - right[0]) ** 2 +
558
+ (left[1] - right[1]) ** 2 +
559
+ (left[2] - right[2]) ** 2);
560
+ }
561
+ function getProceduralInjectorBeltGeometry(belt, state) {
562
+ const key = `injector-belt:${[...belt.startEdge, ...belt.endEdge].map((point) => point.join(",")).join(";")}:${belt.thickness}`;
563
+ const cached = state.primitiveGeometryCache.get(key);
564
+ if (cached) {
565
+ return cached;
566
+ }
567
+ const surfacePositions = [];
568
+ const sidePositions = [];
569
+ const top = [belt.startEdge[0], belt.startEdge[1], belt.endEdge[1], belt.endEdge[0]];
570
+ const bottom = top.map((point) => [point[0], point[1] - belt.thickness, point[2]]);
571
+ const center = [
572
+ top.reduce((sum, point) => sum + point[0], 0) / top.length,
573
+ top.reduce((sum, point) => sum + point[1], 0) / top.length,
574
+ top.reduce((sum, point) => sum + point[2], 0) / top.length
575
+ ];
576
+ appendOrientedQuad(surfacePositions, top[0], top[1], top[2], top[3], [0, 1, 0]);
577
+ appendOrientedQuad(surfacePositions, bottom[0], bottom[3], bottom[2], bottom[1], [0, -1, 0]);
578
+ appendInjectorBeltWall(surfacePositions, top[0], top[1], bottom[1], bottom[0], center);
579
+ appendInjectorBeltWall(sidePositions, top[1], top[2], bottom[2], bottom[1], center);
580
+ appendInjectorBeltWall(surfacePositions, top[2], top[3], bottom[3], bottom[2], center);
581
+ appendInjectorBeltWall(sidePositions, top[3], top[0], bottom[0], bottom[3], center);
582
+ const geometry = new state.three.BufferGeometry();
583
+ geometry.setAttribute("position", new state.three.Float32BufferAttribute([...surfacePositions, ...sidePositions], 3));
584
+ geometry.addGroup(0, surfacePositions.length / 3, 0);
585
+ geometry.addGroup(surfacePositions.length / 3, sidePositions.length / 3, 1);
586
+ geometry.computeVertexNormals();
587
+ geometry.computeBoundingBox();
588
+ geometry.computeBoundingSphere();
589
+ state.primitiveGeometryCache.set(key, geometry);
590
+ state.stats.geometries += 1;
591
+ return geometry;
592
+ }
593
+ function appendInjectorBeltWall(target, topA, topB, bottomB, bottomA, center) {
594
+ const middleX = (topA[0] + topB[0]) / 2;
595
+ const middleZ = (topA[2] + topB[2]) / 2;
596
+ let normalX = topB[2] - topA[2];
597
+ let normalZ = -(topB[0] - topA[0]);
598
+ if ((middleX - center[0]) * normalX + (middleZ - center[2]) * normalZ < 0) {
599
+ normalX = -normalX;
600
+ normalZ = -normalZ;
601
+ }
602
+ appendOrientedQuad(target, topA, topB, bottomB, bottomA, [normalX, 0, normalZ]);
603
+ }
442
604
  function readProceduralBelt(visual, state) {
443
605
  const properties = visual.properties;
444
606
  if (!properties || isFalse(properties.textOf("CenterVisible"))) {
@@ -463,12 +625,12 @@ function readProceduralBelt(visual, state) {
463
625
  const beltLength = numberChildOptional(properties, "BeltLength") ??
464
626
  (serializedLength === undefined ? undefined : serializedLength - startPadding - endPadding);
465
627
  const serializedWidth = numberChildOptional(properties, "Width");
466
- const beltWidth = numberChildOptional(properties, "BeltWidth") ?? serializedWidth;
628
+ const beltWidth = numberChildOptional(properties, "BeltWidth") ?? serializedWidth ?? DEFAULT_BELT_WIDTH;
467
629
  const effectiveLength = beltLength ?? 0;
468
- const effectiveWidth = (beltWidth ?? 0) - leftPadding - rightPadding;
630
+ const effectiveWidth = beltWidth - leftPadding - rightPadding;
469
631
  const centerDiameter = numberChildOptional(properties, "CenterDiameter") ??
470
632
  numberChildOptional(properties, "BeltDiameter") ??
471
- 0;
633
+ DEFAULT_BELT_DIAMETER;
472
634
  const useBeltCenterHeight = isTrue(properties.textOf("UseBeltCenterHeight"));
473
635
  const loopHeight = useBeltCenterHeight
474
636
  ? numberChild(properties, "BeltCenterHeight", centerDiameter)
@@ -533,6 +695,131 @@ function getProceduralBeltGeometry(belt, state) {
533
695
  state.stats.geometries += 1;
534
696
  return geometry;
535
697
  }
698
+ function createCurveBeltConveyorObject(visual, state) {
699
+ const belt = readProceduralCurveBelt(visual, state);
700
+ if (!belt) {
701
+ return undefined;
702
+ }
703
+ const geometry = getProceduralCurveBeltGeometry(belt, state);
704
+ const object = new state.three.Mesh(geometry, [
705
+ getMaterial(belt.surfaceMaterial, state),
706
+ getMaterial(belt.sideMaterial, state)
707
+ ]);
708
+ object.name = visual.displayName ?? visual.id ?? visual.typeName;
709
+ object.userData.demo3d = {
710
+ kind: "procedural-belt",
711
+ id: visual.id,
712
+ name: visual.displayName,
713
+ typeName: visual.typeName,
714
+ xmlPath: visual.xml.path,
715
+ innerRadius: belt.innerRadius,
716
+ width: belt.width,
717
+ loopHeight: belt.thickness,
718
+ angle: belt.angle
719
+ };
720
+ state.stats.meshes += 1;
721
+ state.stats.directVisuals += 1;
722
+ state.stats.proceduralBelts += 1;
723
+ return object;
724
+ }
725
+ function readProceduralCurveBelt(visual, state) {
726
+ const properties = visual.properties;
727
+ if (!properties || isFalse(properties.textOf("CenterVisible"))) {
728
+ return undefined;
729
+ }
730
+ const centerProfile = properties.child("CenterProfile");
731
+ if (centerProfile && (centerProfile.children.length > 0 || centerProfile.text.length > 0)) {
732
+ warn(state, {
733
+ code: "DEMO3D_THREE_UNSUPPORTED_BELT_PROFILE",
734
+ message: "Curve belt conveyors with a custom CenterProfile are not supported.",
735
+ sourceId: visual.id,
736
+ sourceType: visual.typeName,
737
+ xmlPath: visual.xml.path
738
+ });
739
+ return undefined;
740
+ }
741
+ const innerRadius = numberChild(properties, "InnerRadius", 0.5);
742
+ const width = numberChildOptional(properties, "BeltWidth")
743
+ ?? numberChildOptional(properties, "Width")
744
+ ?? DEFAULT_BELT_WIDTH;
745
+ const beltDiameter = numberChildOptional(properties, "CenterDiameter") ??
746
+ numberChildOptional(properties, "BeltDiameter") ??
747
+ DEFAULT_BELT_DIAMETER;
748
+ const thickness = isTrue(properties.textOf("UseBeltCenterHeight"))
749
+ ? numberChild(properties, "BeltCenterHeight", beltDiameter)
750
+ : beltDiameter;
751
+ const angle = degreesToRadians(numberChild(properties, "Angle", 90));
752
+ const stepAngle = Math.abs(degreesToRadians(numberChild(properties, "StepAngle", 15)));
753
+ if (innerRadius <= 0 || width <= 0 || thickness <= 0 || Math.abs(angle) <= 1e-6) {
754
+ warn(state, {
755
+ code: "DEMO3D_THREE_INVALID_BELT_DIMENSIONS",
756
+ message: `Curve belt conveyor dimensions must be positive and its angle non-zero (inner radius ${innerRadius}, width ${width}, height ${thickness}, angle ${numberChild(properties, "Angle", 90)}).`,
757
+ sourceId: visual.id,
758
+ sourceType: visual.typeName,
759
+ xmlPath: visual.xml.path
760
+ });
761
+ return undefined;
762
+ }
763
+ const surfaceMaterial = firstMaterial(properties.child("SurfaceMaterial") ?? properties);
764
+ const sideMaterial = firstMaterial(properties.child("SurfaceSideMaterial") ?? properties) ?? surfaceMaterial;
765
+ return {
766
+ innerRadius,
767
+ width,
768
+ thickness,
769
+ angle,
770
+ stepAngle: stepAngle > 1e-6 ? stepAngle : Math.PI / 12,
771
+ surfaceMaterial,
772
+ sideMaterial
773
+ };
774
+ }
775
+ function getProceduralCurveBeltGeometry(belt, state) {
776
+ const key = `curve-belt:${belt.innerRadius}:${belt.width}:${belt.thickness}:${belt.angle}:${belt.stepAngle}`;
777
+ const cached = state.primitiveGeometryCache.get(key);
778
+ if (cached) {
779
+ return cached;
780
+ }
781
+ const surfacePositions = [];
782
+ const sidePositions = [];
783
+ const outerRadius = belt.innerRadius + belt.width;
784
+ const segments = Math.max(4, Math.ceil(Math.abs(belt.angle) / belt.stepAngle));
785
+ const point = (radius, y, theta) => [
786
+ radius * Math.cos(theta),
787
+ y,
788
+ radius * Math.sin(theta)
789
+ ];
790
+ for (let segment = 0; segment < segments; segment += 1) {
791
+ const theta0 = belt.angle * (segment / segments);
792
+ const theta1 = belt.angle * ((segment + 1) / segments);
793
+ const middle = (theta0 + theta1) / 2;
794
+ const innerTop0 = point(belt.innerRadius, 0, theta0);
795
+ const innerTop1 = point(belt.innerRadius, 0, theta1);
796
+ const outerTop0 = point(outerRadius, 0, theta0);
797
+ const outerTop1 = point(outerRadius, 0, theta1);
798
+ const innerBottom0 = point(belt.innerRadius, -belt.thickness, theta0);
799
+ const innerBottom1 = point(belt.innerRadius, -belt.thickness, theta1);
800
+ const outerBottom0 = point(outerRadius, -belt.thickness, theta0);
801
+ const outerBottom1 = point(outerRadius, -belt.thickness, theta1);
802
+ appendOrientedQuad(surfacePositions, innerTop0, outerTop0, outerTop1, innerTop1, [0, 1, 0]);
803
+ appendOrientedQuad(surfacePositions, innerBottom0, innerBottom1, outerBottom1, outerBottom0, [0, -1, 0]);
804
+ appendOrientedQuad(sidePositions, outerBottom0, outerBottom1, outerTop1, outerTop0, [Math.cos(middle), 0, Math.sin(middle)]);
805
+ appendOrientedQuad(sidePositions, innerBottom0, innerTop0, innerTop1, innerBottom1, [-Math.cos(middle), 0, -Math.sin(middle)]);
806
+ }
807
+ const direction = Math.sign(belt.angle) || 1;
808
+ const start = 0;
809
+ const end = belt.angle;
810
+ appendOrientedQuad(surfacePositions, point(belt.innerRadius, -belt.thickness, start), point(outerRadius, -belt.thickness, start), point(outerRadius, 0, start), point(belt.innerRadius, 0, start), [direction * Math.sin(start), 0, -direction * Math.cos(start)]);
811
+ appendOrientedQuad(surfacePositions, point(belt.innerRadius, -belt.thickness, end), point(belt.innerRadius, 0, end), point(outerRadius, 0, end), point(outerRadius, -belt.thickness, end), [-direction * Math.sin(end), 0, direction * Math.cos(end)]);
812
+ const geometry = new state.three.BufferGeometry();
813
+ geometry.setAttribute("position", new state.three.Float32BufferAttribute([...surfacePositions, ...sidePositions], 3));
814
+ geometry.addGroup(0, surfacePositions.length / 3, 0);
815
+ geometry.addGroup(surfacePositions.length / 3, sidePositions.length / 3, 1);
816
+ geometry.computeVertexNormals();
817
+ geometry.computeBoundingBox();
818
+ geometry.computeBoundingSphere();
819
+ state.primitiveGeometryCache.set(key, geometry);
820
+ state.stats.geometries += 1;
821
+ return geometry;
822
+ }
536
823
  function createRectangularBeltGeometry(belt, state) {
537
824
  const geometry = new state.three.BoxGeometry(belt.length, belt.thickness, belt.width);
538
825
  geometry.translate(belt.xOffset + belt.length / 2, -belt.thickness / 2, belt.zOffset);
@@ -577,6 +864,117 @@ function createRoundedBeltGeometry(belt, state) {
577
864
  }
578
865
  return geometry;
579
866
  }
867
+ function createRackObject(visual, state) {
868
+ const properties = visual.properties;
869
+ if (!properties) {
870
+ return undefined;
871
+ }
872
+ const frameHeight = numberChild(properties, "FrameHeight", 0);
873
+ const uprightWidth = numberChild(properties, "UprightWidth", 0.05);
874
+ const uprightDepth = numberChild(properties, "UprightDepth", 0.05);
875
+ const frameDepth = numberChildOptional(properties, "FrameDepth") ?? numberChild(properties, "BayDepth", 0);
876
+ const strutWidth = numberChild(properties, "StrutWidth", 0.05);
877
+ if (frameHeight <= 0 || frameDepth <= 0 || uprightWidth <= 0 || uprightDepth <= 0 || strutWidth <= 0) {
878
+ warn(state, {
879
+ code: "DEMO3D_THREE_INVALID_RACK_DIMENSIONS",
880
+ message: `Rack dimensions must be positive (height ${frameHeight}, depth ${frameDepth}, upright ${uprightWidth} x ${uprightDepth}, strut ${strutWidth}).`,
881
+ sourceId: visual.id,
882
+ sourceType: visual.typeName,
883
+ xmlPath: visual.xml.path
884
+ });
885
+ return undefined;
886
+ }
887
+ const numBays = Math.max(1, Math.round(numberChild(properties, "NumBays", 1)));
888
+ const bayWidth = Math.max(0, numberChild(properties, "BayWidth", 0));
889
+ const framePitch = bayWidth + uprightWidth + Math.max(0, numberChild(properties, "MinFrameGap", 0));
890
+ const framePositions = [0];
891
+ if (!isFalse(properties.child("MiddleFrames")?.textOf("Visible"))) {
892
+ for (let bay = 1; bay < numBays; bay += 1) {
893
+ framePositions.push(bay * framePitch);
894
+ }
895
+ }
896
+ if (!isFalse(properties.child("LastFrame")?.textOf("Visible")) && framePitch > 0) {
897
+ framePositions.push(numBays * framePitch);
898
+ }
899
+ const initialStrutHeight = clamp(numberChild(properties, "InitialStrutHeight", strutWidth / 2), 0, frameHeight);
900
+ const strutSpanHeight = numberChild(properties, "StrutSpanHeight", frameHeight);
901
+ const strutHeights = [initialStrutHeight];
902
+ if (strutSpanHeight > 0) {
903
+ for (let height = initialStrutHeight + strutSpanHeight; height < frameHeight - 1e-6; height += strutSpanHeight) {
904
+ strutHeights.push(height);
905
+ }
906
+ }
907
+ const extensionHeight = clamp(frameHeight - Math.max(0, numberChild(properties, "ExtensionStrutOffset", strutWidth / 2)), 0, frameHeight);
908
+ if (!strutHeights.some((height) => Math.abs(height - extensionHeight) < 1e-6)) {
909
+ strutHeights.push(extensionHeight);
910
+ }
911
+ strutHeights.sort((left, right) => left - right);
912
+ const uprightMaterial = getColorMaterial(demo3dColorValue(properties.child("UprightColor"), 0xffd3d3d3 | 0), state);
913
+ const strutMaterial = getColorMaterial(demo3dColorValue(properties.child("StrutColor"), 0xffd3d3d3 | 0), state);
914
+ const uprightGeometry = cachedBoxGeometry(uprightWidth, frameHeight, uprightDepth, state);
915
+ const strutGeometry = cachedBoxGeometry(strutWidth, strutWidth, frameDepth, state);
916
+ const group = new state.three.Group();
917
+ group.name = `${visual.displayName ?? visual.id ?? "Rack"} generated geometry`;
918
+ for (const frameX of framePositions) {
919
+ for (const z of [-frameDepth / 2, frameDepth / 2]) {
920
+ const upright = new state.three.Mesh(uprightGeometry, uprightMaterial);
921
+ upright.name = "Rack upright";
922
+ upright.position.set(frameX, frameHeight / 2, z);
923
+ upright.userData.demo3d = { kind: "procedural-rack-upright", frameX };
924
+ group.add(upright);
925
+ state.stats.meshes += 1;
926
+ }
927
+ for (const height of strutHeights) {
928
+ const strut = new state.three.Mesh(strutGeometry, strutMaterial);
929
+ strut.name = "Rack strut";
930
+ strut.position.set(frameX, height, 0);
931
+ strut.userData.demo3d = { kind: "procedural-rack-strut", frameX, height };
932
+ group.add(strut);
933
+ state.stats.meshes += 1;
934
+ }
935
+ }
936
+ group.userData.demo3d = {
937
+ kind: "procedural-rack",
938
+ id: visual.id,
939
+ name: visual.displayName,
940
+ typeName: visual.typeName,
941
+ xmlPath: visual.xml.path,
942
+ frameHeight,
943
+ frameDepth,
944
+ framePositions,
945
+ strutHeights
946
+ };
947
+ state.stats.groups += 1;
948
+ state.stats.directVisuals += 1;
949
+ state.stats.proceduralRacks += 1;
950
+ return group;
951
+ }
952
+ function demo3dColorValue(container, fallback) {
953
+ const numeric = container?.text.trim();
954
+ if (numeric) {
955
+ const parsed = Number(numeric);
956
+ if (Number.isFinite(parsed)) {
957
+ return parsed;
958
+ }
959
+ }
960
+ switch (container?.textOf("name")?.trim().toLowerCase()) {
961
+ case "black":
962
+ return 0xff000000 | 0;
963
+ case "darkgray":
964
+ case "darkgrey":
965
+ return 0xffa9a9a9 | 0;
966
+ case "gray":
967
+ case "grey":
968
+ return 0xff808080 | 0;
969
+ case "lightgray":
970
+ case "lightgrey":
971
+ return 0xffd3d3d3 | 0;
972
+ case "white":
973
+ return 0xffffffff | 0;
974
+ default:
975
+ return fallback;
976
+ }
977
+ }
580
978
  function createSupportStandObject(visual, parentVisual, state) {
581
979
  const stand = visual instanceof Demo3DSupportStand
582
980
  ? visual
@@ -586,7 +984,13 @@ function createSupportStandObject(visual, parentVisual, state) {
586
984
  const footProfile = properties?.footProfile;
587
985
  const floorPlateProfile = properties?.floorPlateProfile;
588
986
  const crossBraceProfile = properties?.crossBraceProfile;
589
- if (!properties || !legProfile || !crossBraceProfile) {
987
+ if (!properties) {
988
+ return undefined;
989
+ }
990
+ if (!legProfile && !crossBraceProfile) {
991
+ return createDefaultSupportStandObject(visual, parentVisual, properties, state);
992
+ }
993
+ if (!legProfile || !crossBraceProfile) {
590
994
  warn(state, {
591
995
  code: "DEMO3D_THREE_UNSUPPORTED_SUPPORT_STAND_PROFILE",
592
996
  message: "Support stand is missing its leg or cross-brace extrusion profile.",
@@ -654,6 +1058,84 @@ function createSupportStandObject(visual, parentVisual, state) {
654
1058
  state.stats.proceduralSupportStands += 1;
655
1059
  return group;
656
1060
  }
1061
+ const DEFAULT_CONVEYOR_HEIGHT = 0.83;
1062
+ const DEFAULT_SUPPORT_LEG_SIZE = 0.04;
1063
+ const DEFAULT_SUPPORT_FOOT_SIZE = 0.06;
1064
+ const DEFAULT_SUPPORT_FLOOR_PLATE_SIZE = 0.1;
1065
+ function createDefaultSupportStandObject(visual, parentVisual, properties, state) {
1066
+ const parentWidth = numberChildOptional(parentVisual?.properties, "RollerWidth") ??
1067
+ numberChildOptional(parentVisual?.properties, "BeltWidth") ??
1068
+ numberChildOptional(parentVisual?.properties, "Width") ??
1069
+ DEFAULT_BELT_WIDTH;
1070
+ const conveyorOffset = properties.conveyorOffset;
1071
+ const sideExtension = Math.abs(conveyorOffset[2] ?? 0);
1072
+ const span = Math.max(parentWidth + sideExtension * 2, 0.1);
1073
+ const top = conveyorOffset[1] ?? 0;
1074
+ const x = conveyorOffset[0] ?? 0;
1075
+ const floorY = numberChild(properties.xml, "FloorY", 0);
1076
+ const explicitStandHeight = numberChildOptional(properties.xml, "StandHeight") ??
1077
+ numberChildOptional(properties.xml, "SupportHeight") ??
1078
+ numberChildOptional(properties.xml, "Height");
1079
+ const explicitLegHeight = numberChildOptional(properties.xml, "LegHeight");
1080
+ const conveyorHeight = numberChildOptional(parentVisual?.properties, "ConveyorHeight") ?? DEFAULT_CONVEYOR_HEIGHT;
1081
+ const componentHeight = explicitLegHeight === undefined
1082
+ ? undefined
1083
+ : explicitLegHeight + properties.footHeight + properties.floorPlateHeight;
1084
+ const supportHeight = Math.max(explicitStandHeight ?? componentHeight ?? conveyorHeight + top - floorY, properties.footHeight + properties.floorPlateHeight + 0.001);
1085
+ const bottom = top - supportHeight;
1086
+ const floorPlateHeight = clamp(properties.floorPlateHeight, 0, supportHeight);
1087
+ const footHeight = clamp(properties.footHeight, 0, supportHeight - floorPlateHeight);
1088
+ const legHeight = Math.max(supportHeight - floorPlateHeight - footHeight, 0.001);
1089
+ const braces = [...properties.crossBraceHeights]
1090
+ .filter((height) => height > 0 && height < supportHeight)
1091
+ .sort((left, right) => left - right);
1092
+ const group = new state.three.Group();
1093
+ group.name = `${visual.displayName ?? visual.id ?? "Support stand"} default geometry`;
1094
+ const legMaterial = supportComponentMaterial(visual.properties?.child("LegMaterial"), properties.legMaterial, state);
1095
+ const footMaterial = supportComponentMaterial(visual.properties?.child("FootMaterial"), properties.footMaterial, state);
1096
+ const floorPlateMaterial = supportComponentMaterial(visual.properties?.child("FloorPlateMaterial"), properties.floorPlateMaterial, state);
1097
+ const braceMaterial = supportComponentMaterial(visual.properties?.child("CrossBraceMaterial"), properties.crossBraceMaterial, state);
1098
+ for (const z of [-span / 2, span / 2]) {
1099
+ addDefaultSupportBox(group, "leg", DEFAULT_SUPPORT_LEG_SIZE, legHeight, DEFAULT_SUPPORT_LEG_SIZE, [x, top - legHeight / 2, z], legMaterial, state);
1100
+ if (footHeight > 0) {
1101
+ addDefaultSupportBox(group, "foot", DEFAULT_SUPPORT_FOOT_SIZE, footHeight, DEFAULT_SUPPORT_FOOT_SIZE, [x, bottom + floorPlateHeight + footHeight / 2, z], footMaterial, state);
1102
+ }
1103
+ if (floorPlateHeight > 0) {
1104
+ addDefaultSupportBox(group, "floor plate", DEFAULT_SUPPORT_FLOOR_PLATE_SIZE, floorPlateHeight, DEFAULT_SUPPORT_FLOOR_PLATE_SIZE, [x, bottom + floorPlateHeight / 2, z], floorPlateMaterial, state);
1105
+ }
1106
+ }
1107
+ for (const height of braces) {
1108
+ addDefaultSupportBox(group, "cross brace", DEFAULT_SUPPORT_LEG_SIZE, DEFAULT_SUPPORT_LEG_SIZE, span, [x, bottom + height, 0], braceMaterial, state);
1109
+ }
1110
+ addDefaultSupportBox(group, "top brace", DEFAULT_SUPPORT_LEG_SIZE, DEFAULT_SUPPORT_LEG_SIZE, span, [x, top - DEFAULT_SUPPORT_LEG_SIZE / 2, 0], braceMaterial, state);
1111
+ group.userData.demo3d = {
1112
+ kind: "procedural-support-stand",
1113
+ id: visual.id,
1114
+ name: visual.displayName,
1115
+ typeName: visual.typeName,
1116
+ xmlPath: visual.xml.path,
1117
+ supportHeight,
1118
+ span,
1119
+ braceHeights: braces,
1120
+ approximate: true,
1121
+ defaultProfiles: true
1122
+ };
1123
+ state.stats.groups += 1;
1124
+ state.stats.directVisuals += 1;
1125
+ state.stats.proceduralSupportStands += 1;
1126
+ return group;
1127
+ }
1128
+ function supportComponentMaterial(container, fallbackColor, state) {
1129
+ return materialFromContainer(container, state) ?? getColorMaterial(fallbackColor, state);
1130
+ }
1131
+ function addDefaultSupportBox(group, component, width, height, depth, position, material, state) {
1132
+ const mesh = new state.three.Mesh(cachedBoxGeometry(width, height, depth, state), material);
1133
+ mesh.name = `Default support ${component}`;
1134
+ mesh.position.set(position[0], position[1], position[2]);
1135
+ mesh.userData.demo3d = { kind: "support-stand-component", component, approximate: true };
1136
+ group.add(mesh);
1137
+ state.stats.meshes += 1;
1138
+ }
657
1139
  function addSupportProfileMeshes(group, profile, axis, length, position, fallbackColor, component, state) {
658
1140
  profile.polygons.forEach((polygon, index) => {
659
1141
  if (polygon.points.length < 3) {
@@ -932,6 +1414,25 @@ function addProceduralRollers(group, visual, state) {
932
1414
  const material = materialFromContainer(properties.child("SurfaceMaterial") ?? properties, state)
933
1415
  ?? getColorMaterial(numberChildOptional(properties, "RollerColor"), state);
934
1416
  const geometry = cachedRollerGeometry(radius, width, state);
1417
+ if (visual.typeName === "e3d:DiverterRollerConveyor") {
1418
+ const length = numberChild(properties, "Length", 1);
1419
+ const pitch = Math.max(numberChild(properties, "RollerPitch", diameter * 1.2), diameter);
1420
+ const count = Math.max(1, Math.round(numberChildOptional(properties, "RollerCount") ?? length / pitch));
1421
+ const across = Math.max(1, Math.round(numberChild(properties, "NumRollersAcrossWidth", 1)));
1422
+ const totalWidth = numberChild(properties, "Width", width);
1423
+ for (let index = 0; index < count; index += 1) {
1424
+ for (let acrossIndex = 0; acrossIndex < across; acrossIndex += 1) {
1425
+ const roller = new state.three.Mesh(geometry, material);
1426
+ roller.name = `Generated divert roller ${index + 1}.${acrossIndex + 1}`;
1427
+ roller.position.set(((index + 0.5) / count) * length, 0, ((acrossIndex + 0.5) / across - 0.5) * totalWidth);
1428
+ roller.userData.demo3d = { kind: "procedural-roller", index, acrossIndex };
1429
+ group.add(roller);
1430
+ state.stats.meshes += 1;
1431
+ state.stats.proceduralRollers += 1;
1432
+ }
1433
+ }
1434
+ return;
1435
+ }
935
1436
  if (visual.typeName === "e3d:CurveRollerConveyor") {
936
1437
  const count = Math.max(1, Math.round(numberChild(properties, "RollerCount", 1)));
937
1438
  const innerRadius = numberChild(properties, "InnerRadius", 0.5);
@@ -1045,6 +1546,7 @@ function cachedBoxGeometry(width, height, depth, state) {
1045
1546
  function isRollerConveyorType(typeName) {
1046
1547
  return (typeName === "e3d:StraightRollerConveyor" ||
1047
1548
  typeName === "e3d:CurveRollerConveyor" ||
1549
+ typeName === "e3d:DiverterRollerConveyor" ||
1048
1550
  typeName === "e3d:InjectorRollerConveyor");
1049
1551
  }
1050
1552
  function createImportedImageObject(visual, state) {
@@ -1207,15 +1709,15 @@ function createMeshObject(meshId, source, material, state) {
1207
1709
  sourceType,
1208
1710
  xmlPath
1209
1711
  });
1210
- return state.options.showPlaceholders === false
1211
- ? undefined
1212
- : createPlaceholderMesh(sourceId ?? meshId, meshId, material, state);
1712
+ return state.options.showPlaceholders === true
1713
+ ? createPlaceholderMesh(sourceId ?? meshId, meshId, material, state)
1714
+ : undefined;
1213
1715
  }
1214
1716
  const geometry = getGeometry(mesh, state);
1215
1717
  if (!geometry) {
1216
- return state.options.showPlaceholders === false
1217
- ? undefined
1218
- : createPlaceholderMesh(sourceId ?? meshId, meshId, material, state);
1718
+ return state.options.showPlaceholders === true
1719
+ ? createPlaceholderMesh(sourceId ?? meshId, meshId, material, state)
1720
+ : undefined;
1219
1721
  }
1220
1722
  const object = new state.three.Mesh(geometry, getMaterial(material, state));
1221
1723
  object.name = source instanceof Demo3DVisual ? source.displayName ?? meshId : meshId;
@@ -1904,6 +2406,18 @@ function findMeshReferenceIds(root) {
1904
2406
  });
1905
2407
  return [...ids];
1906
2408
  }
2409
+ function findVisualMeshReferenceIds(root) {
2410
+ const ids = new Set();
2411
+ for (const child of root.children) {
2412
+ if (child.localName === "C") {
2413
+ continue;
2414
+ }
2415
+ for (const id of findMeshReferenceIds(child)) {
2416
+ ids.add(id);
2417
+ }
2418
+ }
2419
+ return [...ids];
2420
+ }
1907
2421
  function firstMaterial(root) {
1908
2422
  let found;
1909
2423
  visit(root, (node) => {
@@ -2056,6 +2570,11 @@ function isRenderableVisualVisible(visual) {
2056
2570
  const visible = visual.properties?.textOf("Visible");
2057
2571
  return visible === undefined || !(visible === "0" || visible.toLowerCase() === "false");
2058
2572
  }
2573
+ function isLayerVisible(layerName, state) {
2574
+ return (state.options.includeHiddenLayers === true ||
2575
+ layerName === undefined ||
2576
+ state.layerVisibilityByName.get(layerName) !== false);
2577
+ }
2059
2578
  function isTrue(value) {
2060
2579
  return value === "1" || value?.toLowerCase() === "true";
2061
2580
  }