382 lines
13 KiB
JavaScript
382 lines
13 KiB
JavaScript
import {
|
|
DEFAULT_STEP_CLIP_SETTINGS,
|
|
normalizeStepClipSettings,
|
|
stepClipSettingsEqual
|
|
} from "../lib/viewer/clipPlane.js";
|
|
import {
|
|
CAMERA_PROJECTION,
|
|
normalizeCameraProjection
|
|
} from "../lib/perspective.js";
|
|
|
|
export { CAMERA_PROJECTION, normalizeCameraProjection };
|
|
|
|
export const CAD_DISPLAY_MODE = Object.freeze({
|
|
HIDDEN_EDGES: "hidden_edges",
|
|
HIDDEN_LINES_REMOVED: "hidden_lines_removed",
|
|
RENDERED: "rendered",
|
|
SOLID: "solid",
|
|
TRANSPARENT: "transparent",
|
|
UNSHADED: "unshaded",
|
|
WIREFRAME: "wireframe"
|
|
});
|
|
|
|
export const CAD_DISPLAY_MODE_VALUES = Object.freeze(Object.values(CAD_DISPLAY_MODE));
|
|
|
|
export const EXPLODED_VIEW_AXES = Object.freeze(["x", "y", "z", "radial"]);
|
|
export const EXPLODED_VIEW_DIRECTIONS = Object.freeze(["positive", "negative"]);
|
|
export const MAX_EXPLODED_VIEW_DEPTH = 8;
|
|
const HEX_COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3}){1,2}$/;
|
|
|
|
export const CAD_EDGE_COLOR = "#132232";
|
|
export const CAD_EDGE_HIGHLIGHT_COLOR = "#8dc5ff";
|
|
export const CAD_EDGE_CLASS_IDS = Object.freeze(["feature", "tangent", "seam", "degenerate"]);
|
|
|
|
export const DEFAULT_DISPLAY_EDGE_CLASS_SETTINGS = Object.freeze({
|
|
feature: Object.freeze({
|
|
color: CAD_EDGE_COLOR,
|
|
opacity: 1,
|
|
thickness: 1.15
|
|
}),
|
|
tangent: Object.freeze({
|
|
color: CAD_EDGE_COLOR,
|
|
opacity: 0.5,
|
|
thickness: 1.15
|
|
}),
|
|
seam: Object.freeze({
|
|
color: CAD_EDGE_COLOR,
|
|
opacity: 0.85,
|
|
thickness: 1.15
|
|
}),
|
|
degenerate: Object.freeze({
|
|
color: CAD_EDGE_COLOR,
|
|
opacity: 1,
|
|
thickness: 0
|
|
})
|
|
});
|
|
|
|
export const DEFAULT_DISPLAY_EDGE_SETTINGS = Object.freeze({
|
|
enabled: true,
|
|
contrastMode: "manual",
|
|
color: CAD_EDGE_COLOR,
|
|
thickness: 1,
|
|
classes: DEFAULT_DISPLAY_EDGE_CLASS_SETTINGS,
|
|
highlightColor: CAD_EDGE_HIGHLIGHT_COLOR,
|
|
highlightOpacity: 1,
|
|
highlightThickness: 3,
|
|
silhouette: false,
|
|
silhouetteScale: 0
|
|
});
|
|
|
|
export const DISABLED_DISPLAY_EDGE_SETTINGS = Object.freeze({
|
|
...DEFAULT_DISPLAY_EDGE_SETTINGS,
|
|
enabled: false
|
|
});
|
|
|
|
export const DEFAULT_EXPLODED_VIEW_SETTINGS = Object.freeze({
|
|
enabled: false,
|
|
axis: "z",
|
|
direction: "positive",
|
|
spacing: 1.45,
|
|
depth: 1,
|
|
keepBaseGrounded: true,
|
|
mergeCoplanar: false,
|
|
autoFrame: true
|
|
});
|
|
|
|
export const DEFAULT_DISPLAY_SETTINGS = Object.freeze({
|
|
projection: CAMERA_PROJECTION.ORTHOGRAPHIC,
|
|
mode: CAD_DISPLAY_MODE.SOLID,
|
|
clip: DEFAULT_STEP_CLIP_SETTINGS,
|
|
exploded: DEFAULT_EXPLODED_VIEW_SETTINGS,
|
|
edges: DEFAULT_DISPLAY_EDGE_SETTINGS
|
|
});
|
|
|
|
function isObject(value) {
|
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
}
|
|
|
|
function clamp(value, min, max) {
|
|
return Math.min(Math.max(value, min), max);
|
|
}
|
|
|
|
function normalizeNumber(value, fallback, min = -Infinity, max = Infinity) {
|
|
const numericValue = Number(value);
|
|
if (!Number.isFinite(numericValue)) {
|
|
return fallback;
|
|
}
|
|
return clamp(numericValue, min, max);
|
|
}
|
|
|
|
function normalizeColor(value, fallback) {
|
|
const normalized = String(value || "").trim();
|
|
if (!HEX_COLOR_PATTERN.test(normalized)) {
|
|
return fallback;
|
|
}
|
|
return normalized.length === 4
|
|
? `#${normalized[1]}${normalized[1]}${normalized[2]}${normalized[2]}${normalized[3]}${normalized[3]}`.toLowerCase()
|
|
: normalized.toLowerCase();
|
|
}
|
|
|
|
function normalizeBoolean(value, fallback = false) {
|
|
return typeof value === "boolean" ? value : fallback;
|
|
}
|
|
|
|
function normalizeDisplayEdgeContrastMode(value, fallback = "manual") {
|
|
const normalized = String(value || "").trim().toLowerCase();
|
|
return ["auto", "manual"].includes(normalized)
|
|
? normalized
|
|
: fallback;
|
|
}
|
|
|
|
export function normalizeDisplayEdgeClassSettings(
|
|
value = {},
|
|
fallback = DEFAULT_DISPLAY_EDGE_CLASS_SETTINGS,
|
|
colorFallback = CAD_EDGE_COLOR
|
|
) {
|
|
const source = isObject(value) ? value : {};
|
|
const fallbackColor = normalizeColor(colorFallback, CAD_EDGE_COLOR);
|
|
return Object.fromEntries(CAD_EDGE_CLASS_IDS.map((classId) => {
|
|
const classSource = isObject(source[classId]) ? source[classId] : {};
|
|
const classFallback = fallback?.[classId] || DEFAULT_DISPLAY_EDGE_CLASS_SETTINGS[classId];
|
|
const legacyDisabled = classSource.enabled === false;
|
|
return [classId, {
|
|
color: normalizeColor(classSource.color, fallbackColor),
|
|
opacity: normalizeNumber(classSource.opacity, classFallback.opacity, 0, 1),
|
|
thickness: legacyDisabled
|
|
? 0
|
|
: normalizeNumber(classSource.thickness, classFallback.thickness, 0, 6)
|
|
}];
|
|
}));
|
|
}
|
|
|
|
export function normalizeDisplayEdgeSettings(value = null, fallback = DEFAULT_DISPLAY_EDGE_SETTINGS) {
|
|
const source = isObject(value) ? value : {};
|
|
const color = normalizeColor(source.color, fallback.color);
|
|
const normalized = {
|
|
enabled: normalizeBoolean(source.enabled, fallback.enabled),
|
|
contrastMode: normalizeDisplayEdgeContrastMode(source.contrastMode, fallback.contrastMode),
|
|
color,
|
|
thickness: normalizeNumber(source.thickness, fallback.thickness, 0.5, 6),
|
|
classes: normalizeDisplayEdgeClassSettings(source.classes, fallback.classes, color),
|
|
highlightColor: normalizeColor(source.highlightColor, fallback.highlightColor || CAD_EDGE_HIGHLIGHT_COLOR),
|
|
highlightOpacity: normalizeNumber(source.highlightOpacity, fallback.highlightOpacity || 1, 0, 1),
|
|
highlightThickness: normalizeNumber(source.highlightThickness, fallback.highlightThickness || 3, 0.5, 6),
|
|
silhouette: normalizeBoolean(source.silhouette, fallback.silhouette || false),
|
|
silhouetteScale: normalizeNumber(source.silhouetteScale, fallback.silhouetteScale || 0, 0, 0.04)
|
|
};
|
|
if (typeof source.depthTest === "boolean") {
|
|
normalized.depthTest = source.depthTest;
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
function normalizeModeText(value) {
|
|
return String(value || "").trim().toLowerCase().replace(/\s+/g, "_").replace(/-/g, "_");
|
|
}
|
|
|
|
export function normalizeDisplayMode(value) {
|
|
const normalized = normalizeModeText(value);
|
|
if (!normalized) {
|
|
return CAD_DISPLAY_MODE.SOLID;
|
|
}
|
|
if (normalized === "wire" || normalized === "wire_frame") {
|
|
return CAD_DISPLAY_MODE.WIREFRAME;
|
|
}
|
|
if (
|
|
normalized === "edges" ||
|
|
normalized === "edge" ||
|
|
normalized === "shaded_edges" ||
|
|
normalized === "shaded_with_edges" ||
|
|
normalized === "with_edges"
|
|
) {
|
|
return CAD_DISPLAY_MODE.SOLID;
|
|
}
|
|
if (
|
|
normalized === "shaded" ||
|
|
normalized === "shaded_without_edges" ||
|
|
normalized === "without_edges"
|
|
) {
|
|
return CAD_DISPLAY_MODE.RENDERED;
|
|
}
|
|
if (
|
|
normalized === "translucent" ||
|
|
normalized === "xray" ||
|
|
normalized === "x_ray" ||
|
|
normalized === "see_through"
|
|
) {
|
|
return CAD_DISPLAY_MODE.TRANSPARENT;
|
|
}
|
|
if (
|
|
normalized === "hidden_edge" ||
|
|
normalized === "hidden_edges_visible" ||
|
|
normalized === "hidden_edge_display" ||
|
|
normalized === "shaded_hidden_edges"
|
|
) {
|
|
return CAD_DISPLAY_MODE.HIDDEN_EDGES;
|
|
}
|
|
if (
|
|
normalized === "visible_edges" ||
|
|
normalized === "visible_edges_only" ||
|
|
normalized === "hidden_lines" ||
|
|
normalized === "hidden_line_removed" ||
|
|
normalized === "hidden_lines_removed" ||
|
|
normalized === "hidden_edges_removed"
|
|
) {
|
|
return CAD_DISPLAY_MODE.HIDDEN_LINES_REMOVED;
|
|
}
|
|
if (normalized === "flat") {
|
|
return CAD_DISPLAY_MODE.UNSHADED;
|
|
}
|
|
if (normalized === "appearance" || normalized === "material" || normalized === "materials") {
|
|
return CAD_DISPLAY_MODE.RENDERED;
|
|
}
|
|
return CAD_DISPLAY_MODE_VALUES.includes(normalized)
|
|
? normalized
|
|
: CAD_DISPLAY_MODE.SOLID;
|
|
}
|
|
|
|
export function normalizeExplodedViewAxis(value, fallback = DEFAULT_EXPLODED_VIEW_SETTINGS.axis) {
|
|
const normalized = String(value || "").trim().toLowerCase();
|
|
const axis = normalized.startsWith("-") ? normalized.slice(1) : normalized;
|
|
return EXPLODED_VIEW_AXES.includes(axis) ? axis : fallback;
|
|
}
|
|
|
|
export function normalizeExplodedViewDirection(value, fallback = DEFAULT_EXPLODED_VIEW_SETTINGS.direction) {
|
|
const normalized = String(value || "").trim().toLowerCase();
|
|
if (["negative", "reverse", "down", "backward", "-", "-1"].includes(normalized)) {
|
|
return "negative";
|
|
}
|
|
if (["positive", "forward", "up", "+", "+1", "1"].includes(normalized)) {
|
|
return "positive";
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
export function normalizeExplodedViewDepth(value, fallback = DEFAULT_EXPLODED_VIEW_SETTINGS.depth) {
|
|
const normalized = String(value ?? "").trim().toLowerCase();
|
|
if (["all", "full", "parts", "leaves", "leaf"].includes(normalized)) {
|
|
return MAX_EXPLODED_VIEW_DEPTH;
|
|
}
|
|
const numericValue = Math.round(Number(value));
|
|
return Number.isFinite(numericValue)
|
|
? clamp(numericValue, 1, MAX_EXPLODED_VIEW_DEPTH)
|
|
: fallback;
|
|
}
|
|
|
|
export function normalizeExplodedViewSettings(value = null, overrides = {}) {
|
|
const source = isObject(value) ? value : {};
|
|
const merged = isObject(overrides) ? { ...source, ...overrides } : source;
|
|
const axisText = String(merged.axis || "").trim().toLowerCase();
|
|
return {
|
|
enabled: normalizeBoolean(merged.enabled, DEFAULT_EXPLODED_VIEW_SETTINGS.enabled),
|
|
axis: normalizeExplodedViewAxis(merged.axis),
|
|
direction: normalizeExplodedViewDirection(merged.direction || (axisText.startsWith("-") ? "negative" : "positive")),
|
|
spacing: normalizeNumber(merged.spacing ?? merged.distance ?? merged.distanceScale, DEFAULT_EXPLODED_VIEW_SETTINGS.spacing, 0.25, 4),
|
|
depth: normalizeExplodedViewDepth(merged.depth ?? merged.levels ?? merged.scopeDepth),
|
|
keepBaseGrounded: normalizeBoolean(merged.keepBaseGrounded ?? merged.groundBase, DEFAULT_EXPLODED_VIEW_SETTINGS.keepBaseGrounded),
|
|
mergeCoplanar: normalizeBoolean(merged.mergeCoplanar ?? merged.mergeLayers ?? merged.coalesceLayers, DEFAULT_EXPLODED_VIEW_SETTINGS.mergeCoplanar),
|
|
autoFrame: normalizeBoolean(merged.autoFrame, DEFAULT_EXPLODED_VIEW_SETTINGS.autoFrame)
|
|
};
|
|
}
|
|
|
|
export function normalizeDisplaySettings(value = null) {
|
|
const source = isObject(value) ? value : {};
|
|
const explodedOverrides = {};
|
|
if (isObject(source.exploded) && source.exploded.enabled !== undefined) {
|
|
explodedOverrides.enabled = source.exploded.enabled;
|
|
}
|
|
return {
|
|
projection: normalizeCameraProjection(source.projection, DEFAULT_DISPLAY_SETTINGS.projection),
|
|
mode: normalizeDisplayMode(source.mode),
|
|
clip: normalizeStepClipSettings(source.clip),
|
|
exploded: normalizeExplodedViewSettings(source.exploded, explodedOverrides),
|
|
edges: normalizeDisplayEdgeSettings(source.edges)
|
|
};
|
|
}
|
|
|
|
export function cloneDisplaySettings(value = DEFAULT_DISPLAY_SETTINGS) {
|
|
return normalizeDisplaySettings(value);
|
|
}
|
|
|
|
export function displaySettingsEqual(left, right) {
|
|
const a = normalizeDisplaySettings(left);
|
|
const b = normalizeDisplaySettings(right);
|
|
return a.projection === b.projection &&
|
|
a.mode === b.mode &&
|
|
stepClipSettingsEqual(a.clip, b.clip) &&
|
|
JSON.stringify(a.exploded) === JSON.stringify(b.exploded) &&
|
|
JSON.stringify(a.edges) === JSON.stringify(b.edges);
|
|
}
|
|
|
|
export function resolveDisplayMode(displaySettings) {
|
|
return normalizeDisplaySettings(displaySettings).mode;
|
|
}
|
|
|
|
export function resolveDisplayEdgeSettings(displaySettings) {
|
|
return normalizeDisplaySettings(displaySettings).edges;
|
|
}
|
|
|
|
export function displayModeIsWireframe(value) {
|
|
return normalizeDisplayMode(value) === CAD_DISPLAY_MODE.WIREFRAME;
|
|
}
|
|
|
|
export function displayModeForcesEdges(value) {
|
|
return [
|
|
CAD_DISPLAY_MODE.SOLID,
|
|
CAD_DISPLAY_MODE.TRANSPARENT,
|
|
CAD_DISPLAY_MODE.HIDDEN_EDGES,
|
|
CAD_DISPLAY_MODE.HIDDEN_LINES_REMOVED
|
|
].includes(normalizeDisplayMode(value));
|
|
}
|
|
|
|
export function displayModeAllowsEdges(value) {
|
|
return ![
|
|
CAD_DISPLAY_MODE.RENDERED,
|
|
CAD_DISPLAY_MODE.UNSHADED
|
|
].includes(normalizeDisplayMode(value));
|
|
}
|
|
|
|
export function displayModeShowsEdges(value, edgeSettings = null) {
|
|
const mode = normalizeDisplayMode(value);
|
|
return mode === CAD_DISPLAY_MODE.WIREFRAME ||
|
|
displayModeForcesEdges(mode);
|
|
}
|
|
|
|
export function displayModeShowsThroughEdges(value) {
|
|
return [
|
|
CAD_DISPLAY_MODE.TRANSPARENT,
|
|
CAD_DISPLAY_MODE.HIDDEN_EDGES
|
|
].includes(normalizeDisplayMode(value));
|
|
}
|
|
|
|
export function displayModeUsesTransparentSurfaces(value) {
|
|
return [
|
|
CAD_DISPLAY_MODE.TRANSPARENT,
|
|
CAD_DISPLAY_MODE.HIDDEN_LINES_REMOVED,
|
|
CAD_DISPLAY_MODE.WIREFRAME
|
|
].includes(normalizeDisplayMode(value));
|
|
}
|
|
|
|
export function displayModeUsesUnlitSurfaces(value) {
|
|
return [
|
|
CAD_DISPLAY_MODE.UNSHADED,
|
|
CAD_DISPLAY_MODE.HIDDEN_LINES_REMOVED,
|
|
CAD_DISPLAY_MODE.WIREFRAME
|
|
].includes(normalizeDisplayMode(value));
|
|
}
|
|
|
|
export function displayModeSurfaceOpacity(value, fallback = 1) {
|
|
const mode = normalizeDisplayMode(value);
|
|
if (mode === CAD_DISPLAY_MODE.WIREFRAME) {
|
|
return 0.035;
|
|
}
|
|
if (mode === CAD_DISPLAY_MODE.TRANSPARENT) {
|
|
return 0.22;
|
|
}
|
|
if (mode === CAD_DISPLAY_MODE.HIDDEN_LINES_REMOVED) {
|
|
return 0.045;
|
|
}
|
|
const numericFallback = Number(fallback);
|
|
return Number.isFinite(numericFallback) ? numericFallback : 1;
|
|
}
|