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
JavaScript
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;
};