615 lines
20 KiB
JavaScript
615 lines
20 KiB
JavaScript
import {
|
|
normalizeParameterAnimations,
|
|
normalizeParameterDefinitions,
|
|
normalizeParameterValues,
|
|
parameterMapForDefinitions
|
|
} from "../../common/parameters.js";
|
|
import { createImplicitCadSdfEvaluator } from "./sdfEvaluator.js";
|
|
export {
|
|
IMPLICIT_CAD_EXTENSIONS,
|
|
IMPLICIT_CAD_KIND,
|
|
IMPLICIT_CAD_SCHEMA,
|
|
IMPLICIT_EXTENSIONS,
|
|
IMPLICIT_KIND,
|
|
IMPLICIT_SCHEMA
|
|
} from "./schema.js";
|
|
import {
|
|
IMPLICIT_CAD_EXTENSIONS,
|
|
IMPLICIT_CAD_KIND,
|
|
IMPLICIT_CAD_SCHEMA
|
|
} from "./schema.js";
|
|
|
|
const DEFAULT_BOUNDS = Object.freeze({
|
|
min: [-50, -50, -50],
|
|
max: [50, 50, 50]
|
|
});
|
|
const AUTO_BOUNDS_RADII = Object.freeze([4, 8, 16, 32, 64, 128, 256, 512]);
|
|
const AUTO_BOUNDS_SCAN_SAMPLES = 5;
|
|
const AUTO_BOUNDS_REFINE_SAMPLES = 9;
|
|
const AUTO_BOUNDS_CACHE_LIMIT = 32;
|
|
const DEFAULT_COLOR = "#f4f4f5";
|
|
const HEX_COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3}){1,2}$/;
|
|
const GLSL_IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
const UNIFORM_TYPES = Object.freeze(new Set(["float", "int", "bool", "vec2", "vec3", "vec4"]));
|
|
const PARAM_UNIFORM_TYPES = Object.freeze({
|
|
number: "float",
|
|
boolean: "bool",
|
|
color: "vec3",
|
|
button: "int"
|
|
});
|
|
|
|
function isObject(value) {
|
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
}
|
|
|
|
function finiteNumber(value, fallback = 0) {
|
|
const numeric = Number(value);
|
|
return Number.isFinite(numeric) ? numeric : fallback;
|
|
}
|
|
|
|
function finitePositiveNumber(value, fallback) {
|
|
const numeric = Number(value);
|
|
return Number.isFinite(numeric) && numeric > 0 ? numeric : fallback;
|
|
}
|
|
|
|
function finiteVec3(value, fallback) {
|
|
const source = Array.isArray(value) || ArrayBuffer.isView(value) ? value : [];
|
|
return [0, 1, 2].map((index) => finiteNumber(source[index], fallback[index] || 0));
|
|
}
|
|
|
|
function finiteVector(value, length, fallback = []) {
|
|
const source = Array.isArray(value) || ArrayBuffer.isView(value) ? value : [];
|
|
return Array.from({ length }, (_, index) => finiteNumber(source[index], fallback[index] || 0));
|
|
}
|
|
|
|
function normalizeBounds(value, fallback = DEFAULT_BOUNDS) {
|
|
const radius = finitePositiveNumber(value, null);
|
|
const raw = radius
|
|
? { min: [-radius, -radius, -radius], max: [radius, radius, radius] }
|
|
: Array.isArray(value) && value.length >= 2
|
|
? { min: value[0], max: value[1] }
|
|
: isObject(value)
|
|
? value
|
|
: fallback;
|
|
const min = finiteVec3(raw.min, DEFAULT_BOUNDS.min);
|
|
const max = finiteVec3(raw.max, DEFAULT_BOUNDS.max);
|
|
for (let axis = 0; axis < 3; axis += 1) {
|
|
if (max[axis] <= min[axis]) {
|
|
const center = (min[axis] + max[axis]) / 2;
|
|
min[axis] = center - 0.5;
|
|
max[axis] = center + 0.5;
|
|
}
|
|
}
|
|
return { min, max };
|
|
}
|
|
|
|
function cloneBounds(bounds) {
|
|
return {
|
|
min: [...bounds.min],
|
|
max: [...bounds.max]
|
|
};
|
|
}
|
|
|
|
function hasExplicitBounds(value) {
|
|
if (value === undefined || value === null) {
|
|
return false;
|
|
}
|
|
if (String(value).trim().toLowerCase() === "auto") {
|
|
return false;
|
|
}
|
|
return !(isObject(value) && value.auto === true);
|
|
}
|
|
|
|
function boundsSize(bounds) {
|
|
return [
|
|
bounds.max[0] - bounds.min[0],
|
|
bounds.max[1] - bounds.min[1],
|
|
bounds.max[2] - bounds.min[2],
|
|
];
|
|
}
|
|
|
|
function expandBounds(min, max, margin) {
|
|
const safeMargin = Math.max(finiteNumber(margin, 0), 0.25);
|
|
return normalizeBounds({
|
|
min: [min[0] - safeMargin, min[1] - safeMargin, min[2] - safeMargin],
|
|
max: [max[0] + safeMargin, max[1] + safeMargin, max[2] + safeMargin]
|
|
});
|
|
}
|
|
|
|
function sampleAutoBounds(sdf, bounds, samples) {
|
|
const size = boundsSize(bounds);
|
|
const steps = size.map((axisSize) => axisSize / Math.max(samples - 1, 1));
|
|
const threshold = Math.max(Math.min(...steps) * 0.35, 0.002);
|
|
const hitMin = [Infinity, Infinity, Infinity];
|
|
const hitMax = [-Infinity, -Infinity, -Infinity];
|
|
let hitCount = 0;
|
|
let boundaryTouch = false;
|
|
|
|
for (let iz = 0; iz < samples; iz += 1) {
|
|
const z = bounds.min[2] + steps[2] * iz;
|
|
for (let iy = 0; iy < samples; iy += 1) {
|
|
const y = bounds.min[1] + steps[1] * iy;
|
|
for (let ix = 0; ix < samples; ix += 1) {
|
|
const x = bounds.min[0] + steps[0] * ix;
|
|
const value = finiteNumber(sdf(x, y, z), 1e6);
|
|
if (value > threshold) {
|
|
continue;
|
|
}
|
|
hitCount += 1;
|
|
hitMin[0] = Math.min(hitMin[0], x);
|
|
hitMin[1] = Math.min(hitMin[1], y);
|
|
hitMin[2] = Math.min(hitMin[2], z);
|
|
hitMax[0] = Math.max(hitMax[0], x);
|
|
hitMax[1] = Math.max(hitMax[1], y);
|
|
hitMax[2] = Math.max(hitMax[2], z);
|
|
if (value <= 0 && (
|
|
ix === 0 || iy === 0 || iz === 0 ||
|
|
ix === samples - 1 || iy === samples - 1 || iz === samples - 1
|
|
)) {
|
|
boundaryTouch = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
hitCount,
|
|
hitMin,
|
|
hitMax,
|
|
boundaryTouch,
|
|
step: steps,
|
|
threshold
|
|
};
|
|
}
|
|
|
|
function estimateAutoBoundsStartRadius(definition, params) {
|
|
const largest = definition.parameters
|
|
.filter((parameter) => parameter.type === "number")
|
|
.filter((parameter) => !likelyPoseParameter(parameter))
|
|
.reduce((maxValue, parameter) => {
|
|
const value = Math.abs(finiteNumber(params[parameter.id], 0));
|
|
return Math.max(maxValue, value);
|
|
}, 1);
|
|
const target = Math.max(largest * 3, 4);
|
|
return AUTO_BOUNDS_RADII.find((radius) => radius >= target) || AUTO_BOUNDS_RADII[AUTO_BOUNDS_RADII.length - 1];
|
|
}
|
|
|
|
function estimateImplicitCadAutoBounds(partialModel, definition, context) {
|
|
let sdf = null;
|
|
try {
|
|
sdf = createImplicitCadSdfEvaluator(partialModel);
|
|
} catch {
|
|
return cloneBounds(DEFAULT_BOUNDS);
|
|
}
|
|
|
|
let candidate = null;
|
|
const startRadius = estimateAutoBoundsStartRadius(definition, context.params);
|
|
for (const radius of AUTO_BOUNDS_RADII.filter((candidateRadius) => candidateRadius >= startRadius)) {
|
|
const bounds = normalizeBounds(radius);
|
|
const scan = sampleAutoBounds(sdf, bounds, AUTO_BOUNDS_SCAN_SAMPLES);
|
|
if (!scan.hitCount) {
|
|
continue;
|
|
}
|
|
const margin = Math.max(Math.max(...scan.step) * 1.5, radius * 0.06, 0.5);
|
|
candidate = expandBounds(scan.hitMin, scan.hitMax, margin);
|
|
if (!scan.boundaryTouch) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!candidate) {
|
|
return cloneBounds(DEFAULT_BOUNDS);
|
|
}
|
|
|
|
for (let pass = 0; pass < 1; pass += 1) {
|
|
const scan = sampleAutoBounds(sdf, candidate, AUTO_BOUNDS_REFINE_SAMPLES);
|
|
if (!scan.hitCount) {
|
|
break;
|
|
}
|
|
const margin = Math.max(Math.max(...scan.step) * 1.1, Math.max(...boundsSize(candidate)) * 0.025, 0.25);
|
|
candidate = expandBounds(scan.hitMin, scan.hitMax, margin);
|
|
}
|
|
|
|
return candidate;
|
|
}
|
|
|
|
function likelyPoseParameter(parameter) {
|
|
const unit = String(parameter?.unit || "").trim().toLowerCase();
|
|
if (unit === "deg" || unit === "rad") {
|
|
return true;
|
|
}
|
|
const text = `${parameter?.id || ""} ${parameter?.label || ""}`.toLowerCase();
|
|
return [
|
|
"angle",
|
|
"phase",
|
|
"rotation",
|
|
"rotate",
|
|
"orbit",
|
|
"spin",
|
|
"yaw",
|
|
"pitch",
|
|
"roll"
|
|
].some((token) => text.includes(token));
|
|
}
|
|
|
|
function autoBoundsCacheKey(definition, params, glslSource) {
|
|
const values = definition.parameters
|
|
.filter((parameter) => !likelyPoseParameter(parameter))
|
|
.filter((parameter) => PARAM_UNIFORM_TYPES[parameter.type])
|
|
.filter((parameter) => parameter.type !== "color")
|
|
.map((parameter) => {
|
|
const value = params[parameter.id];
|
|
if (typeof value === "number") {
|
|
return `${parameter.id}:${Number.isFinite(value) ? value.toPrecision(8) : "0"}`;
|
|
}
|
|
if (typeof value === "boolean") {
|
|
return `${parameter.id}:${value ? "1" : "0"}`;
|
|
}
|
|
return `${parameter.id}:${String(value ?? "")}`;
|
|
})
|
|
.join(";");
|
|
return `${glslSource.length}:${values}`;
|
|
}
|
|
|
|
function readCachedAutoBounds(cache, key, animationPlaying) {
|
|
if (!cache) {
|
|
return null;
|
|
}
|
|
if (cache.map.has(key)) {
|
|
return cloneBounds(cache.map.get(key));
|
|
}
|
|
if (animationPlaying && cache.last) {
|
|
return cloneBounds(cache.last);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function rememberAutoBounds(cache, key, bounds) {
|
|
if (!cache) {
|
|
return;
|
|
}
|
|
const cloned = cloneBounds(bounds);
|
|
cache.map.set(key, cloned);
|
|
cache.last = cloned;
|
|
while (cache.map.size > AUTO_BOUNDS_CACHE_LIMIT) {
|
|
const oldestKey = cache.map.keys().next().value;
|
|
cache.map.delete(oldestKey);
|
|
}
|
|
}
|
|
|
|
function resolveImplicitCadBounds(value, partialModel, definition, context, cache) {
|
|
if (hasExplicitBounds(value)) {
|
|
return {
|
|
bounds: normalizeBounds(value),
|
|
source: "explicit"
|
|
};
|
|
}
|
|
const key = autoBoundsCacheKey(definition, context.params, partialModel.glslSource);
|
|
const cached = readCachedAutoBounds(cache, key, context.animationState.playing);
|
|
if (cached) {
|
|
return {
|
|
bounds: cached,
|
|
source: "auto"
|
|
};
|
|
}
|
|
const estimated = estimateImplicitCadAutoBounds(partialModel, definition, context);
|
|
rememberAutoBounds(cache, key, estimated);
|
|
return {
|
|
bounds: estimated,
|
|
source: "auto"
|
|
};
|
|
}
|
|
|
|
function normalizeHexColor(value, fallback = DEFAULT_COLOR) {
|
|
const color = String(value || "").trim();
|
|
return HEX_COLOR_PATTERN.test(color) ? color : fallback;
|
|
}
|
|
|
|
function hexToRgb01(hex, fallback = DEFAULT_COLOR) {
|
|
const value = normalizeHexColor(hex, fallback);
|
|
const expanded = value.length === 4
|
|
? `${value[1]}${value[1]}${value[2]}${value[2]}${value[3]}${value[3]}`
|
|
: value.slice(1);
|
|
return [
|
|
parseInt(expanded.slice(0, 2), 16) / 255,
|
|
parseInt(expanded.slice(2, 4), 16) / 255,
|
|
parseInt(expanded.slice(4, 6), 16) / 255,
|
|
];
|
|
}
|
|
|
|
function evaluateValue(value, context) {
|
|
return typeof value === "function" ? value(context) : value;
|
|
}
|
|
|
|
function normalizeMaterial() {
|
|
const material = {};
|
|
return {
|
|
color: normalizeHexColor(material.color, DEFAULT_COLOR),
|
|
roughness: Math.min(Math.max(finiteNumber(material.roughness, 0.75), 0), 1),
|
|
metalness: Math.min(Math.max(finiteNumber(material.metalness, 0.02), 0), 1),
|
|
};
|
|
}
|
|
|
|
function unwrapModuleExports(moduleValue) {
|
|
if (typeof moduleValue === "function") {
|
|
return moduleValue();
|
|
}
|
|
if (!isObject(moduleValue)) {
|
|
return moduleValue;
|
|
}
|
|
if (moduleValue.default !== undefined) {
|
|
return typeof moduleValue.default === "function" ? moduleValue.default() : moduleValue.default;
|
|
}
|
|
if (moduleValue.model !== undefined) {
|
|
return typeof moduleValue.model === "function" ? moduleValue.model() : moduleValue.model;
|
|
}
|
|
return moduleValue;
|
|
}
|
|
|
|
function normalizeUniformValue(rawValue, context = {}) {
|
|
const raw = evaluateValue(rawValue, context);
|
|
const value = isObject(raw) && Object.hasOwn(raw, "value")
|
|
? evaluateValue(raw.value, context)
|
|
: raw;
|
|
const explicitType = isObject(raw) ? String(raw.type || raw.kind || "").trim().toLowerCase() : "";
|
|
const type = UNIFORM_TYPES.has(explicitType)
|
|
? explicitType
|
|
: typeof value === "boolean"
|
|
? "bool"
|
|
: typeof value === "number"
|
|
? "float"
|
|
: typeof value === "string" && HEX_COLOR_PATTERN.test(value)
|
|
? "vec3"
|
|
: Array.isArray(value) || ArrayBuffer.isView(value)
|
|
? `vec${Math.min(Math.max(value.length, 2), 4)}`
|
|
: "";
|
|
if (!UNIFORM_TYPES.has(type)) {
|
|
return null;
|
|
}
|
|
if (type === "bool") {
|
|
return { type, value: value === true };
|
|
}
|
|
if (type === "int") {
|
|
return { type, value: Math.trunc(finiteNumber(value, 0)) };
|
|
}
|
|
if (type === "float") {
|
|
return { type, value: finiteNumber(value, 0) };
|
|
}
|
|
if (typeof value === "string" && HEX_COLOR_PATTERN.test(value)) {
|
|
return { type, value: hexToRgb01(value) };
|
|
}
|
|
const length = Number(type.slice(3));
|
|
const fallback = Array.from({ length }, () => 0);
|
|
return {
|
|
type,
|
|
value: finiteVector(value, length, fallback)
|
|
};
|
|
}
|
|
|
|
function uniformsFromParams(parameters = [], params = {}) {
|
|
return Object.fromEntries(
|
|
parameters
|
|
.map((parameter) => {
|
|
const uniformName = String(parameter?.id || "").trim();
|
|
if (!GLSL_IDENTIFIER_PATTERN.test(uniformName) || uniformName.startsWith("gl_")) {
|
|
throw new Error(`Implicit CAD param "${uniformName}" is not a valid GLSL uniform identifier.`);
|
|
}
|
|
const type = PARAM_UNIFORM_TYPES[parameter.type];
|
|
if (!type) {
|
|
return null;
|
|
}
|
|
const uniform = normalizeUniformValue({ type, value: params[parameter.id] });
|
|
return uniform ? [uniformName, uniform] : null;
|
|
})
|
|
.filter(Boolean)
|
|
);
|
|
}
|
|
|
|
function glslSourceFromModel(model, context = {}) {
|
|
return String(evaluateValue(model.glsl ?? model.glslSource, context) || "").trim();
|
|
}
|
|
|
|
export function pathIsImplicitCadSource(value = "") {
|
|
const pathname = String(value || "").split(/[?#]/, 1)[0].toLowerCase();
|
|
return IMPLICIT_CAD_EXTENSIONS.some((extension) => pathname.endsWith(extension));
|
|
}
|
|
|
|
function normalizeAnimationState(value = {}) {
|
|
const activeId = String(value?.activeId || "").trim();
|
|
return {
|
|
activeId,
|
|
playing: value?.playing === true,
|
|
elapsedSec: Math.max(finiteNumber(value?.elapsedSec ?? value?.elapsed, 0), 0),
|
|
speed: Math.min(Math.max(finiteNumber(value?.speed, 1), 0.1), 5)
|
|
};
|
|
}
|
|
|
|
function findAnimation(definition, animationId) {
|
|
const animations = Array.isArray(definition?.animations) ? definition.animations : [];
|
|
if (!animations.length) {
|
|
return null;
|
|
}
|
|
const normalizedId = String(animationId || "").trim();
|
|
return animations.find((animation) => animation.id === normalizedId) || animations[0] || null;
|
|
}
|
|
|
|
function buildImplicitCadContext(definition, parameterValues = {}, animationState = {}) {
|
|
const params = normalizeParameterValues(definition, parameterValues);
|
|
const normalizedAnimationState = normalizeAnimationState(animationState);
|
|
const animation = findAnimation(definition, normalizedAnimationState.activeId);
|
|
const duration = Math.max(Number(animation?.duration) || 0, 0.001);
|
|
const elapsedSec = Math.min(normalizedAnimationState.elapsedSec, duration);
|
|
const progress = animation ? Math.min(Math.max(elapsedSec / duration, 0), 1) : 0;
|
|
return {
|
|
...params,
|
|
params,
|
|
parameterValues: params,
|
|
animation,
|
|
animationState: {
|
|
...normalizedAnimationState,
|
|
activeId: animation?.id || normalizedAnimationState.activeId,
|
|
duration: animation?.duration || 0,
|
|
loop: animation?.loop !== false
|
|
},
|
|
elapsed: elapsedSec,
|
|
elapsedSec,
|
|
duration: animation?.duration || 0,
|
|
progress,
|
|
cycle: duration > 0 ? elapsedSec / duration : 0,
|
|
t: elapsedSec,
|
|
time: elapsedSec
|
|
};
|
|
}
|
|
|
|
function normalizeImplicitCadRuntimeModel(rawModel, definition, parameterValues = {}, animationState = {}, { sourceUrl = "", autoBoundsCache = null } = {}) {
|
|
const context = buildImplicitCadContext(definition, parameterValues, animationState);
|
|
const model = rawModel;
|
|
if (!isObject(model)) {
|
|
throw new Error("Implicit CAD module must export an object, or a function returning an object.");
|
|
}
|
|
const uniforms = uniformsFromParams(definition.parameters, context.params);
|
|
const glslSource = glslSourceFromModel(model, context);
|
|
if (!glslSource) {
|
|
throw new Error("Implicit CAD module must export GLSL code in glsl.");
|
|
}
|
|
const resolvedBounds = resolveImplicitCadBounds(
|
|
evaluateValue(model.bounds, context),
|
|
{ glslSource, uniforms },
|
|
definition,
|
|
context,
|
|
autoBoundsCache
|
|
);
|
|
const bounds = resolvedBounds.bounds;
|
|
const size = [
|
|
bounds.max[0] - bounds.min[0],
|
|
bounds.max[1] - bounds.min[1],
|
|
bounds.max[2] - bounds.min[2],
|
|
];
|
|
const center = [
|
|
(bounds.min[0] + bounds.max[0]) / 2,
|
|
(bounds.min[1] + bounds.max[1]) / 2,
|
|
(bounds.min[2] + bounds.max[2]) / 2,
|
|
];
|
|
const radius = Math.max(Math.hypot(size[0], size[1], size[2]) / 2, 1e-3);
|
|
const evaluatedRender = evaluateValue(model.render, context);
|
|
const render = isObject(evaluatedRender) ? evaluatedRender : {};
|
|
const epsilon = finitePositiveNumber(evaluateValue(render.epsilon, context), Math.max(radius * 0.0007, 0.002));
|
|
const normalEpsilon = finitePositiveNumber(
|
|
evaluateValue(render.normalEpsilon, context),
|
|
Math.max(epsilon * 2.5, radius * 0.001)
|
|
);
|
|
const stepScale = Math.min(
|
|
Math.max(finiteNumber(evaluateValue(render.stepScale, context), 0.45), 0.05),
|
|
1
|
|
);
|
|
const maxStep = finitePositiveNumber(
|
|
evaluateValue(render.maxStep, context),
|
|
Math.max(radius * 0.035, epsilon * 10)
|
|
);
|
|
return {
|
|
schema: String(model.schema || IMPLICIT_CAD_SCHEMA),
|
|
kind: IMPLICIT_CAD_KIND,
|
|
name: String(model.name || model.title || "Implicit CAD").trim() || "Implicit CAD",
|
|
description: String(model.description || "").trim(),
|
|
units: String(model.units || "mm").trim() || "mm",
|
|
sourceUrl: String(sourceUrl || model.sourceUrl || "").trim(),
|
|
parameters: definition.parameters,
|
|
parameterMap: definition.parameterMap,
|
|
defaultParameterValues: definition.defaultParameterValues,
|
|
parameterValues: context.params,
|
|
animations: definition.animations,
|
|
animationState: context.animationState,
|
|
glslSource,
|
|
distanceSource: glslSource,
|
|
colorSource: /\bvec3\s+color\s*\(/.test(glslSource) ? glslSource : "",
|
|
uniforms,
|
|
uniformSignature: Object.entries(uniforms)
|
|
.map(([name, uniform]) => `${name}:${uniform.type}`)
|
|
.sort()
|
|
.join(";"),
|
|
bounds,
|
|
boundsSource: resolvedBounds.source,
|
|
center,
|
|
size,
|
|
radius,
|
|
material: normalizeMaterial(),
|
|
background: {},
|
|
maxSteps: Math.max(16, Math.min(Math.floor(finiteNumber(evaluateValue(render.steps, context), 192)), 768)),
|
|
maxDistance: finitePositiveNumber(evaluateValue(render.maxDistance, context), radius * 8),
|
|
stepScale,
|
|
maxStep,
|
|
epsilon,
|
|
normalEpsilon,
|
|
};
|
|
}
|
|
|
|
export function normalizeImplicitCadDefinition(moduleValue, { sourceUrl = "" } = {}) {
|
|
const model = unwrapModuleExports(moduleValue);
|
|
if (!isObject(model)) {
|
|
throw new Error("Implicit CAD module must export an object, or a function returning an object.");
|
|
}
|
|
if (model.kind === IMPLICIT_CAD_KIND && model.glslSource && Array.isArray(model.parameters)) {
|
|
return model.definition ? model : { ...model, definition: null };
|
|
}
|
|
const parameters = normalizeParameterDefinitions(model.params);
|
|
const parameterMap = parameterMapForDefinitions(parameters);
|
|
const autoBoundsCache = {
|
|
map: new Map(),
|
|
last: null
|
|
};
|
|
const definition = {
|
|
schema: String(model.schema || IMPLICIT_CAD_SCHEMA),
|
|
kind: IMPLICIT_CAD_KIND,
|
|
name: String(model.name || model.title || "Implicit CAD").trim() || "Implicit CAD",
|
|
description: String(model.description || "").trim(),
|
|
units: String(model.units || "mm").trim() || "mm",
|
|
sourceUrl: String(sourceUrl || model.sourceUrl || "").trim(),
|
|
parameters,
|
|
parameterMap,
|
|
defaultParameterValues: {},
|
|
animations: normalizeParameterAnimations(model.animations),
|
|
buildModel(parameterValues = {}, animationState = {}) {
|
|
return normalizeImplicitCadRuntimeModel(model, definition, parameterValues, animationState, { sourceUrl, autoBoundsCache });
|
|
}
|
|
};
|
|
definition.defaultParameterValues = normalizeParameterValues(
|
|
definition,
|
|
model.values || {}
|
|
);
|
|
const defaultModel = definition.buildModel(definition.defaultParameterValues);
|
|
return {
|
|
...defaultModel,
|
|
definition
|
|
};
|
|
}
|
|
|
|
export function normalizeImplicitCadModel(moduleValue, { sourceUrl = "" } = {}) {
|
|
return normalizeImplicitCadDefinition(moduleValue, { sourceUrl });
|
|
}
|
|
|
|
export function implicitCadBoundsCenterAndRadius(model) {
|
|
let normalized = null;
|
|
try {
|
|
normalized = isObject(model) || typeof model === "function" ? normalizeImplicitCadModel(model) : null;
|
|
} catch {
|
|
normalized = null;
|
|
}
|
|
if (normalized) {
|
|
return {
|
|
center: normalized.center,
|
|
radius: normalized.radius,
|
|
bounds: normalized.bounds,
|
|
};
|
|
}
|
|
return {
|
|
center: [0, 0, 0],
|
|
radius: 1,
|
|
bounds: { ...DEFAULT_BOUNDS },
|
|
};
|
|
}
|
|
|
|
export const pathIsImplicitSource = pathIsImplicitCadSource;
|
|
export const normalizeImplicitDefinition = normalizeImplicitCadDefinition;
|
|
export const normalizeImplicitModel = normalizeImplicitCadModel;
|
|
export const implicitBoundsCenterAndRadius = implicitCadBoundsCenterAndRadius;
|