Files
cad-Integration/text-to-cad-main/viewer/packages/cadjs/src/common/cadScene.js
T
2026-07-22 13:48:46 +08:00

2204 lines
78 KiB
JavaScript

import {
normalizeThemeSettings,
resolveThemeFillColor
} from "./themeSettings.js";
import {
CAD_DISPLAY_MODE,
displayModeAllowsEdges,
displayModeForcesEdges,
displayModeIsWireframe,
displayModeShowsThroughEdges,
displayModeSurfaceOpacity,
normalizeDisplayEdgeSettings,
displayModeUsesUnlitSurfaces,
normalizeDisplayMode
} from "./displaySettings.js";
import {
createDisplayEdgeObject,
syncScreenSpaceLineMaterialResolution
} from "./renderEdges.js";
import { resolveStepModuleFeatures } from "./stepModule.js";
import {
applyStepModuleEffectsToRecords,
buildPartTransformMatrix,
buildStepModuleContext,
createStepModuleEffectsApi,
displayTransformForPart
} from "./stepModuleEffects.js";
import {
applyDisplayRecordTransform
} from "./displayRecordTransform.js";
import { axisIndex, normalizeStepClipSettings } from "../lib/viewer/clipPlane.js";
import {
clampSceneModelRadius,
getSceneScaleSettings,
normalizeSceneScaleMode,
VIEWER_SCENE_SCALE
} from "../lib/viewer/sceneScale.js";
export { CAD_DISPLAY_MODE, normalizeDisplayMode };
export { applyDisplayRecordTransform } from "./displayRecordTransform.js";
export const CAD_SCENE_SCALE = VIEWER_SCENE_SCALE;
const CAD_EDGE_OPACITY = 0.84;
const CAD_EDGE_THRESHOLD_DEG = 16;
const REFERENCE_HOVER_COLOR = "#8dc5ff";
const REFERENCE_SELECTED_COLOR = "#4f9dff";
const PART_HOVER_OPACITY_BOOST = 0.08;
const PART_SELECTED_OPACITY_BOOST = 0.12;
const PART_HIGHLIGHT_SURFACE_RENDER_ORDER = 23;
const PART_HIGHLIGHT_EDGE_RENDER_ORDER = 26;
const FOCUSED_DIMMED_SURFACE_OPACITY = 0.035;
const HEX_COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3}){1,2}$/;
const MODEL_PART_ID = "__model__";
const DEFAULT_THEME = Object.freeze({
surface: "#f4f4f5",
surfaceRoughness: 0.92,
surfaceMetalness: 0.03,
surfaceClearcoat: 0,
surfaceClearcoatRoughness: 0.6,
edge: "#18181b",
edgeThickness: 1,
edgeOpacity: CAD_EDGE_OPACITY
});
const SURFACE_EDGE_BARYCENTRIC_ATTRIBUTE = "_cad_edge_barycentric";
const SURFACE_EDGE_CLASS_ATTRIBUTE = "_cad_edge_class";
const SURFACE_EDGE_CLASS_IDS = Object.freeze(["feature", "tangent", "seam", "degenerate"]);
const SURFACE_EDGE_CLASS_DEFAULTS = Object.freeze({
feature: Object.freeze({ color: "#132232", thickness: 1.15, opacity: 1 }),
tangent: Object.freeze({ color: "#132232", thickness: 1.15, opacity: 0.5 }),
seam: Object.freeze({ color: "#132232", thickness: 1.15, opacity: 0.85 }),
degenerate: Object.freeze({ color: "#132232", thickness: 0, opacity: 1 })
});
const meshGeometryCache = new WeakMap();
function cacheOwnerForMeshData(meshData) {
const geometrySource = meshData?.geometrySource;
return geometrySource && typeof geometrySource === "object" ? geometrySource : meshData;
}
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
function toNumber(value, fallback = 0) {
return Number.isFinite(Number(value)) ? Number(value) : fallback;
}
function toArray(value) {
return Array.isArray(value) ? value : [];
}
function isNumericArray(value, stride = 1) {
return (
(Array.isArray(value) || ArrayBuffer.isView(value)) &&
value.length >= stride &&
value.length % stride === 0
);
}
function normalizeBoolean(value, fallback = false) {
return typeof value === "boolean" ? value : fallback;
}
export function normalizeCadSceneScale(value) {
return normalizeSceneScaleMode(value);
}
export function boundsFromVertices(vertices) {
const min = [Infinity, Infinity, Infinity];
const max = [-Infinity, -Infinity, -Infinity];
for (let index = 0; index + 2 < (vertices?.length || 0); index += 3) {
const x = Number(vertices[index]);
const y = Number(vertices[index + 1]);
const z = Number(vertices[index + 2]);
if (![x, y, z].every(Number.isFinite)) {
continue;
}
min[0] = Math.min(min[0], x);
min[1] = Math.min(min[1], y);
min[2] = Math.min(min[2], z);
max[0] = Math.max(max[0], x);
max[1] = Math.max(max[1], y);
max[2] = Math.max(max[2], z);
}
if (!min.every(Number.isFinite) || !max.every(Number.isFinite)) {
return { min: [0, 0, 0], max: [1, 1, 1] };
}
return { min, max };
}
export function centerAndRadiusFromBounds(THREE, bounds, scale = CAD_SCENE_SCALE.CAD) {
const sceneScale = normalizeCadSceneScale(scale);
const settings = getSceneScaleSettings(sceneScale);
const min = Array.isArray(bounds?.min) ? bounds.min : [0, 0, 0];
const max = Array.isArray(bounds?.max) ? bounds.max : [1, 1, 1];
const center = new THREE.Vector3(
(toNumber(min[0]) + toNumber(max[0], 1)) / 2,
(toNumber(min[1]) + toNumber(max[1], 1)) / 2,
(toNumber(min[2]) + toNumber(max[2], 1)) / 2
);
const size = new THREE.Vector3(
Math.max(toNumber(max[0], 1) - toNumber(min[0]), settings.minModelRadius),
Math.max(toNumber(max[1], 1) - toNumber(min[1]), settings.minModelRadius),
Math.max(toNumber(max[2], 1) - toNumber(min[2]), settings.minModelRadius)
);
return {
center,
size,
radius: clampSceneModelRadius(size.length() / 2, sceneScale)
};
}
function cacheForMeshData(meshData) {
const cacheOwner = cacheOwnerForMeshData(meshData);
let cache = meshGeometryCache.get(cacheOwner);
if (!cache) {
cache = {
whole: new Map(),
part: new Map(),
edge: new Map()
};
meshGeometryCache.set(cacheOwner, cache);
}
return cache;
}
function cacheKey(parts) {
return parts.map((part, index) => String(part?.id || part?.occurrenceId || `part:${index}`)).join("|");
}
function markCachedGeometry(geometry) {
if (geometry) {
geometry.userData = {
...(geometry.userData || {}),
cadSceneCachedGeometry: true
};
}
return geometry;
}
function disposeMaterial(material) {
const materials = Array.isArray(material) ? material : [material];
for (const item of materials) {
item?.map?.dispose?.();
item?.alphaMap?.dispose?.();
item?.dispose?.();
}
}
function disposeSceneObject(object, { disposeCachedGeometry = false } = {}) {
if (!object) {
return;
}
while (object.children?.length) {
disposeSceneObject(object.children[0], { disposeCachedGeometry });
}
object.parent?.remove(object);
if (typeof object.userData?.beforeDispose === "function") {
object.userData.beforeDispose(object);
delete object.userData.beforeDispose;
}
if (disposeCachedGeometry || object.geometry?.userData?.cadSceneCachedGeometry !== true) {
object.geometry?.dispose?.();
}
disposeMaterial(object.material);
}
function clearGroup(group, options = {}) {
while (group?.children?.length) {
disposeSceneObject(group.children[0], options);
}
}
function applyGeometryNormals(THREE, geometry, normals, recomputeNormals) {
const hasNormals = isNumericArray(normals, 3);
if (!recomputeNormals && hasNormals) {
geometry.setAttribute("normal", new THREE.BufferAttribute(new Float32Array(normals), 3));
return;
}
geometry.computeVertexNormals();
}
function setSurfaceEdgeAttributes(THREE, geometry, meshData, vertexOffset = 0, vertexCount = 0) {
const barycentric = meshData?.surfaceEdgeBarycentric;
const edgeClass = meshData?.surfaceEdgeClass;
const componentOffset = Math.max(0, Math.floor(Number(vertexOffset) || 0)) * 3;
const componentCount = Math.max(0, Math.floor(Number(vertexCount) || 0)) * 3;
if (
!(barycentric instanceof Float32Array) ||
!(edgeClass instanceof Uint8Array) ||
componentCount <= 0 ||
barycentric.length < componentOffset + componentCount ||
edgeClass.length < componentOffset + componentCount
) {
return;
}
geometry.setAttribute(
SURFACE_EDGE_BARYCENTRIC_ATTRIBUTE,
new THREE.BufferAttribute(barycentric.slice(componentOffset, componentOffset + componentCount), 3)
);
geometry.setAttribute(
SURFACE_EDGE_CLASS_ATTRIBUTE,
new THREE.BufferAttribute(edgeClass.slice(componentOffset, componentOffset + componentCount), 3)
);
}
function geometryHasSurfaceEdgeAttributes(geometry) {
const barycentric = geometry?.getAttribute?.(SURFACE_EDGE_BARYCENTRIC_ATTRIBUTE);
const edgeClass = geometry?.getAttribute?.(SURFACE_EDGE_CLASS_ATTRIBUTE);
return Boolean(
barycentric?.itemSize === 3 &&
edgeClass?.itemSize === 3 &&
barycentric.count > 0 &&
barycentric.count === edgeClass.count
);
}
function readSourceColor(THREE, value) {
const normalized = String(value || "").trim();
if (!HEX_COLOR_PATTERN.test(normalized)) {
return null;
}
const expanded = normalized.length === 4
? `#${normalized[1]}${normalized[1]}${normalized[2]}${normalized[2]}${normalized[3]}${normalized[3]}`
: normalized;
return new THREE.Color(expanded);
}
function shapeSourceColor(THREE, sourceColor, materialSettings = {}, { applyTint = true } = {}) {
const shaped = (sourceColor || new THREE.Color("#ffffff")).clone();
const tintStrength = clamp(Number(materialSettings.tintStrength) || 0, 0, 1);
if (applyTint && tintStrength > 0) {
const tintColor = new THREE.Color(materialSettings.defaultColor || materialSettings.tintColor || "#ffffff");
if (materialSettings.tintMode === "blend") {
shaped.lerp(tintColor, tintStrength);
} else {
shaped.lerp(shaped.clone().multiply(tintColor), tintStrength);
}
}
const saturation = clamp(Number(materialSettings.saturation) || 1, 0, 2.5);
if (Math.abs(saturation - 1) > 1e-4) {
const hsl = {};
shaped.getHSL(hsl);
shaped.setHSL(hsl.h, clamp(hsl.s * saturation, 0, 1), hsl.l);
}
const contrast = clamp(Number(materialSettings.contrast) || 1, 0, 2.5);
const brightness = clamp(Number(materialSettings.brightness) || 1, 0, 2);
shaped.r = clamp(((shaped.r - 0.5) * contrast + 0.5) * brightness, 0, 1);
shaped.g = clamp(((shaped.g - 0.5) * contrast + 0.5) * brightness, 0, 1);
shaped.b = clamp(((shaped.b - 0.5) * contrast + 0.5) * brightness, 0, 1);
return shaped;
}
function shapeSourceColorBuffer(THREE, colors, materialSettings = {}) {
if (!isNumericArray(colors, 3)) {
return null;
}
const shapedColors = new Float32Array(colors.length);
const color = new THREE.Color();
for (let index = 0; index + 2 < colors.length; index += 3) {
color.setRGB(
clamp(Number(colors[index]) || 0, 0, 1),
clamp(Number(colors[index + 1]) || 0, 0, 1),
clamp(Number(colors[index + 2]) || 0, 0, 1)
);
const shaped = shapeSourceColor(THREE, color, materialSettings);
shapedColors[index] = shaped.r;
shapedColors[index + 1] = shaped.g;
shapedColors[index + 2] = shaped.b;
}
return shapedColors;
}
function shouldUseDisplayVertexColors(meshData) {
return !!meshData?.has_source_colors && isNumericArray(meshData?.colors, 3);
}
function partUsesDisplayVertexColors(meshData, part) {
if (!shouldUseDisplayVertexColors(meshData)) {
return false;
}
if (part && Object.hasOwn(part, "hasSourceColors")) {
return !!part.hasSourceColors;
}
return true;
}
function createMaterialFillColor(THREE, materialSettings = {}, fillIndex = 0) {
return new THREE.Color(resolveThemeFillColor(materialSettings, fillIndex));
}
function resolveMaterialFillBaseColor(THREE, materialSettings = {}, fillIndex = 0) {
return shapeSourceColor(
THREE,
createMaterialFillColor(THREE, materialSettings, fillIndex),
materialSettings,
{ applyTint: false }
);
}
function resolveSourceBaseColor(THREE, {
hasVertexColors = false,
sourceColor = null,
materialSettings,
fallbackColor = "#ffffff",
fillIndex = 0,
forceFill = false
}) {
if (forceFill) {
return resolveMaterialFillBaseColor(THREE, materialSettings, fillIndex);
}
if (hasVertexColors) {
return new THREE.Color("#ffffff");
}
if (!sourceColor) {
return resolveMaterialFillBaseColor(THREE, {
...materialSettings,
defaultColor: fallbackColor || materialSettings?.defaultColor
}, fillIndex);
}
return shapeSourceColor(THREE, sourceColor, materialSettings);
}
function surfaceEdgeClassSetting(edgeSettings = {}, classId, fallbackColor = "#132232") {
const fallback = SURFACE_EDGE_CLASS_DEFAULTS[classId] || SURFACE_EDGE_CLASS_DEFAULTS.feature;
const source = edgeSettings?.classes?.[classId] || {};
return {
color: source.color || fallbackColor || fallback.color,
thickness: clamp(
Number.isFinite(Number(source.thickness)) ? Number(source.thickness) : fallback.thickness,
0,
6
),
opacity: clamp(
Number.isFinite(Number(source.opacity)) ? Number(source.opacity) : fallback.opacity,
0,
1
)
};
}
function addCadSurfaceEdgeShader(THREE, material, edgeSettings = {}, baseTheme = DEFAULT_THEME) {
const baseEdgeColor = edgeSettings?.color || baseTheme?.edge || DEFAULT_THEME.edge;
const classSettings = Object.fromEntries(
SURFACE_EDGE_CLASS_IDS.map((classId) => [classId, surfaceEdgeClassSetting(edgeSettings, classId, baseEdgeColor)])
);
const edgeColor = new THREE.Color(baseEdgeColor);
const classColors = Object.fromEntries(
Object.entries(classSettings).map(([classId, setting]) => [
classId,
readSourceColor(THREE, setting.color) || edgeColor.clone()
])
);
material.userData.cadSurfaceEdges = true;
material.userData.cadSurfaceEdgeBaseColor = edgeColor.clone();
material.userData.cadSurfaceEdgeColor = edgeColor.clone();
material.userData.cadSurfaceEdgeBaseClassSettings = Object.fromEntries(
Object.entries(classSettings).map(([classId, setting]) => [classId, { ...setting }])
);
material.extensions = {
...(material.extensions || {}),
derivatives: true
};
material.onBeforeCompile = (shader) => {
const activeEdgeColor = material.userData.cadSurfaceEdgeColor?.isColor
? material.userData.cadSurfaceEdgeColor
: edgeColor;
material.userData.cadSurfaceEdgeShader = shader;
shader.uniforms.cadSurfaceEdgeColor = { value: activeEdgeColor.clone() };
shader.uniforms.cadSurfaceFeatureColor = { value: classColors.feature.clone() };
shader.uniforms.cadSurfaceTangentColor = { value: classColors.tangent.clone() };
shader.uniforms.cadSurfaceSeamColor = { value: classColors.seam.clone() };
shader.uniforms.cadSurfaceDegenerateColor = { value: classColors.degenerate.clone() };
shader.uniforms.cadSurfaceFeatureThickness = { value: classSettings.feature.thickness };
shader.uniforms.cadSurfaceTangentThickness = { value: classSettings.tangent.thickness };
shader.uniforms.cadSurfaceSeamThickness = { value: classSettings.seam.thickness };
shader.uniforms.cadSurfaceDegenerateThickness = { value: classSettings.degenerate.thickness };
shader.uniforms.cadSurfaceFeatureOpacity = { value: classSettings.feature.opacity };
shader.uniforms.cadSurfaceTangentOpacity = { value: classSettings.tangent.opacity };
shader.uniforms.cadSurfaceSeamOpacity = { value: classSettings.seam.opacity };
shader.uniforms.cadSurfaceDegenerateOpacity = { value: classSettings.degenerate.opacity };
shader.vertexShader = shader.vertexShader
.replace(
"#include <common>",
`#include <common>
attribute vec3 ${SURFACE_EDGE_BARYCENTRIC_ATTRIBUTE};
attribute vec3 ${SURFACE_EDGE_CLASS_ATTRIBUTE};
varying vec3 vCadSurfaceEdgeBarycentric;
varying vec3 vCadSurfaceEdgeClass;`
)
.replace(
"#include <begin_vertex>",
`#include <begin_vertex>
vCadSurfaceEdgeBarycentric = ${SURFACE_EDGE_BARYCENTRIC_ATTRIBUTE};
vCadSurfaceEdgeClass = ${SURFACE_EDGE_CLASS_ATTRIBUTE};`
);
shader.fragmentShader = shader.fragmentShader
.replace(
"#include <common>",
`#include <common>
uniform vec3 cadSurfaceEdgeColor;
uniform vec3 cadSurfaceFeatureColor;
uniform vec3 cadSurfaceTangentColor;
uniform vec3 cadSurfaceSeamColor;
uniform vec3 cadSurfaceDegenerateColor;
uniform float cadSurfaceFeatureThickness;
uniform float cadSurfaceTangentThickness;
uniform float cadSurfaceSeamThickness;
uniform float cadSurfaceDegenerateThickness;
uniform float cadSurfaceFeatureOpacity;
uniform float cadSurfaceTangentOpacity;
uniform float cadSurfaceSeamOpacity;
uniform float cadSurfaceDegenerateOpacity;
varying vec3 vCadSurfaceEdgeBarycentric;
varying vec3 vCadSurfaceEdgeClass;
float cadSurfaceEdgeThicknessFor(float classCode) {
if (classCode < 0.5) {
return 0.0;
}
if (abs(classCode - 2.0) < 0.5) {
return cadSurfaceTangentThickness;
}
if (abs(classCode - 3.0) < 0.5) {
return cadSurfaceSeamThickness;
}
if (abs(classCode - 4.0) < 0.5) {
return cadSurfaceDegenerateThickness;
}
return cadSurfaceFeatureThickness;
}
float cadSurfaceEdgeOpacityFor(float classCode) {
if (classCode < 0.5) {
return 0.0;
}
if (abs(classCode - 2.0) < 0.5) {
return cadSurfaceTangentOpacity;
}
if (abs(classCode - 3.0) < 0.5) {
return cadSurfaceSeamOpacity;
}
if (abs(classCode - 4.0) < 0.5) {
return cadSurfaceDegenerateOpacity;
}
return cadSurfaceFeatureOpacity;
}
vec3 cadSurfaceEdgeColorFor(float classCode) {
if (classCode < 0.5) {
return cadSurfaceEdgeColor;
}
if (abs(classCode - 2.0) < 0.5) {
return cadSurfaceTangentColor;
}
if (abs(classCode - 3.0) < 0.5) {
return cadSurfaceSeamColor;
}
if (abs(classCode - 4.0) < 0.5) {
return cadSurfaceDegenerateColor;
}
return cadSurfaceFeatureColor;
}
float cadSurfaceEdgeCoverage(float barycentric, float classCode) {
float thickness = cadSurfaceEdgeThicknessFor(classCode);
float opacity = cadSurfaceEdgeOpacityFor(classCode);
if (thickness <= 0.0 || opacity <= 0.0) {
return 0.0;
}
float pixelDistance = barycentric / max(fwidth(barycentric), 1e-6);
float halfWidth = max(thickness * 0.5, 0.001);
float coverage = 1.0 - smoothstep(max(halfWidth - 0.75, 0.0), halfWidth + 0.75, pixelDistance);
return clamp(coverage * opacity, 0.0, 1.0);
}
vec4 cadSurfaceEdgeLayerFor(float barycentric, float classCode) {
float edgeAlpha = cadSurfaceEdgeCoverage(barycentric, classCode);
return vec4(cadSurfaceEdgeColorFor(classCode), edgeAlpha);
}
vec4 cadSurfaceEdgeLayer() {
vec4 edge0 = cadSurfaceEdgeLayerFor(vCadSurfaceEdgeBarycentric.x, vCadSurfaceEdgeClass.x);
vec4 edge1 = cadSurfaceEdgeLayerFor(vCadSurfaceEdgeBarycentric.y, vCadSurfaceEdgeClass.y);
vec4 edge2 = cadSurfaceEdgeLayerFor(vCadSurfaceEdgeBarycentric.z, vCadSurfaceEdgeClass.z);
vec4 edge = edge0;
if (edge1.a > edge.a) {
edge = edge1;
}
if (edge2.a > edge.a) {
edge = edge2;
}
return edge;
}`
)
.replace(
"#include <opaque_fragment>",
`vec4 cadSurfaceEdgeMix = cadSurfaceEdgeLayer();
if (cadSurfaceEdgeMix.a > 0.0) {
outgoingLight = mix(outgoingLight, cadSurfaceEdgeMix.rgb, cadSurfaceEdgeMix.a);
}
#include <opaque_fragment>`
);
};
material.customProgramCacheKey = () => "cad-surface-edges-v2";
}
function createSurfaceMaterial(THREE, baseTheme, { color, useVertexColors = false, edgeSettings = null } = {}) {
const opacity = Number.isFinite(Number(baseTheme?.surfaceOpacity))
? Number(baseTheme.surfaceOpacity)
: 1;
const material = new THREE.MeshPhysicalMaterial({
color: color || baseTheme?.surface || DEFAULT_THEME.surface,
roughness: Number.isFinite(Number(baseTheme?.surfaceRoughness)) ? Number(baseTheme.surfaceRoughness) : DEFAULT_THEME.surfaceRoughness,
metalness: Number.isFinite(Number(baseTheme?.surfaceMetalness)) ? Number(baseTheme.surfaceMetalness) : DEFAULT_THEME.surfaceMetalness,
clearcoat: Number.isFinite(Number(baseTheme?.surfaceClearcoat)) ? Number(baseTheme.surfaceClearcoat) : DEFAULT_THEME.surfaceClearcoat,
clearcoatRoughness: Number.isFinite(Number(baseTheme?.surfaceClearcoatRoughness)) ? Number(baseTheme.surfaceClearcoatRoughness) : DEFAULT_THEME.surfaceClearcoatRoughness,
side: THREE.DoubleSide,
vertexColors: useVertexColors,
transparent: opacity < 0.999,
opacity,
emissive: 0x000000,
emissiveIntensity: 0,
polygonOffset: false,
polygonOffsetFactor: 0,
polygonOffsetUnits: 0
});
if (edgeSettings?.enabled) {
addCadSurfaceEdgeShader(THREE, material, edgeSettings, baseTheme);
}
return material;
}
function createWireframeSurfaceMaterial(THREE, materialSettings, fillIndex = 0) {
return new THREE.MeshBasicMaterial({
color: resolveThemeFillColor(materialSettings || {}, fillIndex),
transparent: true,
opacity: 0.035,
depthWrite: false
});
}
function createUnshadedSurfaceMaterial(THREE, { color, useVertexColors = false, opacity = 1 } = {}) {
return new THREE.MeshBasicMaterial({
color: color || DEFAULT_THEME.surface,
side: THREE.DoubleSide,
vertexColors: useVertexColors,
transparent: opacity < 0.999,
opacity,
depthWrite: opacity >= 0.999
});
}
function sourceColorForPart(THREE, part, meshData) {
return readSourceColor(THREE, part?.color || meshData?.sourceColor);
}
function sourceOpacityForPart(part, fallback = 1) {
const opacity = Number(part?.opacity);
return Number.isFinite(opacity) ? clamp(opacity, 0, 1) : fallback;
}
function meshUsesPartSourceColors(meshData, parts) {
const renderableParts = Array.isArray(parts) ? parts : [];
const partColors = renderableParts
.map((part) => String(part?.color || "").trim().toLowerCase())
.filter(Boolean);
if (!partColors.length) {
return false;
}
return partColors.length !== renderableParts.length || new Set(partColors).size > 1;
}
function meshUsesPartSourceOpacity(parts) {
const renderableParts = Array.isArray(parts) ? parts : [];
return renderableParts.some((part) => {
const opacity = Number(part?.opacity);
return Number.isFinite(opacity) && clamp(opacity, 0, 1) < 0.999;
});
}
function emptyLineGeometry(THREE) {
const geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.BufferAttribute(new Float32Array(0), 3));
return geometry;
}
function stablePartFillKey(part, index) {
return [
String(part?.occurrenceId || ""),
String(part?.id || ""),
String(part?.partSourcePath || part?.sourcePath || ""),
String(part?.label || part?.name || ""),
String(index).padStart(8, "0")
].join("\u0000");
}
export function buildPartFillIndexMap(parts = []) {
return new Map(
[...parts]
.map((part, index) => ({ part, key: stablePartFillKey(part, index) }))
.sort((left, right) => left.key.localeCompare(right.key))
.map(({ part }, index) => [part, index])
);
}
function geometryCacheEntry(THREE, meshData, key, createGeometry) {
const cache = cacheForMeshData(meshData);
const cached = cache.part.get(key) || cache.whole.get(key);
if (cached) {
return cached;
}
const entry = createGeometry();
if (!entry?.geometry) {
return null;
}
markCachedGeometry(entry.geometry);
if (key === MODEL_PART_ID) {
cache.whole.set(key, entry);
} else {
cache.part.set(key, entry);
}
return entry;
}
function buildPartGeometryEntry(THREE, meshData, part, recomputeNormals = false) {
const partId = String(part?.id || part?.occurrenceId || "").trim();
const sourceMesh = part?.sourceMesh && typeof part.sourceMesh === "object" ? part.sourceMesh : null;
const sourceMeshColorMode = sourceMesh && part?.hasSourceColors ? "source-colors" : "flat";
const sourceMeshKey = sourceMesh
? `source:${String(part?.sourceMeshKey || part?.meshUrl || part?.partFileRef || partId || "").trim()}:${sourceMeshColorMode}`
: "";
const key = sourceMeshKey || partId || `${toNumber(part?.vertexOffset)}:${toNumber(part?.triangleOffset)}`;
return geometryCacheEntry(THREE, meshData, key, () => {
const vertexOffset = sourceMesh ? 0 : toNumber(part?.vertexOffset, 0);
const vertexCount = sourceMesh
? Math.floor((sourceMesh.vertices?.length || 0) / 3)
: toNumber(part?.vertexCount, 0);
const triangleOffset = sourceMesh ? 0 : toNumber(part?.triangleOffset, 0);
const triangleCount = sourceMesh
? Math.floor((sourceMesh.indices?.length || 0) / 3)
: toNumber(part?.triangleCount, 0);
if (vertexCount <= 0 || triangleCount <= 0) {
return null;
}
let localVertices;
let rawColors;
let localNormals;
let localIndices;
if (sourceMesh) {
localVertices = sourceMesh.vertices || new Float32Array(0);
rawColors = part?.hasSourceColors &&
isNumericArray(sourceMesh.colors, 3) &&
sourceMesh.colors.length === localVertices.length
? new Float32Array(sourceMesh.colors)
: null;
localNormals = isNumericArray(sourceMesh.normals, 3) ? sourceMesh.normals : null;
localIndices = sourceMesh.indices || new Uint32Array(0);
} else {
const positionStart = vertexOffset * 3;
const positionEnd = positionStart + vertexCount * 3;
localVertices = meshData.vertices.slice(positionStart, positionEnd);
rawColors = partUsesDisplayVertexColors(meshData, part)
? new Float32Array(meshData.colors.slice(positionStart, positionEnd))
: null;
localNormals = isNumericArray(meshData.normals, 3) ? meshData.normals.slice(positionStart, positionEnd) : null;
const rawIndices = meshData.indices.slice(triangleOffset * 3, triangleOffset * 3 + triangleCount * 3);
localIndices = new Uint32Array(rawIndices.length);
for (let index = 0; index < rawIndices.length; index += 1) {
localIndices[index] = Math.max(0, Number(rawIndices[index]) - vertexOffset);
}
}
if (!localIndices.length) {
return null;
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute(
"position",
new THREE.BufferAttribute(localVertices instanceof Float32Array ? localVertices : new Float32Array(localVertices), 3)
);
geometry.setIndex(new THREE.BufferAttribute(localIndices instanceof Uint32Array ? localIndices : new Uint32Array(localIndices), 1));
if (rawColors && rawColors.length === localVertices.length) {
geometry.setAttribute(
"color",
new THREE.BufferAttribute(new Float32Array(rawColors), 3)
);
}
if (!sourceMesh) {
setSurfaceEdgeAttributes(THREE, geometry, meshData, vertexOffset, vertexCount);
}
applyGeometryNormals(THREE, geometry, localNormals, recomputeNormals);
geometry.computeBoundingSphere();
return {
geometry,
rawColors
};
});
}
function buildWholeGeometryEntry(THREE, meshData, recomputeNormals = false) {
return geometryCacheEntry(THREE, meshData, MODEL_PART_ID, () => {
const geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.BufferAttribute(new Float32Array(meshData.vertices || []), 3));
geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(meshData.indices || []), 1));
const rawColors = shouldUseDisplayVertexColors(meshData) && meshData.colors?.length === meshData.vertices?.length
? new Float32Array(meshData.colors)
: null;
if (rawColors) {
geometry.setAttribute("color", new THREE.BufferAttribute(new Float32Array(rawColors), 3));
}
setSurfaceEdgeAttributes(THREE, geometry, meshData, 0, Math.floor((meshData.vertices?.length || 0) / 3));
applyGeometryNormals(THREE, geometry, meshData.normals, recomputeNormals);
geometry.computeBoundingSphere();
return {
geometry,
rawColors
};
});
}
function syncRecordVertexColors(THREE, record, materialSettings) {
if (!record?.geometry || !record.rawColors || !record.hasVertexColors) {
return;
}
const shapedColors = shapeSourceColorBuffer(THREE, record.rawColors, materialSettings);
if (!shapedColors) {
return;
}
const attribute = record.geometry.getAttribute("color");
if (attribute?.array?.length === shapedColors.length) {
attribute.array.set(shapedColors);
attribute.needsUpdate = true;
return;
}
record.geometry.setAttribute("color", new THREE.BufferAttribute(shapedColors, 3));
}
function buildEdgeGeometryFromIndices(THREE, vertices, edgeIndices) {
if (!isNumericArray(vertices, 3) || !isNumericArray(edgeIndices, 2)) {
return null;
}
const vertexCount = Math.floor(vertices.length / 3);
const segmentCount = Math.floor(edgeIndices.length / 2);
if (segmentCount <= 0) {
return null;
}
const linePositions = new Float32Array(segmentCount * 6);
let writeOffset = 0;
for (let index = 0; index + 1 < edgeIndices.length; index += 2) {
const a = Number(edgeIndices[index]);
const b = Number(edgeIndices[index + 1]);
if (!Number.isInteger(a) || !Number.isInteger(b) || a < 0 || b < 0 || a >= vertexCount || b >= vertexCount) {
continue;
}
const aOffset = a * 3;
const bOffset = b * 3;
linePositions[writeOffset] = Number(vertices[aOffset]);
linePositions[writeOffset + 1] = Number(vertices[aOffset + 1]);
linePositions[writeOffset + 2] = Number(vertices[aOffset + 2]);
linePositions[writeOffset + 3] = Number(vertices[bOffset]);
linePositions[writeOffset + 4] = Number(vertices[bOffset + 1]);
linePositions[writeOffset + 5] = Number(vertices[bOffset + 2]);
writeOffset += 6;
}
if (!writeOffset) {
return null;
}
const geometry = new THREE.BufferGeometry();
const packedPositions = writeOffset === linePositions.length ? linePositions : linePositions.subarray(0, writeOffset);
geometry.setAttribute("position", new THREE.BufferAttribute(packedPositions, 3));
return geometry;
}
function buildEdgeGeometry(THREE, meshData, part, sourceGeometry, displayMode, edgeSettings = {}) {
void edgeSettings;
const cache = cacheForMeshData(meshData);
const partId = part ? String(part?.id || part?.occurrenceId || "").trim() : MODEL_PART_ID;
const sourceMeshKey = part?.sourceMesh
? String(part?.sourceMeshKey || part?.meshUrl || part?.partFileRef || "").trim()
: "";
const edgeKey = `${displayMode}:${sourceMeshKey ? `source:${sourceMeshKey}` : (partId || MODEL_PART_ID)}`;
const cached = cache.edge.get(edgeKey);
if (cached) {
return cached;
}
let geometry = null;
if (displayModeIsWireframe(displayMode)) {
geometry = new THREE.WireframeGeometry(sourceGeometry);
} else if (part) {
const edgeIndexOffset = toNumber(part?.edgeIndexOffset, 0);
const edgeIndexCount = toNumber(part?.edgeIndexCount, 0);
const hasExplicitPartEdges = edgeIndexCount >= 2 && isNumericArray(meshData?.edge_indices, 2);
if (hasExplicitPartEdges) {
const partEdgeIndices = typeof meshData.edge_indices.subarray === "function"
? meshData.edge_indices.subarray(edgeIndexOffset, edgeIndexOffset + edgeIndexCount)
: meshData.edge_indices.slice(edgeIndexOffset, edgeIndexOffset + edgeIndexCount);
geometry = buildEdgeGeometryFromIndices(THREE, meshData.vertices, partEdgeIndices);
}
geometry ||= new THREE.EdgesGeometry(sourceGeometry, CAD_EDGE_THRESHOLD_DEG);
} else if (isNumericArray(meshData?.edge_indices, 2)) {
geometry = buildEdgeGeometryFromIndices(THREE, meshData.vertices, meshData.edge_indices);
}
geometry ||= new THREE.EdgesGeometry(sourceGeometry, CAD_EDGE_THRESHOLD_DEG);
if (!geometry.getAttribute("position")?.count) {
geometry.dispose();
geometry = emptyLineGeometry(THREE);
}
markCachedGeometry(geometry);
cache.edge.set(edgeKey, geometry);
return geometry;
}
function getEdgeThickness(edgeSettings = null, baseTheme = null) {
const fallbackThickness = Number.isFinite(Number(baseTheme?.edgeThickness))
? Number(baseTheme.edgeThickness)
: DEFAULT_THEME.edgeThickness;
return Number.isFinite(Number(edgeSettings?.thickness))
? clamp(Number(edgeSettings.thickness), 0.5, 6)
: fallbackThickness;
}
function syncLineMaterialOpacity(material, opacity) {
if (!material) {
return;
}
const nextOpacity = clamp(Number(opacity) || 0, 0, 1);
const nextTransparent = nextOpacity < 0.999;
material.opacity = nextOpacity;
material.depthWrite = false;
if (material.transparent !== nextTransparent) {
material.transparent = nextTransparent;
material.needsUpdate = true;
}
}
function createDefaultEdgeObject(THREE, geometry, baseTheme, edgeSettings, partId, displayMode) {
const wireframeMode = displayModeIsWireframe(displayMode);
const depthTest = edgeSettings?.depthTest === false ? false : !wireframeMode;
const material = new THREE.LineBasicMaterial({
color: edgeSettings?.color || baseTheme?.edge || DEFAULT_THEME.edge,
transparent: true,
opacity: wireframeMode
? Math.max(toNumber(edgeSettings?.opacity, 0.92), 0.9)
: toNumber(edgeSettings?.opacity, baseTheme?.edgeOpacity ?? CAD_EDGE_OPACITY),
depthTest,
depthWrite: false
});
const object = new THREE.LineSegments(geometry, material);
object.userData.partId = partId;
return { object, material };
}
function normalizeEdgeResult(result) {
if (!result) {
return { object: null, material: null };
}
const object = result.object || result.edgeMesh || result.mesh || result.line || null;
return {
object,
material: result.material || result.edgeMaterial || object?.material || null
};
}
function normalizeEdgeRendering(edgeRendering = null) {
if (!edgeRendering || typeof edgeRendering !== "object") {
return { mode: "basic" };
}
const mode = String(edgeRendering.mode || edgeRendering.type || "").trim().toLowerCase();
return {
...edgeRendering,
mode: mode === "screen-space" || mode === "screenspace" ? "screen-space" : "basic",
wireframeEdgeColor: String(edgeRendering.wireframeEdgeColor || "").trim()
};
}
function applyEdgeRenderingToRuntime(runtime, edgeRendering = {}) {
runtime.edgeRendering = edgeRendering;
for (const key of ["Line2", "LineGeometry", "LineSegments2", "LineSegmentsGeometry", "LineMaterial"]) {
runtime[key] = edgeRendering[key] || edgeRendering.constructors?.[key] || null;
}
}
function createSilhouetteMesh(THREE, geometry, edgeSettings, radius) {
const offset = radius * clamp(toNumber(edgeSettings?.silhouetteScale, 0.004), 0, 0.04);
if (!(offset > 0)) {
return null;
}
const material = new THREE.ShaderMaterial({
uniforms: {
color: { value: new THREE.Color(edgeSettings?.color || DEFAULT_THEME.edge) },
opacity: { value: clamp(toNumber(edgeSettings?.opacity, 0.9), 0, 1) },
offset: { value: offset }
},
vertexShader: `
uniform float offset;
void main() {
vec3 displaced = position + normal * offset;
gl_Position = projectionMatrix * modelViewMatrix * vec4(displaced, 1.0);
}
`,
fragmentShader: `
uniform vec3 color;
uniform float opacity;
void main() {
gl_FragColor = vec4(color, opacity);
}
`,
side: THREE.BackSide,
transparent: true,
depthTest: true,
depthWrite: false
});
const mesh = new THREE.Mesh(geometry, material);
mesh.renderOrder = -1;
return mesh;
}
function shouldBuildSilhouette(edgeSettings, displayMode, settings = {}) {
return (
settings.silhouette !== false &&
!displayModeIsWireframe(displayMode) &&
edgeSettings.silhouette === true &&
(edgeSettings.enabled === true || settings.silhouette === true)
);
}
export function readBoundsCenter(THREE, bounds) {
const min = Array.isArray(bounds?.min) ? bounds.min : [0, 0, 0];
const max = Array.isArray(bounds?.max) ? bounds.max : min;
return new THREE.Vector3(
(toNumber(min[0]) + toNumber(max[0])) / 2,
(toNumber(min[1]) + toNumber(max[1])) / 2,
(toNumber(min[2]) + toNumber(max[2])) / 2
);
}
function safeColor(THREE, value, fallback = null) {
const text = String(value || "").trim();
if (!text) {
return fallback;
}
try {
return new THREE.Color(text);
} catch {
return fallback;
}
}
export function applyMaterialSettingsToRecord(THREE, record, materialSettings, {
baseTheme = DEFAULT_THEME,
displayMode = CAD_DISPLAY_MODE.SOLID
} = {}) {
if (!record?.material || !materialSettings) {
return;
}
const wireframeMode = displayModeIsWireframe(displayMode);
const forceFill = materialSettings.overrideSourceColors === true || wireframeMode;
const hasVertexColors = !forceFill && !!record.hasVertexColors;
record.useVertexColors = hasVertexColors;
record.baseColor = resolveSourceBaseColor(THREE, {
hasVertexColors,
sourceColor: forceFill ? null : record.sourceColor || null,
materialSettings,
fallbackColor: materialSettings?.defaultColor || baseTheme?.surface || DEFAULT_THEME.surface,
fillIndex: record.fillIndex || 0,
forceFill: forceFill || !record.hasSourceColor
});
record.material.vertexColors = hasVertexColors;
if (wireframeMode) {
if (record.material.color && record.baseColor) {
record.material.color.copy(record.baseColor);
}
record.baseOpacity = displayModeSurfaceOpacity(displayMode, 0.035);
record.material.opacity = record.baseOpacity;
record.material.transparent = true;
record.material.depthWrite = false;
record.material.needsUpdate = true;
return;
}
syncRecordVertexColors(THREE, record, materialSettings);
record.material.roughness = clamp(Number(materialSettings.roughness) || 0, 0, 1);
record.material.metalness = clamp(Number(materialSettings.metalness) || 0, 0, 1);
record.material.clearcoat = clamp(Number(materialSettings.clearcoat) || 0, 0, 1);
record.material.clearcoatRoughness = clamp(Number(materialSettings.clearcoatRoughness) || 0, 0, 1);
const sourceOpacity = Number.isFinite(Number(record.sourceOpacity))
? clamp(Number(record.sourceOpacity), 0, 1)
: 1;
record.baseOpacity = clamp(displayModeSurfaceOpacity(displayMode, materialSettings.opacity) * sourceOpacity, 0, 1);
record.material.opacity = record.baseOpacity;
record.material.transparent = record.baseOpacity < 0.999;
record.material.depthWrite = displayMode === CAD_DISPLAY_MODE.TRANSPARENT ? false : record.baseOpacity >= 0.999;
record.material.envMapIntensity = Math.max(Number(materialSettings.envMapIntensity) || 0, 0);
if (record.material.color && record.baseColor) {
record.material.color.copy(record.baseColor);
}
record.baseEmissiveIntensity = clamp(Number(materialSettings.emissiveIntensity) || 0, 0, 2);
record.baseEmissiveColor = record.baseColor ? record.baseColor.clone() : null;
if ("emissive" in record.material && record.material.emissive) {
if (record.baseEmissiveColor && record.baseEmissiveIntensity > 0) {
record.material.emissive.copy(record.baseEmissiveColor);
} else {
record.material.emissive.set(0x000000);
}
record.material.emissiveIntensity = record.baseEmissiveIntensity;
}
record.material.needsUpdate = true;
}
function normalizePartIdList(value) {
return (Array.isArray(value) ? value : [value])
.map((id) => String(id || "").trim())
.filter(Boolean);
}
function normalizePartSelector(value) {
const text = String(value || "").trim();
return text.startsWith("#") ? text.slice(1).trim() : text;
}
function partIdMatchesSet(partId, set) {
if (!set?.size) {
return false;
}
if (set.has(MODEL_PART_ID)) {
return true;
}
const normalizedPartId = normalizePartSelector(partId);
if (!normalizedPartId) {
return false;
}
for (const candidate of set) {
const normalizedCandidate = normalizePartSelector(candidate);
if (
normalizedCandidate &&
(
normalizedPartId === normalizedCandidate ||
normalizedPartId.startsWith(`${normalizedCandidate}.`)
)
) {
return true;
}
}
return false;
}
function baseObjectRenderOrder(record, object, fieldName) {
if (!object) {
return 0;
}
if (!Number.isFinite(Number(record[fieldName]))) {
record[fieldName] = Number.isFinite(Number(object.renderOrder)) ? Number(object.renderOrder) : 0;
}
return record[fieldName];
}
function syncHighlightRenderOrder(record, object, fieldName, highlighted, highlightRenderOrder) {
if (!object) {
return;
}
const baseRenderOrder = baseObjectRenderOrder(record, object, fieldName);
object.renderOrder = highlighted ? highlightRenderOrder : baseRenderOrder;
}
function syncSurfaceTransparency(record, forceTransparent, opacity, {
writeTransparentDepth = true
} = {}) {
const material = record?.material;
if (!material) {
return;
}
if (!Object.hasOwn(record, "baseDepthWrite")) {
record.baseDepthWrite = material.depthWrite !== false;
}
const nextTransparent = forceTransparent || opacity < 0.999;
if (material.transparent !== nextTransparent) {
material.transparent = nextTransparent;
material.needsUpdate = true;
}
material.depthWrite = nextTransparent && !writeTransparentDepth ? false : record.baseDepthWrite;
}
const CAD_SURFACE_EDGE_OPACITY_UNIFORMS = Object.freeze({
feature: "cadSurfaceFeatureOpacity",
tangent: "cadSurfaceTangentOpacity",
seam: "cadSurfaceSeamOpacity",
degenerate: "cadSurfaceDegenerateOpacity"
});
const CAD_SURFACE_EDGE_COLOR_UNIFORMS = Object.freeze({
feature: "cadSurfaceFeatureColor",
tangent: "cadSurfaceTangentColor",
seam: "cadSurfaceSeamColor",
degenerate: "cadSurfaceDegenerateColor"
});
function syncCadSurfaceEdgeHighlight(THREE, record, edgeColor, edgeOpacity = null) {
const material = record?.material;
const userData = material?.userData;
if (!material || userData?.cadSurfaceEdges !== true) {
return;
}
const nextColor = edgeColor?.isColor
? edgeColor
: readSourceColor(THREE, edgeColor) || userData.cadSurfaceEdgeBaseColor;
if (nextColor?.isColor) {
userData.cadSurfaceEdgeColor = nextColor.clone();
const colorUniform = userData.cadSurfaceEdgeShader?.uniforms?.cadSurfaceEdgeColor;
if (colorUniform?.value?.copy) {
colorUniform.value.copy(nextColor);
}
}
const highlightedOpacity = edgeOpacity !== null && edgeOpacity !== undefined && Number.isFinite(Number(edgeOpacity))
? clamp(Number(edgeOpacity), 0, 1)
: null;
const baseClassSettings = userData.cadSurfaceEdgeBaseClassSettings || {};
const uniforms = userData.cadSurfaceEdgeShader?.uniforms || null;
const overrideClassColor = highlightedOpacity !== null ||
(nextColor?.isColor && userData.cadSurfaceEdgeBaseColor?.isColor && !nextColor.equals(userData.cadSurfaceEdgeBaseColor));
for (const [classId, uniformName] of Object.entries(CAD_SURFACE_EDGE_COLOR_UNIFORMS)) {
const baseClassColor = readSourceColor(THREE, baseClassSettings[classId]?.color) ||
userData.cadSurfaceEdgeBaseColor;
const nextClassColor = overrideClassColor ? nextColor : baseClassColor;
if (nextClassColor?.isColor && uniforms?.[uniformName]?.value?.copy) {
uniforms[uniformName].value.copy(nextClassColor);
}
}
for (const [classId, uniformName] of Object.entries(CAD_SURFACE_EDGE_OPACITY_UNIFORMS)) {
const baseOpacity = Number(baseClassSettings[classId]?.opacity);
const nextOpacity = highlightedOpacity === null
? (Number.isFinite(baseOpacity) ? baseOpacity : null)
: highlightedOpacity;
if (nextOpacity === null) {
continue;
}
userData[`cadSurfaceEdge${classId}Opacity`] = nextOpacity;
if (uniforms?.[uniformName]) {
uniforms[uniformName].value = nextOpacity;
}
}
}
export function applyPartVisualState(THREE, records, {
baseTheme = DEFAULT_THEME,
edgeSettings,
hiddenPartIds,
hoveredPartId,
focusedPartId,
selectedPartIds,
showEdges = true
} = {}) {
const hidden = new Set(Array.isArray(hiddenPartIds) ? hiddenPartIds : []);
const selected = new Set(Array.isArray(selectedPartIds) ? selectedPartIds : []);
const hovered = new Set(
(Array.isArray(hoveredPartId) ? hoveredPartId : [hoveredPartId])
.map((id) => String(id || "").trim())
.filter(Boolean)
);
const baseEdgeColor = edgeSettings?.color || baseTheme?.edge || DEFAULT_THEME.edge;
const defaultSurfaceOpacity = Number.isFinite(Number(baseTheme?.surfaceOpacity))
? Number(baseTheme.surfaceOpacity)
: 1;
const focusIds = new Set(normalizePartIdList(focusedPartId));
const hasFocus = focusIds.size > 0;
const baseEdgeOpacity = Number.isFinite(Number(edgeSettings?.opacity))
? clamp(Number(edgeSettings.opacity), 0, 1)
: (baseTheme?.edgeOpacity ?? DEFAULT_THEME.edgeOpacity ?? CAD_EDGE_OPACITY);
const highlightEdgeOpacity = Number.isFinite(Number(edgeSettings?.highlightOpacity))
? clamp(Number(edgeSettings.highlightOpacity), 0, 1)
: 1;
const edgeHighlightColor = String(edgeSettings?.highlightColor || REFERENCE_SELECTED_COLOR).trim() || REFERENCE_SELECTED_COLOR;
const hoveredSurfaceColor = new THREE.Color(REFERENCE_HOVER_COLOR);
const hoveredEdgeColor = new THREE.Color(edgeHighlightColor);
const selectedSurfaceColor = new THREE.Color(REFERENCE_SELECTED_COLOR);
const selectedEdgeColor = new THREE.Color(edgeHighlightColor);
for (const record of Array.isArray(records) ? records : []) {
if (!record?.mesh || !record?.material) {
continue;
}
const effectStyle = record.effectStyle && typeof record.effectStyle === "object" ? record.effectStyle : {};
const effectHidden = record.effectVisible === false;
const effectColor = readSourceColor(THREE, effectStyle.color);
const effectEdgeColor = readSourceColor(THREE, effectStyle.edgeColor);
const effectEmissive = readSourceColor(THREE, effectStyle.emissive);
const isHidden = partIdMatchesSet(record.partId, hidden);
const isSelected = !isHidden && (partIdMatchesSet(record.partId, selected) || record.effectHighlighted === true);
const isHovered = !isHidden && !effectHidden && partIdMatchesSet(record.partId, hovered);
const isFocused = !isHidden && !effectHidden && hasFocus && partIdMatchesSet(record.partId, focusIds);
const isDimmed = !isHidden && !effectHidden && hasFocus && !isFocused;
const isHighlighted = isSelected || isHovered;
record.mesh.visible = !effectHidden;
if (record.edges) {
record.edges.visible = showEdges && !effectHidden;
}
if (record.silhouette) {
record.silhouette.visible = !effectHidden;
}
syncHighlightRenderOrder(record, record.mesh, "baseMeshRenderOrder", isHighlighted, PART_HIGHLIGHT_SURFACE_RENDER_ORDER);
syncHighlightRenderOrder(record, record.edges, "baseEdgeRenderOrder", isHighlighted, PART_HIGHLIGHT_EDGE_RENDER_ORDER);
const baseSurfaceOpacity = Number.isFinite(Number(record.baseOpacity))
? Number(record.baseOpacity)
: defaultSurfaceOpacity;
const effectOpacity = Number.isFinite(Number(effectStyle.opacity))
? clamp(Number(effectStyle.opacity), 0, 1)
: 1;
const effectEdgeOpacity = Number.isFinite(Number(effectStyle.edgeOpacity))
? clamp(Number(effectStyle.edgeOpacity), 0, 1)
: effectOpacity;
const highlightedEdgeOpacity = (isSelected || isHovered) ? highlightEdgeOpacity * effectEdgeOpacity : null;
const dimmedSurfaceOpacity = Math.min(baseSurfaceOpacity * effectOpacity, FOCUSED_DIMMED_SURFACE_OPACITY);
const highlightedSurfaceOpacity = isSelected
? clamp((baseSurfaceOpacity * effectOpacity) + PART_SELECTED_OPACITY_BOOST, 0, 1)
: isHovered
? clamp((baseSurfaceOpacity * effectOpacity) + PART_HOVER_OPACITY_BOOST, 0, 1)
: baseSurfaceOpacity * effectOpacity;
const nextSurfaceOpacity = isHidden || isDimmed ? dimmedSurfaceOpacity : highlightedSurfaceOpacity;
syncSurfaceTransparency(record, isHidden || isDimmed || isHighlighted, nextSurfaceOpacity, {
writeTransparentDepth: !isHidden && !isDimmed
});
record.material.opacity = nextSurfaceOpacity;
if (record.baseColor && record.material.color) {
record.material.color.copy(
isSelected
? selectedSurfaceColor
: isHovered
? hoveredSurfaceColor
: effectColor || record.baseColor
);
}
if ("emissive" in record.material && record.material.emissive) {
if (isSelected) {
record.material.emissive.set(REFERENCE_SELECTED_COLOR);
} else if (isHovered) {
record.material.emissive.set(REFERENCE_HOVER_COLOR);
} else if (record.baseEmissiveColor && record.baseEmissiveIntensity > 0) {
record.material.emissive.copy(record.baseEmissiveColor);
} else {
record.material.emissive.set(0x000000);
}
record.material.emissiveIntensity = isSelected
? 0.08
: isHovered
? 0.12
: effectEmissive
? clamp(Number(effectStyle.emissiveIntensity) || 0.22, 0, 2)
: clamp(Number(record.baseEmissiveIntensity) || 0, 0, 2);
if (!isSelected && !isHovered && effectEmissive) {
record.material.emissive.copy(effectEmissive);
}
}
const nextEdgeColor = isSelected
? selectedEdgeColor
: isHovered
? hoveredEdgeColor
: effectEdgeColor || baseEdgeColor;
syncCadSurfaceEdgeHighlight(THREE, record, nextEdgeColor, highlightedEdgeOpacity);
if (record.edgeMaterial) {
record.edgeMaterial.color?.set?.(nextEdgeColor);
syncLineMaterialOpacity(record.edgeMaterial, isSelected
? highlightEdgeOpacity * effectEdgeOpacity
: isHovered
? highlightEdgeOpacity * effectEdgeOpacity
: isHidden || isDimmed
? nextSurfaceOpacity
: baseEdgeOpacity * effectEdgeOpacity);
}
}
}
function resetParameterEffects(records) {
for (const record of Array.isArray(records) ? records : []) {
record.effectMatrix = null;
record.effectStyle = null;
record.effectVisible = null;
record.effectHighlighted = false;
}
}
function boundsCorners(THREE, bounds) {
const min = Array.isArray(bounds?.min) ? bounds.min : [0, 0, 0];
const max = Array.isArray(bounds?.max) ? bounds.max : [1, 1, 1];
return [
[min[0], min[1], min[2]],
[min[0], min[1], max[2]],
[min[0], max[1], min[2]],
[min[0], max[1], max[2]],
[max[0], min[1], min[2]],
[max[0], min[1], max[2]],
[max[0], max[1], min[2]],
[max[0], max[1], max[2]]
].map((corner) => new THREE.Vector3(
toNumber(corner[0]),
toNumber(corner[1]),
toNumber(corner[2])
));
}
function transformedBounds(THREE, bounds, matrix = null) {
if (!bounds || !Array.isArray(bounds.min) || !Array.isArray(bounds.max)) {
return null;
}
if (!(matrix instanceof THREE.Matrix4)) {
return {
min: [...bounds.min],
max: [...bounds.max]
};
}
const corners = boundsCorners(THREE, bounds).map((corner) => corner.applyMatrix4(matrix));
const min = [Infinity, Infinity, Infinity];
const max = [-Infinity, -Infinity, -Infinity];
for (const corner of corners) {
min[0] = Math.min(min[0], corner.x);
min[1] = Math.min(min[1], corner.y);
min[2] = Math.min(min[2], corner.z);
max[0] = Math.max(max[0], corner.x);
max[1] = Math.max(max[1], corner.y);
max[2] = Math.max(max[2], corner.z);
}
return min.every(Number.isFinite) && max.every(Number.isFinite) ? { min, max } : null;
}
function mergeBoundsList(boundsList) {
const min = [Infinity, Infinity, Infinity];
const max = [-Infinity, -Infinity, -Infinity];
let count = 0;
for (const bounds of Array.isArray(boundsList) ? boundsList : []) {
if (!bounds || !Array.isArray(bounds.min) || !Array.isArray(bounds.max)) {
continue;
}
count += 1;
for (let axis = 0; axis < 3; axis += 1) {
min[axis] = Math.min(min[axis], toNumber(bounds.min[axis]));
max[axis] = Math.max(max[axis], toNumber(bounds.max[axis]));
}
}
return count > 0 && min.every(Number.isFinite) && max.every(Number.isFinite) ? { min, max } : null;
}
function effectiveBoundsFromRecords(THREE, records, fallbackBounds) {
const boundsList = [];
for (const record of Array.isArray(records) ? records : []) {
if (record.effectVisible === false) {
continue;
}
const baseMatrix = buildPartTransformMatrix(THREE, record.baseTransform);
const effectMatrix = record.effectMatrix instanceof THREE.Matrix4 ? record.effectMatrix.clone() : null;
const combinedMatrix = effectMatrix ? effectMatrix.multiply(baseMatrix) : baseMatrix;
boundsList.push(transformedBounds(THREE, record.partBounds, combinedMatrix));
}
return mergeBoundsList(boundsList) || fallbackBounds;
}
function runParameterSetup(THREE, runtime, parameters, meshData, callbacks = {}) {
const definition = parameters?.definition || null;
const module = definition?.module || null;
if (!definition || !module?.setup) {
return;
}
const effectsByPartId = new Map();
const features = resolveStepModuleFeatures(definition, {
meshData,
selectorRuntime: parameters?.selectorRuntime || null
});
const ctx = buildStepModuleContext({
runtime,
stepModuleRuntime: parameters,
features,
effects: createStepModuleEffectsApi(THREE, {
meshData,
features,
runtime,
effectsByPartId
}),
cleanup: (cleanup) => {
if (typeof cleanup === "function") {
runtime.cleanups.push(cleanup);
}
}
});
try {
module.setup(ctx);
} catch (error) {
callbacks.onWarning?.({
title: "STEP parameter setup failed",
message: error instanceof Error ? error.message : String(error),
error
});
}
}
function cleanupParameterRuntime(runtime, parameters, callbacks = {}) {
while (runtime.cleanups.length) {
try {
runtime.cleanups.pop()?.();
} catch (error) {
callbacks.onWarning?.({
title: "STEP parameter cleanup failed",
message: error instanceof Error ? error.message : String(error),
error
});
}
}
const module = parameters?.definition?.module || null;
if (!module?.dispose) {
return;
}
const ctx = buildStepModuleContext({
runtime,
stepModuleRuntime: parameters,
features: {},
effects: {},
cleanup: () => {}
});
try {
module.dispose(ctx);
} catch (error) {
callbacks.onWarning?.({
title: "STEP parameter dispose failed",
message: error instanceof Error ? error.message : String(error),
error
});
}
}
function applyParameters(THREE, runtime, parameters, meshData, callbacks = {}) {
const definition = parameters?.definition || null;
const module = definition?.module || null;
if (!definition || !module) {
resetParameterEffects(runtime.displayRecords);
for (const record of runtime.displayRecords) {
applyDisplayRecordTransform(THREE, record);
}
return runtime.baseBounds;
}
const effectsByPartId = new Map();
const features = resolveStepModuleFeatures(definition, {
meshData,
selectorRuntime: parameters?.selectorRuntime || null
});
const effects = createStepModuleEffectsApi(THREE, {
meshData,
features,
runtime,
effectsByPartId
});
const ctx = buildStepModuleContext({
runtime,
stepModuleRuntime: parameters,
features,
effects,
cleanup: (cleanup) => {
if (typeof cleanup === "function") {
runtime.cleanups.push(cleanup);
}
}
});
try {
module.update?.(ctx);
module.render?.(ctx);
} catch (error) {
callbacks.onWarning?.({
title: "STEP parameter update failed",
message: error instanceof Error ? error.message : String(error),
error
});
}
applyStepModuleEffectsToRecords(THREE, runtime.displayRecords, effectsByPartId);
for (const record of runtime.displayRecords) {
applyDisplayRecordTransform(THREE, record);
}
return effectiveBoundsFromRecords(THREE, runtime.displayRecords, runtime.baseBounds);
}
export function buildStepClipPlane(THREE, clip, bounds, modelOffset = null) {
const normalized = normalizeStepClipSettings(clip);
if (!normalized.enabled || !bounds) {
return null;
}
const index = axisIndex(normalized.axis);
const boundsMin = Array.isArray(bounds?.min) ? bounds.min : [0, 0, 0];
const boundsMax = Array.isArray(bounds?.max) ? bounds.max : boundsMin;
const min = toNumber(boundsMin[index]);
const max = toNumber(boundsMax[index]);
const low = Math.min(min, max);
const high = Math.max(min, max);
const position = low + ((high - low) * normalized.offset);
const normal = new THREE.Vector3(
index === 0 ? 1 : 0,
index === 1 ? 1 : 0,
index === 2 ? 1 : 0
);
if (normalized.invert) {
normal.multiplyScalar(-1);
}
const point = modelOffset?.clone ? modelOffset.clone() : new THREE.Vector3(0, 0, 0);
point.setComponent(index, point.getComponent(index) + position);
return new THREE.Plane().setFromNormalAndCoplanarPoint(normal, point);
}
export function syncMaterialClipPlanes(material, clipPlanes) {
if (!material) {
return;
}
const materials = Array.isArray(material) ? material : [material];
const clippingEnabled = Array.isArray(clipPlanes) && clipPlanes.length > 0;
for (const item of materials) {
if (!item) {
continue;
}
const previousEnabled = item.userData?.cadClipPlaneEnabled === true;
const previousCount = Number(item.userData?.cadClipPlaneCount) || 0;
const previousShaderClipping = item.clipping === true;
item.clippingPlanes = clippingEnabled ? clipPlanes : null;
item.clipIntersection = false;
item.clipShadows = clippingEnabled;
if ("clipping" in item) {
item.clipping = clippingEnabled;
}
item.userData = {
...(item.userData || {}),
cadClipPlaneEnabled: clippingEnabled,
cadClipPlaneCount: clippingEnabled ? clipPlanes.length : 0
};
if (
previousEnabled !== clippingEnabled ||
previousCount !== (clippingEnabled ? clipPlanes.length : 0) ||
previousShaderClipping !== (item.clipping === true)
) {
item.needsUpdate = true;
}
}
}
function syncClip(runtime, clip, bounds, modelOffset = null) {
const clipPlane = buildStepClipPlane(runtime.THREE, clip, bounds, modelOffset);
const clipPlanes = clipPlane ? [clipPlane] : [];
runtime.activeClipPlane = clipPlane;
runtime.activeClipPlanes = clipPlanes;
for (const record of runtime.displayRecords) {
syncMaterialClipPlanes(record.material, clipPlanes);
syncMaterialClipPlanes(record.edgeMaterial, clipPlanes);
syncMaterialClipPlanes(record.silhouette?.material, clipPlanes);
}
}
function normalizeSelection(selection = {}) {
return selection && typeof selection === "object" ? selection : {};
}
function selectorEntries(value) {
if (Array.isArray(value)) {
return value;
}
const text = String(value || "").trim();
return text.split(",");
}
function selectorValuesFromEntry(value) {
const text = String(value || "").trim();
if (!text) {
return [];
}
const selectorText = text.startsWith("#") ? text.slice(1) : text;
return selectorText.split(",")
.map((entry) => entry.trim())
.filter(Boolean);
}
function normalizedSelectorValues(value) {
return selectorEntries(value).flatMap(selectorValuesFromEntry);
}
function valueMatchesSelector(value, selector, { descendants = false } = {}) {
const normalizedValue = String(value || "").trim();
const normalizedSelector = String(selector || "").trim();
if (!normalizedValue || !normalizedSelector) {
return false;
}
return normalizedValue === normalizedSelector ||
(descendants && normalizedValue.startsWith(`${normalizedSelector}.`));
}
function partMatchesSelector(part, selector) {
const normalized = String(selector || "").trim();
if (!normalized) {
return false;
}
if ([
part?.id,
part?.occurrenceId
].some((value) => valueMatchesSelector(value, normalized, { descendants: true }))) {
return true;
}
return [
part?.name,
part?.label,
part?.linkName
].some((value) => valueMatchesSelector(value, normalized));
}
function mergePartBounds(parts) {
const min = [Infinity, Infinity, Infinity];
const max = [-Infinity, -Infinity, -Infinity];
let count = 0;
for (const part of Array.isArray(parts) ? parts : []) {
const bounds = part?.bounds;
if (!Array.isArray(bounds?.min) || !Array.isArray(bounds?.max)) {
continue;
}
count += 1;
for (let axis = 0; axis < 3; axis += 1) {
min[axis] = Math.min(min[axis], Number(bounds.min[axis]));
max[axis] = Math.max(max[axis], Number(bounds.max[axis]));
}
}
return count > 0 && min.every(Number.isFinite) && max.every(Number.isFinite) ? { min, max } : null;
}
function filterMeshDataForSelection(meshData, selection = {}) {
const parts = Array.isArray(meshData?.parts) ? meshData.parts : [];
if (!parts.length) {
return meshData;
}
const focus = [
...normalizedSelectorValues(selection.focus),
...normalizedSelectorValues(selection.refs)
];
const hide = normalizedSelectorValues(selection.hide);
if (!focus.length && !hide.length) {
return meshData;
}
const nextParts = parts.filter((part) => {
if (focus.length && !focus.some((selector) => partMatchesSelector(part, selector))) {
return false;
}
return !hide.some((selector) => partMatchesSelector(part, selector));
});
if (!nextParts.length) {
throw new Error("No renderable parts remain after applying focus/hide filters");
}
return {
...meshData,
parts: nextParts,
bounds: mergePartBounds(nextParts) || meshData.bounds
};
}
function resolveMaterialSettings(theme, settings = {}) {
if (settings.materialSettings && typeof settings.materialSettings === "object") {
return settings.materialSettings;
}
return theme.materials || {};
}
function resolvePartsToRender(meshData, theme, settings) {
if (Array.isArray(settings.parts)) {
return settings.parts.filter((part) => toNumber(part?.vertexCount) > 0 && toNumber(part?.triangleCount) > 0);
}
const parts = toArray(meshData?.parts).filter((part) => toNumber(part?.vertexCount) > 0 && toNumber(part?.triangleCount) > 0);
if (!parts.length) {
return [];
}
if (settings.renderPartsIndividually === true) {
return parts;
}
if (settings.renderPartsIndividually === false) {
const pickableParts = toArray(settings.pickableParts).filter((part) => toNumber(part?.vertexCount) > 0 && toNumber(part?.triangleCount) > 0);
if (pickableParts.length) {
return pickableParts;
}
if (meshUsesPartSourceColors(meshData, parts) || meshUsesPartSourceOpacity(parts)) {
return parts;
}
const hasFillRotation = theme?.materials?.cycleColors === true &&
Array.isArray(theme?.materials?.fillColors) &&
theme.materials.fillColors.length > 1;
return hasFillRotation ? parts : [];
}
return parts;
}
function addEdgeObject(THREE, runtime, record, edgeGeometry, settings) {
const edgeSettings = runtime.edgeSettings;
const baseTheme = runtime.baseTheme;
const displayMode = runtime.displayMode;
const useScreenSpaceEdges = runtime.edgeRendering?.mode === "screen-space";
const rawResult = useScreenSpaceEdges
? createDisplayEdgeObject(runtime, {
THREE,
geometry: edgeGeometry,
edgeSettings,
baseTheme,
partId: record.partId,
displayMode,
thickness: getEdgeThickness(edgeSettings, baseTheme),
wireframeEdgeColor: runtime.edgeRendering?.wireframeEdgeColor || ""
}, runtime.screenSpaceLineMaterials)
: createDefaultEdgeObject(THREE, edgeGeometry, baseTheme, edgeSettings, record.partId, displayMode);
const { object, material } = normalizeEdgeResult(rawResult);
if (!object) {
return;
}
object.userData.partId = record.partId;
record.edges = object;
record.edgeMaterial = material;
record.baseEdgeColor = material?.color?.isColor ? material.color.clone() : new THREE.Color(edgeSettings?.color || baseTheme?.edge || DEFAULT_THEME.edge);
record.baseEdgeOpacity = Number.isFinite(Number(material?.opacity)) ? Number(material.opacity) : 1;
runtime.edgesGroup.add(object);
}
function buildDisplayRecords(THREE, runtime, meshData, settings) {
const theme = runtime.theme;
const materialSettings = runtime.materialSettings;
const displayMode = runtime.displayMode;
const baseTheme = runtime.baseTheme;
const edgeSettings = runtime.edgeSettings;
const bounds = meshData.bounds || boundsFromVertices(meshData.vertices || []);
const { radius } = centerAndRadiusFromBounds(THREE, bounds, runtime.scale);
const useSilhouette = shouldBuildSilhouette(edgeSettings, displayMode, settings);
const renderParts = resolvePartsToRender(meshData, theme, settings);
const partFillIndexMap = buildPartFillIndexMap(renderParts);
const useWholeMesh = renderParts.length === 0;
const records = [];
const makeRecord = ({ part = null, geometryEntry, fillIndex = 0, baseTransform = null }) => {
const partId = part ? String(part?.id || part?.occurrenceId || `part:${records.length}`) : MODEL_PART_ID;
const wireframeMode = displayModeIsWireframe(displayMode);
const forceFill = materialSettings.overrideSourceColors === true || wireframeMode;
const sourceVertexColors = !!geometryEntry.geometry.getAttribute("color");
const useSurfaceEdges = !wireframeMode &&
!displayModeShowsThroughEdges(displayMode) &&
edgeSettings.enabled &&
geometryHasSurfaceEdgeAttributes(geometryEntry.geometry);
const sourceColor = sourceColorForPart(THREE, part, meshData);
const sourceOpacity = sourceOpacityForPart(part);
const hasSourceColor = sourceVertexColors || !!sourceColor;
const hasVertexColors = !forceFill && sourceVertexColors;
const baseColor = resolveSourceBaseColor(THREE, {
hasVertexColors,
sourceColor: forceFill ? null : sourceColor,
materialSettings,
fallbackColor: materialSettings.defaultColor || baseTheme?.surface || DEFAULT_THEME.surface,
fillIndex,
forceFill: forceFill || !hasSourceColor
});
const material = wireframeMode
? createWireframeSurfaceMaterial(THREE, materialSettings, fillIndex)
: displayModeUsesUnlitSurfaces(displayMode)
? createUnshadedSurfaceMaterial(THREE, {
color: baseColor,
useVertexColors: hasVertexColors,
opacity: displayModeSurfaceOpacity(displayMode, materialSettings.opacity)
})
: createSurfaceMaterial(THREE, baseTheme, {
color: baseColor,
useVertexColors: hasVertexColors,
edgeSettings: useSurfaceEdges ? edgeSettings : null
});
if (edgeSettings.enabled && !wireframeMode && !useSurfaceEdges) {
material.polygonOffset = true;
material.polygonOffsetFactor = 1;
material.polygonOffsetUnits = 1;
}
const mesh = new THREE.Mesh(geometryEntry.geometry, material);
mesh.castShadow = true;
mesh.receiveShadow = false;
mesh.userData.partId = partId;
const faceIds = part
? settings.callbacks?.faceIdsForPart?.(part)
: settings.callbacks?.faceIdsForMesh?.(meshData);
if (faceIds) {
mesh.userData.faceIds = faceIds;
}
runtime.modelGroup.add(mesh);
const record = {
partId,
sourcePart: part || null,
mesh,
edges: null,
silhouette: null,
material,
edgeMaterial: null,
baseColor,
sourceColor,
sourceOpacity,
baseTransform,
partCenter: readBoundsCenter(THREE, part?.bounds || bounds),
partBounds: part?.bounds || part?.sourceBounds || bounds,
effectMatrix: null,
effectStyle: null,
effectVisible: null,
effectHighlighted: false,
fillIndex,
hasSourceColor,
hasVertexColors,
useVertexColors: hasVertexColors,
rawColors: geometryEntry.rawColors,
geometry: geometryEntry.geometry,
baseOpacity: Number.isFinite(Number(material.opacity)) ? Number(material.opacity) : 1,
baseEmissiveColor: baseColor ? baseColor.clone() : null,
baseEmissiveIntensity: 0,
baseEdgeColor: null,
baseEdgeOpacity: 1
};
if (useSilhouette) {
const silhouette = createSilhouetteMesh(THREE, geometryEntry.geometry, edgeSettings, radius);
if (silhouette) {
record.silhouette = silhouette;
runtime.modelGroup.add(silhouette);
}
}
if (settings.selection?.showEdges !== false && !useSurfaceEdges && (edgeSettings.enabled || wireframeMode)) {
addEdgeObject(
THREE,
runtime,
record,
buildEdgeGeometry(THREE, meshData, part, geometryEntry.geometry, displayMode, edgeSettings),
settings
);
}
applyMaterialSettingsToRecord(THREE, record, materialSettings, {
baseTheme,
displayMode
});
applyDisplayRecordTransform(THREE, record);
records.push(record);
};
if (useWholeMesh) {
const geometryEntry = buildWholeGeometryEntry(THREE, meshData, settings.recomputeNormals === true);
if (geometryEntry) {
makeRecord({ geometryEntry, fillIndex: 0 });
}
} else {
for (const part of renderParts) {
const geometryEntry = buildPartGeometryEntry(THREE, meshData, part, settings.recomputeNormals === true);
if (!geometryEntry) {
continue;
}
makeRecord({
part,
geometryEntry,
fillIndex: partFillIndexMap.get(part) ?? records.length,
baseTransform: displayTransformForPart(meshData, part, settings.renderPartsIndividually === true)
});
}
}
return records;
}
function settingsSignature(meshData, theme, settings) {
const edgeSettings = normalizeDisplayEdgeSettings(theme?.edges);
return JSON.stringify({
meshData: meshData ? "mesh" : "",
displayMode: normalizeDisplayMode(settings.displayMode),
parts: cacheKey(resolvePartsToRender(meshData, theme, settings)),
recomputeNormals: settings.recomputeNormals === true,
edgeSettings,
silhouette: settings.silhouette !== false &&
edgeSettings.silhouette === true &&
(edgeSettings.enabled !== false || settings.silhouette === true),
edgeRendering: settings.edgeRendering?.mode || "basic",
wireframeEdgeColor: settings.edgeRendering?.wireframeEdgeColor || ""
});
}
function normalizeSettings(settings = {}) {
const displayMode = normalizeDisplayMode(settings.displayMode);
const sourceTheme = settings.theme || settings.themeSettings || settings.settings || undefined;
const normalizedTheme = normalizeThemeSettings(sourceTheme);
const themeEdgeSettings = normalizeDisplayEdgeSettings(sourceTheme?.edges);
const applyDisplayModeEdgePolicy = settings.applyDisplayModeEdgePolicy !== false;
const theme = {
...normalizedTheme,
edges: applyDisplayModeEdgePolicy
? {
...themeEdgeSettings,
enabled: displayModeAllowsEdges(displayMode) &&
(displayModeForcesEdges(displayMode) || themeEdgeSettings.enabled === true),
depthTest: displayModeShowsThroughEdges(displayMode) ? false : themeEdgeSettings.depthTest
}
: themeEdgeSettings
};
const scale = normalizeCadSceneScale(settings.scale ?? settings.sceneScale ?? settings.sceneScaleMode);
const callbacks = settings.callbacks && typeof settings.callbacks === "object" ? settings.callbacks : {};
const baseTheme = settings.baseTheme && typeof settings.baseTheme === "object" ? settings.baseTheme : DEFAULT_THEME;
return {
...settings,
theme,
displayMode,
scale,
callbacks,
baseTheme,
selection: normalizeSelection(settings.selection),
filterSelection: settings.filterSelection === false
? {}
: normalizeSelection(settings.filterSelection ?? settings.selection),
clip: normalizeStepClipSettings(settings.clip),
stepParameters: settings.stepParameters || null,
parameterSetup: settings.parameterSetup !== false,
materialSettings: resolveMaterialSettings(theme, settings),
edgeRendering: normalizeEdgeRendering(settings.edgeRendering)
};
}
function setRuntimeTheme(runtime, settings) {
runtime.theme = settings.theme;
runtime.displayMode = settings.displayMode;
runtime.scale = settings.scale;
runtime.baseTheme = settings.baseTheme;
runtime.edgeSettings = {
...normalizeDisplayEdgeSettings(settings.theme?.edges),
depthTest: displayModeShowsThroughEdges(settings.displayMode) ? false : undefined
};
runtime.materialSettings = settings.materialSettings;
applyEdgeRenderingToRuntime(runtime, settings.edgeRendering);
}
function meshDataFromSource(source) {
return source?.meshData || source;
}
export function buildModel(THREE, source, settings = {}) {
if (!THREE) {
throw new Error("buildModel requires THREE");
}
const rawMeshData = meshDataFromSource(source);
const normalized = normalizeSettings(settings);
const meshData = filterMeshDataForSelection(rawMeshData, normalized.filterSelection);
const root = new THREE.Group();
const modelGroup = new THREE.Group();
const edgesGroup = new THREE.Group();
root.name = "CadSceneRoot";
modelGroup.name = "CadSceneModel";
edgesGroup.name = "CadSceneEdges";
root.add(modelGroup);
root.add(edgesGroup);
const baseBounds = meshData?.bounds || boundsFromVertices(meshData?.vertices || []);
const runtime = {
THREE,
root,
modelGroup,
edgesGroup,
displayRecords: [],
records: [],
baseBounds,
bounds: baseBounds,
modelBounds: baseBounds,
modelRadius: centerAndRadiusFromBounds(THREE, baseBounds, normalized.scale).radius,
cleanups: [],
activeClipPlane: null,
activeClipPlanes: [],
screenSpaceLineMaterials: new Set(),
syncScreenSpaceLineMaterials(width, height) {
syncScreenSpaceLineMaterialResolution(runtime.screenSpaceLineMaterials, width, height);
},
registerScreenSpaceLineMaterial(material) {
if (!material?.resolution?.set) {
return;
}
runtime.screenSpaceLineMaterials.add(material);
},
unregisterScreenSpaceLineMaterial(material) {
runtime.screenSpaceLineMaterials.delete(material);
},
requestRender: () => {}
};
setRuntimeTheme(runtime, normalized);
let disposed = false;
let currentSettings = normalized;
let currentSignature = "";
let activeParameters = null;
let activeParameterSetup = false;
const rebuild = (nextSettings = currentSettings) => {
clearGroup(modelGroup);
clearGroup(edgesGroup);
setRuntimeTheme(runtime, nextSettings);
runtime.baseBounds = meshData?.bounds || boundsFromVertices(meshData?.vertices || []);
runtime.displayRecords = buildDisplayRecords(THREE, runtime, meshData, nextSettings);
runtime.records = runtime.displayRecords;
runtime.bounds = runtime.baseBounds;
runtime.modelBounds = runtime.baseBounds;
runtime.modelRadius = centerAndRadiusFromBounds(THREE, runtime.baseBounds, runtime.scale).radius;
currentSignature = settingsSignature(meshData, runtime.theme, nextSettings);
};
const applyMutableState = (nextSettings = currentSettings) => {
setRuntimeTheme(runtime, nextSettings);
for (const record of runtime.displayRecords) {
applyMaterialSettingsToRecord(THREE, record, runtime.materialSettings, {
baseTheme: runtime.baseTheme,
displayMode: runtime.displayMode
});
}
const nextParameterSetup = nextSettings.parameterSetup !== false;
const nextParameters = nextSettings.stepParameters || null;
if (activeParameters !== nextParameters || activeParameterSetup !== nextParameterSetup) {
if (activeParameterSetup) {
cleanupParameterRuntime(runtime, activeParameters, nextSettings.callbacks);
}
activeParameters = nextParameters;
activeParameterSetup = nextParameterSetup;
if (activeParameterSetup) {
runParameterSetup(THREE, runtime, activeParameters, meshData, nextSettings.callbacks);
}
}
const effectiveBounds = applyParameters(THREE, runtime, activeParameters, meshData, nextSettings.callbacks);
runtime.bounds = effectiveBounds || runtime.baseBounds;
runtime.modelBounds = runtime.bounds;
runtime.modelRadius = centerAndRadiusFromBounds(THREE, runtime.bounds, runtime.scale).radius;
applyPartVisualState(THREE, runtime.displayRecords, {
baseTheme: runtime.baseTheme,
edgeSettings: runtime.edgeSettings,
...nextSettings.selection,
showEdges: nextSettings.selection?.showEdges !== false
});
syncClip(runtime, nextSettings.clip, runtime.bounds, nextSettings.modelOffset || modelGroup.position);
};
rebuild(currentSettings);
applyMutableState(currentSettings);
const api = {
source,
meshData,
root,
modelGroup,
edgesGroup,
get displayRecords() {
return runtime.displayRecords;
},
get records() {
return runtime.displayRecords;
},
get bounds() {
return runtime.bounds;
},
get radius() {
return runtime.modelRadius;
},
get runtime() {
return runtime;
},
update(nextSettings = {}) {
if (disposed) {
return api;
}
const mergedSettings = {
...currentSettings,
...nextSettings,
selection: {
...(currentSettings.selection || {}),
...(nextSettings.selection || {})
},
callbacks: {
...(currentSettings.callbacks || {}),
...(nextSettings.callbacks || {})
}
};
if (
Object.prototype.hasOwnProperty.call(nextSettings, "theme") &&
!Object.prototype.hasOwnProperty.call(nextSettings, "materialSettings")
) {
delete mergedSettings.materialSettings;
}
currentSettings = normalizeSettings(mergedSettings);
const nextSignature = settingsSignature(meshData, currentSettings.theme, currentSettings);
if (nextSignature !== currentSignature) {
rebuild(currentSettings);
}
applyMutableState(currentSettings);
return api;
},
dispose() {
if (disposed) {
return;
}
disposed = true;
if (activeParameterSetup) {
cleanupParameterRuntime(runtime, activeParameters, currentSettings.callbacks);
}
clearGroup(root);
}
};
return api;
}
export function fitCameraToModel(THREE, camera, bounds, {
direction = [1, -1, 0.8],
up = [0, 0, 1],
width = 1400,
height = 900,
padding = 0.12,
scale = CAD_SCENE_SCALE.CAD,
lockedHalfHeight = null
} = {}) {
const sceneScale = normalizeCadSceneScale(scale);
const settings = getSceneScaleSettings(sceneScale);
const { center, radius } = centerAndRadiusFromBounds(THREE, bounds, sceneScale);
const viewDirection = new THREE.Vector3(...direction).normalize();
const viewUp = new THREE.Vector3(...up).normalize();
const distance = Math.max(radius * 3.2, settings.minModelRadius * 10);
camera.position.copy(center).add(viewDirection.multiplyScalar(distance));
camera.up.copy(viewUp);
camera.lookAt(center);
const aspect = Math.max(width / Math.max(height, 1), 0.01);
const right = new THREE.Vector3().crossVectors(viewDirection, viewUp).normalize();
const screenUp = new THREE.Vector3().crossVectors(right, viewDirection).normalize();
const corners = boundsCorners(THREE, bounds);
const xs = corners.map((corner) => corner.dot(right));
const ys = corners.map((corner) => corner.dot(screenUp));
const minSpan = settings.minModelRadius;
const spanX = Math.max(Math.max(...xs) - Math.min(...xs), minSpan);
const spanY = Math.max(Math.max(...ys) - Math.min(...ys), minSpan);
const safeContentScale = Math.max(1 - (clamp(Number(padding) || 0, 0.1, 0.4) * 2), 0.1);
const halfHeight = lockedHalfHeight || Math.max(
spanY / (2 * safeContentScale),
spanX / (2 * aspect * safeContentScale),
minSpan / 2
);
camera.top = halfHeight;
camera.bottom = -halfHeight;
camera.left = -halfHeight * aspect;
camera.right = halfHeight * aspect;
camera.near = 0.01;
camera.far = Math.max(distance + radius * 6, sceneScale === CAD_SCENE_SCALE.URDF ? 10 : 1000);
camera.updateProjectionMatrix?.();
return {
center,
radius,
halfHeight,
distance
};
}