@jfk-solutions/demo3d-file-format 1.0.0 → 1.1.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.
- package/README.md +22 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/model.d.ts +85 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +287 -1
- package/dist/model.js.map +1 -1
- package/dist/three/index.d.ts +13 -0
- package/dist/three/index.d.ts.map +1 -1
- package/dist/three/index.js +787 -19
- package/dist/three/index.js.map +1 -1
- package/package.json +1 -1
package/dist/three/index.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
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,473 @@ 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
|
+
!visualTreeHasAspectType(visual, "e3d:CylinderRendererAspect", 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 centerRadius = innerRadius + width / 2;
|
|
843
|
+
const frames = [];
|
|
844
|
+
for (let index = 0; index <= segments; index += 1) {
|
|
845
|
+
const theta = angle * (index / segments);
|
|
846
|
+
frames.push({
|
|
847
|
+
center: [
|
|
848
|
+
pathRadius * Math.sin(theta),
|
|
849
|
+
0,
|
|
850
|
+
pathRadius * Math.cos(theta) - centerRadius
|
|
851
|
+
],
|
|
852
|
+
lateral: [Math.sin(theta), 0, Math.cos(theta)]
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
return frames;
|
|
856
|
+
}
|
|
857
|
+
const length = visual.typeName === "e3d:InjectorRollerConveyor"
|
|
858
|
+
? Math.max(numberChild(properties, "ShortSideLength", 0), numberChild(properties, "ExtraLength", 0), numberChild(properties, "RollerPitch", 0.1))
|
|
859
|
+
: numberChild(properties, "Length", 1);
|
|
860
|
+
const z = (side === "left" ? -1 : 1) * width / 2;
|
|
861
|
+
return [
|
|
862
|
+
{ center: [0, 0, z], lateral: [0, 0, 1] },
|
|
863
|
+
{ center: [length, 0, z], lateral: [0, 0, 1] }
|
|
864
|
+
];
|
|
865
|
+
}
|
|
866
|
+
function getSweptProfileGeometry(profile, polygon, frames, state) {
|
|
867
|
+
const pointsKey = polygon.points.map((point) => `${point.x},${point.y}`).join(";");
|
|
868
|
+
const framesKey = frames
|
|
869
|
+
.map((frame) => `${frame.center.join(",")}/${frame.lateral.join(",")}`)
|
|
870
|
+
.join(";");
|
|
871
|
+
const key = `sweep:${profile.anchor.x},${profile.anchor.y}:${polygon.materials.length}:${pointsKey}:${framesKey}`;
|
|
872
|
+
const cached = state.primitiveGeometryCache.get(key);
|
|
873
|
+
if (cached) {
|
|
874
|
+
return cached;
|
|
875
|
+
}
|
|
876
|
+
const positions = [];
|
|
877
|
+
const indices = [];
|
|
878
|
+
const pointCount = polygon.points.length;
|
|
879
|
+
for (const frame of frames) {
|
|
880
|
+
for (const point of polygon.points) {
|
|
881
|
+
const lateral = point.x - profile.anchor.x;
|
|
882
|
+
const vertical = point.y - profile.anchor.y;
|
|
883
|
+
positions.push(frame.center[0] + frame.lateral[0] * lateral, frame.center[1] + vertical, frame.center[2] + frame.lateral[2] * lateral);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
const groups = [];
|
|
887
|
+
for (let point = 0; point < pointCount; point += 1) {
|
|
888
|
+
const start = indices.length;
|
|
889
|
+
for (let frame = 0; frame < frames.length - 1; frame += 1) {
|
|
890
|
+
const current = frame * pointCount;
|
|
891
|
+
const next = (frame + 1) * pointCount;
|
|
892
|
+
const following = (point + 1) % pointCount;
|
|
893
|
+
indices.push(current + point, next + point, current + following, current + following, next + point, next + following);
|
|
894
|
+
}
|
|
895
|
+
groups.push({
|
|
896
|
+
start,
|
|
897
|
+
count: indices.length - start,
|
|
898
|
+
materialIndex: Math.min(point, Math.max(0, polygon.materials.length - 1))
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
const shapePoints = polygon.points.map((point) => new state.three.Vector2(point.x - profile.anchor.x, point.y - profile.anchor.y));
|
|
902
|
+
const capTriangles = state.three.ShapeUtils.triangulateShape(shapePoints, []);
|
|
903
|
+
const finalOffset = (frames.length - 1) * pointCount;
|
|
904
|
+
const capStart = indices.length;
|
|
905
|
+
for (const triangle of capTriangles) {
|
|
906
|
+
indices.push(triangle[2], triangle[1], triangle[0]);
|
|
907
|
+
indices.push(finalOffset + triangle[0], finalOffset + triangle[1], finalOffset + triangle[2]);
|
|
908
|
+
}
|
|
909
|
+
const geometry = new state.three.BufferGeometry();
|
|
910
|
+
geometry.setAttribute("position", new state.three.Float32BufferAttribute(positions, 3));
|
|
911
|
+
geometry.setIndex(indices);
|
|
912
|
+
for (const group of groups) {
|
|
913
|
+
geometry.addGroup(group.start, group.count, group.materialIndex);
|
|
914
|
+
}
|
|
915
|
+
geometry.addGroup(capStart, indices.length - capStart, 0);
|
|
916
|
+
geometry.computeVertexNormals();
|
|
917
|
+
geometry.computeBoundingBox();
|
|
918
|
+
geometry.computeBoundingSphere();
|
|
919
|
+
state.primitiveGeometryCache.set(key, geometry);
|
|
920
|
+
state.stats.geometries += 1;
|
|
921
|
+
return geometry;
|
|
922
|
+
}
|
|
923
|
+
function addProceduralRollers(group, visual, state) {
|
|
924
|
+
const properties = visual.properties;
|
|
925
|
+
if (!properties) {
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
const width = numberChildOptional(properties, "RollerWidth") ??
|
|
929
|
+
numberChildOptional(properties, "Width") ??
|
|
930
|
+
0.5;
|
|
931
|
+
const diameter = numberChild(properties, "RollerDiameter", 0.05);
|
|
932
|
+
const radius = diameter / 2;
|
|
933
|
+
const material = materialFromContainer(properties.child("SurfaceMaterial") ?? properties, state)
|
|
934
|
+
?? getColorMaterial(numberChildOptional(properties, "RollerColor"), state);
|
|
935
|
+
const geometry = cachedRollerGeometry(radius, width, state);
|
|
936
|
+
if (visual.typeName === "e3d:CurveRollerConveyor") {
|
|
937
|
+
const count = Math.max(1, Math.round(numberChild(properties, "RollerCount", 1)));
|
|
938
|
+
const innerRadius = numberChild(properties, "InnerRadius", 0.5);
|
|
939
|
+
const angle = degreesToRadians(numberChild(properties, "Angle", 90));
|
|
940
|
+
const centerRadius = innerRadius + width / 2;
|
|
941
|
+
for (let index = 0; index < count; index += 1) {
|
|
942
|
+
const theta = count === 1 ? angle / 2 : angle * (index / (count - 1));
|
|
943
|
+
const roller = new state.three.Mesh(geometry, material);
|
|
944
|
+
roller.name = `Generated roller ${index + 1}`;
|
|
945
|
+
roller.position.set(centerRadius * Math.sin(theta), 0, centerRadius * Math.cos(theta) - centerRadius);
|
|
946
|
+
roller.rotation.y = theta;
|
|
947
|
+
roller.userData.demo3d = { kind: "procedural-roller", index };
|
|
948
|
+
group.add(roller);
|
|
949
|
+
state.stats.meshes += 1;
|
|
950
|
+
state.stats.proceduralRollers += 1;
|
|
951
|
+
}
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
const length = visual.typeName === "e3d:InjectorRollerConveyor"
|
|
955
|
+
? Math.max(numberChild(properties, "ShortSideLength", 0), numberChild(properties, "RollerPitch", 0.1))
|
|
956
|
+
: numberChild(properties, "Length", 1);
|
|
957
|
+
const pitch = Math.max(numberChild(properties, "RollerPitch", diameter * 1.2), diameter);
|
|
958
|
+
const explicitCount = numberChildOptional(properties, "RollerCount");
|
|
959
|
+
const count = Math.max(1, Math.round(explicitCount ?? length / pitch));
|
|
960
|
+
for (let index = 0; index < count; index += 1) {
|
|
961
|
+
const roller = new state.three.Mesh(geometry, material);
|
|
962
|
+
roller.name = `Generated roller ${index + 1}`;
|
|
963
|
+
roller.position.x = ((index + 0.5) / count) * length;
|
|
964
|
+
roller.userData.demo3d = { kind: "procedural-roller", index };
|
|
965
|
+
group.add(roller);
|
|
966
|
+
state.stats.meshes += 1;
|
|
967
|
+
state.stats.proceduralRollers += 1;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
function cachedRollerGeometry(radius, width, state) {
|
|
971
|
+
const key = `roller:${radius}:${width}`;
|
|
972
|
+
const cached = state.primitiveGeometryCache.get(key);
|
|
973
|
+
if (cached) {
|
|
974
|
+
return cached;
|
|
975
|
+
}
|
|
976
|
+
const geometry = new state.three.CylinderGeometry(radius, radius, width, 16);
|
|
977
|
+
geometry.rotateX(Math.PI / 2);
|
|
978
|
+
geometry.computeBoundingBox();
|
|
979
|
+
geometry.computeBoundingSphere();
|
|
980
|
+
state.primitiveGeometryCache.set(key, geometry);
|
|
981
|
+
state.stats.geometries += 1;
|
|
982
|
+
return geometry;
|
|
983
|
+
}
|
|
984
|
+
function addProceduralMotor(group, visual, state) {
|
|
985
|
+
const properties = visual.properties;
|
|
986
|
+
if (!properties) {
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
const width = numberChildOptional(properties, "RollerWidth") ??
|
|
990
|
+
numberChildOptional(properties, "Width") ??
|
|
991
|
+
0.5;
|
|
992
|
+
const length = numberChildOptional(properties, "Length") ?? 0.2;
|
|
993
|
+
const diameter = numberChild(properties, "RollerDiameter", 0.05);
|
|
994
|
+
const size = Math.max(diameter * 1.6, 0.06);
|
|
995
|
+
const geometry = cachedBoxGeometry(size * 1.4, size, size, state);
|
|
996
|
+
const motor = new state.three.Mesh(geometry, getColorMaterial(0xff303840 | 0, state));
|
|
997
|
+
motor.name = "Generated conveyor motor";
|
|
998
|
+
motor.position.set(length * 0.75, -size / 2, width / 2 + size / 2);
|
|
999
|
+
motor.userData.demo3d = { kind: "procedural-conveyor-motor", approximate: true };
|
|
1000
|
+
group.add(motor);
|
|
1001
|
+
state.stats.meshes += 1;
|
|
1002
|
+
state.stats.proceduralMotors += 1;
|
|
1003
|
+
}
|
|
1004
|
+
function visualTreeHasAspectType(visual, typeName, state) {
|
|
1005
|
+
const stack = [visual];
|
|
1006
|
+
while (stack.length > 0) {
|
|
1007
|
+
const current = stack.pop();
|
|
1008
|
+
if (!current) {
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
if (findVisualAspects(current, state).some((aspect) => aspect.xsiType === typeName)) {
|
|
1012
|
+
return true;
|
|
1013
|
+
}
|
|
1014
|
+
stack.push(...current.children);
|
|
1015
|
+
}
|
|
1016
|
+
return false;
|
|
1017
|
+
}
|
|
1018
|
+
function hasRenderableMotorChild(visual, state) {
|
|
1019
|
+
return visual.children.some((child) => {
|
|
1020
|
+
const name = child.displayName?.toLowerCase() ?? "";
|
|
1021
|
+
return (name.includes("motor") || name.includes("transmission")) && findVisualAspects(child, state).length > 0;
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
function materialFromContainer(root, state) {
|
|
1025
|
+
if (!root || !state) {
|
|
1026
|
+
return undefined;
|
|
1027
|
+
}
|
|
1028
|
+
const material = firstMaterial(root);
|
|
1029
|
+
return material ? getMaterial(material, state) : undefined;
|
|
1030
|
+
}
|
|
1031
|
+
function cachedBoxGeometry(width, height, depth, state) {
|
|
1032
|
+
const key = `box:${width}:${height}:${depth}`;
|
|
1033
|
+
const cached = state.primitiveGeometryCache.get(key);
|
|
1034
|
+
if (cached) {
|
|
1035
|
+
return cached;
|
|
1036
|
+
}
|
|
1037
|
+
const geometry = new state.three.BoxGeometry(width, height, depth);
|
|
1038
|
+
state.primitiveGeometryCache.set(key, geometry);
|
|
1039
|
+
state.stats.geometries += 1;
|
|
1040
|
+
return geometry;
|
|
1041
|
+
}
|
|
1042
|
+
function isRollerConveyorType(typeName) {
|
|
1043
|
+
return (typeName === "e3d:StraightRollerConveyor" ||
|
|
1044
|
+
typeName === "e3d:CurveRollerConveyor" ||
|
|
1045
|
+
typeName === "e3d:InjectorRollerConveyor");
|
|
1046
|
+
}
|
|
518
1047
|
function createImportedImageObject(visual, state) {
|
|
519
1048
|
const properties = visual.properties;
|
|
520
1049
|
if (!properties) {
|
|
@@ -747,13 +1276,16 @@ function getPrimitiveGeometry(aspect, renderable, state) {
|
|
|
747
1276
|
let geometry;
|
|
748
1277
|
if (type === "e3d:CylinderRendererAspect") {
|
|
749
1278
|
const radius = numberChild(renderable, "Radius", 0.5);
|
|
1279
|
+
const innerRadius = numberChild(renderable, "InnerRadius", 0);
|
|
750
1280
|
const radiusRatio = numberChild(renderable, "RadiusRatio", 1);
|
|
751
1281
|
const coneRatio = numberChild(renderable, "ConeRatio", 1);
|
|
752
1282
|
const length = numberChild(renderable, "Length", 1);
|
|
753
1283
|
const slices = Math.max(3, Math.round(numberChild(renderable, "Slices", 16)));
|
|
754
1284
|
const startAngle = degreesToRadians(numberChild(renderable, "StartAngle", 0));
|
|
755
1285
|
const angle = degreesToRadians(numberChild(renderable, "Angle", 360));
|
|
756
|
-
geometry =
|
|
1286
|
+
geometry = innerRadius > 0 && innerRadius < Math.min(radius * radiusRatio, radius * coneRatio)
|
|
1287
|
+
? createAnnularCylinderGeometry(radius * radiusRatio, radius * coneRatio, innerRadius, length, slices, startAngle, angle, state)
|
|
1288
|
+
: new state.three.CylinderGeometry(radius * radiusRatio, radius * coneRatio, length, slices, 1, false, startAngle, angle);
|
|
757
1289
|
}
|
|
758
1290
|
else if (type === "e3d:BoxRendererAspect") {
|
|
759
1291
|
geometry = new state.three.BoxGeometry(numberChild(renderable, "Width", 1), numberChild(renderable, "Height", 1), numberChild(renderable, "Depth", 1));
|
|
@@ -771,12 +1303,71 @@ function getPrimitiveGeometry(aspect, renderable, state) {
|
|
|
771
1303
|
state.stats.geometries += 1;
|
|
772
1304
|
return geometry;
|
|
773
1305
|
}
|
|
1306
|
+
function createAnnularCylinderGeometry(topRadius, bottomRadius, innerRadius, length, slices, startAngle, angle, state) {
|
|
1307
|
+
const positions = [];
|
|
1308
|
+
const halfLength = length / 2;
|
|
1309
|
+
const fullCircle = Math.abs(angle) >= Math.PI * 2 - 1e-6;
|
|
1310
|
+
const segments = Math.max(1, Math.round(slices * Math.abs(angle) / (Math.PI * 2)));
|
|
1311
|
+
const point = (radius, y, theta) => [
|
|
1312
|
+
radius * Math.cos(theta),
|
|
1313
|
+
y,
|
|
1314
|
+
radius * Math.sin(theta)
|
|
1315
|
+
];
|
|
1316
|
+
const addTriangle = (a, b, c, normal) => {
|
|
1317
|
+
const abx = b[0] - a[0];
|
|
1318
|
+
const aby = b[1] - a[1];
|
|
1319
|
+
const abz = b[2] - a[2];
|
|
1320
|
+
const acx = c[0] - a[0];
|
|
1321
|
+
const acy = c[1] - a[1];
|
|
1322
|
+
const acz = c[2] - a[2];
|
|
1323
|
+
const crossX = aby * acz - abz * acy;
|
|
1324
|
+
const crossY = abz * acx - abx * acz;
|
|
1325
|
+
const crossZ = abx * acy - aby * acx;
|
|
1326
|
+
const forward = crossX * normal[0] + crossY * normal[1] + crossZ * normal[2] >= 0;
|
|
1327
|
+
const second = forward ? b : c;
|
|
1328
|
+
const third = forward ? c : b;
|
|
1329
|
+
positions.push(...a, ...second, ...third);
|
|
1330
|
+
};
|
|
1331
|
+
const addQuad = (a, b, c, d, normal) => {
|
|
1332
|
+
addTriangle(a, b, c, normal);
|
|
1333
|
+
addTriangle(a, c, d, normal);
|
|
1334
|
+
};
|
|
1335
|
+
for (let segment = 0; segment < segments; segment += 1) {
|
|
1336
|
+
const theta0 = startAngle + angle * (segment / segments);
|
|
1337
|
+
const theta1 = startAngle + angle * ((segment + 1) / segments);
|
|
1338
|
+
const middle = (theta0 + theta1) / 2;
|
|
1339
|
+
const outerBottom0 = point(bottomRadius, -halfLength, theta0);
|
|
1340
|
+
const outerBottom1 = point(bottomRadius, -halfLength, theta1);
|
|
1341
|
+
const outerTop0 = point(topRadius, halfLength, theta0);
|
|
1342
|
+
const outerTop1 = point(topRadius, halfLength, theta1);
|
|
1343
|
+
const innerBottom0 = point(innerRadius, -halfLength, theta0);
|
|
1344
|
+
const innerBottom1 = point(innerRadius, -halfLength, theta1);
|
|
1345
|
+
const innerTop0 = point(innerRadius, halfLength, theta0);
|
|
1346
|
+
const innerTop1 = point(innerRadius, halfLength, theta1);
|
|
1347
|
+
addQuad(outerBottom0, outerBottom1, outerTop1, outerTop0, [Math.cos(middle), 0, Math.sin(middle)]);
|
|
1348
|
+
addQuad(innerBottom0, innerTop0, innerTop1, innerBottom1, [-Math.cos(middle), 0, -Math.sin(middle)]);
|
|
1349
|
+
addQuad(outerTop0, outerTop1, innerTop1, innerTop0, [0, 1, 0]);
|
|
1350
|
+
addQuad(outerBottom0, innerBottom0, innerBottom1, outerBottom1, [0, -1, 0]);
|
|
1351
|
+
}
|
|
1352
|
+
if (!fullCircle) {
|
|
1353
|
+
const direction = Math.sign(angle) || 1;
|
|
1354
|
+
const endAngle = startAngle + angle;
|
|
1355
|
+
addQuad(point(innerRadius, -halfLength, startAngle), point(innerRadius, halfLength, startAngle), point(topRadius, halfLength, startAngle), point(bottomRadius, -halfLength, startAngle), [direction * Math.sin(startAngle), 0, -direction * Math.cos(startAngle)]);
|
|
1356
|
+
addQuad(point(innerRadius, -halfLength, endAngle), point(bottomRadius, -halfLength, endAngle), point(topRadius, halfLength, endAngle), point(innerRadius, halfLength, endAngle), [-direction * Math.sin(endAngle), 0, direction * Math.cos(endAngle)]);
|
|
1357
|
+
}
|
|
1358
|
+
const geometry = new state.three.BufferGeometry();
|
|
1359
|
+
geometry.setAttribute("position", new state.three.Float32BufferAttribute(positions, 3));
|
|
1360
|
+
geometry.computeVertexNormals();
|
|
1361
|
+
return geometry;
|
|
1362
|
+
}
|
|
774
1363
|
function primitiveGeometryKey(type, renderable) {
|
|
775
1364
|
const fields = [
|
|
776
1365
|
"Angle",
|
|
777
1366
|
"ConeRatio",
|
|
1367
|
+
"CurveRadius",
|
|
778
1368
|
"Depth",
|
|
779
1369
|
"Height",
|
|
1370
|
+
"InnerRadius",
|
|
780
1371
|
"Length",
|
|
781
1372
|
"Radius",
|
|
782
1373
|
"RadiusRatio",
|
|
@@ -791,7 +1382,13 @@ function getMaterial(material, state) {
|
|
|
791
1382
|
return state.defaultMaterial;
|
|
792
1383
|
}
|
|
793
1384
|
const textureReference = material.textureReference;
|
|
794
|
-
const key =
|
|
1385
|
+
const key = [
|
|
1386
|
+
"material",
|
|
1387
|
+
String(material.diffuse ?? "default"),
|
|
1388
|
+
String(material.transparency ?? 0),
|
|
1389
|
+
String(material.reflectivity ?? 0),
|
|
1390
|
+
textureReference ?? ""
|
|
1391
|
+
].join(":");
|
|
795
1392
|
const cached = state.materialCache.get(key);
|
|
796
1393
|
if (cached) {
|
|
797
1394
|
return cached;
|
|
@@ -799,26 +1396,55 @@ function getMaterial(material, state) {
|
|
|
799
1396
|
const created = createDemo3DThreeMaterial(material, state.three);
|
|
800
1397
|
const texture = textureReference ? getTexture(textureReference, state) : undefined;
|
|
801
1398
|
if (texture && "map" in created) {
|
|
802
|
-
|
|
1399
|
+
const standard = created;
|
|
1400
|
+
standard.map = texture;
|
|
1401
|
+
if (textureReference && state.textureImageById.get(textureReference)?.containsAlpha) {
|
|
1402
|
+
standard.transparent = true;
|
|
1403
|
+
standard.depthWrite = false;
|
|
1404
|
+
}
|
|
803
1405
|
created.needsUpdate = true;
|
|
804
1406
|
}
|
|
805
1407
|
state.materialCache.set(key, created);
|
|
806
1408
|
state.stats.materials += 1;
|
|
807
1409
|
return created;
|
|
808
1410
|
}
|
|
1411
|
+
function getColorMaterial(color, state) {
|
|
1412
|
+
if (color === undefined) {
|
|
1413
|
+
return state.defaultMaterial;
|
|
1414
|
+
}
|
|
1415
|
+
const opacity = demo3dColorToOpacity(color);
|
|
1416
|
+
const key = `color:${color}:${opacity}`;
|
|
1417
|
+
const cached = state.materialCache.get(key);
|
|
1418
|
+
if (cached) {
|
|
1419
|
+
return cached;
|
|
1420
|
+
}
|
|
1421
|
+
const created = new state.three.MeshStandardMaterial({
|
|
1422
|
+
color: demo3dColorToHex(color),
|
|
1423
|
+
opacity,
|
|
1424
|
+
transparent: opacity < 1,
|
|
1425
|
+
depthWrite: opacity >= 1,
|
|
1426
|
+
roughness: 0.9,
|
|
1427
|
+
metalness: 0.05
|
|
1428
|
+
});
|
|
1429
|
+
state.materialCache.set(key, created);
|
|
1430
|
+
state.stats.materials += 1;
|
|
1431
|
+
return created;
|
|
1432
|
+
}
|
|
809
1433
|
function getImageMaterial(material, state) {
|
|
810
1434
|
const diffuse = material?.diffuse;
|
|
1435
|
+
const opacity = demo3dMaterialOpacity(material);
|
|
811
1436
|
const textureReference = material?.textureReference;
|
|
812
1437
|
const texture = textureReference ? getTexture(textureReference, state) : undefined;
|
|
813
|
-
const key = `image:${String(diffuse ?? "default")}:${textureReference ?? ""}`;
|
|
1438
|
+
const key = `image:${String(diffuse ?? "default")}:${String(material?.transparency ?? 0)}:${textureReference ?? ""}`;
|
|
814
1439
|
const cached = state.materialCache.get(key);
|
|
815
1440
|
if (cached) {
|
|
816
1441
|
return cached;
|
|
817
1442
|
}
|
|
818
1443
|
const parameters = {
|
|
819
1444
|
color: diffuse === undefined ? 0xffffff : demo3dColorToHex(diffuse),
|
|
820
|
-
transparent: texture !== undefined ||
|
|
821
|
-
opacity
|
|
1445
|
+
transparent: texture !== undefined || opacity < 1,
|
|
1446
|
+
opacity,
|
|
1447
|
+
depthWrite: texture === undefined && opacity >= 1,
|
|
822
1448
|
side: state.three.DoubleSide
|
|
823
1449
|
};
|
|
824
1450
|
if (texture) {
|
|
@@ -901,12 +1527,18 @@ function createFallbackTextMesh(text, lineHeight, material, state) {
|
|
|
901
1527
|
}
|
|
902
1528
|
function getLineMaterial(material, state) {
|
|
903
1529
|
const color = material?.diffuse === undefined ? 0x202124 : demo3dColorToHex(material.diffuse);
|
|
904
|
-
const
|
|
1530
|
+
const opacity = demo3dMaterialOpacity(material);
|
|
1531
|
+
const key = `line:${color}:${opacity}`;
|
|
905
1532
|
const cached = state.materialCache.get(key);
|
|
906
1533
|
if (cached) {
|
|
907
1534
|
return cached;
|
|
908
1535
|
}
|
|
909
|
-
const created = new state.three.LineBasicMaterial({
|
|
1536
|
+
const created = new state.three.LineBasicMaterial({
|
|
1537
|
+
color,
|
|
1538
|
+
opacity,
|
|
1539
|
+
transparent: opacity < 1,
|
|
1540
|
+
depthWrite: opacity >= 1
|
|
1541
|
+
});
|
|
910
1542
|
state.materialCache.set(key, created);
|
|
911
1543
|
state.stats.materials += 1;
|
|
912
1544
|
return created;
|
|
@@ -987,7 +1619,8 @@ function indexTextureImages(project) {
|
|
|
987
1619
|
base64: bytes,
|
|
988
1620
|
mimeType: imageMimeType(bytes),
|
|
989
1621
|
width: numberChildOptional(value, "Width"),
|
|
990
|
-
height: numberChildOptional(value, "Height")
|
|
1622
|
+
height: numberChildOptional(value, "Height"),
|
|
1623
|
+
containsAlpha: isTrue(value?.textOf("ContainsAlpha"))
|
|
991
1624
|
});
|
|
992
1625
|
}
|
|
993
1626
|
return indexed;
|
|
@@ -1033,9 +1666,12 @@ function findVisualAspects(visual, state) {
|
|
|
1033
1666
|
}
|
|
1034
1667
|
const aspect = state.serializedObjectById.get(id);
|
|
1035
1668
|
if (aspect) {
|
|
1036
|
-
|
|
1669
|
+
if (!isFalse(aspect.textOf("IsEnabled"))) {
|
|
1670
|
+
found.push(aspect);
|
|
1671
|
+
}
|
|
1672
|
+
continue;
|
|
1037
1673
|
}
|
|
1038
|
-
|
|
1674
|
+
if (state.options.includeUnsupported) {
|
|
1039
1675
|
warn(state, {
|
|
1040
1676
|
code: "DEMO3D_THREE_MISSING_ASPECT",
|
|
1041
1677
|
message: `Visual aspect ${id} does not exist in SerializedObjects.`,
|
|
@@ -1112,6 +1748,125 @@ function positionSerializedPreviewMesh(mesh, index) {
|
|
|
1112
1748
|
mesh.scale.setScalar(scale);
|
|
1113
1749
|
mesh.position.set(gridX - center.x * scale, -center.y * scale, gridZ - center.z * scale);
|
|
1114
1750
|
}
|
|
1751
|
+
function createDimensionGroup(state) {
|
|
1752
|
+
const group = new state.three.Group();
|
|
1753
|
+
group.name = "Demo3D dimensions";
|
|
1754
|
+
for (const xml of state.serializedObjectById.values()) {
|
|
1755
|
+
if (xml.xsiType !== "e3d:DimensionAspect") {
|
|
1756
|
+
continue;
|
|
1757
|
+
}
|
|
1758
|
+
const dimension = createDimensionObject(new Demo3DDimensionAspect(xml.xsiType, xml), state);
|
|
1759
|
+
if (dimension) {
|
|
1760
|
+
group.add(dimension);
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
if (group.children.length > 0) {
|
|
1764
|
+
group.userData.demo3d = { kind: "dimensions" };
|
|
1765
|
+
state.stats.groups += 1;
|
|
1766
|
+
}
|
|
1767
|
+
return group;
|
|
1768
|
+
}
|
|
1769
|
+
function createDimensionObject(dimension, state) {
|
|
1770
|
+
const start = dimensionPointToWorld(dimension.start, state);
|
|
1771
|
+
const end = dimensionPointToWorld(dimension.end, state);
|
|
1772
|
+
if (!start || !end) {
|
|
1773
|
+
return undefined;
|
|
1774
|
+
}
|
|
1775
|
+
const extensionValues = dimension.extensionDirection;
|
|
1776
|
+
const extension = new state.three.Vector3(extensionValues[0] ?? 0, extensionValues[1] ?? 1, -(extensionValues[2] ?? 0));
|
|
1777
|
+
if (extension.lengthSq() === 0) {
|
|
1778
|
+
extension.set(0, 1, 0);
|
|
1779
|
+
}
|
|
1780
|
+
extension.normalize().multiplyScalar(dimension.height);
|
|
1781
|
+
const lineStart = start.clone().add(extension);
|
|
1782
|
+
const lineEnd = end.clone().add(extension);
|
|
1783
|
+
const positions = [];
|
|
1784
|
+
pushVectorLine(positions, start, lineStart);
|
|
1785
|
+
pushVectorLine(positions, end, lineEnd);
|
|
1786
|
+
pushVectorLine(positions, lineStart, lineEnd);
|
|
1787
|
+
const direction = lineEnd.clone().sub(lineStart);
|
|
1788
|
+
const distance = direction.length();
|
|
1789
|
+
if (distance > 0) {
|
|
1790
|
+
direction.normalize();
|
|
1791
|
+
const normal = extension.clone().normalize();
|
|
1792
|
+
const perpendicular = new state.three.Vector3().crossVectors(direction, normal);
|
|
1793
|
+
if (perpendicular.lengthSq() === 0) {
|
|
1794
|
+
perpendicular.set(0, 0, 1);
|
|
1795
|
+
}
|
|
1796
|
+
perpendicular.normalize();
|
|
1797
|
+
const startArrow = dimensionArrowSize(dimension.startArrow, distance);
|
|
1798
|
+
const endArrow = dimensionArrowSize(dimension.endArrow, distance);
|
|
1799
|
+
const startDirection = dimension.arrowsInside ? direction : direction.clone().negate();
|
|
1800
|
+
const endDirection = dimension.arrowsInside ? direction.clone().negate() : direction;
|
|
1801
|
+
addDimensionArrow(positions, lineStart, startDirection, perpendicular, startArrow.length, startArrow.width);
|
|
1802
|
+
addDimensionArrow(positions, lineEnd, endDirection, perpendicular, endArrow.length, endArrow.width);
|
|
1803
|
+
}
|
|
1804
|
+
const geometry = new state.three.BufferGeometry();
|
|
1805
|
+
geometry.setAttribute("position", new state.three.Float32BufferAttribute(positions, 3));
|
|
1806
|
+
geometry.computeBoundingBox();
|
|
1807
|
+
geometry.computeBoundingSphere();
|
|
1808
|
+
const object = new state.three.Group();
|
|
1809
|
+
const lines = new state.three.LineSegments(geometry, getLineMaterial(dimension.material, state));
|
|
1810
|
+
lines.name = "Dimension lines";
|
|
1811
|
+
object.add(lines);
|
|
1812
|
+
if (distance > 0) {
|
|
1813
|
+
const label = distance.toFixed(3);
|
|
1814
|
+
const color = demo3dColorToHex(dimension.material?.diffuse ?? -16777216);
|
|
1815
|
+
const lineHeight = clamp(distance * 0.035, 0.025, 0.12);
|
|
1816
|
+
const text = canUseCanvas()
|
|
1817
|
+
? createCanvasTextMesh(label, lineHeight, color, false, state)
|
|
1818
|
+
: createFallbackTextMesh(label, lineHeight, dimension.material, state);
|
|
1819
|
+
text.position.copy(lineStart).lerp(lineEnd, 0.5).add(extension.clone().normalize().multiplyScalar(lineHeight));
|
|
1820
|
+
text.name = `Dimension ${label}`;
|
|
1821
|
+
object.add(text);
|
|
1822
|
+
state.stats.meshes += 1;
|
|
1823
|
+
}
|
|
1824
|
+
object.name = dimension.id ?? "Dimension";
|
|
1825
|
+
object.userData.demo3d = {
|
|
1826
|
+
kind: "dimension",
|
|
1827
|
+
id: dimension.id,
|
|
1828
|
+
distance,
|
|
1829
|
+
unit: dimension.unit,
|
|
1830
|
+
format: dimension.format,
|
|
1831
|
+
arrowsInside: dimension.arrowsInside,
|
|
1832
|
+
flipText: dimension.flipText,
|
|
1833
|
+
lockDirection: dimension.lockDirection,
|
|
1834
|
+
depth: dimension.depth,
|
|
1835
|
+
xmlPath: dimension.xml.path
|
|
1836
|
+
};
|
|
1837
|
+
state.stats.geometries += 1;
|
|
1838
|
+
state.stats.lines += positions.length / 6;
|
|
1839
|
+
state.stats.groups += 1;
|
|
1840
|
+
state.stats.dimensions += 1;
|
|
1841
|
+
return object;
|
|
1842
|
+
}
|
|
1843
|
+
function dimensionArrowSize(profile, distance) {
|
|
1844
|
+
const points = profile?.polygons.flatMap((polygon) => polygon.points) ?? [];
|
|
1845
|
+
const anchorX = profile?.anchor.x ?? 0;
|
|
1846
|
+
const anchorY = profile?.anchor.y ?? 0;
|
|
1847
|
+
const profileLength = points.reduce((largest, point) => Math.max(largest, Math.abs(point.y - anchorY)), 0);
|
|
1848
|
+
const profileWidth = points.reduce((largest, point) => Math.max(largest, Math.abs(point.x - anchorX)), 0);
|
|
1849
|
+
const fallbackLength = clamp(distance * 0.04, 0.02, 0.08);
|
|
1850
|
+
const length = profileLength > 0 ? Math.min(profileLength, distance * 0.25) : fallbackLength;
|
|
1851
|
+
const width = profileWidth > 0 ? Math.min(profileWidth, distance * 0.125) : length * 0.45;
|
|
1852
|
+
return { length, width };
|
|
1853
|
+
}
|
|
1854
|
+
function dimensionPointToWorld(point, state) {
|
|
1855
|
+
if (!point) {
|
|
1856
|
+
return undefined;
|
|
1857
|
+
}
|
|
1858
|
+
const local = new state.three.Vector3(point.point[0] ?? 0, point.point[1] ?? 0, -(point.point[2] ?? 0));
|
|
1859
|
+
const visual = point.visualId ? state.visualObjectById.get(point.visualId) : undefined;
|
|
1860
|
+
return visual ? visual.localToWorld(local) : local;
|
|
1861
|
+
}
|
|
1862
|
+
function pushVectorLine(positions, start, end) {
|
|
1863
|
+
positions.push(start.x, start.y, start.z, end.x, end.y, end.z);
|
|
1864
|
+
}
|
|
1865
|
+
function addDimensionArrow(positions, tip, direction, perpendicular, length, width) {
|
|
1866
|
+
const base = tip.clone().addScaledVector(direction, length);
|
|
1867
|
+
pushVectorLine(positions, tip, base.clone().addScaledVector(perpendicular, width));
|
|
1868
|
+
pushVectorLine(positions, tip, base.clone().addScaledVector(perpendicular, -width));
|
|
1869
|
+
}
|
|
1115
1870
|
function createPlaceholderMesh(name, meshReferenceId, material, state) {
|
|
1116
1871
|
const geometry = new state.three.BoxGeometry(0.25, 0.25, 0.25);
|
|
1117
1872
|
convertTriangleGeometryToThreeCoordinates(geometry);
|
|
@@ -1208,7 +1963,20 @@ function demo3dColorToHex(color) {
|
|
|
1208
1963
|
}
|
|
1209
1964
|
function demo3dColorToOpacity(color) {
|
|
1210
1965
|
const alpha = (color >>> 24) & 0xff;
|
|
1211
|
-
return alpha
|
|
1966
|
+
return alpha / 255;
|
|
1967
|
+
}
|
|
1968
|
+
function demo3dMaterialOpacity(material) {
|
|
1969
|
+
const colorOpacity = material?.diffuse === undefined
|
|
1970
|
+
? 1
|
|
1971
|
+
: demo3dColorToOpacity(material.diffuse);
|
|
1972
|
+
const transparency = clamp01(material?.transparency ?? 0);
|
|
1973
|
+
return colorOpacity * (1 - transparency);
|
|
1974
|
+
}
|
|
1975
|
+
function clamp01(value) {
|
|
1976
|
+
return clamp(value, 0, 1);
|
|
1977
|
+
}
|
|
1978
|
+
function clamp(value, minimum, maximum) {
|
|
1979
|
+
return Math.min(maximum, Math.max(minimum, value));
|
|
1212
1980
|
}
|
|
1213
1981
|
function applyDemo3DTransform(object, transformText) {
|
|
1214
1982
|
const values = parsePipeNumbers(transformText);
|