Research-Stack/5-Applications/text-to-cad/viewer/components/CadWorkspace.js

3766 lines
123 KiB
JavaScript

"use client";
import { startTransition, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { ArrowLeftRight, ArrowRight, Circle, Eraser, Minus, PaintBucket, PenTool, Square } from "lucide-react";
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
import CadRenderPane from "./workbench/CadRenderPane";
import DxfFileSheet from "./workbench/DxfFileSheet";
import FileExplorerSidebar from "./workbench/FileExplorerSidebar";
import LookSettingsPopover from "./workbench/LookSettingsPopover";
import StepAssemblyFileSheet from "./workbench/StepAssemblyFileSheet";
import StatusToast from "./workbench/StatusToast";
import UrdfFileSheet from "./workbench/UrdfFileSheet";
import ViewerAlertDialog from "./workbench/ViewerAlertDialog";
import ViewerLoadingOverlay from "./workbench/ViewerLoadingOverlay";
import CadWorkspaceAssemblyInspectPill from "./workbench/CadWorkspaceAssemblyInspectPill";
import FloatingToolBar from "./workbench/FloatingToolBar";
import CadWorkspaceTopBar from "./workbench/CadWorkspaceTopBar";
import { useCadAssets } from "./workbench/hooks/useCadAssets";
import { useCadWorkspaceLayout } from "./workbench/hooks/useCadWorkspaceLayout";
import { useCadWorkspaceSelection } from "./workbench/hooks/useCadWorkspaceSelection";
import { useCadWorkspaceSession } from "./workbench/hooks/useCadWorkspaceSession";
import { useCadWorkspaceSelectors } from "./workbench/hooks/useCadWorkspaceSelectors";
import { useCadWorkspaceShortcuts } from "./workbench/hooks/useCadWorkspaceShortcuts";
import {
applyThemeToDocument,
DARK_THEME_ID
} from "../lib/themes";
import {
cloneLookSettings,
DEFAULT_LOOK_SETTINGS,
normalizeLookSettings
} from "../lib/lookSettings";
import { clonePerspectiveSnapshot } from "../lib/perspective";
import {
ASSET_STATUS,
DRAWING_TOOL,
RENDER_FORMAT,
REFERENCE_STATUS,
TAB_TOOL_MODE
} from "../lib/workbench/constants";
import {
buildCadWorkspaceSessionState,
cloneDrawingStrokes,
cloneTabSnapshot,
createTabRecord,
dxfBendSettingsEqual,
drawingStrokesEqual,
LOOK_SETTINGS_STORAGE_KEY,
CAD_WORKSPACE_DEFAULT_GLASS_TONE,
normalizeCadWorkspaceGlassTone,
readDxfBendOverridesForEntry,
readLookSettings,
readCadWorkspaceGlassTone,
readCadWorkspaceSessionState,
writeDxfBendOverridesForEntry,
writeLookSettings,
writeThemePreference,
writeCadWorkspaceGlassTone,
tabSnapshotEqual,
CAD_WORKSPACE_DEFAULT_SIDEBAR_WIDTH,
CAD_WORKSPACE_DEFAULT_TAB_TOOLS_WIDTH,
writeCadWorkspaceSessionState
} from "../lib/workbench/persistence";
import {
buildSidebarDirectoryTree,
cadPathForEntry,
collectAncestorDirectoryIds,
collectSidebarDirectoryIds,
fileKey,
readCadParam,
readCadRefQueryParams,
selectedEntryKeyFromUrl,
sidebarDirectoryIdForEntry,
sidebarLabelForEntry,
writeCadParam,
writeCadRefQueryParams,
} from "../lib/workbench/sidebar";
import { buildCadRefToken, parseCadRefSelector, parseCadRefToken, sortCadRefSelectors } from "../lib/cadRefs";
import { loadRenderSelectorBundle } from "../lib/renderAssetClient";
import {
buildDxfPreviewMeshData,
extractOrderedDxfBendLines,
normalizeDxfBendAngleDeg,
normalizeDxfBendDirection,
normalizeDxfBendSettings,
DEFAULT_DXF_PREVIEW_THICKNESS_MM,
normalizeDxfPreviewThicknessMm
} from "../lib/dxf/buildPreviewMesh";
import {
buildDefaultUrdfJointValues,
buildUrdfMeshGeometry,
clampJointValueDeg,
poseUrdfMeshData
} from "../lib/urdf/kinematics";
import {
advanceUrdfJointValues,
jointValueMapsClose,
URDF_JOINT_ANIMATION_FOLLOW_MS
} from "../lib/urdf/jointAnimation";
import { buildSelectorRuntime } from "../lib/selectors/runtime";
import {
assemblyBreadcrumb,
buildAssemblyLeafToNodePickMap,
descendantLeafPartIds,
findAssemblyNode,
flattenAssemblyNodes,
flattenAssemblyLeafParts,
leafPartIdsForAssemblySelection
} from "../lib/assembly/meshData";
import { copyTextToClipboard } from "../lib/clipboard";
const DEFAULT_DOCUMENT_TITLE = "CAD Explorer";
const EMPTY_LIST = Object.freeze([]);
const CAD_BUILD_COMMANDS = {
dxf: "python skills/cad/scripts/gen_dxf",
stepAssembly: "python skills/cad/scripts/gen_step_assembly",
stepPart: "python skills/cad/scripts/gen_step_part",
urdf: "python skills/urdf/scripts/gen_urdf"
};
const DEFAULT_URDF_ANIMATION_SETTINGS = Object.freeze({
introEnabled: true,
speed: 1
});
const MIN_URDF_ANIMATION_SPEED = 0.25;
const MAX_URDF_ANIMATION_SPEED = 2.5;
const DESKTOP_SIDEBAR_MIN_WIDTH = 150;
const DESKTOP_SIDEBAR_MAX_WIDTH = 520;
const DEFAULT_SIDEBAR_WIDTH = CAD_WORKSPACE_DEFAULT_SIDEBAR_WIDTH;
const DESKTOP_TAB_TOOLS_MIN_WIDTH = 160;
const DESKTOP_TAB_TOOLS_MAX_WIDTH = 560;
const DEFAULT_TAB_TOOLS_WIDTH = CAD_WORKSPACE_DEFAULT_TAB_TOOLS_WIDTH;
const CAD_WORKSPACE_TOP_BAR_HEIGHT = 44;
const CAD_WORKSPACE_SESSION_PERSIST_DELAY_MS = 120;
const MOBILE_FILE_EXPLORER_MEDIA_QUERY = "(max-width: 767px)";
function clampPanelWidth(value, minWidth, maxWidth) {
return Math.min(Math.max(value, minWidth), Math.max(minWidth, maxWidth));
}
function toFiniteNumber(value, fallback = 0) {
const numericValue = Number(value);
return Number.isFinite(numericValue) ? numericValue : fallback;
}
function animationNowMs() {
if (typeof performance !== "undefined" && typeof performance.now === "function") {
return performance.now();
}
return Date.now();
}
function cloneJointValueMap(values) {
if (!values || typeof values !== "object") {
return {};
}
return Object.fromEntries(
Object.entries(values)
.map(([name, value]) => [String(name || "").trim(), toFiniteNumber(value, 0)])
.filter(([name]) => name)
);
}
function roundedUrdfJointValue(value) {
const numericValue = toFiniteNumber(value, 0);
const rounded = Math.round(numericValue * 1000) / 1000;
return Object.is(rounded, -0) ? 0 : rounded;
}
function normalizeUrdfIntroAnimationEnabled(value) {
return value === true;
}
function normalizeUrdfAnimationSpeed(value, fallback = DEFAULT_URDF_ANIMATION_SETTINGS.speed) {
const numericValue = toFiniteNumber(value, fallback);
return Math.min(Math.max(numericValue, MIN_URDF_ANIMATION_SPEED), MAX_URDF_ANIMATION_SPEED);
}
function buildUrdfJointAnglesCopyText(joints, jointValues) {
const movableJoints = Array.isArray(joints) ? joints : [];
return JSON.stringify(
Object.fromEntries(
movableJoints.map((joint) => {
const jointName = String(joint?.name || "").trim();
const value = roundedUrdfJointValue(jointValues?.[jointName] ?? joint?.defaultValueDeg ?? 0);
return [jointName, value];
}).filter(([name]) => name)
),
null,
2
);
}
function buildUrdfLinkIntroOrderMap(urdfData) {
const orderByLink = new Map();
const rootLink = String(urdfData?.rootLink || "").trim();
if (rootLink) {
orderByLink.set(rootLink, 0);
}
const joints = Array.isArray(urdfData?.joints) ? urdfData.joints : [];
joints.forEach((joint, index) => {
const parentLink = String(joint?.parentLink || "").trim();
const childLink = String(joint?.childLink || "").trim();
if (!childLink) {
return;
}
const parentOrder = orderByLink.get(parentLink);
const introOrder = Number.isFinite(parentOrder) ? parentOrder + 1 : index + 1;
orderByLink.set(childLink, introOrder);
});
return orderByLink;
}
function buildUrdfWrappedJointNameSet(urdfData) {
return new Set(
(Array.isArray(urdfData?.joints) ? urdfData.joints : [])
.filter((joint) => String(joint?.type || "").trim().toLowerCase() === "continuous")
.map((joint) => String(joint?.name || "").trim())
.filter(Boolean)
);
}
function useAnimatedUrdfJointValues(
targetValues,
animationKey,
animationSettings = DEFAULT_URDF_ANIMATION_SETTINGS,
wrappedJointNames = null
) {
const initialAnimationKey = String(animationKey || "");
const [displayValues, setDisplayValues] = useState(() => cloneJointValueMap(targetValues));
const displayValuesRef = useRef(displayValues);
const targetValuesRef = useRef(cloneJointValueMap(targetValues));
const animationRef = useRef({
frameId: 0,
key: initialAnimationKey,
lastTimestamp: 0
});
useEffect(() => {
displayValuesRef.current = displayValues;
}, [displayValues]);
const animationSpeed = normalizeUrdfAnimationSpeed(animationSettings?.speed);
const cancelAnimation = () => {
if (animationRef.current.frameId && typeof cancelAnimationFrame === "function") {
cancelAnimationFrame(animationRef.current.frameId);
}
animationRef.current.frameId = 0;
animationRef.current.lastTimestamp = 0;
};
const snapToTarget = (targetSnapshot) => {
displayValuesRef.current = targetSnapshot;
setDisplayValues(targetSnapshot);
};
const scheduleAnimation = (key) => {
if (animationRef.current.frameId || typeof requestAnimationFrame !== "function") {
return;
}
const stepAnimation = (timestamp) => {
if (animationRef.current.key !== key) {
animationRef.current.frameId = 0;
animationRef.current.lastTimestamp = 0;
return;
}
const timestampMs = toFiniteNumber(timestamp, animationNowMs());
const lastTimestamp = animationRef.current.lastTimestamp || Math.max(timestampMs - 16, 0);
const deltaMs = Math.min(Math.max(timestampMs - lastTimestamp, 0), 50);
animationRef.current.lastTimestamp = timestampMs;
const { values, done } = advanceUrdfJointValues(
displayValuesRef.current,
targetValuesRef.current,
deltaMs,
URDF_JOINT_ANIMATION_FOLLOW_MS / animationSpeed,
undefined,
wrappedJointNames
);
displayValuesRef.current = values;
setDisplayValues(values);
if (done) {
animationRef.current.frameId = 0;
animationRef.current.lastTimestamp = 0;
return;
}
animationRef.current.frameId = requestAnimationFrame(stepAnimation);
};
animationRef.current.frameId = requestAnimationFrame(stepAnimation);
};
useEffect(() => {
const nextKey = String(animationKey || "");
const targetSnapshot = cloneJointValueMap(targetValues);
const keyChanged = animationRef.current.key !== nextKey;
targetValuesRef.current = targetSnapshot;
if (keyChanged) {
cancelAnimation();
animationRef.current.key = nextKey;
snapToTarget(targetSnapshot);
return undefined;
}
if (!nextKey || typeof requestAnimationFrame !== "function") {
cancelAnimation();
snapToTarget(targetSnapshot);
return undefined;
}
if (jointValueMapsClose(displayValuesRef.current, targetSnapshot)) {
cancelAnimation();
snapToTarget(targetSnapshot);
return undefined;
}
scheduleAnimation(nextKey);
return undefined;
}, [animationKey, animationSpeed, targetValues, wrappedJointNames]);
useEffect(() => () => {
cancelAnimation();
}, []);
return displayValues;
}
function meshAssetKeyForEntry(entry) {
return entry?.kind === "stl" ? "stl" : "glb";
}
function buildMeshCacheKey(entry) {
const fileRef = fileKey(entry);
const meshHash = String(
entry?.kind === "assembly"
? [entryAssetHash(entry, "topology"), entryAssetHash(entry, "glb")].filter(Boolean).join(":")
: entryAssetHash(entry, meshAssetKeyForEntry(entry))
);
return fileRef && meshHash ? `${fileRef}:${meshHash}` : "";
}
function buildReferenceCacheKey(entry) {
const fileRef = fileKey(entry);
const referenceHash = entryAssetHash(entry, "topology") || String(entry?.step?.hash || "");
return fileRef && referenceHash ? `${fileRef}:${referenceHash}` : "";
}
function buildDxfCacheKey(entry) {
const fileRef = fileKey(entry);
const dxfHash = entryAssetHash(entry, "dxf");
return fileRef && dxfHash ? `${fileRef}:${dxfHash}` : "";
}
function buildUrdfCacheKey(entry) {
const fileRef = fileKey(entry);
const urdfHash = entryAssetHash(entry, "urdf");
return fileRef && urdfHash ? `${fileRef}:${urdfHash}` : "";
}
function entryAsset(entry, key) {
return entry?.assets?.[key] || null;
}
function entryAssetUrl(entry, key) {
return String(entryAsset(entry, key)?.url || "").trim();
}
function entryAssetHash(entry, key) {
return String(entryAsset(entry, key)?.hash || "").trim();
}
function buildCadCommand(fileRef, entry = null) {
const sourceFormat = entrySourceFormat(entry);
if (sourceFormat === RENDER_FORMAT.DXF) {
return `${CAD_BUILD_COMMANDS.dxf} ${fileRef}`;
}
if (sourceFormat === RENDER_FORMAT.URDF) {
return `${CAD_BUILD_COMMANDS.urdf} ${fileRef}`;
}
if (sourceFormat === RENDER_FORMAT.STL) {
return "";
}
const command = entry?.kind === "assembly" ? CAD_BUILD_COMMANDS.stepAssembly : CAD_BUILD_COMMANDS.stepPart;
return `${command} ${fileRef}`;
}
function entryHasMesh(entry) {
if (entry?.kind === "assembly") {
return Boolean(
entryAssetUrl(entry, "topology") &&
entryAssetHash(entry, "topology") &&
entryAssetUrl(entry, "glb") &&
entryAssetHash(entry, "glb")
);
}
const meshKey = meshAssetKeyForEntry(entry);
return Boolean(entryAssetUrl(entry, meshKey) && entryAssetHash(entry, meshKey));
}
function entryHasUrdf(entry) {
return Boolean(entryAssetUrl(entry, "urdf") && entryAssetHash(entry, "urdf"));
}
function entryHasReferences(entry) {
return Boolean(
entryAssetUrl(entry, "topology") &&
entryAssetUrl(entry, "topologyBinary") &&
entryAssetHash(entry, "topology") &&
entryAssetHash(entry, "topologyBinary")
);
}
function entryHasDxf(entry) {
return Boolean(entryAssetUrl(entry, "dxf") && entryAssetHash(entry, "dxf"));
}
function entrySourceFormat(entry) {
const kind = String(entry?.kind || "").trim().toLowerCase();
if (kind === "dxf") {
return RENDER_FORMAT.DXF;
}
if (kind === RENDER_FORMAT.STL) {
return RENDER_FORMAT.STL;
}
if (kind === RENDER_FORMAT.URDF) {
return RENDER_FORMAT.URDF;
}
return RENDER_FORMAT.STEP;
}
function fileSheetKindForEntry(entry) {
const kind = String(entry?.kind || "").trim().toLowerCase();
if (kind === "dxf") {
return "dxf";
}
if (kind === "urdf") {
return "urdf";
}
if (kind === "assembly") {
return "stepAssembly";
}
return "";
}
function normalizeReferenceList(value) {
if (!Array.isArray(value)) {
return [];
}
return value
.filter((reference) => reference && typeof reference === "object")
.map((reference) => ({
...reference,
id: String(reference.id || "").trim(),
label: String(reference.label || reference.id || "Reference").trim() || "Reference",
summary: String(reference.summary || reference.shortSummary || "").trim(),
shortSummary: String(reference.shortSummary || reference.summary || "").trim(),
copyText: String(reference.copyText || "").trim(),
partId: String(reference.partId || "").trim(),
entityType: String(reference.entityType || "").trim(),
selectorType: String(reference.selectorType || "").trim(),
normalizedSelector: String(reference.normalizedSelector || "").trim(),
displaySelector: String(reference.displaySelector || "").trim()
}))
.filter((reference) => reference.id);
}
function readReferenceCounts(referencePayload = null) {
return {
faces: Math.max(0, Number(referencePayload?.manifest?.stats?.faceCount || 0)),
edges: Math.max(0, Number(referencePayload?.manifest?.stats?.edgeCount || 0)),
vertices: Math.max(0, Number(referencePayload?.manifest?.stats?.vertexCount || 0))
};
}
function buildNormalizedReferenceState(entry, referencePayload = null, {
copyCadPath,
partId = "",
transform = null,
remapOccurrenceId = ""
} = {}) {
const counts = readReferenceCounts(referencePayload);
const selectorRuntime = buildSelectorRuntime(referencePayload, {
copyCadPath: copyCadPath || cadPathForEntry(entry),
partId,
transform,
remapOccurrenceId
});
const references = normalizeReferenceList(selectorRuntime.references);
return {
fileRef: fileKey(entry),
kind: entry.kind,
referenceHash: buildReferenceCacheKey(entry),
stepRelPath: entry?.step?.path || "",
stepHash: String(selectorRuntime.stepHash || entry?.step?.hash || ""),
counts: {
faces: Number(selectorRuntime.faces?.length || 0),
edges: Number(selectorRuntime.edges?.length || 0),
vertices: Number(selectorRuntime.vertices?.length || 0)
},
parts: [],
selectorRuntime,
references,
disabledReason: ""
};
}
function parseAssemblyPartReferenceSelectionId(referenceId) {
const normalizedReferenceId = String(referenceId || "").trim();
const prefix = "assembly-part:";
if (normalizedReferenceId.startsWith(prefix)) {
const partId = normalizedReferenceId.slice(prefix.length).trim();
if (!partId) {
return null;
}
return { partId };
}
if (normalizedReferenceId.startsWith("topology|")) {
const parts = normalizedReferenceId.split("|");
const partId = String(parts[1] || "").trim();
if (!partId) {
return null;
}
return { partId };
}
return null;
}
function buildCadRefGroupKey(cadPath, selector = "") {
const compactCadPath = String(cadPath || "").trim();
if (!compactCadPath) {
return "";
}
const groupKind = String(selector || "").trim() || "root";
return `${compactCadPath}::${groupKind}`;
}
function ensureCadRefGroup(groups, outputOrder, groupKey, cadPath) {
if (!groupKey) {
return null;
}
let group = groups.get(groupKey);
if (group) {
return group;
}
group = {
cadPath,
selectors: [],
seenSelectors: new Set()
};
groups.set(groupKey, group);
outputOrder.push({
kind: "group",
key: groupKey
});
return group;
}
function appendUniquePlainLine(plainLines, outputOrder, text, key = "") {
const normalizedText = String(text || "").trim();
const normalizedKey = String(key || "").trim() || normalizedText;
if (!normalizedText || !normalizedKey || plainLines.has(normalizedKey)) {
return false;
}
plainLines.set(normalizedKey, normalizedText);
outputOrder.push({
kind: "plain",
key: normalizedKey
});
return true;
}
function appendCadRefText(groups, plainLines, outputOrder, text, key = "") {
const normalizedText = String(text || "").trim();
if (!normalizedText) {
return 0;
}
const parsedToken = parseCadRefToken(normalizedText);
if (!parsedToken) {
appendUniquePlainLine(plainLines, outputOrder, normalizedText, key);
return 0;
}
const { cadPath, selectors } = parsedToken;
if (!selectors.length) {
const group = ensureCadRefGroup(groups, outputOrder, buildCadRefGroupKey(cadPath, "root"), cadPath);
if (!group || group.seenSelectors.has("")) {
return 0;
}
group.seenSelectors.add("");
return 1;
}
const group = ensureCadRefGroup(groups, outputOrder, buildCadRefGroupKey(cadPath, "selectors"), cadPath);
if (!group) {
return 0;
}
let addedCount = 0;
for (const selector of selectors) {
if (group.seenSelectors.has(selector)) {
continue;
}
group.seenSelectors.add(selector);
group.selectors.push(selector);
addedCount += 1;
}
return addedCount;
}
function copySelectedReferenceText(references) {
const groups = new Map();
const plainLines = new Map();
const outputOrder = [];
for (const reference of references) {
appendCadRefText(
groups,
plainLines,
outputOrder,
String(reference?.copyText || "").trim(),
String(reference?.id || "").trim()
);
}
const lines = outputOrder
.map((item) => {
if (item.kind === "plain") {
return plainLines.get(item.key) || "";
}
const group = groups.get(item.key);
if (!group) {
return "";
}
return buildCadRefToken({
cadPath: group.cadPath,
selectors: item.key.endsWith("::selectors") ? sortCadRefSelectors(group.selectors) : []
});
})
.filter(Boolean);
return {
text: lines.join("\n")
};
}
function buildAssemblyPartCopyText(part, entry) {
const cadPath = cadPathForEntry(entry);
if (!cadPath) {
return "";
}
const partId = String(part?.id || "").trim();
const selector = String(part?.occurrenceId || partId).trim();
if (!partId || !selector) {
return "";
}
const partName = String(part?.name || partId).trim() || partId;
return `${buildCadRefToken({
cadPath,
selector
})} Assembly part "${partName}"`;
}
function buildSelectionCopyPayload({ references = [], parts = [], entry = null } = {}) {
const referencesForCopy = Array.isArray(references) ? [...references] : [];
const missingPartNames = [];
for (const part of parts) {
const copyText = buildAssemblyPartCopyText(part, entry);
if (!copyText) {
missingPartNames.push(String(part?.name || part?.id || "part"));
continue;
}
referencesForCopy.push({
id: `assembly-part:${String(part?.id || "").trim()}`,
copyText
});
}
const { text: referenceText } = copySelectedReferenceText(referencesForCopy);
const lines = String(referenceText || "").split("\n").map((line) => line.trim()).filter(Boolean);
return {
lines,
missingPartNames
};
}
function buildSelectionCopyButtonLabel(lines, { limit = 3 } = {}) {
const copyLines = Array.isArray(lines) ? lines : [];
const normalizedLimit = Math.max(1, Number(limit) || 1);
const tokens = copyLines
.map((line) => parseCadRefToken(String(line || "").trim())?.token || String(line || "").trim())
.filter(Boolean);
if (!tokens.length) {
return "Copy refs";
}
const visibleTokens = tokens.slice(0, normalizedLimit);
const remainingCount = tokens.length - visibleTokens.length;
return remainingCount > 0
? `Copy ${visibleTokens.join(", ")} and ${remainingCount} other${remainingCount === 1 ? "" : "s"}`
: `Copy ${visibleTokens.join(", ")}`;
}
function orderedStringListEqual(a, b) {
if (a === b) {
return true;
}
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
return false;
}
for (let index = 0; index < a.length; index += 1) {
if (a[index] !== b[index]) {
return false;
}
}
return true;
}
function uniqueStringList(values) {
const seen = new Set();
const result = [];
for (const value of Array.isArray(values) ? values : []) {
const normalizedValue = String(value || "").trim();
if (!normalizedValue || seen.has(normalizedValue)) {
continue;
}
seen.add(normalizedValue);
result.push(normalizedValue);
}
return result;
}
function normalizePosixPath(path) {
const parts = [];
for (const part of String(path || "").replace(/\\/g, "/").split("/")) {
if (!part || part === ".") {
continue;
}
if (part === "..") {
parts.pop();
continue;
}
parts.push(part);
}
return parts.join("/");
}
function resolveTopologyRelativeFile(entry, sourcePath) {
const relativeSourcePath = String(sourcePath || "").trim();
const stepPath = String(entry?.step?.path || entry?.source?.path || "").trim();
if (!relativeSourcePath || !stepPath) {
return "";
}
const stepParts = stepPath.split("/");
const stepFilename = stepParts.pop();
const stepDirectory = stepParts.join("/");
const topologyDirectory = stepDirectory ? `${stepDirectory}/.${stepFilename}` : `.${stepFilename}`;
return normalizePosixPath(`${topologyDirectory}/${relativeSourcePath}`);
}
function cadRefQueryHasKnownEntry(cadRefs, entries) {
const cadPaths = new Set();
for (const cadRef of Array.isArray(cadRefs) ? cadRefs : []) {
const cadPath = String(parseCadRefToken(cadRef)?.cadPath || "").trim();
if (cadPath) {
cadPaths.add(cadPath);
}
}
if (!cadPaths.size) {
return false;
}
return (Array.isArray(entries) ? entries : []).some((entry) => cadPaths.has(cadPathForEntry(entry)));
}
function collectCadRefSelectionRequest(cadRefs, entry) {
const cadPath = cadPathForEntry(entry);
const selectors = [];
let hasMatchingToken = false;
let hasWholeEntryToken = false;
if (!cadPath) {
return {
hasMatchingToken,
hasWholeEntryToken,
selectors,
needsParts: false,
needsReferences: false
};
}
for (const cadRef of Array.isArray(cadRefs) ? cadRefs : []) {
const parsedToken = parseCadRefToken(cadRef);
if (!parsedToken || parsedToken.cadPath !== cadPath) {
continue;
}
hasMatchingToken = true;
if (!parsedToken.selectors.length) {
hasWholeEntryToken = true;
continue;
}
selectors.push(...parsedToken.selectors);
}
const normalizedSelectors = sortCadRefSelectors(selectors);
let needsParts = false;
let needsReferences = false;
for (const selector of normalizedSelectors) {
const parsedSelector = parseCadRefSelector(selector);
if (entry?.kind === "assembly" && parsedSelector?.selectorType === "occurrence") {
needsParts = true;
} else {
needsReferences = true;
}
}
return {
hasMatchingToken,
hasWholeEntryToken,
selectors: normalizedSelectors,
needsParts,
needsReferences
};
}
function addTokenSelectorsToMap(map, copyText, value, cadPath) {
const parsedToken = parseCadRefToken(copyText);
if (!parsedToken || parsedToken.cadPath !== cadPath) {
return;
}
for (const selector of parsedToken.selectors) {
if (selector && !map.has(selector)) {
map.set(selector, value);
}
}
}
function addReferenceIdSelectorToMap(map, reference, value) {
const displaySelector = String(reference?.displaySelector || reference?.normalizedSelector || "").trim();
if (!displaySelector) {
return;
}
const parsedSelector = parseCadRefSelector(displaySelector);
if (parsedSelector?.canonical && !map.has(parsedSelector.canonical)) {
map.set(parsedSelector.canonical, value);
}
if (reference?.normalizedSelector && !map.has(reference.normalizedSelector)) {
map.set(reference.normalizedSelector, value);
}
}
function buildReferenceSelectorMap(references, cadPath) {
const map = new Map();
for (const reference of Array.isArray(references) ? references : []) {
const referenceId = String(reference?.id || "").trim();
if (!referenceId) {
continue;
}
const value = {
id: referenceId,
partId: String(reference?.partId || "").trim()
};
addTokenSelectorsToMap(map, reference?.copyText, value, cadPath);
addReferenceIdSelectorToMap(map, reference, value);
}
return map;
}
function buildAssemblyPartSelectorMap(parts, cadPath) {
const map = new Map();
for (const part of Array.isArray(parts) ? parts : []) {
const partId = String(part?.id || "").trim();
const selector = String(part?.occurrenceId || partId).trim();
if (!partId || !selector) {
continue;
}
const copyText = buildCadRefToken({
cadPath,
selector
});
addTokenSelectorsToMap(map, copyText, partId, cadPath);
addTokenSelectorsToMap(map, selector, partId, cadPath);
}
return map;
}
function resolveCadRefSelection({ cadRefs = [], entry = null, references = [], assemblyParts = [], isAssemblyView = false } = {}) {
const request = collectCadRefSelectionRequest(cadRefs, entry);
const cadPath = cadPathForEntry(entry);
const referenceSelectorMap = buildReferenceSelectorMap(references, cadPath);
const assemblyPartSelectorMap = buildAssemblyPartSelectorMap(assemblyParts, cadPath);
const selectedReferenceIds = [];
const selectedPartIds = [];
const expandedAssemblyPartIds = [];
for (const selector of request.selectors) {
const parsedSelector = parseCadRefSelector(selector);
const canonicalSelector = String(parsedSelector?.canonical || selector || "").trim();
if (!canonicalSelector) {
continue;
}
if (isAssemblyView && parsedSelector?.selectorType === "occurrence") {
const partId = assemblyPartSelectorMap.get(canonicalSelector);
if (partId) {
selectedPartIds.push(partId);
}
continue;
}
const reference = referenceSelectorMap.get(canonicalSelector);
if (!reference) {
continue;
}
selectedReferenceIds.push(reference.id);
if (isAssemblyView && reference.partId) {
expandedAssemblyPartIds.push(reference.partId);
}
}
return {
...request,
selectedReferenceIds: uniqueStringList(selectedReferenceIds),
selectedPartIds: uniqueStringList(selectedPartIds),
expandedAssemblyPartIds: uniqueStringList(expandedAssemblyPartIds).slice(0, 1)
};
}
function computeNextSelectionIds(currentIds, selectionId, { multiSelect = false } = {}) {
const normalizedSelectionId = String(selectionId || "").trim();
if (!normalizedSelectionId) {
return [];
}
const current = Array.isArray(currentIds) ? currentIds : [];
if (multiSelect) {
return current.includes(normalizedSelectionId)
? current.filter((id) => id !== normalizedSelectionId)
: [...current, normalizedSelectionId];
}
if (current.length === 1 && current[0] === normalizedSelectionId) {
return [];
}
return [normalizedSelectionId];
}
function buildViewerMeshAlert(entry, hasMeshData, loadError) {
const fileRef = fileKey(entry);
if (!fileRef) {
return null;
}
const sourceFormat = entrySourceFormat(entry);
const command = buildCadCommand(fileRef, entry);
const reloadResolution = sourceFormat === RENDER_FORMAT.STL
? "Confirm the STL exists in the repo and reload the page."
: "Try reloading the page. If the problem persists, rebuild the render assets for this entry.";
const missingResolution = sourceFormat === RENDER_FORMAT.STL
? "Confirm the STL exists in the repo and reload the page."
: "Rebuild the CAD assets for this entry, then reload the page.";
if (loadError) {
return {
severity: "error",
summary: "Mesh load failed",
title: "Failed to load render mesh",
message: loadError,
resolution: reloadResolution,
command
};
}
if (!hasMeshData) {
return {
severity: "error",
summary: "Mesh unavailable",
title: "No mesh data is available",
message: "The selected entry is listed in the CAD catalog but no renderable mesh data could be loaded for it.",
resolution: missingResolution,
command
};
}
return null;
}
function buildViewerDxfAlert(fileRef, hasDxfData, loadError, previewError) {
if (!fileRef) {
return null;
}
const command = `${CAD_BUILD_COMMANDS.dxf} ${fileRef}`;
if (loadError) {
return {
severity: "error",
summary: "DXF load failed",
title: "Failed to load DXF flat pattern",
message: loadError,
resolution: "Try reloading the page. If the problem persists, rebuild the CAD assets for this entry.",
command
};
}
if (previewError) {
return {
severity: "warning",
summary: "DXF 3D preview unavailable",
title: "Failed to build the DXF 3D preview",
message: previewError,
resolution: "The flat pattern can still be shown, but the 3D extrusion preview could not be built from the current DXF geometry.",
command
};
}
if (!hasDxfData) {
return {
severity: "error",
summary: "DXF unavailable",
title: "No DXF flat pattern is available",
message: "The selected entry does not have a ready DXF companion asset for the flat-pattern viewer.",
resolution: "Rebuild the CAD assets for this entry, then reload the page.",
command
};
}
return null;
}
export default function CadWorkspace({
manifestEntries: manifestEntriesProp = [],
catalogRootName = "models",
manifestRevision = 0
}) {
const manifestEntries = Array.isArray(manifestEntriesProp) ? manifestEntriesProp : [];
const [catalogEntries, setCatalogEntries] = useState(manifestEntries);
const [query, setQuery] = useState("");
const [expandedDirectoryIds, setExpandedDirectoryIds] = useState(() => new Set());
const [openTabs, setOpenTabs] = useState([]);
const [selectedKey, setSelectedKey] = useState("");
const [dxfThicknessMm, setDxfThicknessMm] = useState(0);
const [dxfBendSettings, setDxfBendSettings] = useState([]);
const [dxfBendSettingsLoadedFileRef, setDxfBendSettingsLoadedFileRef] = useState("");
const [referenceQuery, setReferenceQuery] = useState("");
const [selectedReferenceIds, setSelectedReferenceIds] = useState([]);
const [hoveredListReferenceId, setHoveredListReferenceId] = useState("");
const [hoveredModelReferenceId, setHoveredModelReferenceId] = useState("");
const [selectedPartIds, setSelectedPartIds] = useState([]);
const [selectedRenderPartIdByAssemblyPartId, setSelectedRenderPartIdByAssemblyPartId] = useState({});
const [selectedWholeEntryCadRefToken, setSelectedWholeEntryCadRefToken] = useState("");
const [expandedAssemblyPartIds, setExpandedAssemblyPartIds] = useState([]);
const [hiddenPartIds, setHiddenPartIds] = useState([]);
const [hoveredListPartId, setHoveredListPartId] = useState("");
const [hoveredModelPartId, setHoveredModelPartId] = useState("");
const [copyStatus, setCopyStatus] = useState("");
const [stepUpdateInProgress, setStepUpdateInProgress] = useState(false);
const [screenshotStatus, setScreenshotStatus] = useState("");
const [persistenceStatus, setPersistenceStatus] = useState("");
const [tabToolsOpen, setTabToolsOpen] = useState(false);
const [sidebarOpen, setSidebarOpen] = useState(true);
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
const [sidebarWidth, setSidebarWidth] = useState(DEFAULT_SIDEBAR_WIDTH);
const [isDesktop, setIsDesktop] = useState(false);
const [viewerAlertOpen, setViewerAlertOpen] = useState(false);
const [viewerRuntimeAlert, setViewerRuntimeAlert] = useState(null);
const [lookMenuOpen, setLookMenuOpen] = useState(false);
const [lookSettings, setLookSettings] = useState(readLookSettings);
const [cadWorkspaceGlassTone, setCadWorkspaceGlassTone] = useState(readCadWorkspaceGlassTone);
const [previewMode, setPreviewMode] = useState(false);
const [tabToolsWidth, setTabToolsWidth] = useState(DEFAULT_TAB_TOOLS_WIDTH);
const [drawingTool, setDrawingTool] = useState(DRAWING_TOOL.FREEHAND);
const [viewerPerspective, setViewerPerspective] = useState(null);
const [tabToolMode, setTabToolMode] = useState(TAB_TOOL_MODE.REFERENCES);
const [drawingStrokes, setDrawingStrokes] = useState([]);
const [drawingUndoStack, setDrawingUndoStack] = useState([]);
const [drawingRedoStack, setDrawingRedoStack] = useState([]);
const [jointValuesByFileRef, setJointValuesByFileRef] = useState({});
const [urdfAnimationSettingsByFileRef, setUrdfAnimationSettingsByFileRef] = useState({});
const [urdfEntryAnimationEnabled, setUrdfEntryAnimationEnabled] = useState(DEFAULT_URDF_ANIMATION_SETTINGS.introEnabled);
const [selectedUrdfIntroReplayToken, setSelectedUrdfIntroReplayToken] = useState(0);
const [pendingCadRefQueryParams, setPendingCadRefQueryParams] = useState(() => readCadRefQueryParams());
const [inspectedAssemblyReferenceState, setInspectedAssemblyReferenceState] = useState(null);
const [, setInspectedAssemblyReferenceStatus] = useState(REFERENCE_STATUS.IDLE);
const [, setInspectedAssemblyReferenceError] = useState("");
const lastPersistenceFailureKeyRef = useRef("");
const handlePersistenceWriteError = useCallback(({ key }) => {
const failureKey = String(key || "browser-storage");
if (lastPersistenceFailureKeyRef.current === failureKey) {
return;
}
lastPersistenceFailureKeyRef.current = failureKey;
setPersistenceStatus("Browser storage could not save CAD Explorer state.");
}, []);
const entryMap = useMemo(() => {
const map = new Map();
for (const entry of catalogEntries) {
map.set(fileKey(entry), entry);
}
return map;
}, [catalogEntries]);
const {
meshState,
setMeshState,
meshLoadInProgress,
meshLoadTargetFile,
status,
setStatus,
error,
setError,
dxfState,
setDxfState,
dxfStatus,
setDxfStatus,
dxfError,
setDxfError,
urdfState,
setUrdfState,
urdfStatus,
setUrdfStatus,
urdfError,
setUrdfError,
referenceState,
setReferenceState,
setReferenceStatus,
setReferenceError,
getCachedMeshState,
getCachedReferenceState,
getCachedDxfState,
getCachedUrdfState,
cancelMeshLoad,
cancelDxfLoad,
cancelUrdfLoad,
cancelReferenceLoad,
loadMeshForEntry,
loadDxfForEntry,
loadUrdfForEntry,
loadReferencesForEntry
} = useCadAssets({
entryHasMesh,
entryHasReferences,
entryHasDxf,
buildNormalizedReferenceState,
});
const filteredEntries = useMemo(() => {
const q = query.trim().toLowerCase();
if (!q) {
return catalogEntries;
}
return catalogEntries.filter((entry) => {
return (
sidebarLabelForEntry(entry).toLowerCase().includes(q) ||
String(entry.name || "").toLowerCase().includes(q) ||
entry.kind.toLowerCase().includes(q) ||
fileKey(entry).toLowerCase().includes(q) ||
String(entry.source?.path || entry.step?.path || "").toLowerCase().includes(q)
);
});
}, [catalogEntries, query]);
const allEntriesTree = useMemo(
() => buildSidebarDirectoryTree(catalogEntries, { rootName: catalogRootName }),
[catalogEntries, catalogRootName]
);
const filteredEntriesTree = useMemo(
() => buildSidebarDirectoryTree(filteredEntries, { rootName: catalogRootName }),
[filteredEntries, catalogRootName]
);
const allDirectoryIds = useMemo(() => collectSidebarDirectoryIds(allEntriesTree), [allEntriesTree]);
const selectedEntry = entryMap.get(selectedKey) ?? null;
const selectedEntrySourceFormat = entrySourceFormat(selectedEntry);
const selectedFileSheetKind = fileSheetKindForEntry(selectedEntry);
const isAssemblyView = selectedEntry?.kind === "assembly";
const isUrdfView = selectedEntry?.kind === "urdf";
const selectedEntryHasMesh = entryHasMesh(selectedEntry);
const selectedEntryHasUrdf = entryHasUrdf(selectedEntry);
const selectedEntryHasReferences = entryHasReferences(selectedEntry);
const selectedEntryHasDxf = entryHasDxf(selectedEntry);
const selectedMeshHash = String(
selectedEntry?.kind === "assembly"
? [entryAssetHash(selectedEntry, "topology"), entryAssetHash(selectedEntry, "glb")].filter(Boolean).join(":")
: entryAssetHash(selectedEntry, meshAssetKeyForEntry(selectedEntry))
);
const selectedMeshMatches =
!!meshState &&
!!selectedEntry &&
meshState.file === fileKey(selectedEntry) &&
meshState.meshHash === selectedMeshHash;
const selectedAssemblyStructureReady =
selectedEntry?.kind === "assembly" &&
selectedMeshMatches &&
!!meshState?.assemblyStructureReady;
const selectedAssemblyInteractionReady =
selectedEntry?.kind === "assembly" &&
selectedMeshMatches &&
!!meshState?.assemblyInteractionReady;
const selectedAssemblyHydrationFailed =
selectedEntry?.kind === "assembly" &&
selectedMeshMatches &&
!!meshState?.assemblyBackgroundError;
const selectedDxfMatches =
!!dxfState &&
!!selectedEntry &&
dxfState.file === fileKey(selectedEntry) &&
dxfState.dxfHash === entryAssetHash(selectedEntry, "dxf");
const selectedUrdfMatches =
!!urdfState &&
!!selectedEntry &&
urdfState.file === fileKey(selectedEntry) &&
urdfState.urdfHash === entryAssetHash(selectedEntry, "urdf");
const selectedUrdfData = selectedUrdfMatches ? urdfState.urdfData : null;
const selectedUrdfMeshes = selectedUrdfMatches ? urdfState.meshesByUrl : null;
const selectedDxfData = selectedDxfMatches ? dxfState.dxfData : null;
const selectedDxfFileRef = selectedEntrySourceFormat === RENDER_FORMAT.DXF
? fileKey(selectedEntry)
: "";
const selectedUrdfFileRef = selectedEntrySourceFormat === RENDER_FORMAT.URDF
? fileKey(selectedEntry)
: "";
const defaultSelectedUrdfJointValues = useMemo(
() => buildDefaultUrdfJointValues(selectedUrdfData),
[selectedUrdfData]
);
const storedSelectedUrdfJointValues = useMemo(() => {
if (!selectedUrdfFileRef) {
return {};
}
const storedValues = jointValuesByFileRef?.[selectedUrdfFileRef];
return storedValues && typeof storedValues === "object" ? storedValues : {};
}, [jointValuesByFileRef, selectedUrdfFileRef]);
const selectedUrdfJointValues = useMemo(
() => ({ ...defaultSelectedUrdfJointValues, ...storedSelectedUrdfJointValues }),
[defaultSelectedUrdfJointValues, storedSelectedUrdfJointValues]
);
const selectedUrdfAnimationSettings = useMemo(() => {
return {
introEnabled: normalizeUrdfIntroAnimationEnabled(urdfEntryAnimationEnabled),
speed: selectedUrdfFileRef
? normalizeUrdfAnimationSpeed(urdfAnimationSettingsByFileRef?.[selectedUrdfFileRef]?.speed)
: DEFAULT_URDF_ANIMATION_SETTINGS.speed
};
}, [selectedUrdfFileRef, urdfAnimationSettingsByFileRef, urdfEntryAnimationEnabled]);
const selectedUrdfPoses = useMemo(
() => (
Array.isArray(selectedUrdfData?.poses)
? selectedUrdfData.poses.map((pose) => ({
...pose,
resolvedJointValues: {
...defaultSelectedUrdfJointValues,
...(pose?.jointValuesByName && typeof pose.jointValuesByName === "object" ? pose.jointValuesByName : {})
}
}))
: []
),
[defaultSelectedUrdfJointValues, selectedUrdfData]
);
const activeSelectedUrdfPoseName = useMemo(
() => (
selectedUrdfPoses.find((pose) => jointValueMapsClose(selectedUrdfJointValues, pose.resolvedJointValues))?.name || ""
),
[selectedUrdfJointValues, selectedUrdfPoses]
);
const selectedUrdfAnimationKey = selectedUrdfFileRef
? `${selectedUrdfFileRef}:${entryAssetHash(selectedEntry, "urdf") || ""}`
: "";
const selectedUrdfIntroOrderByLink = useMemo(
() => buildUrdfLinkIntroOrderMap(selectedUrdfData),
[selectedUrdfData]
);
const selectedUrdfWrappedJointNames = useMemo(
() => buildUrdfWrappedJointNameSet(selectedUrdfData),
[selectedUrdfData]
);
const animatedSelectedUrdfJointValues = useAnimatedUrdfJointValues(
selectedUrdfJointValues,
selectedUrdfAnimationKey,
selectedUrdfAnimationSettings,
selectedUrdfWrappedJointNames
);
const selectedUrdfMeshGeometryResult = useMemo(() => {
if (!selectedUrdfData || !selectedUrdfMeshes) {
return {
meshData: null,
error: ""
};
}
try {
return {
meshData: buildUrdfMeshGeometry(selectedUrdfData, selectedUrdfMeshes),
error: ""
};
} catch (error) {
return {
meshData: null,
error: error instanceof Error ? error.message : String(error)
};
}
}, [selectedUrdfData, selectedUrdfMeshes]);
const movableUrdfJoints = useMemo(
() => (
Array.isArray(selectedUrdfData?.joints)
? selectedUrdfData.joints.filter((joint) => String(joint?.type || "") !== "fixed" && !joint?.mimic)
: []
),
[selectedUrdfData]
);
const selectedUrdfPreview = useMemo(() => {
if (!selectedUrdfData || !selectedUrdfMeshGeometryResult.meshData) {
return {
meshData: null,
error: selectedUrdfMeshGeometryResult.error,
linkWorldTransforms: new Map()
};
}
try {
const posedPreview = poseUrdfMeshData(
selectedUrdfData,
selectedUrdfMeshGeometryResult.meshData,
animatedSelectedUrdfJointValues
);
selectedUrdfMeshGeometryResult.meshData.parts = posedPreview.meshData.parts.map((part) => ({
...part,
introOrder: selectedUrdfIntroOrderByLink.get(String(part?.linkName || "").trim()) ?? 0
}));
selectedUrdfMeshGeometryResult.meshData.bounds = posedPreview.meshData.bounds;
return {
...posedPreview,
meshData: selectedUrdfMeshGeometryResult.meshData,
error: ""
};
} catch (error) {
return {
meshData: null,
error: error instanceof Error ? error.message : String(error),
linkWorldTransforms: new Map()
};
}
}, [animatedSelectedUrdfJointValues, selectedUrdfData, selectedUrdfIntroOrderByLink, selectedUrdfMeshGeometryResult]);
const selectedMeshData = selectedEntrySourceFormat === RENDER_FORMAT.URDF
? selectedUrdfPreview.meshData
: selectedMeshMatches
? meshState.meshData
: null;
const assemblyRoot = selectedAssemblyStructureReady
? selectedMeshData?.assemblyRoot || null
: null;
const assemblyLeafParts = useMemo(() => {
return Array.isArray(selectedMeshData?.parts) ? selectedMeshData.parts : flattenAssemblyLeafParts(assemblyRoot);
}, [assemblyRoot, selectedMeshData?.parts]);
const assemblyNodes = useMemo(() => flattenAssemblyNodes(assemblyRoot), [assemblyRoot]);
const assemblyCurrentNodeId = expandedAssemblyPartIds[expandedAssemblyPartIds.length - 1] || "root";
const assemblyCurrentNode = useMemo(
() => findAssemblyNode(assemblyRoot, assemblyCurrentNodeId) || assemblyRoot,
[assemblyCurrentNodeId, assemblyRoot]
);
const assemblyBreadcrumbNodes = useMemo(
() => assemblyBreadcrumb(assemblyRoot, assemblyCurrentNodeId),
[assemblyCurrentNodeId, assemblyRoot]
);
const assemblyParts = useMemo(() => {
return String(assemblyCurrentNode?.nodeType || "").trim() === "assembly"
? (Array.isArray(assemblyCurrentNode?.children) ? assemblyCurrentNode.children : []).map((node) => ({
...node,
leafPartIds: descendantLeafPartIds(node)
}))
: [];
}, [assemblyCurrentNode]);
const assemblyPickPartIdMap = useMemo(() => {
return buildAssemblyLeafToNodePickMap(assemblyParts);
}, [assemblyParts]);
const assemblyPartsLoaded = selectedAssemblyStructureReady;
const supportsPartSelection = isAssemblyView && assemblyPartsLoaded && assemblyLeafParts.length > 0;
const assemblyPartMap = useMemo(() => {
const map = new Map();
for (const node of assemblyNodes) {
map.set(node.id, node);
}
for (const part of assemblyLeafParts) {
map.set(part.id, part);
}
return map;
}, [assemblyLeafParts, assemblyNodes]);
const validAssemblySelectionIds = useMemo(
() => assemblyNodes.map((node) => String(node?.id || "").trim()).filter(Boolean),
[assemblyNodes]
);
const validAssemblyLeafIds = useMemo(
() => assemblyLeafParts.map((part) => String(part?.id || "").trim()).filter(Boolean),
[assemblyLeafParts]
);
const validAssemblyLeafIdSet = useMemo(
() => new Set(validAssemblyLeafIds),
[validAssemblyLeafIds]
);
const renderPartIdsForAssemblySelection = useCallback((partId, fallbackPartId = "") => {
return leafPartIdsForAssemblySelection(partId, {
assemblyPartMap,
fallbackPartId,
validLeafPartIds: validAssemblyLeafIdSet
});
}, [assemblyPartMap, validAssemblyLeafIdSet]);
const renderPartIdForAssemblySelection = useCallback((partId, fallbackPartId = "") => {
return renderPartIdsForAssemblySelection(partId, fallbackPartId)[0] || "";
}, [renderPartIdsForAssemblySelection]);
const selectedUrdfPreviewError = selectedUrdfPreview.error;
const selectedDxfBendLines = useMemo(() => {
if (!selectedDxfData) {
return [];
}
try {
return extractOrderedDxfBendLines(selectedDxfData);
} catch {
return [];
}
}, [selectedDxfData]);
const defaultSelectedDxfBendSettings = useMemo(() => {
if (!selectedDxfData) {
return [];
}
try {
return normalizeDxfBendSettings(selectedDxfData, null);
} catch {
return [];
}
}, [selectedDxfData]);
const normalizedSelectedDxfBendSettings = useMemo(() => {
if (!selectedDxfData) {
return [];
}
try {
return normalizeDxfBendSettings(selectedDxfData, dxfBendSettings);
} catch {
return [];
}
}, [dxfBendSettings, selectedDxfData]);
const effectiveDxfThicknessMm = useMemo(() => {
return normalizeDxfPreviewThicknessMm(
dxfThicknessMm,
toFiniteNumber(selectedDxfData?.defaultThicknessMm, DEFAULT_DXF_PREVIEW_THICKNESS_MM)
);
}, [dxfThicknessMm, selectedDxfData]);
const selectedDxfPreview = useMemo(() => {
if (!selectedDxfData) {
return {
meshData: null,
error: ""
};
}
try {
return {
meshData: buildDxfPreviewMeshData(selectedDxfData, effectiveDxfThicknessMm, normalizedSelectedDxfBendSettings),
error: ""
};
} catch (error) {
return {
meshData: null,
error: error instanceof Error ? error.message : String(error)
};
}
}, [effectiveDxfThicknessMm, normalizedSelectedDxfBendSettings, selectedDxfData]);
const selectedDxfMeshData = selectedDxfPreview.meshData;
const selectedDxfPreviewError = selectedDxfPreview.error;
const selectedDxfPreviewKey = useMemo(() => {
const baseKey = buildDxfCacheKey(selectedEntry);
if (!baseKey || !selectedDxfData) {
return baseKey;
}
const bendsKey = normalizedSelectedDxfBendSettings
.map((setting) => `${normalizeDxfBendDirection(setting?.direction)}:${normalizeDxfBendAngleDeg(setting?.angleDeg).toFixed(1)}`)
.join("|");
return `${baseKey}:t=${effectiveDxfThicknessMm.toFixed(2)}:b=${bendsKey}`;
}, [
effectiveDxfThicknessMm,
normalizedSelectedDxfBendSettings,
selectedDxfData,
selectedEntry
]);
const effectiveRenderFormat = selectedEntrySourceFormat;
const dxfViewerLoading =
!!selectedEntry &&
dxfStatus !== ASSET_STATUS.ERROR &&
(!selectedDxfMatches || dxfStatus === ASSET_STATUS.LOADING);
const urdfViewerLoading =
!!selectedEntry &&
urdfStatus !== ASSET_STATUS.ERROR &&
(!selectedUrdfMatches || urdfStatus === ASSET_STATUS.LOADING);
const stepViewerLoading =
!!selectedEntry &&
status !== ASSET_STATUS.ERROR &&
(!selectedMeshMatches || status === ASSET_STATUS.LOADING);
const viewerLoading = effectiveRenderFormat === RENDER_FORMAT.DXF
? dxfViewerLoading
: effectiveRenderFormat === RENDER_FORMAT.URDF
? urdfViewerLoading
: stepViewerLoading;
const assemblySidebarLoading =
isAssemblyView &&
selectedMeshMatches &&
!assemblyPartsLoaded &&
!selectedAssemblyHydrationFailed;
const viewerLoadingLabel = effectiveRenderFormat === RENDER_FORMAT.DXF
? selectedEntry && !selectedEntryHasDxf
? "Generating DXF preview..."
: "Loading DXF preview..."
: effectiveRenderFormat === RENDER_FORMAT.URDF
? "Loading URDF robot..."
: effectiveRenderFormat === RENDER_FORMAT.STL
? "Loading STL..."
: stepUpdateInProgress
? "STEP changed. Updating/regenerating CAD..."
: selectedEntry && !selectedEntryHasMesh
? "Generating CAD assets..."
: "Loading CAD...";
const viewerAlert = useMemo(() => {
if (!selectedEntry || viewerLoading) {
return null;
}
if (effectiveRenderFormat === RENDER_FORMAT.DXF) {
return buildViewerDxfAlert(
fileKey(selectedEntry),
!!selectedDxfData,
dxfStatus === ASSET_STATUS.ERROR ? dxfError : "",
selectedDxfPreviewError
);
}
if (effectiveRenderFormat === RENDER_FORMAT.URDF) {
return buildViewerMeshAlert(
selectedEntry,
!!selectedMeshData,
urdfStatus === ASSET_STATUS.ERROR ? urdfError : selectedUrdfPreviewError
) || viewerRuntimeAlert;
}
const meshAlert = buildViewerMeshAlert(
selectedEntry,
!!selectedMeshData,
status === ASSET_STATUS.ERROR ? error : ""
);
return meshAlert || viewerRuntimeAlert;
}, [
dxfError,
selectedDxfPreviewError,
dxfStatus,
effectiveRenderFormat,
error,
selectedDxfData,
selectedEntry,
selectedMeshData,
selectedUrdfPreviewError,
status,
urdfError,
urdfStatus,
viewerLoading,
viewerRuntimeAlert
]);
const viewerAlertKey = viewerAlert
? [
fileKey(selectedEntry),
viewerAlert.severity,
viewerAlert.summary,
viewerAlert.title
].join(":")
: "";
useEffect(() => {
if (selectedEntrySourceFormat !== RENDER_FORMAT.DXF || !selectedDxfData || dxfThicknessMm > 0) {
return;
}
setDxfThicknessMm(normalizeDxfPreviewThicknessMm(
selectedDxfData.defaultThicknessMm,
DEFAULT_DXF_PREVIEW_THICKNESS_MM
));
}, [dxfThicknessMm, selectedDxfData, selectedEntrySourceFormat]);
useEffect(() => {
if (!selectedDxfFileRef || !selectedDxfData) {
setDxfBendSettingsLoadedFileRef("");
setDxfBendSettings([]);
return;
}
const storedSettings = readDxfBendOverridesForEntry(selectedDxfFileRef);
setDxfBendSettings(normalizeDxfBendSettings(selectedDxfData, storedSettings?.bends));
setDxfBendSettingsLoadedFileRef(selectedDxfFileRef);
}, [selectedDxfData, selectedDxfFileRef]);
useEffect(() => {
if (
!selectedDxfFileRef ||
!selectedDxfData ||
dxfBendSettingsLoadedFileRef !== selectedDxfFileRef
) {
return;
}
writeDxfBendOverridesForEntry(
selectedDxfFileRef,
dxfBendSettingsEqual(normalizedSelectedDxfBendSettings, defaultSelectedDxfBendSettings)
? null
: { bends: normalizedSelectedDxfBendSettings },
{ onWriteError: handlePersistenceWriteError }
);
}, [
defaultSelectedDxfBendSettings,
dxfBendSettingsLoadedFileRef,
handlePersistenceWriteError,
normalizedSelectedDxfBendSettings,
selectedDxfData,
selectedDxfFileRef
]);
const viewerInAssemblyMode =
isAssemblyView &&
String(assemblyCurrentNode?.nodeType || "assembly").trim() === "assembly";
const viewerMode = viewerInAssemblyMode ? "assembly" : "part";
const drawModeActive = selectedEntrySourceFormat === RENDER_FORMAT.STEP && tabToolMode === TAB_TOOL_MODE.DRAW;
const selectionCountBase = selectedPartIds.length + selectedReferenceIds.length;
const selectedReferenceIdsRef = useRef(selectedReferenceIds);
const selectedPartIdsRef = useRef(selectedPartIds);
const selectedEntryBuildSnapshotRef = useRef({
fileRef: "",
stepHash: ""
});
const drawingStrokesRef = useRef(drawingStrokes);
const drawingUndoStackRef = useRef(drawingUndoStack);
const drawingRedoStackRef = useRef(drawingRedoStack);
const viewportReadyRef = useRef(false);
const viewerRef = useRef(null);
const previewUiStateRef = useRef(null);
const panelResizeStateRef = useRef(null);
const openTabsRef = useRef(openTabs);
const activePerspectiveRef = useRef(null);
const cadWorkspaceSessionPersistTimeoutRef = useRef(0);
const buildPersistedCadWorkspaceSessionRef = useRef(null);
const tabToolsResizeStateRef = useRef(null);
const selectedFileSheetKeyRef = useRef("");
const restoredCadWorkspaceSessionRef = useRef(false);
const cadWorkspaceSessionBootstrappedRef = useRef(false);
const cadWorkspaceSessionPersistenceReadyRef = useRef(false);
useEffect(() => {
openTabsRef.current = openTabs;
}, [openTabs]);
const updateLookSettings = useCallback((updater) => {
setLookSettings((current) => {
const next = typeof updater === "function" ? updater(current) : updater;
return normalizeLookSettings(next);
});
}, []);
const handleResetLookSettings = useCallback(() => {
setLookSettings(cloneLookSettings(DEFAULT_LOOK_SETTINGS));
setCadWorkspaceGlassTone(CAD_WORKSPACE_DEFAULT_GLASS_TONE);
}, []);
const handleViewerAlertChange = useCallback((nextAlert) => {
setViewerRuntimeAlert(nextAlert || null);
}, []);
const endPanelResize = useCallback(() => {
document.querySelector("[data-slot='sidebar-wrapper']")?.removeAttribute("data-sidebar-resizing");
panelResizeStateRef.current = null;
if (!tabToolsResizeStateRef.current) {
document.body.style.cursor = "";
document.body.style.userSelect = "";
}
}, []);
const endTabToolsResize = useCallback(() => {
tabToolsResizeStateRef.current = null;
if (!panelResizeStateRef.current) {
document.body.style.cursor = "";
document.body.style.userSelect = "";
}
}, []);
const handleStartSidebarResize = useCallback((event) => {
if (event.button !== 0) {
return;
}
if (!isDesktop || !sidebarOpen) {
return;
}
event.preventDefault();
const nextWidth = sidebarWidth;
document.querySelector("[data-slot='sidebar-wrapper']")?.setAttribute("data-sidebar-resizing", "true");
panelResizeStateRef.current = {
startX: event.clientX,
startWidth: nextWidth,
latestWidth: nextWidth,
animationFrame: 0
};
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
}, [isDesktop, sidebarOpen, sidebarWidth]);
const handleSidebarOpenChange = useCallback((value) => {
setSidebarOpen((current) => {
const nextOpen = typeof value === "function" ? value(current) : value;
if (!current && nextOpen) {
setSidebarWidth(DEFAULT_SIDEBAR_WIDTH);
}
return nextOpen;
});
}, []);
const resetSelectionForStepUpdate = useCallback(() => {
selectedPartIdsRef.current = [];
selectedReferenceIdsRef.current = [];
setSelectedPartIds([]);
setSelectedReferenceIds([]);
setSelectedRenderPartIdByAssemblyPartId({});
setSelectedWholeEntryCadRefToken("");
setHoveredListReferenceId("");
setHoveredModelReferenceId("");
setHoveredListPartId("");
setHoveredModelPartId("");
setCopyStatus("");
}, []);
const upsertTabRecord = useCallback((tabs, key, snapshot = null) => {
if (!key) {
return tabs;
}
const normalizedSnapshot = snapshot ? cloneTabSnapshot(snapshot) : null;
const index = tabs.findIndex((tab) => tab.key === key);
if (index === -1) {
if (!normalizedSnapshot) {
return [...tabs, createTabRecord(key)];
}
return [...tabs, createTabRecord(key, normalizedSnapshot)];
}
if (!normalizedSnapshot) {
return tabs;
}
const current = tabs[index];
if (tabSnapshotEqual(current, normalizedSnapshot)) {
return tabs;
}
const next = [...tabs];
next[index] = {
key,
...normalizedSnapshot
};
return next;
}, []);
const buildActiveTabSnapshot = useCallback(() => {
return cloneTabSnapshot({
dxfThicknessMm,
referenceQuery,
selectedReferenceIds,
selectedPartIds,
expandedAssemblyPartIds,
hiddenPartIds,
perspective: activePerspectiveRef.current,
drawingTool,
tabToolsOpen,
tabToolMode,
drawingStrokes,
drawingUndoStack,
drawingRedoStack
});
}, [
dxfThicknessMm,
drawingTool,
drawingRedoStack,
drawingStrokes,
drawingUndoStack,
expandedAssemblyPartIds,
hiddenPartIds,
referenceQuery,
selectedPartIds,
selectedReferenceIds,
tabToolMode,
tabToolsOpen
]);
const buildPersistedCadWorkspaceSession = useCallback(() => {
const nextOpenTabs = selectedKey
? upsertTabRecord(openTabs, selectedKey, buildActiveTabSnapshot())
: openTabs;
return buildCadWorkspaceSessionState({
openTabs: nextOpenTabs,
selectedKey,
query,
expandedDirectoryIds: Array.from(expandedDirectoryIds),
sidebarOpen,
sidebarWidth,
tabToolsWidth,
urdfEntryAnimationEnabled
});
}, [
buildActiveTabSnapshot,
expandedDirectoryIds,
openTabs,
query,
selectedKey,
sidebarOpen,
sidebarWidth,
tabToolsWidth,
urdfEntryAnimationEnabled,
upsertTabRecord
]);
useEffect(() => {
buildPersistedCadWorkspaceSessionRef.current = buildPersistedCadWorkspaceSession;
}, [buildPersistedCadWorkspaceSession]);
const flushCadWorkspaceSessionPersistence = useCallback(() => {
if (!cadWorkspaceSessionPersistenceReadyRef.current) {
return;
}
const buildSnapshot = buildPersistedCadWorkspaceSessionRef.current;
if (!buildSnapshot) {
return;
}
writeCadWorkspaceSessionState(buildSnapshot(), { onWriteError: handlePersistenceWriteError });
}, [handlePersistenceWriteError]);
const scheduleCadWorkspaceSessionPersistence = useCallback(() => {
if (!cadWorkspaceSessionPersistenceReadyRef.current) {
return;
}
if (cadWorkspaceSessionPersistTimeoutRef.current) {
return;
}
cadWorkspaceSessionPersistTimeoutRef.current = window.setTimeout(() => {
cadWorkspaceSessionPersistTimeoutRef.current = 0;
flushCadWorkspaceSessionPersistence();
}, CAD_WORKSPACE_SESSION_PERSIST_DELAY_MS);
}, [flushCadWorkspaceSessionPersistence]);
const handleDxfBendSettingChange = useCallback((bendIndex, patch) => {
setDxfBendSettings((current) => {
if (!selectedDxfData) {
return current;
}
const next = normalizeDxfBendSettings(selectedDxfData, current).map((setting) => ({ ...setting }));
if (bendIndex < 0 || bendIndex >= next.length) {
return next;
}
if (Object.prototype.hasOwnProperty.call(patch || {}, "direction")) {
next[bendIndex].direction = normalizeDxfBendDirection(patch.direction);
}
if (Object.prototype.hasOwnProperty.call(patch || {}, "angleDeg")) {
next[bendIndex].angleDeg = normalizeDxfBendAngleDeg(patch.angleDeg);
}
return next;
});
}, [selectedDxfData]);
const applyTabRecord = useCallback((tabRecord) => {
const nextTab = createTabRecord(tabRecord?.key || "", tabRecord || {});
const nextPerspective = clonePerspectiveSnapshot(nextTab.perspective);
setDxfThicknessMm(nextTab.dxfThicknessMm);
setReferenceQuery(nextTab.referenceQuery);
selectedReferenceIdsRef.current = nextTab.selectedReferenceIds;
setSelectedReferenceIds(nextTab.selectedReferenceIds);
selectedPartIdsRef.current = nextTab.selectedPartIds;
setSelectedPartIds(nextTab.selectedPartIds);
setSelectedRenderPartIdByAssemblyPartId({});
setSelectedWholeEntryCadRefToken("");
setExpandedAssemblyPartIds(nextTab.expandedAssemblyPartIds);
setHiddenPartIds(nextTab.hiddenPartIds);
setHoveredListReferenceId("");
setHoveredModelReferenceId("");
setHoveredListPartId("");
setHoveredModelPartId("");
setCopyStatus("");
setScreenshotStatus("");
setTabToolsOpen(nextTab.tabToolsOpen);
setTabToolMode(nextTab.tabToolMode);
setDrawingTool(nextTab.drawingTool);
activePerspectiveRef.current = nextPerspective;
setViewerPerspective(nextPerspective);
setDrawingStrokes(nextTab.drawingStrokes);
setDrawingUndoStack(nextTab.drawingUndoStack);
setDrawingRedoStack(nextTab.drawingRedoStack);
setSelectedKey(nextTab.key);
}, []);
const resetActiveWorkspace = useCallback(() => {
selectedReferenceIdsRef.current = [];
selectedPartIdsRef.current = [];
setSelectedWholeEntryCadRefToken("");
setDxfBendSettingsLoadedFileRef("");
setDxfThicknessMm(0);
setDxfBendSettings([]);
setReferenceQuery("");
setSelectedReferenceIds([]);
setSelectedPartIds([]);
setSelectedRenderPartIdByAssemblyPartId({});
setExpandedAssemblyPartIds([]);
setHiddenPartIds([]);
setHoveredListReferenceId("");
setHoveredModelReferenceId("");
setHoveredListPartId("");
setHoveredModelPartId("");
setCopyStatus("");
setScreenshotStatus("");
setTabToolsOpen(false);
setTabToolMode(TAB_TOOL_MODE.REFERENCES);
setDrawingTool(DRAWING_TOOL.FREEHAND);
activePerspectiveRef.current = null;
setViewerPerspective(null);
setDrawingStrokes([]);
setDrawingUndoStack([]);
setDrawingRedoStack([]);
setSelectedKey("");
}, []);
const activateEntryTab = useCallback((key) => {
if (!key || !entryMap.has(key)) {
return;
}
if (key === selectedKey) {
return;
}
const nextTabs = openTabsRef.current;
const nextEntry = entryMap.get(key);
const nextTab = nextTabs.find((tab) => tab.key === key) || createTabRecord(key, {
drawingTool: selectedKey ? drawingTool : DRAWING_TOOL.FREEHAND,
tabToolsOpen: isDesktop && !!fileSheetKindForEntry(nextEntry),
tabToolMode: selectedKey ? tabToolMode : TAB_TOOL_MODE.REFERENCES
});
const cachedMeshState = nextEntry ? getCachedMeshState(nextEntry) : null;
const cachedReferenceState = nextEntry ? getCachedReferenceState(nextEntry) : null;
const cachedDxfState = nextEntry ? getCachedDxfState(nextEntry) : null;
const cachedUrdfState = nextEntry ? getCachedUrdfState(nextEntry) : null;
const currentSnapshot = selectedKey ? buildActiveTabSnapshot() : null;
setOpenTabs((current) => {
let next = current;
if (selectedKey) {
next = upsertTabRecord(next, selectedKey, currentSnapshot);
}
next = upsertTabRecord(next, key, nextTab);
return next;
});
if (!entryHasMesh(nextEntry)) {
setStatus(ASSET_STATUS.PENDING);
setError("");
} else if (cachedMeshState) {
setMeshState(cachedMeshState);
setStatus(ASSET_STATUS.READY);
setError("");
}
if (!entryHasReferences(nextEntry)) {
setReferenceState(null);
setReferenceStatus(REFERENCE_STATUS.DISABLED);
setReferenceError("");
} else if (cachedReferenceState) {
setReferenceState(cachedReferenceState);
setReferenceStatus(cachedReferenceState.disabledReason ? REFERENCE_STATUS.DISABLED : REFERENCE_STATUS.READY);
setReferenceError(cachedReferenceState.disabledReason || "");
}
if (!entryHasDxf(nextEntry)) {
setDxfState(null);
setDxfStatus(ASSET_STATUS.PENDING);
setDxfError("");
} else if (cachedDxfState) {
setDxfState(cachedDxfState);
setDxfStatus(ASSET_STATUS.READY);
setDxfError("");
}
if (!entryHasUrdf(nextEntry)) {
setUrdfState(null);
setUrdfStatus(ASSET_STATUS.PENDING);
setUrdfError("");
} else if (cachedUrdfState) {
setUrdfState(cachedUrdfState);
setUrdfStatus(ASSET_STATUS.READY);
setUrdfError("");
}
applyTabRecord(nextTab);
}, [
applyTabRecord,
buildActiveTabSnapshot,
drawingTool,
entryMap,
getCachedDxfState,
getCachedMeshState,
getCachedReferenceState,
getCachedUrdfState,
isDesktop,
selectedKey,
setDxfError,
setDxfState,
setDxfStatus,
setUrdfError,
setUrdfState,
setUrdfStatus,
tabToolMode,
tabToolsOpen,
upsertTabRecord
]);
useCadWorkspaceSession({
manifestEntries,
fileKey,
readCadWorkspaceSessionState,
restoredCadWorkspaceSessionRef,
cadWorkspaceSessionBootstrappedRef,
cadWorkspaceSessionPersistenceReadyRef,
setQuery,
setExpandedDirectoryIds,
setSidebarOpen,
setSidebarWidth,
setTabToolsWidth,
setUrdfEntryAnimationEnabled,
setOpenTabs,
applyTabRecord,
selectedEntryKeyFromUrl,
createTabRecord,
initialSelectedTabSnapshot: {
drawingTool: DRAWING_TOOL.FREEHAND,
tabToolsOpen: false,
tabToolMode: TAB_TOOL_MODE.REFERENCES
},
upsertTabRecord,
buildPersistedCadWorkspaceSession,
scheduleCadWorkspaceSessionPersistence,
flushCadWorkspaceSessionPersistence,
selectedEntry,
defaultDocumentTitle: DEFAULT_DOCUMENT_TITLE,
selectedKey,
entryMap,
buildActiveTabSnapshot,
catalogEntries,
manifestRevision,
defaultSidebarWidth: DEFAULT_SIDEBAR_WIDTH,
sidebarMinWidth: DESKTOP_SIDEBAR_MIN_WIDTH,
readCadParam,
readCadRefQueryParams,
setPendingCadRefQueryParams,
activateEntryTab,
resetActiveWorkspace,
writeCadParam
});
useEffect(() => {
applyThemeToDocument(DARK_THEME_ID, document.documentElement);
writeThemePreference(DARK_THEME_ID, { onWriteError: handlePersistenceWriteError });
}, [handlePersistenceWriteError]);
useEffect(() => {
writeLookSettings(lookSettings, { onWriteError: handlePersistenceWriteError });
}, [handlePersistenceWriteError, lookSettings]);
useEffect(() => {
const normalizedTone = normalizeCadWorkspaceGlassTone(cadWorkspaceGlassTone);
document.documentElement.dataset.glassTone = normalizedTone;
writeCadWorkspaceGlassTone(normalizedTone, { onWriteError: handlePersistenceWriteError });
return () => {
delete document.documentElement.dataset.glassTone;
};
}, [handlePersistenceWriteError, cadWorkspaceGlassTone]);
useEffect(() => {
const handleStorage = (event) => {
if (event.key !== LOOK_SETTINGS_STORAGE_KEY) {
return;
}
if (!event.newValue) {
setLookSettings(cloneLookSettings(DEFAULT_LOOK_SETTINGS));
return;
}
try {
const parsed = JSON.parse(event.newValue);
setLookSettings(normalizeLookSettings(parsed));
} catch (error) {
console.warn("Failed to sync look settings from another tab", error);
}
};
window.addEventListener("storage", handleStorage);
return () => {
window.removeEventListener("storage", handleStorage);
};
}, []);
useEffect(() => {
if (lookMenuOpen) {
setTabToolsOpen(false);
}
}, [lookMenuOpen]);
useEffect(() => {
selectedReferenceIdsRef.current = selectedReferenceIds;
}, [selectedReferenceIds]);
useEffect(() => {
selectedPartIdsRef.current = selectedPartIds;
}, [selectedPartIds]);
useEffect(() => {
const nextFileSheetKey = selectedKey && selectedFileSheetKind
? `${selectedKey}:${selectedFileSheetKind}`
: "";
if (!nextFileSheetKey) {
selectedFileSheetKeyRef.current = "";
if (tabToolsOpen) {
setTabToolsOpen(false);
}
return;
}
if (selectedFileSheetKeyRef.current === nextFileSheetKey) {
return;
}
selectedFileSheetKeyRef.current = nextFileSheetKey;
setTabToolsOpen(isDesktop);
}, [isDesktop, selectedFileSheetKind, selectedKey, tabToolsOpen]);
useEffect(() => {
const fileRef = fileKey(selectedEntry);
const stepHash = String(selectedEntry?.step?.hash || entryAssetHash(selectedEntry, "topology") || "").trim();
if (!fileRef) {
selectedEntryBuildSnapshotRef.current = {
fileRef: "",
stepHash: ""
};
setStepUpdateInProgress(false);
return;
}
const previous = selectedEntryBuildSnapshotRef.current;
const sameEntry = previous.fileRef === fileRef;
const stepChanged = sameEntry && !!previous.stepHash && !!stepHash && previous.stepHash !== stepHash;
if (stepChanged) {
resetSelectionForStepUpdate();
setStepUpdateInProgress(true);
} else if (!sameEntry) {
setStepUpdateInProgress(false);
}
selectedEntryBuildSnapshotRef.current = {
fileRef,
stepHash
};
}, [
resetSelectionForStepUpdate,
selectedEntry
]);
useEffect(() => {
if (!stepUpdateInProgress) {
return;
}
if (!selectedEntry) {
setStepUpdateInProgress(false);
return;
}
if (selectedMeshMatches && status !== ASSET_STATUS.LOADING) {
setStepUpdateInProgress(false);
}
}, [selectedEntry, selectedMeshMatches, status, stepUpdateInProgress]);
useEffect(() => {
drawingStrokesRef.current = drawingStrokes;
}, [drawingStrokes]);
useEffect(() => {
drawingUndoStackRef.current = drawingUndoStack;
}, [drawingUndoStack]);
useEffect(() => {
drawingRedoStackRef.current = drawingRedoStack;
}, [drawingRedoStack]);
useEffect(() => {
if (effectiveRenderFormat !== RENDER_FORMAT.STEP || !selectedEntryHasReferences) {
return;
}
setTabToolMode((current) => {
if (current !== TAB_TOOL_MODE.DRAW) {
return current;
}
return drawingStrokesRef.current.length ? current : TAB_TOOL_MODE.REFERENCES;
});
}, [effectiveRenderFormat, selectedKey, selectedEntryHasReferences]);
useEffect(() => {
setViewerAlertOpen(false);
}, [viewerAlertKey]);
useEffect(() => {
setViewerRuntimeAlert(null);
}, [selectedKey]);
const clampSidebarWidth = useCallback((value) => {
const maxWidth = typeof window === "undefined"
? DESKTOP_SIDEBAR_MAX_WIDTH
: Math.min(DESKTOP_SIDEBAR_MAX_WIDTH, Math.floor(window.innerWidth * 0.42));
return clampPanelWidth(value, DESKTOP_SIDEBAR_MIN_WIDTH, maxWidth);
}, []);
const clampTabToolsWidth = useCallback((value) => {
const maxWidth = typeof window === "undefined"
? DESKTOP_TAB_TOOLS_MAX_WIDTH
: Math.min(DESKTOP_TAB_TOOLS_MAX_WIDTH, Math.floor(window.innerWidth * 0.42));
return clampPanelWidth(value, DESKTOP_TAB_TOOLS_MIN_WIDTH, maxWidth);
}, []);
useCadWorkspaceLayout({
restoredCadWorkspaceSessionRef,
viewportReadyRef,
hasSelectedEntry: Boolean(selectedKey),
isDesktop,
setIsDesktop,
setSidebarOpen,
setTabToolsOpen,
clampSidebarWidth,
clampTabToolsWidth,
setSidebarWidth,
setTabToolsWidth,
panelResizeStateRef,
tabToolsResizeStateRef,
defaultSidebarWidth: DEFAULT_SIDEBAR_WIDTH,
sidebarMinWidth: DESKTOP_SIDEBAR_MIN_WIDTH,
tabToolsMinWidth: DESKTOP_TAB_TOOLS_MIN_WIDTH,
endPanelResize,
endTabToolsResize
});
useEffect(() => {
if (selectedKey) {
setMobileSidebarOpen(false);
return undefined;
}
if (previewMode || typeof window === "undefined") {
return undefined;
}
const mediaQuery = window.matchMedia(MOBILE_FILE_EXPLORER_MEDIA_QUERY);
const openExplorerForEmptyMobileWorkspace = () => {
if (mediaQuery.matches) {
setMobileSidebarOpen(true);
}
};
openExplorerForEmptyMobileWorkspace();
mediaQuery.addEventListener("change", openExplorerForEmptyMobileWorkspace);
return () => {
mediaQuery.removeEventListener("change", openExplorerForEmptyMobileWorkspace);
};
}, [previewMode, selectedKey]);
useEffect(() => {
setCatalogEntries(manifestEntries);
}, [manifestEntries]);
useEffect(() => {
setOpenTabs((current) => {
const next = current.filter((tab) => entryMap.has(tab.key));
return next.length === current.length ? current : next;
});
}, [entryMap]);
useEffect(() => {
setExpandedDirectoryIds((current) => {
const next = new Set(current);
const knownDirectoryIds = new Set(allDirectoryIds);
let changed = false;
for (const directoryId of current) {
if (!knownDirectoryIds.has(directoryId)) {
next.delete(directoryId);
changed = true;
}
}
return changed ? next : current;
});
}, [allDirectoryIds]);
useEffect(() => {
const directoryId = sidebarDirectoryIdForEntry(selectedEntry);
if (!directoryId) {
return;
}
const ancestorIds = collectAncestorDirectoryIds(directoryId);
if (!ancestorIds.length) {
return;
}
setExpandedDirectoryIds((current) => {
let changed = false;
const next = new Set(current);
for (const directoryId of ancestorIds) {
if (!next.has(directoryId)) {
next.add(directoryId);
changed = true;
}
}
return changed ? next : current;
});
}, [selectedEntry]);
useEffect(() => {
if (!selectedEntry) {
cancelMeshLoad();
return;
}
if (![RENDER_FORMAT.STEP, RENDER_FORMAT.STL].includes(effectiveRenderFormat)) {
cancelMeshLoad();
return;
}
if (meshLoadInProgress && meshLoadTargetFile === fileKey(selectedEntry)) {
return;
}
if (
selectedMeshMatches &&
(
!isAssemblyView ||
selectedAssemblyInteractionReady ||
selectedAssemblyHydrationFailed
)
) {
return;
}
loadMeshForEntry(selectedEntry).catch((err) => {
setStatus(ASSET_STATUS.ERROR);
setError(err instanceof Error ? err.message : String(err));
});
}, [
cancelMeshLoad,
effectiveRenderFormat,
isAssemblyView,
loadMeshForEntry,
meshLoadInProgress,
meshLoadTargetFile,
selectedAssemblyHydrationFailed,
selectedAssemblyInteractionReady,
selectedEntry,
selectedMeshMatches
]);
useEffect(() => {
if (!selectedEntry) {
cancelDxfLoad();
return;
}
if (effectiveRenderFormat !== RENDER_FORMAT.DXF) {
cancelDxfLoad();
return;
}
if (!selectedEntryHasDxf) {
cancelDxfLoad();
setDxfState(null);
setDxfStatus(ASSET_STATUS.PENDING);
setDxfError("");
return;
}
if (selectedDxfMatches) {
return;
}
loadDxfForEntry(selectedEntry).catch((err) => {
setDxfStatus(ASSET_STATUS.ERROR);
setDxfError(err instanceof Error ? err.message : String(err));
});
}, [
cancelDxfLoad,
effectiveRenderFormat,
loadDxfForEntry,
selectedDxfMatches,
selectedEntry,
selectedEntryHasDxf,
setDxfError,
setDxfState,
setDxfStatus
]);
useEffect(() => {
if (!selectedEntry) {
cancelUrdfLoad();
return;
}
if (effectiveRenderFormat !== RENDER_FORMAT.URDF) {
cancelUrdfLoad();
return;
}
if (!selectedEntryHasUrdf) {
cancelUrdfLoad();
setUrdfState(null);
setUrdfStatus(ASSET_STATUS.PENDING);
setUrdfError("");
return;
}
if (selectedUrdfMatches) {
return;
}
loadUrdfForEntry(selectedEntry).catch((err) => {
setUrdfStatus(ASSET_STATUS.ERROR);
setUrdfError(err instanceof Error ? err.message : String(err));
});
}, [
cancelUrdfLoad,
effectiveRenderFormat,
loadUrdfForEntry,
selectedEntry,
selectedEntryHasUrdf,
selectedUrdfMatches,
setUrdfError,
setUrdfState,
setUrdfStatus
]);
const selectedReferencesMatch =
!!referenceState &&
!!selectedEntry &&
selectedEntryHasReferences &&
referenceState.fileRef === fileKey(selectedEntry) &&
referenceState.referenceHash === buildReferenceCacheKey(selectedEntry);
const selectedSelectorRuntime = selectedReferencesMatch ? referenceState?.selectorRuntime || null : null;
const referenceLoadingEnabled =
pendingCadRefQueryParams.length > 0 ||
(effectiveRenderFormat === RENDER_FORMAT.STEP && selectedEntryHasReferences && !isAssemblyView);
useEffect(() => {
if (!selectedEntry) {
cancelReferenceLoad();
return;
}
if (!selectedEntryHasReferences) {
cancelReferenceLoad();
setReferenceState(null);
setReferenceStatus(REFERENCE_STATUS.DISABLED);
setReferenceError("");
return;
}
if (!referenceLoadingEnabled) {
cancelReferenceLoad();
setReferenceState(null);
setReferenceStatus(REFERENCE_STATUS.IDLE);
setReferenceError("");
return;
}
if (selectedReferencesMatch) {
return;
}
loadReferencesForEntry(selectedEntry).catch((err) => {
setReferenceStatus(REFERENCE_STATUS.ERROR);
setReferenceError(err instanceof Error ? err.message : String(err));
});
}, [
cancelReferenceLoad,
isAssemblyView,
loadReferencesForEntry,
referenceLoadingEnabled,
selectedEntry,
selectedEntryHasReferences,
selectedReferencesMatch
]);
useEffect(() => {
if (effectiveRenderFormat !== RENDER_FORMAT.DXF || !previewMode) {
return;
}
previewUiStateRef.current = null;
setPreviewMode(false);
}, [effectiveRenderFormat, previewMode]);
const {
inspectedAssemblyPartId,
inspectedAssemblyPart,
isInspectingAssemblyPart,
activeReferenceMap,
inspectedAssemblyPartReferences,
hoveredReferenceId,
hoveredPartId,
visibleReferences,
filteredAssemblyParts
} = useCadWorkspaceSelectors({
selectedEntry,
selectedReferencesMatch,
referenceState,
isAssemblyView,
supportsPartSelection,
assemblyParts,
assemblyPartMap,
expandedAssemblyPartIds,
inspectedAssemblyPartTopologyReferences: inspectedAssemblyReferenceState?.references || [],
selectedReferenceIds,
selectedPartIds,
hoveredListReferenceId,
hoveredModelReferenceId,
hoveredListPartId,
hoveredModelPartId
});
useCadWorkspaceSelection({
isAssemblyView,
supportsPartSelection,
assemblyPartsLoaded,
selectedEntryHasReferences,
setSelectedReferenceIds,
selectedReferenceIdsRef,
setHoveredListReferenceId,
setHoveredModelReferenceId,
assemblyParts,
validAssemblyPartIds: validAssemblySelectionIds,
validHiddenPartIds: validAssemblyLeafIds,
selectedPartIdsRef,
setSelectedPartIds,
parseAssemblyPartReferenceSelectionId,
setExpandedAssemblyPartIds,
setHiddenPartIds,
setHoveredListPartId,
setHoveredModelPartId
});
const inspectedAssemblyPartEntry = useMemo(() => {
const partFileRef = resolveTopologyRelativeFile(
selectedEntry,
inspectedAssemblyPart?.sourcePath || inspectedAssemblyPart?.partSourcePath
);
return partFileRef ? entryMap.get(partFileRef) || null : null;
}, [entryMap, inspectedAssemblyPart?.partSourcePath, inspectedAssemblyPart?.sourcePath, selectedEntry]);
useEffect(() => {
let cancelled = false;
if (!isAssemblyView || !inspectedAssemblyPartId || !isInspectingAssemblyPart) {
setInspectedAssemblyReferenceState(null);
setInspectedAssemblyReferenceStatus(REFERENCE_STATUS.IDLE);
setInspectedAssemblyReferenceError("");
return () => {
cancelled = true;
};
}
if (!inspectedAssemblyPartEntry && String(inspectedAssemblyPart?.sourceKind || "") === "native" && entryHasReferences(selectedEntry)) {
const occurrenceId = String(inspectedAssemblyPart?.occurrenceId || inspectedAssemblyPart?.id || "").trim();
const cachedBundle = loadRenderSelectorBundle(
entryAssetUrl(selectedEntry, "topology"),
entryAssetUrl(selectedEntry, "topologyBinary")
);
setInspectedAssemblyReferenceStatus(REFERENCE_STATUS.LOADING);
setInspectedAssemblyReferenceError("");
cachedBundle.then((bundle) => {
if (cancelled) {
return;
}
const nextReferenceState = buildNormalizedReferenceState(selectedEntry, bundle, {
copyCadPath: cadPathForEntry(selectedEntry),
partId: inspectedAssemblyPart.id
});
const references = nextReferenceState.references
.filter((reference) => String(reference?.occurrenceId || "").trim() === occurrenceId)
.map((reference) => ({ ...reference, partId: inspectedAssemblyPart.id }));
setInspectedAssemblyReferenceState({
...nextReferenceState,
references
});
setInspectedAssemblyReferenceStatus(references.length ? REFERENCE_STATUS.READY : REFERENCE_STATUS.DISABLED);
setInspectedAssemblyReferenceError(references.length ? "" : "No topology references are available for this component");
}).catch((loadError) => {
if (cancelled) {
return;
}
setInspectedAssemblyReferenceState(null);
setInspectedAssemblyReferenceStatus(REFERENCE_STATUS.ERROR);
setInspectedAssemblyReferenceError(loadError instanceof Error ? loadError.message : String(loadError));
});
return () => {
cancelled = true;
};
}
if (!inspectedAssemblyPartEntry) {
setInspectedAssemblyReferenceState(null);
setInspectedAssemblyReferenceStatus(REFERENCE_STATUS.DISABLED);
setInspectedAssemblyReferenceError("");
return () => {
cancelled = true;
};
}
if (!entryHasReferences(inspectedAssemblyPartEntry)) {
setInspectedAssemblyReferenceState(null);
setInspectedAssemblyReferenceStatus(REFERENCE_STATUS.DISABLED);
setInspectedAssemblyReferenceError("");
return () => {
cancelled = true;
};
}
const transform = Array.isArray(inspectedAssemblyPart?.transform) && inspectedAssemblyPart.transform.length === 16
? inspectedAssemblyPart.transform.map((value) => Number(value))
: null;
const cachedBundle = loadRenderSelectorBundle(
entryAssetUrl(inspectedAssemblyPartEntry, "topology"),
entryAssetUrl(inspectedAssemblyPartEntry, "topologyBinary")
);
setInspectedAssemblyReferenceStatus(REFERENCE_STATUS.LOADING);
setInspectedAssemblyReferenceError("");
cachedBundle.then((bundle) => {
if (cancelled) {
return;
}
const nextReferenceState = buildNormalizedReferenceState(inspectedAssemblyPartEntry, bundle, {
copyCadPath: cadPathForEntry(selectedEntry) || cadPathForEntry(inspectedAssemblyPartEntry),
partId: inspectedAssemblyPart.id,
transform,
remapOccurrenceId: String(inspectedAssemblyPart?.occurrenceId || inspectedAssemblyPart?.id || "").trim()
});
setInspectedAssemblyReferenceState(nextReferenceState);
setInspectedAssemblyReferenceStatus(
nextReferenceState.disabledReason ? REFERENCE_STATUS.DISABLED : REFERENCE_STATUS.READY
);
setInspectedAssemblyReferenceError(nextReferenceState.disabledReason || "");
}).catch((loadError) => {
if (cancelled) {
return;
}
setInspectedAssemblyReferenceState(null);
setInspectedAssemblyReferenceStatus(REFERENCE_STATUS.ERROR);
setInspectedAssemblyReferenceError(loadError instanceof Error ? loadError.message : String(loadError));
});
return () => {
cancelled = true;
};
}, [
inspectedAssemblyPart,
inspectedAssemblyPartEntry,
inspectedAssemblyPartId,
isInspectingAssemblyPart,
isAssemblyView,
selectedEntry
]);
const isFaceReference = useCallback((reference) => (
String(reference?.selectorType || "").trim() === "face"
), []);
const isEdgeReference = useCallback((reference) => (
String(reference?.selectorType || "").trim() === "edge"
), []);
const isCornerReference = useCallback((reference) => (
String(reference?.selectorType || "").trim() === "vertex"
), []);
const referencePartId = useCallback((reference) => {
const explicitPartId = String(reference?.partId || "").trim();
if (explicitPartId) {
return explicitPartId;
}
return parseAssemblyPartReferenceSelectionId(reference?.id)?.partId || "";
}, []);
const effectiveInspectedAssemblyPartReferences = useMemo(() => {
if (!isAssemblyView || !inspectedAssemblyPartId) {
return inspectedAssemblyPartReferences;
}
const topologyReferences = (Array.isArray(visibleReferences) ? visibleReferences : [])
.filter((reference) => {
const partId = referencePartId(reference);
if (!partId || partId !== inspectedAssemblyPartId) {
return false;
}
return isFaceReference(reference) || isEdgeReference(reference) || isCornerReference(reference);
});
if (topologyReferences.length) {
return topologyReferences;
}
return inspectedAssemblyPartReferences;
}, [
inspectedAssemblyPartId,
inspectedAssemblyPartReferences,
isCornerReference,
isAssemblyView,
isEdgeReference,
isFaceReference,
referencePartId,
visibleReferences
]);
const effectiveVisibleReferences = useMemo(() => {
if (isAssemblyView && isInspectingAssemblyPart) {
return effectiveInspectedAssemblyPartReferences;
}
return visibleReferences;
}, [effectiveInspectedAssemblyPartReferences, isAssemblyView, isInspectingAssemblyPart, visibleReferences]);
const effectiveSelectorRuntime = useMemo(() => {
if (isAssemblyView && isInspectingAssemblyPart) {
return inspectedAssemblyReferenceState?.selectorRuntime || null;
}
return selectedSelectorRuntime;
}, [inspectedAssemblyReferenceState?.selectorRuntime, isAssemblyView, isInspectingAssemblyPart, selectedSelectorRuntime]);
const effectiveActiveReferenceMap = useMemo(() => {
const map = new Map(activeReferenceMap);
for (const reference of effectiveVisibleReferences) {
const referenceId = String(reference?.id || "").trim();
if (referenceId) {
map.set(referenceId, reference);
}
}
return map;
}, [activeReferenceMap, effectiveVisibleReferences]);
const viewerPickableReferences = useMemo(() => {
if (viewerInAssemblyMode) {
return [];
}
return effectiveVisibleReferences;
}, [effectiveVisibleReferences, viewerInAssemblyMode]);
const viewerPickableFaces = useMemo(
() => viewerPickableReferences.filter((reference) => isFaceReference(reference)),
[isFaceReference, viewerPickableReferences]
);
const viewerPickableEdges = useMemo(
() => viewerPickableReferences.filter((reference) => isEdgeReference(reference)),
[isEdgeReference, viewerPickableReferences]
);
const viewerPickableVertices = useMemo(
() => viewerPickableReferences.filter((reference) => isCornerReference(reference)),
[isCornerReference, viewerPickableReferences]
);
const viewerSelectedPartIds = useMemo(() => {
if (isAssemblyView && isInspectingAssemblyPart) {
return [];
}
if (!isAssemblyView) {
return selectedPartIds;
}
return uniqueStringList(
selectedPartIds.flatMap((id) => renderPartIdsForAssemblySelection(
id,
selectedRenderPartIdByAssemblyPartId[String(id || "").trim()]
))
);
}, [
isAssemblyView,
isInspectingAssemblyPart,
renderPartIdsForAssemblySelection,
selectedPartIds,
selectedRenderPartIdByAssemblyPartId
]);
const viewerHoveredPartIds = useMemo(() => {
if (!isAssemblyView || isInspectingAssemblyPart || !hoveredPartId) {
return hoveredPartId;
}
const normalizedHoveredPartId = String(hoveredPartId || "").trim();
const hoveredSelectionId = assemblyPickPartIdMap.get(normalizedHoveredPartId) || normalizedHoveredPartId;
const highlightedPartIds = renderPartIdsForAssemblySelection(hoveredSelectionId, normalizedHoveredPartId);
return highlightedPartIds.length ? highlightedPartIds : hoveredPartId;
}, [
assemblyPickPartIdMap,
hoveredPartId,
isAssemblyView,
isInspectingAssemblyPart,
renderPartIdsForAssemblySelection
]);
const handleUrdfJointValueChange = useCallback((joint, nextValueDeg) => {
const jointName = String(joint?.name || "").trim();
if (!selectedUrdfFileRef || !jointName) {
return;
}
const clampedValueDeg = clampJointValueDeg(joint, nextValueDeg);
startTransition(() => {
setJointValuesByFileRef((current) => {
const currentEntryValues = current?.[selectedUrdfFileRef] && typeof current[selectedUrdfFileRef] === "object"
? current[selectedUrdfFileRef]
: defaultSelectedUrdfJointValues;
return {
...current,
[selectedUrdfFileRef]: {
...currentEntryValues,
[jointName]: clampedValueDeg
}
};
});
});
}, [defaultSelectedUrdfJointValues, selectedUrdfFileRef]);
const handleResetUrdfPose = useCallback(() => {
if (!selectedUrdfFileRef) {
return;
}
startTransition(() => {
setJointValuesByFileRef((current) => {
if (!current?.[selectedUrdfFileRef]) {
return current;
}
const next = { ...current };
delete next[selectedUrdfFileRef];
return next;
});
});
}, [selectedUrdfFileRef]);
const handleSelectUrdfPose = useCallback((pose) => {
if (!selectedUrdfFileRef || !pose?.resolvedJointValues || typeof pose.resolvedJointValues !== "object") {
return;
}
const nextJointValues = cloneJointValueMap(pose.resolvedJointValues);
startTransition(() => {
setJointValuesByFileRef((current) => ({
...current,
[selectedUrdfFileRef]: nextJointValues
}));
});
}, [selectedUrdfFileRef]);
const handleCopyUrdfJointAngles = useCallback(async () => {
setScreenshotStatus("");
if (!movableUrdfJoints.length) {
setCopyStatus("No movable joints are available");
return;
}
try {
await copyTextToClipboard(buildUrdfJointAnglesCopyText(movableUrdfJoints, selectedUrdfJointValues));
setCopyStatus("Copied joint angles");
} catch (error) {
setCopyStatus(error instanceof Error ? error.message : "Clipboard write failed");
}
}, [movableUrdfJoints, selectedUrdfJointValues]);
const handleUrdfIntroAnimationEnabledChange = useCallback((nextEnabled) => {
setUrdfEntryAnimationEnabled(normalizeUrdfIntroAnimationEnabled(nextEnabled));
}, []);
const handleReplayUrdfIntroAnimation = useCallback(() => {
if (!selectedUrdfFileRef) {
return;
}
startTransition(() => {
setSelectedUrdfIntroReplayToken((current) => current + 1);
});
}, [selectedUrdfFileRef]);
const handleUrdfAnimationSpeedChange = useCallback((nextSpeed) => {
if (!selectedUrdfFileRef) {
return;
}
const normalizedSpeed = normalizeUrdfAnimationSpeed(nextSpeed);
startTransition(() => {
setUrdfAnimationSettingsByFileRef((current) => ({
...current,
[selectedUrdfFileRef]: {
...DEFAULT_URDF_ANIMATION_SETTINGS,
...(current?.[selectedUrdfFileRef] && typeof current[selectedUrdfFileRef] === "object"
? current[selectedUrdfFileRef]
: {}),
speed: normalizedSpeed
}
}));
});
}, [selectedUrdfFileRef]);
useEffect(() => {
setSelectedUrdfIntroReplayToken(0);
}, [selectedUrdfFileRef]);
const copySelectionPayload = useMemo(() => {
const selectedReferencesForCopy = selectedReferenceIds
.map((id) => effectiveActiveReferenceMap.get(id))
.filter(Boolean);
const selectedPartsForCopy = supportsPartSelection && !isInspectingAssemblyPart
? selectedPartIds.map((id) => assemblyPartMap.get(id)).filter(Boolean)
: [];
return buildSelectionCopyPayload({
references: selectedReferencesForCopy,
parts: selectedPartsForCopy,
entry: selectedEntry
});
}, [
assemblyPartMap,
effectiveActiveReferenceMap,
isInspectingAssemblyPart,
selectedEntry,
selectedPartIds,
selectedReferenceIds,
supportsPartSelection
]);
const copyButtonLabel = useMemo(
() => buildSelectionCopyButtonLabel(copySelectionPayload.lines),
[copySelectionPayload.lines]
);
useEffect(() => {
if (!pendingCadRefQueryParams.length) {
return;
}
if (!selectedEntry) {
if (!cadRefQueryHasKnownEntry(pendingCadRefQueryParams, catalogEntries)) {
setPendingCadRefQueryParams([]);
}
return;
}
const selectionRequest = collectCadRefSelectionRequest(pendingCadRefQueryParams, selectedEntry);
if (!selectionRequest.hasMatchingToken) {
if (!cadRefQueryHasKnownEntry(pendingCadRefQueryParams, catalogEntries)) {
setPendingCadRefQueryParams([]);
}
return;
}
if (selectionRequest.needsParts && !assemblyPartsLoaded) {
return;
}
if (selectionRequest.needsReferences && selectedEntryHasReferences && !selectedReferencesMatch) {
return;
}
const resolvedSelection = resolveCadRefSelection({
cadRefs: pendingCadRefQueryParams,
entry: selectedEntry,
references: visibleReferences,
assemblyParts,
isAssemblyView
});
if (!orderedStringListEqual(selectedReferenceIdsRef.current, resolvedSelection.selectedReferenceIds)) {
selectedReferenceIdsRef.current = resolvedSelection.selectedReferenceIds;
setSelectedReferenceIds(resolvedSelection.selectedReferenceIds);
}
if (!orderedStringListEqual(selectedPartIdsRef.current, resolvedSelection.selectedPartIds)) {
selectedPartIdsRef.current = resolvedSelection.selectedPartIds;
setSelectedPartIds(resolvedSelection.selectedPartIds);
}
setSelectedRenderPartIdByAssemblyPartId({});
setSelectedWholeEntryCadRefToken(
resolvedSelection.hasWholeEntryToken
? buildCadRefToken({ cadPath: cadPathForEntry(selectedEntry) })
: ""
);
setExpandedAssemblyPartIds((current) => (
orderedStringListEqual(current, resolvedSelection.expandedAssemblyPartIds)
? current
: resolvedSelection.expandedAssemblyPartIds
));
setHoveredListReferenceId("");
setHoveredModelReferenceId("");
setHoveredListPartId("");
setHoveredModelPartId("");
setCopyStatus("");
setTabToolMode(TAB_TOOL_MODE.REFERENCES);
setPendingCadRefQueryParams([]);
}, [
assemblyParts,
assemblyPartsLoaded,
catalogEntries,
isAssemblyView,
pendingCadRefQueryParams,
selectedEntry,
selectedEntryHasReferences,
selectedReferencesMatch,
selectedReferenceIdsRef,
selectedPartIdsRef,
visibleReferences
]);
useEffect(() => {
if (!cadWorkspaceSessionPersistenceReadyRef.current || pendingCadRefQueryParams.length) {
return;
}
writeCadRefQueryParams(selectedEntry ? [
...(selectedWholeEntryCadRefToken ? [selectedWholeEntryCadRefToken] : []),
...copySelectionPayload.lines
] : []);
}, [
copySelectionPayload.lines,
pendingCadRefQueryParams,
selectedEntry,
selectedWholeEntryCadRefToken,
cadWorkspaceSessionPersistenceReadyRef
]);
const toggleReferenceSelection = useCallback((referenceId, { multiSelect = false } = {}) => {
if (stepUpdateInProgress) {
return;
}
const next = computeNextSelectionIds(selectedReferenceIdsRef.current, referenceId, { multiSelect });
if (next.length && !isDesktop) {
setSidebarOpen(false);
}
setSelectedWholeEntryCadRefToken("");
selectedReferenceIdsRef.current = next;
setSelectedReferenceIds(next);
}, [isDesktop, stepUpdateInProgress]);
const clearReferenceSelection = useCallback(() => {
selectedReferenceIdsRef.current = [];
setSelectedWholeEntryCadRefToken("");
setSelectedReferenceIds([]);
setCopyStatus("");
}, []);
const resetReferenceInteractionState = useCallback(() => {
selectedReferenceIdsRef.current = [];
setSelectedWholeEntryCadRefToken("");
setSelectedReferenceIds([]);
setHoveredListReferenceId("");
setHoveredModelReferenceId("");
setCopyStatus("");
}, []);
const handleCopySelection = useCallback(async () => {
setScreenshotStatus("");
if (stepUpdateInProgress) {
setCopyStatus("STEP update in progress. Please wait.");
return;
}
const selectedReferencesForCopy = selectedReferenceIdsRef.current
.map((id) => effectiveActiveReferenceMap.get(id))
.filter(Boolean);
const selectedPartsForCopy = supportsPartSelection && !isInspectingAssemblyPart
? selectedPartIdsRef.current.map((id) => assemblyPartMap.get(id)).filter(Boolean)
: [];
if (!selectedReferencesForCopy.length && !selectedPartsForCopy.length) {
setCopyStatus("Nothing selected");
return;
}
const { lines, missingPartNames } = buildSelectionCopyPayload({
references: selectedReferencesForCopy,
parts: selectedPartsForCopy,
entry: selectedEntry
});
if (!lines.length) {
setCopyStatus(
missingPartNames.length === 1
? `No CAD reference is available for ${missingPartNames[0]}`
: "No CAD references are available for the selection"
);
return;
}
try {
await copyTextToClipboard(lines.join("\n"));
const copiedCount = selectedReferencesForCopy.length + selectedPartsForCopy.length - missingPartNames.length;
const missingSuffix = missingPartNames.length
? ` (${missingPartNames.length} unavailable)`
: "";
setCopyStatus(`Copied ${copiedCount} ref${copiedCount === 1 ? "" : "s"}${missingSuffix}`);
} catch (err) {
setCopyStatus(err instanceof Error ? err.message : "Clipboard write failed");
}
}, [
assemblyPartMap,
effectiveActiveReferenceMap,
isInspectingAssemblyPart,
selectedEntry,
setScreenshotStatus,
supportsPartSelection,
stepUpdateInProgress
]);
const handleInspectAssemblyPart = useCallback((partId) => {
const normalizedPartId = String(partId || "").trim();
if (!normalizedPartId) {
return;
}
const node = assemblyPartMap.get(normalizedPartId);
if (!node) {
return;
}
setViewerAlertOpen(false);
setSelectedWholeEntryCadRefToken("");
selectedPartIdsRef.current = [];
setSelectedPartIds([]);
setSelectedRenderPartIdByAssemblyPartId({});
setHoveredListPartId("");
setHoveredModelPartId("");
resetReferenceInteractionState();
setHiddenPartIds((current) => current.filter((id) => !descendantLeafPartIds(node).includes(id)));
setExpandedAssemblyPartIds((current) => {
const existingIndex = current.indexOf(normalizedPartId);
if (existingIndex >= 0) {
return current.slice(0, existingIndex + 1);
}
return [...current, normalizedPartId];
});
}, [assemblyPartMap, resetReferenceInteractionState]);
const handleBackAssemblyInspection = useCallback(() => {
setViewerAlertOpen(false);
setHoveredListPartId("");
setHoveredModelPartId("");
resetReferenceInteractionState();
setExpandedAssemblyPartIds((current) => current.slice(0, -1));
}, [resetReferenceInteractionState]);
const handleExitAssemblyPartInspection = useCallback(() => {
setViewerAlertOpen(false);
setHoveredListPartId("");
setHoveredModelPartId("");
resetReferenceInteractionState();
setExpandedAssemblyPartIds([]);
}, [resetReferenceInteractionState]);
const togglePartSelection = useCallback((partId, { multiSelect = false, renderPartId = "" } = {}) => {
if (stepUpdateInProgress) {
return;
}
const normalizedPartId = String(partId || "").trim();
const next = computeNextSelectionIds(selectedPartIdsRef.current, partId, { multiSelect });
if (next.length && !isDesktop) {
setSidebarOpen(false);
}
setSelectedWholeEntryCadRefToken("");
selectedPartIdsRef.current = next;
setSelectedPartIds(next);
setSelectedRenderPartIdByAssemblyPartId((current) => {
const nextMap = {};
for (const selectedPartId of next) {
const normalizedSelectedPartId = String(selectedPartId || "").trim();
if (!normalizedSelectedPartId) {
continue;
}
const selectedRenderPartId = normalizedSelectedPartId === normalizedPartId
? renderPartIdForAssemblySelection(normalizedSelectedPartId, renderPartId)
: renderPartIdForAssemblySelection(normalizedSelectedPartId, current[normalizedSelectedPartId]);
if (selectedRenderPartId) {
nextMap[normalizedSelectedPartId] = selectedRenderPartId;
}
}
return nextMap;
});
}, [isDesktop, renderPartIdForAssemblySelection, stepUpdateInProgress]);
const clearAssemblySelection = useCallback(() => {
selectedPartIdsRef.current = [];
selectedReferenceIdsRef.current = [];
setSelectedWholeEntryCadRefToken("");
setSelectedPartIds([]);
setSelectedRenderPartIdByAssemblyPartId({});
setSelectedReferenceIds([]);
setCopyStatus("");
}, []);
const togglePartVisibility = useCallback((partId) => {
const leafIds = descendantLeafPartIds(assemblyPartMap.get(partId) || null);
if (!leafIds.length) {
return;
}
setHiddenPartIds((current) => {
const hidden = new Set(current);
const allHidden = leafIds.every((id) => hidden.has(id));
if (allHidden) {
return current.filter((id) => !leafIds.includes(id));
}
for (const id of leafIds) {
hidden.add(id);
}
return [...hidden];
});
}, [assemblyPartMap]);
const handleHideSelectedParts = useCallback(() => {
const nextSelectedPartIds = [...new Set(
selectedPartIdsRef.current
.map((partId) => String(partId || "").trim())
.filter(Boolean)
)];
if (nextSelectedPartIds.length < 2) {
return;
}
setHiddenPartIds((current) => {
const next = [...current];
const hidden = new Set(current);
let changed = false;
for (const partId of nextSelectedPartIds.flatMap((id) => descendantLeafPartIds(assemblyPartMap.get(id) || null))) {
if (!partId || hidden.has(partId)) {
continue;
}
hidden.add(partId);
next.push(partId);
changed = true;
}
return changed ? next : current;
});
}, [assemblyPartMap]);
const handleShowAllHiddenParts = useCallback(() => {
setHiddenPartIds((current) => (current.length > 1 ? [] : current));
}, []);
const handleModelHoverChange = useCallback((referenceId) => {
if (viewerInAssemblyMode) {
const pickedPartId = String(referenceId || "").trim();
if (!pickedPartId) {
setHoveredModelReferenceId("");
setHoveredModelPartId("");
return;
}
setHoveredModelReferenceId("");
setHoveredModelPartId(assemblyPickPartIdMap.get(pickedPartId) || pickedPartId);
return;
}
const nextReferenceId = String(referenceId || "").trim();
setHoveredModelReferenceId(nextReferenceId);
}, [assemblyPickPartIdMap, viewerInAssemblyMode]);
const handleModelReferenceActivate = useCallback((referenceId, { multiSelect = false } = {}) => {
if (stepUpdateInProgress) {
return;
}
if (viewerInAssemblyMode) {
const pickedPartId = String(referenceId || "").trim();
const nextPartId = assemblyPickPartIdMap.get(pickedPartId) || pickedPartId;
if (!nextPartId) {
clearAssemblySelection();
return;
}
togglePartSelection(nextPartId, { multiSelect, renderPartId: pickedPartId });
return;
}
const nextReferenceId = String(referenceId || "").trim();
if (!nextReferenceId) {
clearReferenceSelection();
return;
}
if (!effectiveActiveReferenceMap.has(nextReferenceId)) {
return;
}
toggleReferenceSelection(nextReferenceId, { multiSelect });
}, [
clearAssemblySelection,
clearReferenceSelection,
effectiveActiveReferenceMap,
assemblyPickPartIdMap,
stepUpdateInProgress,
toggleReferenceSelection,
togglePartSelection,
viewerInAssemblyMode
]);
const handleModelReferenceDoubleActivate = useCallback((referenceId) => {
const pickedPartId = String(referenceId || "").trim();
const nextPartId = assemblyPickPartIdMap.get(pickedPartId) || pickedPartId;
if (viewerInAssemblyMode) {
if (!nextPartId) {
return;
}
handleInspectAssemblyPart(nextPartId);
return;
}
if (!isAssemblyView || !isInspectingAssemblyPart || nextPartId) {
return;
}
handleExitAssemblyPartInspection();
}, [
handleExitAssemblyPartInspection,
handleInspectAssemblyPart,
isAssemblyView,
isInspectingAssemblyPart,
assemblyPickPartIdMap,
viewerInAssemblyMode
]);
const handleSelectEntry = useCallback((key) => {
activateEntryTab(key);
if (!isDesktop) {
setSidebarOpen(false);
}
}, [activateEntryTab, isDesktop]);
const handleSelectTabToolMode = useCallback((mode) => {
setViewerAlertOpen(false);
const normalizedMode = mode === TAB_TOOL_MODE.DRAW ? TAB_TOOL_MODE.DRAW : TAB_TOOL_MODE.REFERENCES;
setTabToolMode(normalizedMode);
if (normalizedMode === TAB_TOOL_MODE.DRAW && drawingTool === DRAWING_TOOL.SURFACE_LINE) {
setDrawingTool(DRAWING_TOOL.FREEHAND);
}
}, [drawingTool]);
const handleToggleFileSheet = useCallback(() => {
if (!selectedFileSheetKind) {
return;
}
setLookMenuOpen(false);
setViewerAlertOpen(false);
setTabToolsOpen((current) => !current);
}, [selectedFileSheetKind]);
const handleDrawingStrokesChange = useCallback((nextStrokes) => {
const normalized = cloneDrawingStrokes(nextStrokes);
const current = drawingStrokesRef.current;
if (drawingStrokesEqual(current, normalized)) {
return;
}
setDrawingUndoStack((history) => [...history, cloneDrawingStrokes(current)]);
setDrawingRedoStack([]);
setDrawingStrokes(normalized);
}, []);
const handleSelectDrawingTool = useCallback((tool) => {
setTabToolMode(TAB_TOOL_MODE.DRAW);
setDrawingTool(tool === DRAWING_TOOL.SURFACE_LINE ? DRAWING_TOOL.FREEHAND : tool);
}, []);
const handleUndoDrawing = useCallback(() => {
const history = drawingUndoStackRef.current;
if (!history.length) {
return;
}
const previous = cloneDrawingStrokes(history[history.length - 1]);
const current = cloneDrawingStrokes(drawingStrokesRef.current);
setDrawingUndoStack(history.slice(0, -1));
setDrawingRedoStack((future) => [...future, current]);
setDrawingStrokes(previous);
}, []);
const handleRedoDrawing = useCallback(() => {
const future = drawingRedoStackRef.current;
if (!future.length) {
return;
}
const next = cloneDrawingStrokes(future[future.length - 1]);
const current = cloneDrawingStrokes(drawingStrokesRef.current);
setDrawingRedoStack(future.slice(0, -1));
setDrawingUndoStack((history) => [...history, current]);
setDrawingStrokes(next);
}, []);
const handleClearDrawings = useCallback(() => {
if (!drawingStrokesRef.current.length) {
return;
}
setDrawingUndoStack((history) => [...history, cloneDrawingStrokes(drawingStrokesRef.current)]);
setDrawingRedoStack([]);
setDrawingStrokes([]);
}, []);
const handlePerspectiveChange = useCallback((nextPerspective) => {
const normalizedPerspective = clonePerspectiveSnapshot(nextPerspective);
if (normalizedPerspective) {
activePerspectiveRef.current = normalizedPerspective;
scheduleCadWorkspaceSessionPersistence();
}
const hasPerspectiveDependentDrawings =
drawingStrokesRef.current.length > 0 ||
drawingUndoStackRef.current.some((strokes) => strokes.length > 0) ||
drawingRedoStackRef.current.some((strokes) => strokes.length > 0);
if (!hasPerspectiveDependentDrawings) {
return;
}
drawingStrokesRef.current = [];
drawingUndoStackRef.current = [];
drawingRedoStackRef.current = [];
setDrawingStrokes([]);
setDrawingUndoStack([]);
setDrawingRedoStack([]);
}, [scheduleCadWorkspaceSessionPersistence]);
useEffect(() => {
return () => {
if (cadWorkspaceSessionPersistTimeoutRef.current) {
window.clearTimeout(cadWorkspaceSessionPersistTimeoutRef.current);
cadWorkspaceSessionPersistTimeoutRef.current = 0;
}
flushCadWorkspaceSessionPersistence();
};
}, [flushCadWorkspaceSessionPersistence]);
useCadWorkspaceShortcuts({
copyStatus,
screenshotStatus,
setCopyStatus,
setScreenshotStatus,
previewMode,
viewerAlertOpen,
lookSheetOpen: lookMenuOpen && !previewMode,
tabToolsOpen,
isDesktop,
sidebarOpen,
previewUiStateRef,
tabToolMode,
drawingUndoStackRef,
drawingRedoStackRef,
handleUndoDrawing,
handleRedoDrawing,
setPreviewMode,
setViewerAlertOpen,
setLookMenuOpen,
setTabToolsOpen,
setSidebarOpen,
setTabToolMode
});
const handleScreenshotDownload = useCallback(async () => {
if (!selectedEntry) {
return;
}
try {
const filename = `${fileKey(selectedEntry).replace(/[^a-zA-Z0-9._-]+/g, "-")}.png`;
if (!viewerRef.current?.captureScreenshot) {
throw new Error("Viewer not ready");
}
await viewerRef.current.captureScreenshot({ filename, mode: "download" });
setCopyStatus("");
setScreenshotStatus(`Saved ${filename}`);
} catch (captureError) {
setCopyStatus("");
setScreenshotStatus(captureError instanceof Error ? captureError.message : "Screenshot capture failed");
}
}, [selectedEntry]);
const handleScreenshotCopy = useCallback(async () => {
if (!selectedEntry) {
return;
}
try {
const filename = `${fileKey(selectedEntry).replace(/[^a-zA-Z0-9._-]+/g, "-")}.png`;
if (!viewerRef.current?.captureScreenshot) {
throw new Error("Viewer not ready");
}
await viewerRef.current.captureScreenshot({ filename, mode: "clipboard" });
setCopyStatus("");
setScreenshotStatus("Copied screenshot to clipboard");
} catch (captureError) {
setCopyStatus("");
setScreenshotStatus(captureError instanceof Error ? captureError.message : "Clipboard copy failed");
}
}, [selectedEntry]);
const handleEnterPreviewMode = useCallback(() => {
if (effectiveRenderFormat === RENDER_FORMAT.DXF || viewerLoading || !selectedMeshData || previewMode) {
return;
}
previewUiStateRef.current = {
sidebarOpen,
tabToolsOpen,
tabToolMode,
lookMenuOpen,
viewerAlertOpen
};
setCopyStatus("");
setScreenshotStatus("");
setDrawingStrokes([]);
setDrawingUndoStack([]);
setDrawingRedoStack([]);
setViewerAlertOpen(false);
setLookMenuOpen(false);
setSidebarOpen(false);
setTabToolsOpen(false);
setPreviewMode(true);
}, [
effectiveRenderFormat,
previewMode,
sidebarOpen,
selectedMeshData,
tabToolMode,
tabToolsOpen,
lookMenuOpen,
viewerAlertOpen,
viewerLoading
]);
const toggleDirectory = (directoryId) => {
setExpandedDirectoryIds((current) => {
const next = new Set(current);
if (next.has(directoryId)) {
next.delete(directoryId);
} else {
next.add(directoryId);
}
return next;
});
};
const selectionToolActive = effectiveRenderFormat === RENDER_FORMAT.STEP && tabToolMode === TAB_TOOL_MODE.REFERENCES;
const drawToolActive = drawModeActive;
const selectionCount = selectionCountBase;
const canUndoDrawing = drawingUndoStack.length > 0;
const canRedoDrawing = drawingRedoStack.length > 0;
const lookSheetOpen = lookMenuOpen && !previewMode;
const fileSheetOpen = !!selectedFileSheetKind && tabToolsOpen && !previewMode && !lookSheetOpen;
const activeSidebarWidth = isDesktop && sidebarOpen && !previewMode
? clampSidebarWidth(sidebarWidth)
: 0;
const activeSheetWidth = isDesktop && (fileSheetOpen || lookSheetOpen)
? DEFAULT_TAB_TOOLS_WIDTH
: 0;
const viewportFrameInsets = {
top: previewMode ? 0 : CAD_WORKSPACE_TOP_BAR_HEIGHT,
right: activeSheetWidth,
bottom: 0,
left: activeSidebarWidth
};
const floatingCadToolbarPosition = {
top: "14px",
right: "14px"
};
const mobileCadBottomBarPosition = {
left: "12px",
right: "12px",
bottom: "calc(env(safe-area-inset-bottom, 0px) + 0.75rem)"
};
const drawingToolOptions = [
{ id: DRAWING_TOOL.FREEHAND, label: "Freehand", Icon: PenTool },
{ id: DRAWING_TOOL.LINE, label: "Line", Icon: Minus },
{ id: DRAWING_TOOL.ARROW, label: "Arrow", Icon: ArrowRight },
{ id: DRAWING_TOOL.DOUBLE_ARROW, label: "Expand", Icon: ArrowLeftRight },
{ id: DRAWING_TOOL.RECTANGLE, label: "Rectangle", Icon: Square },
{ id: DRAWING_TOOL.CIRCLE, label: "Circle", Icon: Circle },
{ id: DRAWING_TOOL.FILL, label: "Fill", Icon: PaintBucket },
{ id: DRAWING_TOOL.ERASE, label: "Erase", Icon: Eraser }
];
return (
<SidebarProvider
open={sidebarOpen}
onOpenChange={handleSidebarOpenChange}
mobileOpen={mobileSidebarOpen}
onMobileOpenChange={setMobileSidebarOpen}
data-glass-tone={normalizeCadWorkspaceGlassTone(cadWorkspaceGlassTone)}
style={{ "--sidebar-width": `${clampSidebarWidth(sidebarWidth)}px` }}
className="relative h-svh overflow-hidden bg-transparent"
>
<div className="fixed inset-0 z-0">
<CadRenderPane
viewerRef={viewerRef}
renderFormat={effectiveRenderFormat}
renderPartsIndividually={isUrdfView}
selectedMeshData={selectedMeshData}
selectedDxfData={selectedDxfData}
selectedDxfMeshData={selectedDxfMeshData}
selectedKey={selectedKey}
selectedDxfKey={selectedDxfPreviewKey}
viewerPerspective={viewerPerspective}
viewerPerspectiveRef={activePerspectiveRef}
lookSettings={lookSettings}
previewMode={previewMode}
isDesktop={isDesktop}
viewportFrameInsets={viewportFrameInsets}
viewerLoading={viewerLoading}
viewerAlert={viewerAlert}
stepUpdateInProgress={effectiveRenderFormat === RENDER_FORMAT.STEP && stepUpdateInProgress}
viewPlaneOffsetRight={viewportFrameInsets.right + 16}
viewerMode={viewerMode}
assemblyParts={isAssemblyView && selectedAssemblyInteractionReady ? assemblyLeafParts : EMPTY_LIST}
hiddenPartIds={hiddenPartIds}
selectedPartIds={viewerSelectedPartIds}
hoveredPartId={viewerHoveredPartIds}
hoveredReferenceId={hoveredReferenceId}
selectedReferenceIds={selectedReferenceIds}
selectorRuntime={effectiveSelectorRuntime}
pickableFaces={viewerPickableFaces}
pickableEdges={viewerPickableEdges}
pickableVertices={viewerPickableVertices}
inspectedAssemblyPartId={isInspectingAssemblyPart ? inspectedAssemblyPartId : ""}
drawToolActive={drawToolActive}
drawingTool={drawingTool}
drawingStrokes={drawingStrokes}
handleDrawingStrokesChange={handleDrawingStrokesChange}
handlePerspectiveChange={handlePerspectiveChange}
handleModelHoverChange={handleModelHoverChange}
handleModelReferenceActivate={handleModelReferenceActivate}
handleModelReferenceDoubleActivate={handleModelReferenceDoubleActivate}
handleViewerAlertChange={handleViewerAlertChange}
selectionCount={selectionCount}
copyButtonLabel={copyButtonLabel}
handleCopySelection={handleCopySelection}
handleScreenshotCopy={handleScreenshotCopy}
partIntroAnimation={isUrdfView ? {
enabled: selectedUrdfAnimationSettings.introEnabled,
introKey: selectedUrdfAnimationKey,
replayToken: selectedUrdfIntroReplayToken
} : null}
/>
</div>
<FileExplorerSidebar
previewMode={previewMode}
query={query}
onQueryChange={setQuery}
filteredEntries={filteredEntries}
catalogEntries={catalogEntries}
filteredEntriesTree={filteredEntriesTree}
selectedKey={selectedKey}
expandedDirectoryIds={expandedDirectoryIds}
onToggleDirectory={toggleDirectory}
onSelectEntry={handleSelectEntry}
entrySourceFormat={entrySourceFormat}
entryHasMesh={entryHasMesh}
entryHasDxf={entryHasDxf}
entryHasUrdf={entryHasUrdf}
onStartResize={handleStartSidebarResize}
/>
<SidebarInset className="pointer-events-none relative z-10 h-svh min-w-0 overflow-hidden bg-transparent">
<CadWorkspaceTopBar
previewMode={previewMode}
lookMenuOpen={lookMenuOpen}
sidebarLabelForEntry={sidebarLabelForEntry}
selectedEntry={selectedEntry}
setLookMenuOpen={setLookMenuOpen}
fileSheetKind={selectedFileSheetKind}
fileSheetOpen={fileSheetOpen}
onToggleFileSheet={handleToggleFileSheet}
/>
<div className="pointer-events-none relative min-h-0 flex-1 overflow-hidden">
<div className="flex h-full min-w-0">
<div className="pointer-events-none relative min-w-0 flex-1 overflow-hidden">
<FloatingToolBar
previewMode={previewMode}
selectedEntry={selectedEntry}
isDesktop={isDesktop}
sidebarOpen={false}
renderFormat={effectiveRenderFormat}
floatingCadToolbarPosition={floatingCadToolbarPosition}
mobileCadBottomBarPosition={mobileCadBottomBarPosition}
selectionToolActive={selectionToolActive}
drawToolActive={drawToolActive}
handleSelectTabToolMode={handleSelectTabToolMode}
viewerLoading={viewerLoading}
selectedMeshData={selectedMeshData}
selectedDxfData={selectedDxfData}
drawingToolOptions={drawingToolOptions}
drawingTool={drawingTool}
handleSelectDrawingTool={handleSelectDrawingTool}
handleUndoDrawing={handleUndoDrawing}
handleRedoDrawing={handleRedoDrawing}
handleClearDrawings={handleClearDrawings}
canUndoDrawing={canUndoDrawing}
canRedoDrawing={canRedoDrawing}
drawingStrokes={drawingStrokes}
handleEnterPreviewMode={handleEnterPreviewMode}
handleScreenshotCopy={handleScreenshotCopy}
handleScreenshotDownload={handleScreenshotDownload}
/>
<CadWorkspaceAssemblyInspectPill
previewMode={previewMode}
inspectedAssemblyPart={inspectedAssemblyPart}
toolbarHeight={0}
onExit={handleExitAssemblyPartInspection}
/>
<ViewerLoadingOverlay
viewerLoading={viewerLoading}
previewMode={previewMode}
/>
</div>
{selectedFileSheetKind === "dxf" ? (
<DxfFileSheet
key={`dxf:${selectedKey}`}
open={fileSheetOpen}
isDesktop={isDesktop}
width={DEFAULT_TAB_TOOLS_WIDTH}
valueMm={effectiveDxfThicknessMm}
bendLines={selectedDxfBendLines}
bendSettings={normalizedSelectedDxfBendSettings}
hasDxfData={!!selectedDxfData}
viewerLoading={viewerLoading}
onThicknessChange={setDxfThicknessMm}
onBendChange={handleDxfBendSettingChange}
/>
) : null}
{selectedFileSheetKind === "stepAssembly" ? (
<StepAssemblyFileSheet
key={`stepAssembly:${selectedKey}`}
open={fileSheetOpen}
isDesktop={isDesktop}
width={DEFAULT_TAB_TOOLS_WIDTH}
selectedEntry={selectedEntry}
viewerLoading={viewerLoading || assemblySidebarLoading}
isAssemblyView={isAssemblyView}
assemblyParts={assemblyParts}
assemblyBreadcrumbNodes={assemblyBreadcrumbNodes}
filteredAssemblyParts={filteredAssemblyParts}
selectedPartIds={selectedPartIds}
hoveredPartId={hoveredPartId}
hiddenPartIds={hiddenPartIds}
togglePartSelection={togglePartSelection}
clearAssemblySelection={clearAssemblySelection}
setHoveredListPartId={setHoveredListPartId}
handleInspectAssemblyPart={handleInspectAssemblyPart}
handleBackAssemblyInspection={handleBackAssemblyInspection}
handleExitAssemblyPartInspection={handleExitAssemblyPartInspection}
togglePartVisibility={togglePartVisibility}
hideSelectedParts={handleHideSelectedParts}
showAllHiddenParts={handleShowAllHiddenParts}
inspectedAssemblyPart={inspectedAssemblyPart}
/>
) : null}
{selectedFileSheetKind === "urdf" ? (
<UrdfFileSheet
key={`urdf:${selectedKey}`}
open={fileSheetOpen}
isDesktop={isDesktop}
width={DEFAULT_TAB_TOOLS_WIDTH}
joints={movableUrdfJoints}
poses={selectedUrdfPoses}
activePoseName={activeSelectedUrdfPoseName}
jointValues={selectedUrdfJointValues}
onJointValueChange={handleUrdfJointValueChange}
onPoseSelect={handleSelectUrdfPose}
onCopyJointAngles={handleCopyUrdfJointAngles}
introAnimationEnabled={selectedUrdfAnimationSettings.introEnabled}
animationSpeed={selectedUrdfAnimationSettings.speed}
onIntroAnimationEnabledChange={handleUrdfIntroAnimationEnabledChange}
onReplayIntroAnimation={handleReplayUrdfIntroAnimation}
onAnimationSpeedChange={handleUrdfAnimationSpeedChange}
onResetPose={handleResetUrdfPose}
/>
) : null}
<LookSettingsPopover
open={lookSheetOpen}
isDesktop={isDesktop}
width={DEFAULT_TAB_TOOLS_WIDTH}
lookSettings={lookSettings}
cadWorkspaceGlassTone={normalizeCadWorkspaceGlassTone(cadWorkspaceGlassTone)}
updateLookSettings={updateLookSettings}
updateCadWorkspaceGlassTone={(nextTone) => setCadWorkspaceGlassTone(normalizeCadWorkspaceGlassTone(nextTone))}
handleResetLookSettings={handleResetLookSettings}
/>
</div>
</div>
<StatusToast
copyStatus={copyStatus}
screenshotStatus={screenshotStatus}
persistenceStatus={persistenceStatus}
previewMode={previewMode}
onClear={() => {
setCopyStatus("");
setScreenshotStatus("");
setPersistenceStatus("");
lastPersistenceFailureKeyRef.current = "";
}}
/>
<ViewerAlertDialog
viewerAlertOpen={viewerAlertOpen}
viewerAlert={viewerAlert}
previewMode={previewMode}
setViewerAlertOpen={setViewerAlertOpen}
/>
</SidebarInset>
</SidebarProvider>
);
}