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