import { buildCadRefToken, parseCadRefToken } from "../cadRefs.js"; const CAD_QUERY_PARAM = "file"; const CAD_REF_QUERY_PARAM = "refs"; const CAD_DIRECTORY_QUERY_PARAM = "dir"; const DEFAULT_CAD_DIRECTORY = "models"; export function fileKey(entry) { return String(entry?.file || "").trim(); } export function cadPathForEntry(entry) { return String(entry?.cadPath || "").trim(); } function replaceUrl(url) { const nextSearch = url.searchParams.toString(); window.history.replaceState({}, "", `${url.pathname}${nextSearch ? `?${nextSearch}` : ""}${url.hash}`); } function normalizeUrlPath(value) { return String(value || "").trim().replace(/\\/g, "/").replace(/^\/+/, "").replace(/\/+$/, ""); } function readCadDirectoryParam(params) { return normalizeUrlPath(params.get(CAD_DIRECTORY_QUERY_PARAM)) || DEFAULT_CAD_DIRECTORY; } export function normalizeCadFileQueryParam(value, dir = DEFAULT_CAD_DIRECTORY) { const normalizedValue = normalizeUrlPath(value); const normalizedDir = normalizeUrlPath(dir) || DEFAULT_CAD_DIRECTORY; if (!normalizedValue) { return ""; } if (normalizedValue === normalizedDir) { return ""; } if (normalizedValue.startsWith(`${normalizedDir}/`)) { return normalizedValue.slice(normalizedDir.length + 1); } return normalizedValue; } export function normalizeCadRefQueryParams(values) { const sourceValues = Array.isArray(values) ? values : [values]; const seenTokens = new Set(); const tokens = []; for (const sourceValue of sourceValues) { const lines = String(sourceValue || "").split(/\r?\n/); for (const line of lines) { const normalizedLine = String(line || "").trim(); const parsedToken = parseCadRefToken(normalizedLine) || parseCadRefToken(`@cad[${normalizedLine}]`); const token = String( parsedToken ? buildCadRefToken({ cadPath: parsedToken.cadPath, selectors: parsedToken.selectors }) : "" ).trim(); if (!token || seenTokens.has(token)) { continue; } seenTokens.add(token); tokens.push(token); } } return tokens; } function cadRefQueryValueFromToken(token) { const parsedToken = parseCadRefToken(token); return parsedToken?.token ? parsedToken.token.slice("@cad[".length, -1) : ""; } export function readCadParam() { if (typeof window === "undefined") { return null; } const params = new URLSearchParams(window.location.search); const value = params.get(CAD_QUERY_PARAM); const normalizedValue = typeof value === "string" ? normalizeCadFileQueryParam(value, readCadDirectoryParam(params)) : ""; return normalizedValue || null; } export function readCadRefQueryParams() { if (typeof window === "undefined") { return []; } const params = new URLSearchParams(window.location.search); return normalizeCadRefQueryParams(params.getAll(CAD_REF_QUERY_PARAM)); } export function findEntryByUrlPath(entries, urlPath) { if (!urlPath) { return null; } return entries.find((entry) => fileKey(entry) === urlPath) || null; } export function findEntryByCadRefParams(entries, cadRefs = readCadRefQueryParams()) { for (const cadRef of Array.isArray(cadRefs) ? cadRefs : [cadRefs]) { const cadPath = String(parseCadRefToken(cadRef)?.cadPath || "").trim(); if (!cadPath) { continue; } const match = entries.find((entry) => cadPathForEntry(entry) === cadPath); if (match) { return match; } } return null; } export function selectedEntryKeyFromUrl(entries) { const match = findEntryByCadRefParams(entries) || findEntryByUrlPath(entries, readCadParam()); return match ? fileKey(match) : ""; } export function writeCadParam(urlPath) { if (typeof window === "undefined") { return; } const url = new URL(window.location.href); if (urlPath) { url.searchParams.set(CAD_QUERY_PARAM, urlPath); } else { url.searchParams.delete(CAD_QUERY_PARAM); } replaceUrl(url); } export function writeCadRefQueryParams(cadRefs) { if (typeof window === "undefined") { return; } const url = new URL(window.location.href); url.searchParams.delete(CAD_REF_QUERY_PARAM); for (const token of Array.isArray(cadRefs) ? cadRefs : [cadRefs]) { const queryValue = cadRefQueryValueFromToken(token); if (queryValue) { url.searchParams.append(CAD_REF_QUERY_PARAM, queryValue); } } replaceUrl(url); } function compareSidebarLabels(a, b) { return String(a || "").localeCompare(String(b || ""), undefined, { numeric: true, sensitivity: "base" }); } function entryLeafName(entry) { const sourceRelPath = String( (["part", "assembly"].includes(String(entry?.kind || "").trim().toLowerCase()) && entry?.step?.path) ? entry.step.path : (entry?.source?.path || entry?.step?.path || "") ); if (sourceRelPath) { const parts = sourceRelPath.split("/"); return parts[parts.length - 1] || sourceRelPath; } const file = fileKey(entry); if (!file) { return ""; } const parts = file.split("/"); return parts[parts.length - 1] || file; } function normalizedEntryStem(entry) { return entryLeafName(entry) .replace(/\.step\.json$/i, "") .replace(/\.urdf\.json$/i, "") .replace(/\.(step|stp|stl|dxf|urdf|py)$/i, ""); } export function sidebarDirectoryIdForEntry(entry) { const file = fileKey(entry); const parts = file.split("/").filter(Boolean); parts.pop(); return parts.join("/"); } function sourceExtensionForEntry(entry) { const leafName = entryLeafName(entry); const match = /\.([^.]+)$/.exec(leafName); return String(match?.[1] || "").trim().toLowerCase(); } export function filenameLabelForEntry(entry) { const stem = normalizedEntryStem(entry); if (!stem) { return ""; } const sourceFormat = String( entry?.kind === "dxf" ? "dxf" : entry?.kind === "urdf" ? "urdf" : entry?.kind === "stl" ? "stl" : sourceExtensionForEntry(entry) || "step" ).trim().toLowerCase(); if (sourceFormat === "dxf") { return `${stem}.dxf`; } if (sourceFormat === "urdf" || entry?.kind === "urdf") { return `${stem}.urdf`; } if (sourceFormat === "stl" || entry?.kind === "stl") { return `${stem}.stl`; } return `${stem}.${sourceFormat === "stp" ? "stp" : "step"}`; } export function sidebarLabelForEntry(entry) { return filenameLabelForEntry(entry); } function compareSidebarEntries(a, b) { const nameDiff = sidebarLabelForEntry(a).localeCompare(sidebarLabelForEntry(b), undefined, { numeric: true, sensitivity: "base" }); if (nameDiff !== 0) { return nameDiff; } return fileKey(a).localeCompare(fileKey(b), undefined, { numeric: true, sensitivity: "base" }); } function createSidebarDirectoryNode(id, name) { return { id, name, entries: [], children: new Map() }; } function finalizeSidebarDirectoryNode(node) { return { id: node.id, name: node.name, entries: [...node.entries].sort(compareSidebarEntries), directories: [...node.children.values()] .map(finalizeSidebarDirectoryNode) .sort((a, b) => compareSidebarLabels(a.name, b.name)) }; } export function buildSidebarDirectoryTree(entries, { rootName = "models" } = {}) { const root = createSidebarDirectoryNode("", String(rootName || "models")); for (const entry of entries) { const directoryId = sidebarDirectoryIdForEntry(entry); const directoryParts = directoryId ? directoryId.split("/") : []; let currentNode = root; let currentId = ""; for (const part of directoryParts) { currentId = currentId ? `${currentId}/${part}` : part; const childNode = currentNode.children.get(part) || createSidebarDirectoryNode(currentId, part); currentNode.children.set(part, childNode); currentNode = childNode; } currentNode.entries.push(entry); } return finalizeSidebarDirectoryNode(root); } export function collectSidebarDirectoryIds(directoryNode, result = []) { for (const directory of directoryNode.directories || []) { result.push(directory.id); collectSidebarDirectoryIds(directory, result); } return result; } export function listSidebarItems(directory) { return [ ...(directory.directories || []).map((childDirectory) => ({ type: "directory", key: `directory:${childDirectory.id}`, label: childDirectory.name, value: childDirectory })), ...(directory.entries || []).map((entry) => ({ type: "entry", key: `entry:${fileKey(entry)}`, label: sidebarLabelForEntry(entry), value: entry })) ].sort((a, b) => { const labelDiff = compareSidebarLabels(a.label, b.label); if (labelDiff !== 0) { return labelDiff; } return a.key.localeCompare(b.key, undefined, { numeric: true, sensitivity: "base" }); }); } export function collectAncestorDirectoryIds(directoryId) { if (!directoryId) { return []; } const parts = String(directoryId).split("/").filter(Boolean); const ancestorIds = []; let currentId = ""; for (const part of parts) { currentId = currentId ? `${currentId}/${part}` : part; ancestorIds.push(currentId); } return ancestorIds; }