Files
cadSet/text-to-cad/viewer/src/client/workbench/persistence.js
T
2026-07-22 19:38:36 +08:00

1174 lines
37 KiB
JavaScript

import { clonePerspectiveSnapshot, perspectiveSnapshotEqual } from "cadjs/lib/perspective.js";
import {
cloneThemePresetSettings,
DEFAULT_THEME_PRESET,
DEFAULT_THEME_PRESET_ID,
getThemePresetById,
inferThemeSettingsSceneTone,
THEME_PRESETS,
normalizeThemePresetId,
normalizeThemeSettings
} from "cadjs/lib/themeSettings.js";
import { THEME_STORAGE_KEY } from "../ui/colorScheme.js";
import { normalizeRenderFormat } from "cadjs/lib/fileFormats.js";
import { isCadWorkspaceCompactFileSheetViewport } from "./breakpoints.js";
import { DRAWING_TOOL, RENDER_FORMAT, TAB_TOOL_MODE } from "./constants.js";
export { THEME_STORAGE_KEY };
const THEME_STORAGE_VERSION = 11;
const CUSTOM_THEME_PRESET_ID_PREFIX = "custom:";
export const THEME_APPEARANCE_QUERY_PARAM = "appearance";
export const CAD_DIRECTORY_SESSION_STORAGE_VERSION = 1;
export const CAD_DIRECTORY_SESSION_STORAGE_KEY = `cad-viewer:directory-session:v${CAD_DIRECTORY_SESSION_STORAGE_VERSION}`;
export const CAD_WORKSPACE_DEFAULT_SIDEBAR_WIDTH = 280;
export const CAD_WORKSPACE_DEFAULT_TAB_TOOLS_WIDTH = 365;
export const CAD_WORKSPACE_COMPACT_TAB_TOOLS_WIDTH = 280;
export const CAD_WORKSPACE_DEFAULT_GLASS_TONE = inferThemeSettingsSceneTone(DEFAULT_THEME_PRESET.settings);
function hasOwn(object, key) {
return Object.prototype.hasOwnProperty.call(object, key);
}
function normalizeString(value, fallback = "") {
return String(value ?? fallback);
}
function normalizeStringList(value) {
if (!Array.isArray(value)) {
return [];
}
return value
.map((entry) => String(entry ?? "").trim())
.filter(Boolean);
}
function normalizeUniqueStringList(value) {
return [...new Set(normalizeStringList(value))];
}
function normalizeNullableUniqueStringList(value) {
return Array.isArray(value) ? normalizeUniqueStringList(value) : null;
}
function normalizeNumber(value, fallback) {
const numericValue = Number(value);
return Number.isFinite(numericValue) ? numericValue : fallback;
}
function normalizeNullablePositiveInteger(value) {
const numericValue = Number(value);
if (!Number.isFinite(numericValue) || numericValue <= 0) {
return null;
}
return Math.round(numericValue);
}
function normalizeBoolean(value, fallback = false) {
return typeof value === "boolean" ? value : fallback;
}
function normalizeNullableBoolean(value) {
return typeof value === "boolean" ? value : null;
}
export function fileSheetWidthPxForSessionState(value, defaultWidth = CAD_WORKSPACE_DEFAULT_TAB_TOOLS_WIDTH) {
const normalizedWidth = normalizeNullablePositiveInteger(value);
const normalizedDefaultWidth = (
normalizeNullablePositiveInteger(defaultWidth) ||
normalizeNullablePositiveInteger(CAD_WORKSPACE_DEFAULT_TAB_TOOLS_WIDTH)
);
if (!normalizedWidth || normalizedWidth === normalizedDefaultWidth) {
return null;
}
return normalizedWidth;
}
export function fileViewerWidthPxForSessionState(value, defaultWidth = CAD_WORKSPACE_DEFAULT_SIDEBAR_WIDTH) {
const normalizedWidth = normalizeNullablePositiveInteger(value);
const normalizedDefaultWidth = (
normalizeNullablePositiveInteger(defaultWidth) ||
normalizeNullablePositiveInteger(CAD_WORKSPACE_DEFAULT_SIDEBAR_WIDTH)
);
if (!normalizedWidth || normalizedWidth === normalizedDefaultWidth) {
return null;
}
return normalizedWidth;
}
export function cadWorkspaceDefaultFileSheetWidthForViewport(width) {
return isCadWorkspaceCompactFileSheetViewport(width)
? CAD_WORKSPACE_COMPACT_TAB_TOOLS_WIDTH
: CAD_WORKSPACE_DEFAULT_TAB_TOOLS_WIDTH;
}
function cloneStringList(value) {
return Array.isArray(value) ? [...value] : [];
}
function stringListEqual(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 cloneDrawingPoint(point) {
return {
x: Number(point?.x) || 0,
y: Number(point?.y) || 0
};
}
function clonePoint3(point) {
return Array.isArray(point) ? [
Number(point[0]) || 0,
Number(point[1]) || 0,
Number(point[2]) || 0
] : null;
}
function clonePoint2(point) {
return Array.isArray(point) ? [
Number(point[0]) || 0,
Number(point[1]) || 0
] : null;
}
function normalizeTabCameraSnapshot(value) {
const snapshot = clonePerspectiveSnapshot(value);
if (!snapshot) {
return null;
}
const zoom = Number(snapshot.zoom);
return {
position: snapshot.position,
target: snapshot.target,
up: snapshot.up,
zoom: Number.isFinite(zoom) && zoom > 0 ? zoom : 1
};
}
function normalizeDrawingTool(value) {
const normalized = normalizeString(value || DRAWING_TOOL.FREEHAND);
switch (normalized) {
case DRAWING_TOOL.LINE:
case DRAWING_TOOL.ARROW:
case DRAWING_TOOL.DOUBLE_ARROW:
case DRAWING_TOOL.RECTANGLE:
case DRAWING_TOOL.CIRCLE:
case DRAWING_TOOL.FILL:
case DRAWING_TOOL.ERASE:
case DRAWING_TOOL.FREEHAND:
return normalized;
default:
return DRAWING_TOOL.FREEHAND;
}
}
function normalizeTabToolMode(value) {
const normalized = normalizeString(value || TAB_TOOL_MODE.REFERENCES);
return normalized === TAB_TOOL_MODE.DRAW ? TAB_TOOL_MODE.DRAW : TAB_TOOL_MODE.REFERENCES;
}
function pointsEqualN(a, b, length) {
if (a === b) {
return true;
}
if (!Array.isArray(a) || !Array.isArray(b) || a.length < length || b.length < length) {
return false;
}
for (let index = 0; index < length; index += 1) {
if (a[index] !== b[index]) {
return false;
}
}
return true;
}
function cloneSurfaceLineData(surfaceLine) {
if (!surfaceLine || typeof surfaceLine !== "object") {
return null;
}
return {
referenceId: String(surfaceLine.referenceId || ""),
selector: String(surfaceLine.selector || ""),
normalizedSelector: String(surfaceLine.normalizedSelector || ""),
faceToken: String(surfaceLine.faceToken || ""),
partId: String(surfaceLine.partId || ""),
surfaceType: String(surfaceLine.surfaceType || ""),
startPoint: clonePoint3(surfaceLine.startPoint),
endPoint: clonePoint3(surfaceLine.endPoint),
startUv: clonePoint2(surfaceLine.startUv),
endUv: clonePoint2(surfaceLine.endUv)
};
}
function surfaceLineEqual(a, b) {
if (a === b) {
return true;
}
if (!a || !b) {
return false;
}
return (
a.referenceId === b.referenceId &&
a.selector === b.selector &&
a.normalizedSelector === b.normalizedSelector &&
a.faceToken === b.faceToken &&
a.partId === b.partId &&
a.surfaceType === b.surfaceType &&
pointsEqualN(a.startPoint, b.startPoint, 3) &&
pointsEqualN(a.endPoint, b.endPoint, 3) &&
pointsEqualN(a.startUv, b.startUv, 2) &&
pointsEqualN(a.endUv, b.endUv, 2)
);
}
function drawingPointsEqual(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]?.x !== b[index]?.x || a[index]?.y !== b[index]?.y) {
return false;
}
}
return true;
}
function cloneDrawingStroke(stroke) {
const rawTool = normalizeString(stroke?.tool || DRAWING_TOOL.FREEHAND);
if (rawTool === DRAWING_TOOL.SURFACE_LINE) {
return null;
}
return {
id: String(stroke?.id || ""),
tool: normalizeDrawingTool(rawTool),
points: Array.isArray(stroke?.points) ? stroke.points.map(cloneDrawingPoint) : [],
fillPoints: Array.isArray(stroke?.fillPoints) ? stroke.fillPoints.map(cloneDrawingPoint) : [],
guessed: stroke?.guessed === true,
surfaceLine: cloneSurfaceLineData(stroke?.surfaceLine)
};
}
export function cloneDrawingStrokes(strokes) {
return Array.isArray(strokes) ? strokes.map(cloneDrawingStroke).filter(Boolean) : [];
}
export function drawingStrokesEqual(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]?.id !== b[index]?.id ||
a[index]?.tool !== b[index]?.tool ||
a[index]?.guessed !== b[index]?.guessed ||
!surfaceLineEqual(a[index]?.surfaceLine, b[index]?.surfaceLine) ||
!drawingPointsEqual(a[index]?.points, b[index]?.points) ||
!drawingPointsEqual(a[index]?.fillPoints, b[index]?.fillPoints)
) {
return false;
}
}
return true;
}
function cloneDrawingHistoryStack(stack) {
return Array.isArray(stack) ? stack.map(cloneDrawingStrokes) : [];
}
function drawingHistoryStackEqual(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 (!drawingStrokesEqual(a[index], b[index])) {
return false;
}
}
return true;
}
const TAB_STATE_SCHEMA = [
{
key: "renderFormat",
defaultValue: RENDER_FORMAT.STEP,
normalize: (value) => normalizeRenderFormat(value)
},
{
key: "dxfThicknessMm",
defaultValue: 0,
normalize: (value) => {
const numericValue = normalizeNumber(value, 0);
return numericValue > 0 ? numericValue : 0;
}
},
{
key: "referenceQuery",
defaultValue: "",
normalize: normalizeString
},
{
key: "selectedReferenceIds",
defaultValue: [],
normalize: normalizeStringList,
clone: cloneStringList,
equals: stringListEqual
},
{
key: "selectedPartIds",
defaultValue: [],
normalize: normalizeStringList,
clone: cloneStringList,
equals: stringListEqual
},
{
key: "inspectedAssemblyNodeId",
defaultValue: "",
normalize: (value) => normalizeString(value).trim()
},
{
key: "expandedAssemblyPartIds",
defaultValue: [],
normalize: normalizeStringList,
clone: cloneStringList,
equals: stringListEqual
},
{
key: "expandedStepTreeNodeIds",
defaultValue: [],
normalize: normalizeUniqueStringList,
clone: cloneStringList,
equals: stringListEqual
},
{
key: "stepTreeRootShowMore",
defaultValue: false,
normalize: normalizeBoolean
},
{
key: "fileSheetOpenSectionIds",
defaultValue: null,
normalize: normalizeNullableUniqueStringList,
clone: (value) => (Array.isArray(value) ? cloneStringList(value) : null),
equals: (a, b) => (
Array.isArray(a) || Array.isArray(b)
? stringListEqual(a || [], b || [])
: a === b
)
},
{
key: "hiddenPartIds",
defaultValue: [],
normalize: normalizeStringList,
clone: cloneStringList,
equals: stringListEqual
},
{
key: "camera",
defaultValue: null,
normalize: normalizeTabCameraSnapshot,
clone: normalizeTabCameraSnapshot,
equals: perspectiveSnapshotEqual
},
{
key: "drawingTool",
defaultValue: DRAWING_TOOL.FREEHAND,
normalize: normalizeDrawingTool
},
{
key: "tabToolMode",
defaultValue: TAB_TOOL_MODE.REFERENCES,
normalize: normalizeTabToolMode
},
{
key: "drawingStrokes",
defaultValue: [],
normalize: cloneDrawingStrokes,
clone: cloneDrawingStrokes,
equals: drawingStrokesEqual
},
{
key: "drawingUndoStack",
defaultValue: [],
normalize: cloneDrawingHistoryStack,
clone: cloneDrawingHistoryStack,
equals: drawingHistoryStackEqual
},
{
key: "drawingRedoStack",
defaultValue: [],
normalize: cloneDrawingHistoryStack,
clone: cloneDrawingHistoryStack,
equals: drawingHistoryStackEqual
}
];
function normalizeSchemaState(schema, source = {}) {
const normalized = {};
for (const field of schema) {
let value = hasOwn(source || {}, field.key) ? source[field.key] : undefined;
if (typeof value === "undefined") {
value = field.defaultValue;
}
normalized[field.key] = field.normalize ? field.normalize(value, field.defaultValue) : value;
}
return normalized;
}
function cloneSchemaState(schema, source = {}) {
const normalized = normalizeSchemaState(schema, source);
const cloned = {};
for (const field of schema) {
const value = normalized[field.key];
cloned[field.key] = field.clone ? field.clone(value) : value;
}
return cloned;
}
function schemaStateEqual(schema, a = {}, b = {}) {
for (const field of schema) {
const left = normalizeSchemaState([field], a)[field.key];
const right = normalizeSchemaState([field], b)[field.key];
const equals = field.equals || Object.is;
if (!equals(left, right)) {
return false;
}
}
return true;
}
function normalizeTabKey(value) {
return String(value || "").trim();
}
function readStorageJson(storage, key) {
try {
const rawValue = storage.getItem(key);
return rawValue ? JSON.parse(rawValue) : null;
} catch {
return null;
}
}
function reportStorageWriteFailure(key, error, options = {}) {
if (typeof options.onWriteError === "function") {
options.onWriteError({ key, error });
}
}
function writeStorageJson(storage, key, value, options = {}) {
try {
storage.setItem(key, JSON.stringify(value));
return true;
} catch (error) {
reportStorageWriteFailure(key, error, options);
return false;
}
}
function removeStorageItem(storage, key, options = {}) {
try {
storage.removeItem(key);
return true;
} catch (error) {
reportStorageWriteFailure(key, error, options);
return false;
}
}
function browserSessionStorage() {
return typeof window !== "undefined" ? window.sessionStorage : null;
}
export function createCadDirectorySessionState(overrides = {}, options = {}) {
return {
fileViewerOpen: normalizeBoolean(overrides?.fileViewerOpen, false),
fileViewerExpandedDirectoryIds: normalizeNullableUniqueStringList(overrides?.fileViewerExpandedDirectoryIds),
fileViewerWidthPx: fileViewerWidthPxForSessionState(
overrides?.fileViewerWidthPx,
options.defaultFileViewerWidthPx
),
fileSheetOpen: normalizeNullableBoolean(overrides?.fileSheetOpen),
fileSheetWidthPx: fileSheetWidthPxForSessionState(
overrides?.fileSheetWidthPx,
options.defaultFileSheetWidthPx
),
theme: normalizeDirectorySessionThemeSlice(overrides?.theme)
};
}
function buildCadDirectorySessionStoragePayload(state = {}, options = {}) {
const normalizedState = createCadDirectorySessionState(state, options);
const payload = {
version: CAD_DIRECTORY_SESSION_STORAGE_VERSION
};
if (normalizedState.fileViewerOpen || hasOwn(state || {}, "fileViewerOpen")) {
payload.fileViewerOpen = normalizedState.fileViewerOpen;
}
if (Array.isArray(normalizedState.fileViewerExpandedDirectoryIds)) {
payload.fileViewerExpandedDirectoryIds = normalizedState.fileViewerExpandedDirectoryIds;
}
if (normalizedState.fileViewerWidthPx) {
payload.fileViewerWidthPx = normalizedState.fileViewerWidthPx;
}
if (typeof normalizedState.fileSheetOpen === "boolean") {
payload.fileSheetOpen = normalizedState.fileSheetOpen;
}
if (normalizedState.fileSheetWidthPx) {
payload.fileSheetWidthPx = normalizedState.fileSheetWidthPx;
}
if (normalizedState.theme) {
payload.theme = normalizedState.theme;
}
return Object.keys(payload).length > 1 ? payload : null;
}
export function readCadDirectorySessionState(options = {}) {
const storage = options.storage || browserSessionStorage();
if (!storage) {
return createCadDirectorySessionState({}, options);
}
const rawValue = readStorageJson(storage, CAD_DIRECTORY_SESSION_STORAGE_KEY);
if (!rawValue || rawValue.version !== CAD_DIRECTORY_SESSION_STORAGE_VERSION) {
return createCadDirectorySessionState({}, options);
}
return createCadDirectorySessionState(rawValue, options);
}
export function writeCadDirectorySessionState(state = {}, options = {}) {
const storage = options.storage || browserSessionStorage();
if (!storage) {
return true;
}
const payload = buildCadDirectorySessionStoragePayload(state, options);
if (!payload) {
return removeStorageItem(storage, CAD_DIRECTORY_SESSION_STORAGE_KEY, options);
}
return writeStorageJson(storage, CAD_DIRECTORY_SESSION_STORAGE_KEY, payload, options);
}
function readSystemPrefersDark() {
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
return false;
}
try {
return window.matchMedia("(prefers-color-scheme: dark)").matches === true;
} catch {
return false;
}
}
function normalizeThemeLibraryId(value) {
return String(value || "").trim();
}
function normalizeThemeLibraryThemeId(value, themes = []) {
const normalizedThemeId = normalizeThemeLibraryId(value);
return normalizedThemeId && themes.some((theme) => theme.id === normalizedThemeId)
? normalizedThemeId
: "";
}
function slugifyCustomThemePresetName(value) {
return String(value || "")
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
|| "theme";
}
function normalizeThemeLibraryLabel(value) {
return String(value || "").trim().replace(/\s+/g, " ");
}
function createCustomThemePresetId(label) {
return `${CUSTOM_THEME_PRESET_ID_PREFIX}${slugifyCustomThemePresetName(label)}`;
}
function createUniqueThemePresetId(label, themes = []) {
const baseId = createCustomThemePresetId(label);
const existingIds = new Set(themes.map((theme) => theme.id));
if (!existingIds.has(baseId)) {
return baseId;
}
for (let index = 2; index < 1000; index += 1) {
const nextId = `${baseId}-${index}`;
if (!existingIds.has(nextId)) {
return nextId;
}
}
return `${baseId}-${Date.now().toString(36)}`;
}
function backgroundPreviewForThemeSettings(settings) {
const background = settings?.background || {};
const type = String(background.type || "").trim().toLowerCase();
if (type === "radial") {
return `radial-gradient(circle at 32% 24%, ${background.radialInner || background.solidColor || "#111827"} 0%, ${background.radialOuter || background.solidColor || "#030712"} 100%)`;
}
if (type === "linear") {
return `linear-gradient(135deg, ${background.linearStart || background.solidColor || "#111827"} 0%, ${background.solidColor || background.linearStart || "#111827"} 52%, ${background.linearEnd || background.solidColor || "#030712"} 100%)`;
}
const color = background.solidColor || settings?.floor?.color || "#111827";
return `linear-gradient(135deg, ${color} 0%, ${color} 100%)`;
}
function createCustomThemePresetPreview(settings) {
const modelColor = settings?.materials?.fillColors?.[0] ||
settings?.materials?.defaultColor ||
DEFAULT_THEME_PRESET.settings.materials.defaultColor;
return {
background: backgroundPreviewForThemeSettings(settings),
modelColor,
accentColor: settings?.floor?.color || modelColor || DEFAULT_THEME_PRESET.settings.floor.color
};
}
function createThemeRecordFromPreset(preset) {
const settings = normalizeThemeSettings(preset?.settings);
return {
id: normalizeThemeLibraryId(preset?.id),
label: normalizeThemeLibraryLabel(preset?.label) || normalizeThemeLibraryId(preset?.id),
description: String(preset?.description || "Built-in theme"),
presetId: normalizeThemePresetId(preset?.id),
preview: preset?.preview || createCustomThemePresetPreview(settings),
settings
};
}
function createDefaultThemeLibrary() {
return THEME_PRESETS
.map(createThemeRecordFromPreset)
.filter((theme) => theme.id && theme.label);
}
function isCustomThemePresetId(value) {
return normalizeThemeLibraryId(value).startsWith(CUSTOM_THEME_PRESET_ID_PREFIX);
}
function normalizeCustomThemeLibraryThemeId(value, themes = []) {
const normalizedThemeId = normalizeThemeLibraryId(value);
return isCustomThemePresetId(normalizedThemeId) && themes.some((theme) => theme.id === normalizedThemeId)
? normalizedThemeId
: "";
}
function normalizeStoredCustomThemePreset(value) {
const rawId = normalizeThemeLibraryId(value?.id);
const builtInId = normalizeThemePresetId(rawId);
if (rawId && builtInId) {
return null;
}
const rawPresetId = normalizeThemeLibraryId(value?.presetId || value?.sourcePresetId);
const fallbackPresetId = normalizeThemePresetId(rawPresetId);
if (rawPresetId && !fallbackPresetId) {
return null;
}
const fallbackPreset = fallbackPresetId ? getThemePresetById(fallbackPresetId) : null;
const id = rawId || createCustomThemePresetId(value?.label || value?.name);
const label = normalizeThemeLibraryLabel(value?.label || value?.name || fallbackPreset?.label || id);
const rawTheme = value?.theme && typeof value.theme === "object" ? value.theme : value?.settings;
const settings = normalizeThemeSettings(
rawTheme && typeof rawTheme === "object"
? rawTheme
: fallbackPreset?.settings
);
if (!id || !label || !settings || normalizeThemePresetId(id)) {
return null;
}
const presetId = fallbackPresetId || "";
return {
id,
label,
description: String(value?.description || "Saved theme"),
presetId,
preview: createCustomThemePresetPreview(settings),
settings
};
}
function normalizeStoredCustomThemePresets(rawValue) {
const values = Array.isArray(rawValue) ? rawValue : rawValue?.themes;
if (!Array.isArray(values)) {
return [];
}
const seenIds = new Set();
const themes = [];
for (const value of values) {
const theme = normalizeStoredCustomThemePreset(value);
if (!theme || seenIds.has(theme.id)) {
continue;
}
seenIds.add(theme.id);
themes.push(theme);
}
return themes;
}
function normalizeThemeLibraryPayload(rawValue, options = {}) {
const fallbackToDefault = options.fallbackToDefault !== false;
const defaultThemes = fallbackToDefault ? createDefaultThemeLibrary() : [];
return [
...defaultThemes,
...normalizeStoredCustomThemePresets(rawValue)
];
}
function storedThemePresetPayload(preset) {
return {
id: preset.id,
label: preset.label,
description: preset.description,
presetId: preset.presetId || "",
theme: preset.settings
};
}
function settingsSignature(settings) {
return JSON.stringify(normalizeThemeSettings(settings));
}
function resolveDefaultActiveThemeId(themes = createDefaultThemeLibrary()) {
return normalizeThemeLibraryThemeId(DEFAULT_THEME_PRESET_ID, themes) || themes[0]?.id || DEFAULT_THEME_PRESET_ID;
}
function buildThemeStoragePayload({ activeThemeId = "", themeId = "", customPresets = [], themes = null } = {}) {
const themeLibrary = normalizeThemeLibraryPayload(themes || customPresets);
const customThemes = normalizeStoredCustomThemePresets(themeLibrary);
const normalizedThemeId = normalizeThemeLibraryThemeId(activeThemeId || themeId, themeLibrary);
const defaultThemeId = resolveDefaultActiveThemeId(themeLibrary);
if (!customThemes.length && (!normalizedThemeId || normalizedThemeId === defaultThemeId)) {
return null;
}
return {
version: THEME_STORAGE_VERSION,
activeThemeId: normalizedThemeId === defaultThemeId ? "" : normalizedThemeId,
themes: customThemes.map(storedThemePresetPayload)
};
}
function readThemeStorageState() {
const defaultThemes = createDefaultThemeLibrary();
if (typeof window === "undefined") {
return {
customPresets: defaultThemes,
themeId: resolveDefaultActiveThemeId(defaultThemes)
};
}
const rawValue = readStorageJson(window.localStorage, THEME_STORAGE_KEY);
if (!rawValue || rawValue.version !== THEME_STORAGE_VERSION) {
return {
customPresets: defaultThemes,
themeId: resolveDefaultActiveThemeId(defaultThemes)
};
}
const customPresets = normalizeThemeLibraryPayload(rawValue);
return {
customPresets,
themeId: normalizeThemeLibraryThemeId(rawValue.activeThemeId, customPresets) ||
resolveDefaultActiveThemeId(customPresets)
};
}
function writeThemeStorageState(state = {}, options = {}) {
if (typeof window === "undefined") {
return true;
}
const payload = buildThemeStoragePayload(state);
if (!payload) {
return removeStorageItem(window.localStorage, THEME_STORAGE_KEY, options);
}
return writeStorageJson(window.localStorage, THEME_STORAGE_KEY, payload, options);
}
export function readCustomThemePresets() {
return readThemeStorageState().customPresets;
}
export function writeCustomThemePresets(customPresets, options = {}) {
const currentThemeState = readThemeStorageState();
const presets = normalizeThemeLibraryPayload(customPresets);
return writeThemeStorageState({
themeId: currentThemeState.themeId,
customPresets: presets
}, options);
}
export function writeCustomThemePresetLibrary(customPresets, options = {}) {
const presets = normalizeThemeLibraryPayload(customPresets);
return writeThemeStorageState({
themeId: "",
customPresets: presets
}, options);
}
function createCustomThemePresetRecord(label, themeSettings, existingPresets, options = {}) {
const normalizedLabel = normalizeThemeLibraryLabel(label);
if (!normalizedLabel) {
return null;
}
const settings = normalizeThemeSettings(themeSettings);
const sourceTheme = getAvailableThemePresetById(options.sourceThemeId || options.presetId, existingPresets);
return normalizeStoredCustomThemePreset({
id: createUniqueThemePresetId(normalizedLabel, existingPresets),
label: normalizedLabel,
presetId: normalizeThemePresetId(options.sourcePresetId || sourceTheme?.presetId || sourceTheme?.id),
settings
});
}
export function saveCustomThemePreset(label, themeSettings, options = {}) {
const existingPresets = normalizeThemeLibraryPayload(options.customPresets || readCustomThemePresets());
const preset = createCustomThemePresetRecord(label, themeSettings, existingPresets, options);
if (!preset) {
return null;
}
const nextPresets = [
...existingPresets,
preset
];
if (!writeCustomThemePresets(nextPresets, options)) {
return null;
}
return preset;
}
export function saveAndActivateCustomThemePreset(label, themeSettings, options = {}) {
const existingPresets = normalizeThemeLibraryPayload(options.customPresets || readCustomThemePresets());
const preset = createCustomThemePresetRecord(label, themeSettings, existingPresets, options);
if (!preset) {
return null;
}
const customPresets = [
...existingPresets,
preset
];
if (!writeThemeStorageState({
activeThemeId: preset.id,
customPresets
}, options)) {
return null;
}
return {
preset,
customPresets
};
}
export function deleteCustomThemePreset(presetId, options = {}) {
const currentThemeState = readThemeStorageState();
const existingPresets = normalizeStoredCustomThemePresets(currentThemeState.customPresets);
const normalizedPresetId = normalizeCustomThemeLibraryThemeId(presetId, existingPresets);
if (!normalizedPresetId) {
return false;
}
const nextPresets = existingPresets.filter((preset) => preset.id !== normalizedPresetId);
if (nextPresets.length === existingPresets.length) {
return true;
}
const nextThemeId = currentThemeState.themeId === normalizedPresetId
? ""
: currentThemeState.themeId;
return writeThemeStorageState({
themeId: nextThemeId,
customPresets: nextPresets
}, options);
}
export function updateThemePresetSettings(presetId, themeSettings, options = {}) {
const currentThemeState = readThemeStorageState();
const existingPresets = normalizeStoredCustomThemePresets(currentThemeState.customPresets);
const normalizedPresetId = normalizeCustomThemeLibraryThemeId(presetId, existingPresets);
if (!normalizedPresetId) {
return false;
}
const settings = normalizeThemeSettings(themeSettings);
const nextPresets = existingPresets.map((preset) => (
preset.id === normalizedPresetId
? {
...preset,
preview: createCustomThemePresetPreview(settings),
settings
}
: preset
));
return writeThemeStorageState({
themeId: normalizedPresetId,
customPresets: nextPresets
}, options);
}
export function resetThemePresetToDefault(presetId, options = {}) {
const currentThemeState = readThemeStorageState();
const existingPresets = normalizeStoredCustomThemePresets(currentThemeState.customPresets);
const normalizedPresetId = normalizeCustomThemeLibraryThemeId(presetId, existingPresets);
const theme = existingPresets.find((candidate) => candidate.id === normalizedPresetId);
const sourcePresetId = normalizeThemePresetId(theme?.presetId || theme?.id);
if (!theme || !sourcePresetId) {
return false;
}
const presetRecord = createThemeRecordFromPreset(getThemePresetById(sourcePresetId));
const nextTheme = {
...theme,
presetId: sourcePresetId,
preview: createCustomThemePresetPreview(presetRecord.settings),
settings: presetRecord.settings
};
const nextPresets = existingPresets.map((candidate) => (
candidate.id === normalizedPresetId ? nextTheme : candidate
));
return writeThemeStorageState({
themeId: currentThemeState.themeId,
customPresets: nextPresets
}, options);
}
export function restoreDefaultThemePresets(options = {}) {
if (typeof window === "undefined") {
return true;
}
return removeStorageItem(window.localStorage, THEME_STORAGE_KEY, options);
}
export function buildAvailableThemePresets(customPresets = readCustomThemePresets()) {
return normalizeThemeLibraryPayload(customPresets);
}
export function getAvailableThemePresetById(presetId, customPresets = readCustomThemePresets()) {
const normalizedCustomPresets = normalizeThemeLibraryPayload(customPresets);
const normalizedPresetId = normalizeThemeLibraryThemeId(presetId, normalizedCustomPresets);
return buildAvailableThemePresets(customPresets).find((preset) => preset.id === normalizedPresetId) || null;
}
export function cloneAvailableThemePresetSettings(presetId, customPresets = readCustomThemePresets()) {
const preset = getAvailableThemePresetById(presetId, customPresets);
return normalizeThemeSettings(preset?.settings || cloneThemePresetSettings(presetId));
}
export function getAvailableThemePresetIdForSettings(themeSettings, customPresets = readCustomThemePresets()) {
const normalizedSettings = normalizeThemeSettings(themeSettings);
const normalizedSignature = JSON.stringify(normalizedSettings);
for (const preset of normalizeThemeLibraryPayload(customPresets)) {
if (JSON.stringify(normalizeThemeSettings(preset.settings)) === normalizedSignature) {
return preset.id;
}
}
return null;
}
function cloneThemeSettingsState(
presetId = "",
settings = null,
customPresets = readCustomThemePresets()
) {
const themes = normalizeThemeLibraryPayload(customPresets);
const normalizedPresetId = normalizeThemeLibraryThemeId(
presetId || resolveDefaultActiveThemeId(themes),
themes
) || themes[0]?.id || DEFAULT_THEME_PRESET_ID;
return {
presetId: normalizedPresetId,
settings: normalizeThemeSettings(settings || cloneAvailableThemePresetSettings(normalizedPresetId, customPresets))
};
}
export function readThemeSettingsStateFromAppearanceQuery(customPresets = readCustomThemePresets()) {
if (typeof window === "undefined") {
return null;
}
const search = String(window.location?.search || "").trim();
if (!search) {
return null;
}
let params;
try {
params = new URLSearchParams(search);
} catch {
return null;
}
const requestedPresetId = String(params.get(THEME_APPEARANCE_QUERY_PARAM) || "").trim();
if (!requestedPresetId) {
return null;
}
const presets = normalizeThemeLibraryPayload(customPresets);
const normalizedPresetId = normalizeThemeLibraryThemeId(requestedPresetId, presets);
return normalizedPresetId ? cloneThemeSettingsState(normalizedPresetId, null, presets) : null;
}
function isPlainStorageObject(value) {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function normalizeDirectorySessionThemeSlice(value) {
if (!isPlainStorageObject(value) || !isPlainStorageObject(value.settings)) {
return null;
}
const settings = normalizeThemeSettings(value.settings);
if (!settings) {
return null;
}
return {
presetId: normalizeThemeLibraryId(value.presetId),
settings
};
}
export function createDirectorySessionThemeSlice(themeState = {}, customPresets = readCustomThemePresets()) {
if (!isPlainStorageObject(themeState) || !isPlainStorageObject(themeState.settings)) {
return null;
}
const themes = normalizeThemeLibraryPayload(customPresets);
const settings = normalizeThemeSettings(themeState.settings);
const presetId = normalizeThemeLibraryThemeId(themeState.presetId, themes) ||
getAvailableThemePresetIdForSettings(settings, themes) ||
resolveDefaultActiveThemeId(themes);
const savedPreset = getAvailableThemePresetById(presetId, themes);
if (savedPreset && settingsSignature(settings) === settingsSignature(savedPreset.settings)) {
return null;
}
return {
presetId,
settings
};
}
export function isDirectorySessionThemeSlice(themeSlice) {
return normalizeDirectorySessionThemeSlice(themeSlice) !== null;
}
export function readDirectoryThemeSettingsState(customPresets = readCustomThemePresets()) {
const queryState = readThemeSettingsStateFromAppearanceQuery(customPresets);
if (queryState) {
return queryState;
}
const globalThemeState = readThemeSettingsState(customPresets);
const sessionTheme = readCadDirectorySessionState({ customPresets }).theme;
if (!isDirectorySessionThemeSlice(sessionTheme)) {
return globalThemeState;
}
const themes = normalizeThemeLibraryPayload(customPresets);
const presetId = normalizeThemeLibraryThemeId(sessionTheme.presetId, themes) ||
globalThemeState.presetId;
return {
presetId,
settings: normalizeThemeSettings(sessionTheme.settings)
};
}
export function serializeThemeSettingsForStorage(themeSettings, options = {}) {
const customPresets = normalizeThemeLibraryPayload(options.themes || options.customPresets || readCustomThemePresets());
const presetId = normalizeThemeLibraryThemeId(
options.presetId || getAvailableThemePresetIdForSettings(themeSettings, customPresets),
customPresets
) || resolveDefaultActiveThemeId(customPresets);
return buildThemeStoragePayload({
themeId: presetId,
customPresets
});
}
function storageValuesEqual(a, b) {
return JSON.stringify(a) === JSON.stringify(b);
}
export function parseThemeSettingsStateFromStorage(rawValue, customPresets = null) {
const presets = rawValue?.version === THEME_STORAGE_VERSION
? normalizeThemeLibraryPayload(rawValue)
: normalizeThemeLibraryPayload(customPresets);
const storedPresetId = rawValue?.version === THEME_STORAGE_VERSION
? normalizeThemeLibraryThemeId(rawValue.activeThemeId, presets)
: "";
return cloneThemeSettingsState(storedPresetId, null, presets);
}
export function parseThemeSettingsFromStorage(rawValue) {
return parseThemeSettingsStateFromStorage(rawValue).settings;
}
export function readThemeSettingsState(customPresets = readCustomThemePresets()) {
const queryState = readThemeSettingsStateFromAppearanceQuery(customPresets);
if (queryState) {
return queryState;
}
if (typeof window === "undefined") {
return cloneThemeSettingsState("", null, customPresets);
}
const storageState = readThemeStorageState();
const presets = customPresets ? normalizeThemeLibraryPayload(customPresets) : storageState.customPresets;
const storedPresetId = normalizeThemeLibraryThemeId(storageState.themeId, presets);
return cloneThemeSettingsState(storedPresetId, null, presets);
}
export function readThemeSettings() {
return readThemeSettingsState().settings;
}
export function writeThemeSettings(themeSettings, options = {}) {
if (typeof window === "undefined") {
return true;
}
const serialized = serializeThemeSettingsForStorage(themeSettings, options);
if (!serialized) {
return removeStorageItem(window.localStorage, THEME_STORAGE_KEY, options);
}
return writeStorageJson(window.localStorage, THEME_STORAGE_KEY, serialized, options);
}
export function normalizeCadWorkspaceGlassTone(value, fallback = CAD_WORKSPACE_DEFAULT_GLASS_TONE) {
const normalized = String(value || "").trim().toLowerCase();
if (normalized === "dark" || normalized === "light") {
return normalized;
}
return fallback === "light" ? "light" : "dark";
}
export function readCadWorkspaceGlassTone() {
return CAD_WORKSPACE_DEFAULT_GLASS_TONE;
}
export function createTabSnapshot(overrides = {}) {
return normalizeSchemaState(TAB_STATE_SCHEMA, overrides || {});
}
export function cloneTabSnapshot(snapshot) {
return cloneSchemaState(TAB_STATE_SCHEMA, snapshot || {});
}
export function tabSnapshotEqual(a, b) {
return schemaStateEqual(TAB_STATE_SCHEMA, a || {}, b || {});
}
export function createTabRecord(key, overrides = {}) {
const snapshot = cloneTabSnapshot(overrides);
if (!snapshot.inspectedAssemblyNodeId && Array.isArray(overrides?.expandedAssemblyPartIds)) {
snapshot.inspectedAssemblyNodeId = String(
overrides.expandedAssemblyPartIds[overrides.expandedAssemblyPartIds.length - 1] || ""
).trim();
}
return {
key: normalizeTabKey(key),
...snapshot
};
}