@jfk-solutions/demo3d-file-format 1.4.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.
@@ -151,6 +151,7 @@ function createState(three, project, options, buffers) {
151
151
  lines: 0,
152
152
  directVisuals: 0,
153
153
  proceduralBelts: 0,
154
+ proceduralRacks: 0,
154
155
  proceduralSupportStands: 0,
155
156
  proceduralConveyorSides: 0,
156
157
  proceduralPhotoEyes: 0,
@@ -380,9 +381,16 @@ function createDirectVisualObject(visual, state, parentVisual) {
380
381
  if (visual.typeName === "e3d:ImportedImageVisual") {
381
382
  return createImportedImageObject(visual, state);
382
383
  }
383
- if (visual.typeName === "e3d:StraightBeltConveyor") {
384
+ if (visual.typeName === "e3d:StraightBeltConveyor" ||
385
+ visual.typeName === "e3d:CurveBeltConveyor" ||
386
+ visual.typeName === "e3d:InjectorBeltConveyor") {
384
387
  return state.options.renderProceduralBelts === true
385
- ? 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)
386
394
  : undefined;
387
395
  }
388
396
  if (visual.typeName === "e3d:SupportStand") {
@@ -415,7 +423,16 @@ function createDirectVisualObject(visual, state, parentVisual) {
415
423
  state.stats.directVisuals += 1;
416
424
  return object;
417
425
  }
418
- 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
+ }
419
436
  const belt = readProceduralBelt(visual, state);
420
437
  if (!belt) {
421
438
  return undefined;
@@ -444,6 +461,146 @@ function createStraightBeltConveyorObject(visual, state) {
444
461
  state.stats.proceduralBelts += 1;
445
462
  return object;
446
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
+ }
447
604
  function readProceduralBelt(visual, state) {
448
605
  const properties = visual.properties;
449
606
  if (!properties || isFalse(properties.textOf("CenterVisible"))) {
@@ -468,12 +625,12 @@ function readProceduralBelt(visual, state) {
468
625
  const beltLength = numberChildOptional(properties, "BeltLength") ??
469
626
  (serializedLength === undefined ? undefined : serializedLength - startPadding - endPadding);
470
627
  const serializedWidth = numberChildOptional(properties, "Width");
471
- const beltWidth = numberChildOptional(properties, "BeltWidth") ?? serializedWidth;
628
+ const beltWidth = numberChildOptional(properties, "BeltWidth") ?? serializedWidth ?? DEFAULT_BELT_WIDTH;
472
629
  const effectiveLength = beltLength ?? 0;
473
- const effectiveWidth = (beltWidth ?? 0) - leftPadding - rightPadding;
630
+ const effectiveWidth = beltWidth - leftPadding - rightPadding;
474
631
  const centerDiameter = numberChildOptional(properties, "CenterDiameter") ??
475
632
  numberChildOptional(properties, "BeltDiameter") ??
476
- 0;
633
+ DEFAULT_BELT_DIAMETER;
477
634
  const useBeltCenterHeight = isTrue(properties.textOf("UseBeltCenterHeight"));
478
635
  const loopHeight = useBeltCenterHeight
479
636
  ? numberChild(properties, "BeltCenterHeight", centerDiameter)
@@ -538,6 +695,131 @@ function getProceduralBeltGeometry(belt, state) {
538
695
  state.stats.geometries += 1;
539
696
  return geometry;
540
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
+ }
541
823
  function createRectangularBeltGeometry(belt, state) {
542
824
  const geometry = new state.three.BoxGeometry(belt.length, belt.thickness, belt.width);
543
825
  geometry.translate(belt.xOffset + belt.length / 2, -belt.thickness / 2, belt.zOffset);
@@ -582,6 +864,117 @@ function createRoundedBeltGeometry(belt, state) {
582
864
  }
583
865
  return geometry;
584
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
+ }
585
978
  function createSupportStandObject(visual, parentVisual, state) {
586
979
  const stand = visual instanceof Demo3DSupportStand
587
980
  ? visual
@@ -591,7 +984,13 @@ function createSupportStandObject(visual, parentVisual, state) {
591
984
  const footProfile = properties?.footProfile;
592
985
  const floorPlateProfile = properties?.floorPlateProfile;
593
986
  const crossBraceProfile = properties?.crossBraceProfile;
594
- 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) {
595
994
  warn(state, {
596
995
  code: "DEMO3D_THREE_UNSUPPORTED_SUPPORT_STAND_PROFILE",
597
996
  message: "Support stand is missing its leg or cross-brace extrusion profile.",
@@ -659,6 +1058,84 @@ function createSupportStandObject(visual, parentVisual, state) {
659
1058
  state.stats.proceduralSupportStands += 1;
660
1059
  return group;
661
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
+ }
662
1139
  function addSupportProfileMeshes(group, profile, axis, length, position, fallbackColor, component, state) {
663
1140
  profile.polygons.forEach((polygon, index) => {
664
1141
  if (polygon.points.length < 3) {
@@ -937,6 +1414,25 @@ function addProceduralRollers(group, visual, state) {
937
1414
  const material = materialFromContainer(properties.child("SurfaceMaterial") ?? properties, state)
938
1415
  ?? getColorMaterial(numberChildOptional(properties, "RollerColor"), state);
939
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
+ }
940
1436
  if (visual.typeName === "e3d:CurveRollerConveyor") {
941
1437
  const count = Math.max(1, Math.round(numberChild(properties, "RollerCount", 1)));
942
1438
  const innerRadius = numberChild(properties, "InnerRadius", 0.5);
@@ -1050,6 +1546,7 @@ function cachedBoxGeometry(width, height, depth, state) {
1050
1546
  function isRollerConveyorType(typeName) {
1051
1547
  return (typeName === "e3d:StraightRollerConveyor" ||
1052
1548
  typeName === "e3d:CurveRollerConveyor" ||
1549
+ typeName === "e3d:DiverterRollerConveyor" ||
1053
1550
  typeName === "e3d:InjectorRollerConveyor");
1054
1551
  }
1055
1552
  function createImportedImageObject(visual, state) {