UNPKG

ducjs

Version:

The duc 2D CAD file format is a cornerstone of our advanced design system, conceived to cater to professionals seeking precision and efficiency in their design work.

717 lines (716 loc) 32.3 kB
import { BEZIER_MIRRORING, BLENDING, BOOLEAN_OPERATION, ELEMENT_CONTENT_PREFERENCE, IMAGE_STATUS, LINE_HEAD, PRUNING_LEVEL, STROKE_CAP, STROKE_JOIN, STROKE_PLACEMENT, STROKE_PREFERENCE, STROKE_SIDE_PREFERENCE, TEXT_ALIGN, VERTICAL_ALIGN, } from "../flatbuffers/duc"; import { restoreElements } from "./restoreElements"; import { isStandardIdPresent, restoreStandards, } from "./restoreStandards"; import { getPrecisionScope } from "../technical/measurements"; import { getPrecisionValueFromRaw, getPrecisionValueFromScoped, NEUTRAL_SCOPE, ScaleFactors } from "../technical/scopes"; import { PREDEFINED_STANDARDS } from "../technical/standards"; import { base64ToUint8Array, getDefaultGlobalState, getDefaultLocalState, getZoom, isEncodedFunctionString, isFiniteNumber, reviveEncodedFunction, } from "../utils"; import { DEFAULT_ELEMENT_PROPS, DEFAULT_POLYGON_SIDES, DEFAULT_TEXT_ALIGN, DEFAULT_VERTICAL_ALIGN, DEFAULT_ZOOM_STEP, MAX_ZOOM_STEP, MIN_ZOOM_STEP, } from "../utils/constants"; import { getDefaultStackProperties } from "../utils/elements"; import tinycolor from "tinycolor2"; export const restore = (data, elementsConfig) => { const restoredStandards = restoreStandards(data === null || data === void 0 ? void 0 : data.standards); const restoredDictionary = restoreDictionary(data === null || data === void 0 ? void 0 : data.dictionary); const restoredGlobalState = restoreGlobalState(data === null || data === void 0 ? void 0 : data.globalState); const restoredLocalState = restoreLocalState(data === null || data === void 0 ? void 0 : data.localState, restoredGlobalState, restoredStandards); const restoredElementsConfig = Object.assign(Object.assign({}, elementsConfig), { localState: restoredLocalState }); const restoredBlocks = restoreBlocks(data === null || data === void 0 ? void 0 : data.blocks, restoredLocalState.scope, restoredElementsConfig); const restoredRegions = restoreRegions(data === null || data === void 0 ? void 0 : data.regions); const restoredGroups = restoreGroups(data === null || data === void 0 ? void 0 : data.groups); const restoredLayers = restoreLayers(data === null || data === void 0 ? void 0 : data.layers, restoredLocalState.scope); const restoredElements = restoreElements(data === null || data === void 0 ? void 0 : data.elements, restoredLocalState.scope, restoredBlocks, restoredElementsConfig); const restoredVersionGraph = restoreVersionGraph(data === null || data === void 0 ? void 0 : data.versionGraph); return { dictionary: restoredDictionary, thumbnail: isValidUint8Array(data === null || data === void 0 ? void 0 : data.thumbnail), elements: restoredElements, blocks: restoredBlocks, groups: restoredGroups, regions: restoredRegions, layers: restoredLayers, standards: restoredStandards, versionGraph: restoredVersionGraph, localState: restoredLocalState, globalState: restoredGlobalState, files: restoreFiles(data === null || data === void 0 ? void 0 : data.files), }; }; export const restoreFiles = (importedFiles) => { var _a; if (!importedFiles || typeof importedFiles !== "object") { return {}; } const restoredFiles = {}; const files = importedFiles; for (const key in files) { if (Object.prototype.hasOwnProperty.call(files, key)) { const fileData = files[key]; if (!fileData || typeof fileData !== "object") { continue; } const id = isValidExternalFileId(fileData.id); const mimeType = isValidString(fileData.mimeType); const created = isFiniteNumber(fileData.created) ? fileData.created : Date.now(); // Check for data under 'data' or 'dataURL' to be more flexible. const dataSource = (_a = fileData.data) !== null && _a !== void 0 ? _a : fileData.dataURL; const data = isValidUint8Array(dataSource); if (id && mimeType && data) { restoredFiles[id] = { id, mimeType, data, created, lastRetrieved: isFiniteNumber(fileData.lastRetrieved) ? fileData.lastRetrieved : undefined, version: isFiniteNumber(fileData.version) ? fileData.version : undefined, }; } } } return restoredFiles; }; export const restoreDictionary = (importedDictionary) => { if (!importedDictionary || typeof importedDictionary !== "object") { return {}; } const restoredDictionary = {}; const dict = importedDictionary; for (const key in dict) { if (Object.prototype.hasOwnProperty.call(dict, key)) { restoredDictionary[key] = typeof dict[key] === "string" ? dict[key] : String(dict[key]); } } return restoredDictionary; }; /** * Restores the groups array from imported data, ensuring each item * conforms to the DucGroup type. * * This function iterates through the raw input, filters out any invalid entries, * and constructs a new array of clean, validated DucGroup objects. * * @param groups - The raw, untrusted array of group-like objects. * @returns A validated array of DucGroup objects, or an empty array if the input is invalid. */ export const restoreGroups = (groups) => { if (!Array.isArray(groups)) { return []; } return groups .filter((g) => { if (!g || typeof g !== "object") return false; return typeof g.id === "string"; }) .map((g) => { return Object.assign({ id: g.id }, restoreDucStackProperties(g)); }); }; /** * Restores the layers array from imported data, ensuring each item * conforms to the DucLayer type. * * This function deeply validates each layer, including its nested 'overrides' * for default stroke and background styles. It provides safe defaults for any * missing or invalid properties. * * @param layers - The raw, untrusted array of layer-like objects. * @param currentScope - The current drawing scope, required for restoring * scope-dependent properties like stroke width. * @returns A validated array of DucLayer objects, or an empty array if the input is invalid. */ export const restoreLayers = (layers, currentScope) => { if (!Array.isArray(layers)) { return []; } return layers .filter((g) => { if (!g || typeof g !== "object") return false; return typeof g.id === "string"; }) .map((l) => { return Object.assign(Object.assign({ id: l.id }, restoreDucStackProperties(l)), { readonly: isValidBoolean(l.readonly, false), overrides: { stroke: validateStroke(l.overrides.stroke, currentScope, currentScope), background: validateBackground(l.overrides.background), } }); }); }; /** * Restores the regions array, ensuring correct structure and types. */ export const restoreRegions = (regions) => { if (!Array.isArray(regions)) { return []; } return regions .filter((g) => { if (!g || typeof g !== "object") return false; return typeof g.id === "string"; }) .map((r) => { return Object.assign(Object.assign({ type: "region", id: r.id }, restoreDucStackProperties(r)), { booleanOperation: Object.values(BOOLEAN_OPERATION).includes(r.booleanOperation) ? r.booleanOperation : BOOLEAN_OPERATION.UNION }); }); }; /** * Restores the blocks array using a two-pass approach to resolve circular dependencies. * * Pass 1: Creates a "shallow" version of each block with all top-level properties * but an empty `elements` array. This provides a complete reference list of all blocks. * * Pass 2: Iterates over the shallow blocks, calling `restoreElements` for each one, * providing the complete block list from Pass 1 as the necessary context. */ export const restoreBlocks = (blocks, currentScope, elementsConfig) => { if (!Array.isArray(blocks)) { return []; } const partiallyRestoredBlocks = blocks .filter((b) => { if (!b || typeof b !== "object") return false; const obj = b; return typeof obj.id === "string"; }) .map((b) => { const obj = b; return { id: obj.id, label: typeof obj.label === "string" ? obj.label : "", description: typeof obj.description === "string" ? obj.description : undefined, version: typeof obj.version === "number" ? obj.version : 1, attributes: obj.attributes || undefined, attributeDefinitions: obj.attributeDefinitions && typeof obj.attributeDefinitions === "object" ? obj.attributeDefinitions : {}, elements: [], }; }); partiallyRestoredBlocks.forEach((restoredBlock) => { const originalBlockData = blocks.find((b) => b.id === restoredBlock.id); if (originalBlockData && originalBlockData.elements) { const finalElements = restoreElements(originalBlockData.elements, currentScope, partiallyRestoredBlocks, elementsConfig); restoredBlock.elements = finalElements; } }); return partiallyRestoredBlocks; }; /** * Restores the global state of the document from imported data. * It validates and provides defaults for missing or invalid properties. * * @param importedState - The partially imported global state data. * @returns A complete and valid DucGlobalState object. */ export const restoreGlobalState = (importedState = {}) => { var _a, _b, _c, _d, _e, _f, _g, _h, _j; const defaults = getDefaultGlobalState(); const linearPrecision = isValidFinitePositiveByteValue(importedState.coordDecimalPlaces, defaults.displayPrecision.linear); return Object.assign(Object.assign({}, defaults), { name: (_a = importedState.name) !== null && _a !== void 0 ? _a : defaults.name, viewBackgroundColor: (_b = importedState.viewBackgroundColor) !== null && _b !== void 0 ? _b : defaults.viewBackgroundColor, mainScope: (_c = isValidAppStateScopeValue(importedState.mainScope)) !== null && _c !== void 0 ? _c : defaults.mainScope, scopeExponentThreshold: isValidAppStateScopeExponentThresholdValue(importedState.scopeExponentThreshold, defaults.scopeExponentThreshold), dashSpacingScale: (_d = importedState.dashSpacingScale) !== null && _d !== void 0 ? _d : defaults.dashSpacingScale, isDashSpacingAffectedByViewportScale: (_e = importedState.isDashSpacingAffectedByViewportScale) !== null && _e !== void 0 ? _e : defaults.isDashSpacingAffectedByViewportScale, dimensionsAssociativeByDefault: (_f = importedState.dimensionsAssociativeByDefault) !== null && _f !== void 0 ? _f : defaults.dimensionsAssociativeByDefault, useAnnotativeScaling: (_g = importedState.useAnnotativeScaling) !== null && _g !== void 0 ? _g : defaults.useAnnotativeScaling, displayPrecision: { linear: linearPrecision, angular: (_j = (_h = importedState.displayPrecision) === null || _h === void 0 ? void 0 : _h.angular) !== null && _j !== void 0 ? _j : defaults.displayPrecision.angular, }, pruningLevel: importedState.pruningLevel && Object.values(PRUNING_LEVEL).includes(importedState.pruningLevel) ? importedState.pruningLevel : PRUNING_LEVEL.BALANCED }); }; /** * Restores the user's local session state from imported data. * It requires the already-restored global state to correctly calculate dependent values * like zoom and scope. * * @param importedState - The partially imported local state data. * @param restoredGlobalState - The complete and valid global state for the document. * @returns A complete and valid DucLocalState object. */ export const restoreLocalState = (importedState = {}, restoredGlobalState, restoredStandards) => { var _a, _b, _c, _d, _e, _f, _g, _h; const defaults = getDefaultLocalState(); const zoom = getZoom((_b = (_a = importedState.zoom) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : defaults.zoom.value, restoredGlobalState.mainScope, restoredGlobalState.scopeExponentThreshold); const scope = isValidPrecisionScopeValue(zoom.value, restoredGlobalState.mainScope, restoredGlobalState.scopeExponentThreshold); return Object.assign(Object.assign(Object.assign({}, defaults), importedState), { scope, activeStandardId: isValidStandardId(importedState.activeStandardId, restoredStandards, defaults.activeStandardId), isBindingEnabled: isValidBoolean(importedState.isBindingEnabled, defaults.isBindingEnabled), penMode: isValidBoolean(importedState.penMode, defaults.penMode), scrollX: importedState.scrollX ? restorePrecisionValue(importedState.scrollX, NEUTRAL_SCOPE, scope) : getPrecisionValueFromRaw(defaults.scrollX.value, NEUTRAL_SCOPE, scope), scrollY: importedState.scrollY ? restorePrecisionValue(importedState.scrollY, NEUTRAL_SCOPE, scope) : getPrecisionValueFromRaw(defaults.scrollY.value, NEUTRAL_SCOPE, scope), zoom, activeGridSettings: (_c = importedState.activeGridSettings) !== null && _c !== void 0 ? _c : defaults.activeGridSettings, activeSnapSettings: (_d = importedState.activeSnapSettings) !== null && _d !== void 0 ? _d : defaults.activeSnapSettings, currentItemStroke: (_e = validateStroke(importedState.currentItemStroke, scope, scope)) !== null && _e !== void 0 ? _e : defaults.currentItemStroke, currentItemBackground: (_f = validateBackground(importedState.currentItemBackground)) !== null && _f !== void 0 ? _f : defaults.currentItemBackground, currentItemOpacity: isValidPercentageValue(importedState.currentItemOpacity, defaults.currentItemOpacity), currentItemStartLineHead: (_g = isValidLineHeadValue(importedState.currentItemStartLineHead)) !== null && _g !== void 0 ? _g : defaults.currentItemStartLineHead, currentItemEndLineHead: (_h = isValidLineHeadValue(importedState.currentItemEndLineHead)) !== null && _h !== void 0 ? _h : defaults.currentItemEndLineHead }); }; export const restoreVersionGraph = (importedGraph) => { if (!importedGraph || typeof importedGraph !== "object") { return undefined; } const userCheckpointVersionId = isValidString(importedGraph.userCheckpointVersionId); const latestVersionId = isValidString(importedGraph.latestVersionId); if (!userCheckpointVersionId || !latestVersionId) { return undefined; } const checkpoints = []; if (Array.isArray(importedGraph.checkpoints)) { for (const c of importedGraph.checkpoints) { if (!c || typeof c !== "object" || c.type !== "checkpoint") { continue; } const id = isValidString(c.id); if (!id) { continue; } const parentId = typeof c.parentId === "string" ? c.parentId : null; const timestamp = isFiniteNumber(c.timestamp) ? c.timestamp : 0; const isManualSave = isValidBoolean(c.isManualSave, false); const sizeBytes = isFiniteNumber(c.sizeBytes) && c.sizeBytes >= 0 ? c.sizeBytes : 0; const data = isValidUint8Array(c.data); if (!data) { continue; } checkpoints.push({ type: "checkpoint", id, parentId, timestamp, isManualSave, sizeBytes, data, description: isValidString(c.description) || undefined, userId: isValidString(c.userId) || undefined, }); } } const deltas = []; if (Array.isArray(importedGraph.deltas)) { for (const d of importedGraph.deltas) { if (!d || typeof d !== "object" || d.type !== "delta") { continue; } const id = isValidString(d.id); if (!id) { continue; } const parentId = typeof d.parentId === "string" ? d.parentId : null; const timestamp = isFiniteNumber(d.timestamp) ? d.timestamp : 0; const isManualSave = isValidBoolean(d.isManualSave, false); if (!Array.isArray(d.patch)) { continue; } const patch = []; let isPatchValid = true; for (const op of d.patch) { if (!op || typeof op !== "object" || !isValidString(op.op) || !isValidString(op.path)) { isPatchValid = false; break; } patch.push({ op: op.op, path: op.path, value: op.value }); } if (!isPatchValid) { continue; } deltas.push({ type: "delta", id, parentId, timestamp, isManualSave, patch, description: isValidString(d.description) || undefined, userId: isValidString(d.userId) || undefined, }); } } const importedMetadata = importedGraph.metadata; const metadata = { lastPruned: isFiniteNumber(importedMetadata === null || importedMetadata === void 0 ? void 0 : importedMetadata.lastPruned) ? importedMetadata.lastPruned : 0, totalSize: isFiniteNumber(importedMetadata === null || importedMetadata === void 0 ? void 0 : importedMetadata.totalSize) && importedMetadata.totalSize >= 0 ? importedMetadata.totalSize : 0, }; return { userCheckpointVersionId, latestVersionId, checkpoints, deltas, metadata, }; }; /** * Restores common properties for elements leveraging _DucStackBase. */ export const restoreDucStackProperties = (stack) => { const defaultStackProperties = getDefaultStackProperties(); return { label: typeof stack.label === "string" ? stack.label : "", description: typeof stack.description === "string" ? stack.description : null, isCollapsed: isValidBoolean(stack.isCollapsed, defaultStackProperties.isCollapsed), locked: isValidBoolean(stack.locked, defaultStackProperties.locked), isVisible: isValidBoolean(stack.isVisible, defaultStackProperties.isVisible), isPlot: isValidBoolean(stack.isPlot, defaultStackProperties.isPlot), opacity: isValidPercentageValue(stack.opacity, defaultStackProperties.opacity), labelingColor: isValidColor(stack.labelingColor, defaultStackProperties.labelingColor), }; }; export const isValidAppStateScopeValue = (value) => { if (value !== undefined && Object.keys(ScaleFactors).includes(value)) { return value; } return NEUTRAL_SCOPE; }; export const isValidAppStateScopeExponentThresholdValue = (value, defaultValue) => { const finite = isValidFinitePositiveByteValue(value, defaultValue); if (finite >= 1 && finite <= 36) { return finite; } return defaultValue; }; export const isValidPrecisionScopeValue = (zoom, mainScope, scopeExponentThreshold) => { return getPrecisionScope(zoom, mainScope, scopeExponentThreshold); }; /** * Converts a plain number or legacy value to a PrecisionValue object * @param value - The value to convert (can be a raw number or legacy value) * @param elementScope - The scope to use for the precision value * @returns A properly formatted PrecisionValue object */ export const restorePrecisionValue = (value, elementScope, currentScope, defaultValue, fromScoped = false) => { const fallbackValue = getPrecisionValueFromRaw((defaultValue !== null && defaultValue !== void 0 ? defaultValue : 0), currentScope, currentScope); if (value === undefined || value === null) { return fallbackValue; } if (typeof value === "number") { if (!Number.isFinite(value)) { return fallbackValue; } return fromScoped ? getPrecisionValueFromScoped(value, elementScope, currentScope) : getPrecisionValueFromRaw(value, elementScope, currentScope); } return getPrecisionValueFromRaw(value.value, elementScope, currentScope); }; export const isValidFillStyleValue = (value) => { if (value === undefined || !Object.values(ELEMENT_CONTENT_PREFERENCE).includes(value)) return ELEMENT_CONTENT_PREFERENCE.SOLID; return value; }; export const isValidStrokePreferenceValue = (value) => { if (value === undefined || !Object.values(STROKE_PREFERENCE).includes(value)) return STROKE_PREFERENCE.SOLID; return value; }; export const isValidVerticalAlignValue = (value) => { if (value === undefined || !Object.values(VERTICAL_ALIGN).includes(value)) return DEFAULT_VERTICAL_ALIGN; return value; }; export const isValidTextAlignValue = (value) => { if (value === undefined || !Object.values(TEXT_ALIGN).includes(value)) return DEFAULT_TEXT_ALIGN; return value; }; export const isValidScopeValue = (value, localState, mainScope) => { if (value !== undefined && Object.keys(ScaleFactors).includes(value)) { return value; } if (mainScope && Object.keys(ScaleFactors).includes(mainScope)) { return mainScope; } if ((localState === null || localState === void 0 ? void 0 : localState.scope) && Object.keys(ScaleFactors).includes(localState.scope)) { return localState.scope; } return NEUTRAL_SCOPE; }; export const isValidImageStatusValue = (value) => { if (value === undefined || !Object.values(IMAGE_STATUS).includes(value)) return IMAGE_STATUS.PENDING; return value; }; export const isValidDucHead = (value, blocks, elementScope, currentScope) => { if (value === undefined || value === null) return null; const type = isValidLineHeadValue(value.type); const blockId = isValidBlockId(value.blockId, blocks); if (type === null || blockId === null) return null; return { type, blockId, size: restorePrecisionValue(value.size, elementScope, currentScope), }; }; export const isValidLineHeadValue = (value) => { if (value === undefined || value === null || !Object.values(LINE_HEAD).includes(value)) return null; return value; }; export const isValidZoomStepValue = (value) => { if (value === undefined || value < MIN_ZOOM_STEP || value > MAX_ZOOM_STEP) return DEFAULT_ZOOM_STEP; return value; }; export const isValidImageScaleValue = (value) => { if (value === undefined || value[0] === 0 || value[1] === 0) return [1, 1]; return value; }; export const isValidBezierMirroringValue = (value) => { if (value === undefined || !Object.values(BEZIER_MIRRORING).includes(value)) return undefined; return value; }; export const isValidStrokeSidePreferenceValue = (value) => { if (value === undefined || !Object.values(STROKE_SIDE_PREFERENCE).includes(value)) return STROKE_SIDE_PREFERENCE.TOP; return value; }; export const isValidStrokeCapValue = (value) => { if (value === undefined || !Object.values(STROKE_CAP).includes(value)) return STROKE_CAP.BUTT; return value; }; export const isValidStrokeJoinValue = (value) => { if (value === undefined || !Object.values(STROKE_JOIN).includes(value)) return STROKE_JOIN.MITER; return value; }; export const isValidStrokeDashValue = (value) => { if (!value || !Array.isArray(value)) return []; return value; }; export const isValidStrokeMiterLimitValue = (value) => { if (value === undefined || value < 0 || value > 100) return 4; return value; }; export const isValidBlendingValue = (value) => { if (value === undefined || !Object.values(BLENDING).includes(value)) return undefined; return value; }; export const validateElementContent = ({ content, defaultContent, }) => { var _a; return { preference: isValidFillStyleValue(content === null || content === void 0 ? void 0 : content.preference), src: (_a = content === null || content === void 0 ? void 0 : content.src) !== null && _a !== void 0 ? _a : defaultContent.src, visible: isValidBoolean(content === null || content === void 0 ? void 0 : content.visible, defaultContent.visible), opacity: isValidPercentageValue(content === null || content === void 0 ? void 0 : content.opacity, defaultContent.opacity), tiling: (content === null || content === void 0 ? void 0 : content.tiling) || defaultContent.tiling, }; }; export const validateStrokeStyle = (style) => { if (!style) { return { preference: STROKE_PREFERENCE.SOLID, cap: STROKE_CAP.BUTT, join: STROKE_JOIN.MITER, dash: [], miterLimit: 4, }; } return { preference: isValidStrokePreferenceValue(style.preference), cap: isValidStrokeCapValue(style.cap), join: isValidStrokeJoinValue(style.join), dash: isValidStrokeDashValue(style.dash), miterLimit: isValidStrokeMiterLimitValue(style.miterLimit), }; }; const validateStrokeSides = (sides) => { if (!sides) return undefined; return { preference: isValidStrokeSidePreferenceValue(sides.preference), values: sides.values || undefined, }; }; export const validateStroke = (stroke, elementScope, currentScope) => { var _a; return { content: validateElementContent({ content: stroke === null || stroke === void 0 ? void 0 : stroke.content, defaultContent: DEFAULT_ELEMENT_PROPS.stroke.content, }), placement: (_a = stroke === null || stroke === void 0 ? void 0 : stroke.placement) !== null && _a !== void 0 ? _a : STROKE_PLACEMENT.CENTER, width: restorePrecisionValue(stroke === null || stroke === void 0 ? void 0 : stroke.width, elementScope, currentScope, DEFAULT_ELEMENT_PROPS.stroke.width.value), style: validateStrokeStyle(stroke === null || stroke === void 0 ? void 0 : stroke.style), strokeSides: validateStrokeSides(stroke === null || stroke === void 0 ? void 0 : stroke.strokeSides), }; }; export const validateBackground = (bg) => { return { content: validateElementContent({ content: bg === null || bg === void 0 ? void 0 : bg.content, defaultContent: DEFAULT_ELEMENT_PROPS.background.content, }), }; }; export const isValidFinitePositiveByteValue = (value, defaultValue) => { if (value === undefined || !Number.isFinite(value)) { return defaultValue; } const roundedValue = Math.round(value); return Math.max(0, Math.min(255, roundedValue)); }; export const isValidPolygonSides = (sides) => { if (sides >= 3) { if (Number.isInteger(sides)) { return sides; } else { return Math.round(sides); } } return DEFAULT_POLYGON_SIDES; }; export const isValidRadianValue = (value, defaultValue) => { if (value === undefined || !Number.isFinite(value)) { return defaultValue; } if (value > Math.PI * 2 || value < -Math.PI * 2) { return defaultValue; } return value; }; /** * Validates a Percentage value. * Returns a clamped value within the <0,1> range or a provided default. */ export const isValidPercentageValue = (value, defaultValue, allowNegative = false) => { if (value === undefined || !Number.isFinite(value)) { return defaultValue; } if (value > 1 && value <= 100) { value /= 100; } return Math.max(allowNegative ? -1 : 0, Math.min(1, value)); }; export const isValidBoolean = (value, defaultValue = false) => { return typeof value === "boolean" ? value : defaultValue; }; /** * Ensures the supplied easing function is valid. Falls back to the default easing otherwise. */ export const isValidFunction = (value, defaultFn) => { if (typeof value === "function") return value; if (typeof value === "string" && isEncodedFunctionString(value)) { const revived = reviveEncodedFunction(value); if (typeof revived === "function") return revived; } return defaultFn; }; export const isValidColor = (value, defaultValue = "#000000") => { const color = tinycolor(value); return color.isValid() ? color.toHexString() : defaultValue; }; /** * Validates a string value. * Returns the string if valid, otherwise returns the provided defaultValue or an empty string. */ export const isValidString = (value, defaultValue = "") => { if (typeof value !== "string") return defaultValue; if (value.trim().length === 0) return defaultValue; return value; }; export const isValidExternalFileId = (value, defaultValue = "") => { if (typeof value !== "string") return defaultValue; if (value.trim().length === 0) return defaultValue; return value; }; /** * Validates a value to ensure it is or can be converted to a non-empty Uint8Array. * * This function handles three types of input: * 1. An existing `Uint8Array`. * 2. A Base64-encoded string. * 3. A full Data URL string (e.g., "data:image/png;base64,..."). * * @param value The unknown value to validate. * @returns A valid, non-empty `Uint8Array` if the conversion is successful, otherwise `undefined`. */ export const isValidUint8Array = (value) => { if (value instanceof Uint8Array) { return value.byteLength > 0 ? value : undefined; } if (typeof value === "string") { let base64String = value; if (value.startsWith("data:")) { const commaIndex = value.indexOf(","); if (commaIndex === -1) { console.warn("Invalid Data URL format: missing comma."); return undefined; // Malformed Data URL } // Ensure it's a base64-encoded Data URL const header = value.substring(0, commaIndex); if (!header.includes(";base64")) { console.warn("Unsupported Data URL: only base64 encoding is supported."); return undefined; } // Extract the actual base64 payload base64String = value.substring(commaIndex + 1); } try { if (base64String.trim().length === 0) { return undefined; } const decodedData = base64ToUint8Array(base64String); return decodedData.byteLength > 0 ? decodedData : undefined; } catch (error) { console.warn("Failed to decode base64 string:", error); return undefined; } } return undefined; }; /** * Validates a Standard id. * Returns the id if valid and present in standards, otherwise returns the default DUC standard id. */ export const isValidStandardId = (id, standards, defaultId = PREDEFINED_STANDARDS.DUC) => { const validId = isValidString(id); if (isStandardIdPresent(validId, standards)) return validId; return defaultId; }; /** * Validates a block id. * Returns the id if present in restored blocks, otherwise returns null. */ export const isValidBlockId = (blockId, blocks) => { const validId = isValidString(blockId); if (blocks.some((b) => b.id === validId)) return validId; return null; }; /** * A generic helper to validate an enum value from a lookup object. * @param value The value to check. * @param enumObject The object containing valid enum values (e.g., VIEWPORT_SHADE_PLOT). * @param defaultValue The value to return if the input is invalid. */ export const isValidEnumValue = (value, enumObject, defaultValue) => { if (value !== undefined && Object.values(enumObject).includes(value)) { return value; } return defaultValue; };