731 lines
20 KiB
JavaScript
731 lines
20 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import * as THREE from "three";
|
|
import { LineSegments2 } from "three/examples/jsm/lines/LineSegments2.js";
|
|
import { LineSegmentsGeometry } from "three/examples/jsm/lines/LineSegmentsGeometry.js";
|
|
import { LineMaterial } from "three/examples/jsm/lines/LineMaterial.js";
|
|
|
|
import {
|
|
applyPartVisualState,
|
|
buildModel,
|
|
CAD_DISPLAY_MODE,
|
|
normalizeDisplayMode
|
|
} from "./cadScene.js";
|
|
import {
|
|
DEFAULT_DISPLAY_EDGE_SETTINGS
|
|
} from "./displaySettings.js";
|
|
import {
|
|
cloneThemeSettings
|
|
} from "./themeSettings.js";
|
|
|
|
function sampleMeshData() {
|
|
return {
|
|
vertices: new Float32Array([
|
|
0, 0, 0,
|
|
1, 0, 0,
|
|
0, 1, 0,
|
|
2, 0, 0,
|
|
3, 0, 0,
|
|
2, 1, 0
|
|
]),
|
|
indices: new Uint32Array([0, 1, 2, 3, 4, 5]),
|
|
normals: new Float32Array([
|
|
0, 0, 1,
|
|
0, 0, 1,
|
|
0, 0, 1,
|
|
0, 0, 1,
|
|
0, 0, 1,
|
|
0, 0, 1
|
|
]),
|
|
bounds: {
|
|
min: [0, 0, 0],
|
|
max: [3, 1, 0]
|
|
},
|
|
parts: [
|
|
{
|
|
id: "left",
|
|
vertexOffset: 0,
|
|
vertexCount: 3,
|
|
triangleOffset: 0,
|
|
triangleCount: 1,
|
|
bounds: { min: [0, 0, 0], max: [1, 1, 0] }
|
|
},
|
|
{
|
|
id: "right",
|
|
vertexOffset: 3,
|
|
vertexCount: 3,
|
|
triangleOffset: 1,
|
|
triangleCount: 1,
|
|
bounds: { min: [2, 0, 0], max: [3, 1, 0] }
|
|
}
|
|
]
|
|
};
|
|
}
|
|
|
|
function nestedAssemblyMeshData() {
|
|
return {
|
|
vertices: new Float32Array([
|
|
0, 0, 0,
|
|
1, 0, 0,
|
|
0, 1, 0,
|
|
2, 0, 0,
|
|
3, 0, 0,
|
|
2, 1, 0,
|
|
10, 0, 0,
|
|
11, 0, 0,
|
|
10, 1, 0
|
|
]),
|
|
indices: new Uint32Array([0, 1, 2, 3, 4, 5, 6, 7, 8]),
|
|
normals: new Float32Array([
|
|
0, 0, 1,
|
|
0, 0, 1,
|
|
0, 0, 1,
|
|
0, 0, 1,
|
|
0, 0, 1,
|
|
0, 0, 1,
|
|
0, 0, 1,
|
|
0, 0, 1,
|
|
0, 0, 1
|
|
]),
|
|
bounds: {
|
|
min: [0, 0, 0],
|
|
max: [11, 1, 0]
|
|
},
|
|
parts: [
|
|
{
|
|
id: "o1.2.1",
|
|
occurrenceId: "o1.2.1",
|
|
vertexOffset: 0,
|
|
vertexCount: 3,
|
|
triangleOffset: 0,
|
|
triangleCount: 1,
|
|
bounds: { min: [0, 0, 0], max: [1, 1, 0] }
|
|
},
|
|
{
|
|
id: "o1.2.2",
|
|
occurrenceId: "o1.2.2",
|
|
vertexOffset: 3,
|
|
vertexCount: 3,
|
|
triangleOffset: 1,
|
|
triangleCount: 1,
|
|
bounds: { min: [2, 0, 0], max: [3, 1, 0] }
|
|
},
|
|
{
|
|
id: "o1.3",
|
|
occurrenceId: "o1.3",
|
|
vertexOffset: 6,
|
|
vertexCount: 3,
|
|
triangleOffset: 2,
|
|
triangleCount: 1,
|
|
bounds: { min: [10, 0, 0], max: [11, 1, 0] }
|
|
}
|
|
]
|
|
};
|
|
}
|
|
|
|
function createDisplayRecord(partId, {
|
|
baseOpacity = 1
|
|
} = {}) {
|
|
const material = new THREE.MeshStandardMaterial({
|
|
color: "#aaaaaa",
|
|
emissive: "#000000",
|
|
transparent: false,
|
|
opacity: 1
|
|
});
|
|
const edgeMaterial = new THREE.LineBasicMaterial({
|
|
color: "#222222",
|
|
transparent: true,
|
|
opacity: 1
|
|
});
|
|
return {
|
|
partId,
|
|
mesh: { visible: true, renderOrder: 2 },
|
|
edges: { visible: true, renderOrder: 3 },
|
|
material,
|
|
edgeMaterial,
|
|
baseOpacity,
|
|
baseColor: new THREE.Color("#aaaaaa"),
|
|
baseEmissiveColor: new THREE.Color("#000000"),
|
|
baseEmissiveIntensity: 0
|
|
};
|
|
}
|
|
|
|
function squareMeshData() {
|
|
return {
|
|
vertices: new Float32Array([
|
|
0, 0, 0,
|
|
1, 0, 0,
|
|
1, 1, 0,
|
|
0, 1, 0
|
|
]),
|
|
indices: new Uint32Array([0, 1, 2, 0, 2, 3]),
|
|
normals: new Float32Array([
|
|
0, 0, 1,
|
|
0, 0, 1,
|
|
0, 0, 1,
|
|
0, 0, 1
|
|
]),
|
|
bounds: {
|
|
min: [0, 0, 0],
|
|
max: [1, 1, 0]
|
|
},
|
|
parts: []
|
|
};
|
|
}
|
|
|
|
function edgeSegmentCount(record) {
|
|
return Math.floor((record?.edges?.geometry?.getAttribute("position")?.count || 0) / 2);
|
|
}
|
|
|
|
test("applyPartVisualState keeps dimmed context from depth-occluding highlights", () => {
|
|
const dimmed = createDisplayRecord("dimmed");
|
|
const selected = createDisplayRecord("selected");
|
|
|
|
applyPartVisualState(THREE, [dimmed, selected], {
|
|
baseTheme: {
|
|
edge: "#111111",
|
|
edgeOpacity: 0.5
|
|
},
|
|
edgeSettings: {
|
|
opacity: 0.5
|
|
},
|
|
hiddenPartIds: [],
|
|
hoveredPartId: "",
|
|
focusedPartId: ["selected"],
|
|
selectedPartIds: ["selected"],
|
|
showEdges: true
|
|
});
|
|
|
|
assert.equal(dimmed.material.transparent, true);
|
|
assert.equal(dimmed.material.depthWrite, false);
|
|
assert.equal(dimmed.mesh.renderOrder, 2);
|
|
assert.equal(dimmed.edges.renderOrder, 3);
|
|
assert.equal(selected.material.transparent, true);
|
|
assert.equal(selected.material.depthWrite, true);
|
|
assert.equal(selected.mesh.renderOrder, 23);
|
|
assert.equal(selected.edges.renderOrder, 26);
|
|
|
|
applyPartVisualState(THREE, [selected], {
|
|
baseTheme: {},
|
|
edgeSettings: {},
|
|
hiddenPartIds: [],
|
|
hoveredPartId: "",
|
|
focusedPartId: [],
|
|
selectedPartIds: [],
|
|
showEdges: true
|
|
});
|
|
|
|
assert.equal(selected.material.transparent, false);
|
|
assert.equal(selected.material.depthWrite, true);
|
|
assert.equal(selected.mesh.renderOrder, 2);
|
|
assert.equal(selected.edges.renderOrder, 3);
|
|
});
|
|
|
|
test("buildModel renders solid part records and updates theme without rebuilding geometry", () => {
|
|
const theme = cloneThemeSettings("workbench");
|
|
const scene = buildModel(THREE, sampleMeshData(), {
|
|
theme,
|
|
renderPartsIndividually: true
|
|
});
|
|
const firstMesh = scene.displayRecords[0].mesh;
|
|
const firstGeometry = firstMesh.geometry;
|
|
|
|
assert.equal(scene.displayRecords.length, 2);
|
|
assert.equal(scene.displayRecords[0].partId, "left");
|
|
assert.equal(scene.displayRecords[0].edges.visible, true);
|
|
assert.equal(scene.displayRecords[0].mesh.castShadow, true);
|
|
assert.equal(scene.displayRecords[0].mesh.receiveShadow, false);
|
|
|
|
scene.update({
|
|
theme: {
|
|
...theme,
|
|
materials: {
|
|
...theme.materials,
|
|
defaultColor: "#ff0000",
|
|
fillColors: ["#ff0000"]
|
|
}
|
|
}
|
|
});
|
|
|
|
assert.equal(scene.displayRecords[0].mesh, firstMesh);
|
|
assert.equal(scene.displayRecords[0].mesh.geometry, firstGeometry);
|
|
assert.equal(scene.displayRecords[0].material.color.getHexString(), "ff0000");
|
|
scene.dispose();
|
|
});
|
|
|
|
test("buildModel keeps source-mesh color buffers immutable across material refreshes", () => {
|
|
const sourceColors = new Float32Array([
|
|
0.2, 0.4, 0.6,
|
|
0.8, 0.45, 0.2,
|
|
0.55, 0.3, 0.75
|
|
]);
|
|
const originalColors = Array.from(sourceColors);
|
|
const sourceMesh = {
|
|
vertices: new Float32Array([
|
|
0, 0, 0,
|
|
1, 0, 0,
|
|
0, 1, 0
|
|
]),
|
|
indices: new Uint32Array([0, 1, 2]),
|
|
normals: new Float32Array([
|
|
0, 0, 1,
|
|
0, 0, 1,
|
|
0, 0, 1
|
|
]),
|
|
colors: sourceColors
|
|
};
|
|
const meshData = {
|
|
vertices: new Float32Array(0),
|
|
indices: new Uint32Array(0),
|
|
normals: new Float32Array(0),
|
|
bounds: { min: [0, 0, 0], max: [1, 1, 0] },
|
|
parts: [
|
|
{
|
|
id: "camera-source",
|
|
sourceMeshKey: "camera-source",
|
|
sourceMesh,
|
|
hasSourceColors: true,
|
|
vertexCount: 3,
|
|
triangleCount: 1,
|
|
bounds: { min: [0, 0, 0], max: [1, 1, 0] }
|
|
}
|
|
]
|
|
};
|
|
const theme = cloneThemeSettings("workbench");
|
|
const scene = buildModel(THREE, meshData, {
|
|
theme,
|
|
renderPartsIndividually: true
|
|
});
|
|
const record = scene.displayRecords[0];
|
|
const colorAttribute = record.geometry.getAttribute("color");
|
|
|
|
assert.notEqual(record.rawColors, sourceColors);
|
|
assert.notEqual(colorAttribute.array, sourceColors);
|
|
assert.deepEqual(Array.from(sourceColors), originalColors);
|
|
|
|
scene.update({
|
|
theme: {
|
|
...theme,
|
|
materials: {
|
|
...theme.materials,
|
|
brightness: 0.72,
|
|
saturation: 1.8
|
|
}
|
|
}
|
|
});
|
|
|
|
assert.deepEqual(Array.from(sourceColors), originalColors);
|
|
assert.deepEqual(Array.from(record.rawColors), originalColors);
|
|
scene.dispose();
|
|
});
|
|
|
|
test("buildModel selection can focus and hide subassembly occurrence descendants", () => {
|
|
const focused = buildModel(THREE, nestedAssemblyMeshData(), {
|
|
theme: cloneThemeSettings("workbench"),
|
|
renderPartsIndividually: true,
|
|
selection: {
|
|
focus: ["#o1.2"]
|
|
}
|
|
});
|
|
|
|
assert.deepEqual(focused.displayRecords.map((record) => record.partId), ["o1.2.1", "o1.2.2"]);
|
|
assert.deepEqual(focused.bounds, {
|
|
min: [0, 0, 0],
|
|
max: [3, 1, 0]
|
|
});
|
|
focused.dispose();
|
|
|
|
const hidden = buildModel(THREE, nestedAssemblyMeshData(), {
|
|
theme: cloneThemeSettings("workbench"),
|
|
renderPartsIndividually: true,
|
|
selection: {
|
|
hide: ["o1.2"]
|
|
}
|
|
});
|
|
|
|
assert.deepEqual(hidden.displayRecords.map((record) => record.partId), ["o1.3"]);
|
|
assert.deepEqual(hidden.bounds, {
|
|
min: [10, 0, 0],
|
|
max: [11, 1, 0]
|
|
});
|
|
hidden.dispose();
|
|
});
|
|
|
|
test("buildModel renders STEP surface-owned edges from mesh attributes without line objects", () => {
|
|
const meshData = sampleMeshData();
|
|
meshData.surfaceEdgeBarycentric = new Float32Array([
|
|
1, 0, 0,
|
|
0, 1, 0,
|
|
0, 0, 1,
|
|
1, 0, 0,
|
|
0, 1, 0,
|
|
0, 0, 1
|
|
]);
|
|
meshData.surfaceEdgeClass = new Uint8Array([
|
|
1, 1, 0,
|
|
1, 1, 0,
|
|
1, 1, 0,
|
|
2, 3, 0,
|
|
2, 3, 0,
|
|
2, 3, 0
|
|
]);
|
|
const scene = buildModel(THREE, meshData, {
|
|
theme: cloneThemeSettings("workbench"),
|
|
displayMode: CAD_DISPLAY_MODE.SOLID,
|
|
renderPartsIndividually: true
|
|
});
|
|
|
|
assert.equal(scene.displayRecords.length, 2);
|
|
assert.equal(scene.edgesGroup.children.length, 0);
|
|
assert.equal(scene.displayRecords[0].edges, null);
|
|
assert.equal(scene.displayRecords[0].material.userData.cadSurfaceEdges, true);
|
|
assert.equal(scene.displayRecords[0].material.polygonOffset, false);
|
|
assert.equal(scene.displayRecords[0].geometry.getAttribute("_cad_edge_barycentric").count, 3);
|
|
assert.equal(scene.displayRecords[0].geometry.getAttribute("_cad_edge_class").count, 3);
|
|
scene.dispose();
|
|
});
|
|
|
|
test("buildModel rebuilds surface edge shader materials when edge theme settings change", () => {
|
|
const meshData = sampleMeshData();
|
|
meshData.surfaceEdgeBarycentric = new Float32Array([
|
|
1, 0, 0,
|
|
0, 1, 0,
|
|
0, 0, 1,
|
|
1, 0, 0,
|
|
0, 1, 0,
|
|
0, 0, 1
|
|
]);
|
|
meshData.surfaceEdgeClass = new Uint8Array([
|
|
1, 2, 0,
|
|
1, 2, 0,
|
|
1, 2, 0,
|
|
1, 2, 0,
|
|
1, 2, 0,
|
|
1, 2, 0
|
|
]);
|
|
const theme = cloneThemeSettings("workbench");
|
|
const scene = buildModel(THREE, meshData, {
|
|
theme,
|
|
displayMode: CAD_DISPLAY_MODE.SOLID,
|
|
renderPartsIndividually: true
|
|
});
|
|
const originalMaterial = scene.displayRecords[0].material;
|
|
|
|
scene.update({
|
|
displayMode: CAD_DISPLAY_MODE.SOLID,
|
|
theme: {
|
|
...theme,
|
|
edges: {
|
|
...DEFAULT_DISPLAY_EDGE_SETTINGS,
|
|
color: "#0055ff",
|
|
classes: {
|
|
...DEFAULT_DISPLAY_EDGE_SETTINGS.classes,
|
|
tangent: {
|
|
...DEFAULT_DISPLAY_EDGE_SETTINGS.classes.tangent,
|
|
thickness: 2.5
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
assert.notEqual(scene.displayRecords[0].material, originalMaterial);
|
|
assert.equal(scene.displayRecords[0].material.userData.cadSurfaceEdges, true);
|
|
assert.equal(scene.edgesGroup.children.length, 0);
|
|
scene.dispose();
|
|
});
|
|
|
|
test("buildModel reuses cached geometry for posed wrappers with the same geometry source", () => {
|
|
const geometrySource = sampleMeshData();
|
|
const posedMeshData = {
|
|
...geometrySource,
|
|
geometrySource,
|
|
parts: geometrySource.parts.map((part) => ({
|
|
...part,
|
|
transform: [
|
|
1, 0, 0, 0,
|
|
0, 1, 0, 0,
|
|
0, 0, 1, 0,
|
|
0, 0, 0, 1
|
|
]
|
|
}))
|
|
};
|
|
const movedMeshData = {
|
|
...geometrySource,
|
|
geometrySource,
|
|
parts: geometrySource.parts.map((part, index) => ({
|
|
...part,
|
|
bounds: {
|
|
min: [part.bounds.min[0] + index, part.bounds.min[1], part.bounds.min[2]],
|
|
max: [part.bounds.max[0] + index, part.bounds.max[1], part.bounds.max[2]]
|
|
},
|
|
transform: [
|
|
1, 0, 0, index,
|
|
0, 1, 0, 0,
|
|
0, 0, 1, 0,
|
|
0, 0, 0, 1
|
|
]
|
|
}))
|
|
};
|
|
const scene = buildModel(THREE, posedMeshData, {
|
|
theme: cloneThemeSettings("workbench"),
|
|
renderPartsIndividually: true
|
|
});
|
|
const firstGeometry = scene.displayRecords[0].mesh.geometry;
|
|
const movedScene = buildModel(THREE, movedMeshData, {
|
|
theme: cloneThemeSettings("workbench"),
|
|
renderPartsIndividually: true
|
|
});
|
|
|
|
assert.equal(movedScene.displayRecords[0].mesh.geometry, firstGeometry);
|
|
scene.dispose();
|
|
movedScene.dispose();
|
|
});
|
|
|
|
test("buildModel wireframe mode keeps a translucent surface and wire edges", () => {
|
|
const theme = cloneThemeSettings("workbench");
|
|
const scene = buildModel(THREE, sampleMeshData(), {
|
|
theme,
|
|
displayMode: CAD_DISPLAY_MODE.WIREFRAME,
|
|
renderPartsIndividually: true
|
|
});
|
|
|
|
assert.equal(normalizeDisplayMode("wireframe"), CAD_DISPLAY_MODE.WIREFRAME);
|
|
assert.equal(scene.displayRecords.length, 2);
|
|
assert.equal(scene.displayRecords[0].material.type, "MeshBasicMaterial");
|
|
assert.equal(scene.displayRecords[0].material.opacity, 0.035);
|
|
assert.equal(scene.displayRecords[0].edges.geometry.type, "WireframeGeometry");
|
|
scene.dispose();
|
|
});
|
|
|
|
test("buildModel display modes control edges, transparency, and flat surfaces", () => {
|
|
const theme = cloneThemeSettings("workbench");
|
|
const renderedScene = buildModel(THREE, sampleMeshData(), {
|
|
theme,
|
|
displayMode: CAD_DISPLAY_MODE.RENDERED,
|
|
renderPartsIndividually: true
|
|
});
|
|
assert.equal(renderedScene.displayRecords[0].edges, null);
|
|
assert.equal(renderedScene.displayRecords[0].material.opacity, 1);
|
|
|
|
const transparentScene = buildModel(THREE, sampleMeshData(), {
|
|
theme,
|
|
displayMode: CAD_DISPLAY_MODE.TRANSPARENT,
|
|
renderPartsIndividually: true
|
|
});
|
|
assert.equal(transparentScene.displayRecords[0].material.opacity, 0.22);
|
|
assert.equal(transparentScene.displayRecords[0].material.transparent, true);
|
|
assert.equal(transparentScene.displayRecords[0].material.depthWrite, false);
|
|
assert.equal(transparentScene.displayRecords[0].edgeMaterial.depthTest, false);
|
|
|
|
const hiddenEdgeScene = buildModel(THREE, sampleMeshData(), {
|
|
theme,
|
|
displayMode: CAD_DISPLAY_MODE.HIDDEN_EDGES,
|
|
renderPartsIndividually: true
|
|
});
|
|
assert.equal(hiddenEdgeScene.displayRecords[0].material.opacity, 1);
|
|
assert.equal(hiddenEdgeScene.displayRecords[0].edgeMaterial.depthTest, false);
|
|
|
|
const unshadedScene = buildModel(THREE, sampleMeshData(), {
|
|
theme,
|
|
displayMode: CAD_DISPLAY_MODE.UNSHADED,
|
|
renderPartsIndividually: true
|
|
});
|
|
assert.equal(unshadedScene.displayRecords[0].material.type, "MeshBasicMaterial");
|
|
assert.equal(unshadedScene.displayRecords[0].edges, null);
|
|
|
|
renderedScene.dispose();
|
|
transparentScene.dispose();
|
|
hiddenEdgeScene.dispose();
|
|
unshadedScene.dispose();
|
|
});
|
|
|
|
test("buildModel applies source part opacity from GLB material metadata", () => {
|
|
const meshData = sampleMeshData();
|
|
meshData.parts = meshData.parts.map((part, index) => index === 0
|
|
? { ...part, color: "#ff0000", opacity: 0.2, hasSourceColors: true }
|
|
: part
|
|
);
|
|
const scene = buildModel(THREE, meshData, {
|
|
theme: cloneThemeSettings("workbench"),
|
|
renderPartsIndividually: true
|
|
});
|
|
|
|
const left = scene.displayRecords.find((record) => record.partId === "left");
|
|
|
|
assert.equal(left.baseOpacity, 0.2);
|
|
assert.equal(left.material.opacity, 0.2);
|
|
assert.equal(left.material.transparent, true);
|
|
assert.equal(left.material.depthWrite, false);
|
|
scene.dispose();
|
|
});
|
|
|
|
test("buildModel uses part records when only source opacity differs", () => {
|
|
const meshData = sampleMeshData();
|
|
meshData.sourceColor = "#ff0000";
|
|
meshData.has_source_colors = true;
|
|
meshData.parts = meshData.parts.map((part) => ({
|
|
...part,
|
|
color: "#ff0000",
|
|
opacity: 0.2,
|
|
hasSourceColors: true
|
|
}));
|
|
const scene = buildModel(THREE, meshData, {
|
|
theme: cloneThemeSettings("workbench"),
|
|
renderPartsIndividually: false
|
|
});
|
|
|
|
assert.equal(scene.displayRecords.length, 2);
|
|
for (const record of scene.displayRecords) {
|
|
assert.equal(record.baseOpacity, 0.2);
|
|
assert.equal(record.material.opacity, 0.2);
|
|
assert.equal(record.material.transparent, true);
|
|
assert.equal(record.material.depthWrite, false);
|
|
}
|
|
scene.dispose();
|
|
});
|
|
|
|
test("buildModel ignores deprecated mesh edge detail and keeps wireframe all-edge mode", () => {
|
|
const baseTheme = cloneThemeSettings("workbench");
|
|
const deprecatedDetailScene = buildModel(THREE, squareMeshData(), {
|
|
theme: {
|
|
...baseTheme,
|
|
edges: {
|
|
...DEFAULT_DISPLAY_EDGE_SETTINGS,
|
|
topologyFilter: "all"
|
|
}
|
|
},
|
|
displayMode: CAD_DISPLAY_MODE.SOLID
|
|
});
|
|
const wireScene = buildModel(THREE, squareMeshData(), {
|
|
theme: baseTheme,
|
|
displayMode: CAD_DISPLAY_MODE.WIREFRAME
|
|
});
|
|
|
|
assert.equal(edgeSegmentCount(deprecatedDetailScene.displayRecords[0]), 4);
|
|
assert.notEqual(deprecatedDetailScene.displayRecords[0].edges.geometry.type, "WireframeGeometry");
|
|
assert.equal(edgeSegmentCount(wireScene.displayRecords[0]), 5);
|
|
assert.equal(wireScene.displayRecords[0].edges.geometry.type, "WireframeGeometry");
|
|
deprecatedDetailScene.dispose();
|
|
wireScene.dispose();
|
|
});
|
|
|
|
test("buildModel creates screen-space edges from declarative edge rendering options", () => {
|
|
const scene = buildModel(THREE, squareMeshData(), {
|
|
theme: cloneThemeSettings("workbench"),
|
|
displayMode: CAD_DISPLAY_MODE.SOLID,
|
|
edgeRendering: {
|
|
mode: "screen-space",
|
|
LineSegments2,
|
|
LineSegmentsGeometry,
|
|
LineMaterial
|
|
}
|
|
});
|
|
|
|
assert.equal(scene.runtime.edgeRendering.mode, "screen-space");
|
|
assert.equal(scene.displayRecords[0].edges instanceof LineSegments2, true);
|
|
assert.equal(scene.runtime.screenSpaceLineMaterials.size, 1);
|
|
|
|
scene.dispose();
|
|
assert.equal(scene.runtime.screenSpaceLineMaterials.size, 0);
|
|
});
|
|
|
|
test("buildModel can render silhouette contours without derived mesh edges", () => {
|
|
const theme = cloneThemeSettings("workbench");
|
|
const scene = buildModel(THREE, sampleMeshData(), {
|
|
theme: {
|
|
...theme,
|
|
edges: {
|
|
...DEFAULT_DISPLAY_EDGE_SETTINGS,
|
|
enabled: false,
|
|
silhouette: true,
|
|
silhouetteScale: 0.004
|
|
}
|
|
},
|
|
displayMode: CAD_DISPLAY_MODE.RENDERED,
|
|
silhouette: true,
|
|
renderPartsIndividually: true
|
|
});
|
|
|
|
assert.equal(scene.displayRecords.length, 2);
|
|
assert.equal(scene.displayRecords[0].edges, null);
|
|
assert.equal(scene.displayRecords[0].silhouette?.isMesh, true);
|
|
scene.dispose();
|
|
});
|
|
|
|
test("buildModel applies selection, clipping, and STEP parameter effects", () => {
|
|
const scene = buildModel(THREE, sampleMeshData(), {
|
|
theme: cloneThemeSettings("workbench"),
|
|
renderPartsIndividually: true,
|
|
selection: {
|
|
selectedPartIds: ["left"],
|
|
hiddenPartIds: ["right"]
|
|
},
|
|
clip: {
|
|
enabled: true,
|
|
axis: "x",
|
|
offset: 0.5
|
|
},
|
|
stepParameters: {
|
|
definition: {
|
|
module: {
|
|
render(ctx) {
|
|
if (ctx.params.hideLeft) {
|
|
ctx.effects.visible("left", false);
|
|
}
|
|
}
|
|
},
|
|
manifest: {},
|
|
cadPath: "part.step"
|
|
},
|
|
parameterValues: {
|
|
hideLeft: true
|
|
}
|
|
}
|
|
});
|
|
|
|
const left = scene.displayRecords.find((record) => record.partId === "left");
|
|
const right = scene.displayRecords.find((record) => record.partId === "right");
|
|
|
|
assert.equal(left.mesh.visible, false);
|
|
assert.equal(right.mesh.visible, true);
|
|
assert.equal(right.material.transparent, true);
|
|
assert.equal(right.material.depthWrite, false);
|
|
assert.equal(right.material.opacity, 0.035);
|
|
assert.equal(left.material.clippingPlanes.length, 1);
|
|
assert.equal(scene.bounds.min[0], 2);
|
|
assert.equal(scene.bounds.max[0], 3);
|
|
scene.dispose();
|
|
});
|
|
|
|
test("buildModel can apply STEP parameter effects while deferring setup lifecycle", () => {
|
|
let setupCalls = 0;
|
|
const scene = buildModel(THREE, sampleMeshData(), {
|
|
theme: cloneThemeSettings("workbench"),
|
|
renderPartsIndividually: true,
|
|
parameterSetup: false,
|
|
stepParameters: {
|
|
definition: {
|
|
module: {
|
|
setup() {
|
|
setupCalls += 1;
|
|
},
|
|
render(ctx) {
|
|
ctx.effects.transform("left", { translate: [5, 0, 0] });
|
|
}
|
|
},
|
|
manifest: {},
|
|
cadPath: "part.step"
|
|
}
|
|
}
|
|
});
|
|
|
|
const left = scene.displayRecords.find((record) => record.partId === "left");
|
|
|
|
assert.equal(setupCalls, 0);
|
|
assert.equal(left.mesh.matrix.elements[12], 5);
|
|
assert.equal(scene.bounds.min[0], 2);
|
|
assert.equal(scene.bounds.max[0], 6);
|
|
scene.dispose();
|
|
});
|