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.

911 lines 71 kB
import { isValidBezierMirroringValue, isValidBlendingValue, isValidBoolean, isValidBoolean as isValidBooleanValue, // Renaming to avoid conflict isValidColor, isValidDucHead, isValidEnumValue, isValidFunction, isValidImageScaleValue, isValidImageStatusValue, isValidPercentageValue, isValidPolygonSides, isValidRadianValue, isValidString, isValidTextAlignValue, isValidVerticalAlignValue, restoreDucStackProperties, restorePrecisionValue, validateBackground, validateStroke } from "./restoreDataState"; import { getPrecisionValueFromRaw, getPrecisionValueFromScoped, getScaledZoomValueForScope, getScopedBezierPointFromDucPoint, getScopedZoomValue, NEUTRAL_SCOPE, ScaleFactors, } from "../technical/scopes"; import { isElbowArrow, isLinearElement, isTextElement, } from "../types"; import { arrayToMap, bumpVersion, DEFAULT_ELEMENT_PROPS, DEFAULT_ELLIPSE_ELEMENT, DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, DEFAULT_FREEDRAW_ELEMENT, detectLineHeight, FONT_FAMILY, getContainerElement, getDefaultLocalState, getDefaultTableData, getNormalizedDimensions, getNormalizedPoints, getNormalizedZoom, getSizeFromPoints, getUpdatedTimestamp, isFiniteNumber, isInvisiblySmallElement, LINE_CONFIRM_THRESHOLD, mergeOverlappingPoints, migratePoints, normalizeFixedPoint, normalizeLink, randomId, refreshTextDimensions, validateClosedPath, } from "../utils"; import { AXIS, BLOCK_ATTACHMENT, COLUMN_TYPE, DATUM_BRACKET_STYLE, DIMENSION_FIT_RULE, DIMENSION_TEXT_PLACEMENT, DIMENSION_TYPE, GDT_SYMBOL, LEADER_CONTENT_TYPE, LINE_SPACING_TYPE, MARK_ELLIPSE_CENTER, MATERIAL_CONDITION, PARAMETRIC_SOURCE_TYPE, STACKED_TEXT_ALIGN, TABLE_CELL_ALIGNMENT, TABLE_FLOW_DIRECTION, TEXT_FLOW_DIRECTION, TOLERANCE_DISPLAY, TOLERANCE_ZONE_TYPE, VERTICAL_ALIGN, VIEWPORT_SHADE_PLOT, } from "../flatbuffers/duc"; import tinycolor from "tinycolor2"; const restoreElementWithProperties = (element, extra, localState, globalState) => { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; const _element = Object.assign(Object.assign({}, element), extra); const elementScope = isValidElementScopeValue(_element.scope, globalState === null || globalState === void 0 ? void 0 : globalState.mainScope); const currentScope = isValidElementScopeValue(localState === null || localState === void 0 ? void 0 : localState.scope, globalState === null || globalState === void 0 ? void 0 : globalState.mainScope); // Handle legacy stroke properties first let stroke = [DEFAULT_ELEMENT_PROPS.stroke]; if (_element.strokeColor) { // Legacy strokeColor property stroke = [ Object.assign(Object.assign({}, DEFAULT_ELEMENT_PROPS.stroke), { content: validateDeprecatedElementContent(_element.strokeColor, DEFAULT_ELEMENT_PROPS.stroke.content), width: restorePrecisionValue(_element.strokeWidth || DEFAULT_ELEMENT_PROPS.stroke.width.value, elementScope, currentScope, DEFAULT_ELEMENT_PROPS.stroke.width.value) }), ]; } else if (_element.stroke) { // New stroke array property stroke = _element.stroke.map((s) => validateStroke(s, elementScope, currentScope)); } // Handle legacy background properties let background = [DEFAULT_ELEMENT_PROPS.background]; if (_element.backgroundColor) { // Legacy backgroundColor property background = [ Object.assign(Object.assign({}, DEFAULT_ELEMENT_PROPS.background), { content: validateDeprecatedElementContent(_element.backgroundColor, DEFAULT_ELEMENT_PROPS.background.content) }), ]; } else if (_element.background) { // New background array property background = _element.background.map((bg) => validateBackground(bg)); } const base = { id: _element.id || randomId(), version: _element.version || 1, versionNonce: (_a = _element.versionNonce) !== null && _a !== void 0 ? _a : 0, index: (_b = _element.index) !== null && _b !== void 0 ? _b : null, isDeleted: isValidBooleanValue(_element.isDeleted, false), blending: isValidBlendingValue(_element.blending), stroke, background, opacity: isValidPercentageValue(_element.opacity, DEFAULT_ELEMENT_PROPS.opacity), angle: isValidRadianValue(_element.angle, DEFAULT_ELEMENT_PROPS.angle), x: restorePrecisionValue(_element.x, elementScope, currentScope, 0), y: restorePrecisionValue(_element.y, elementScope, currentScope, 0), scope: elementScope, label: (_c = _element.label) !== null && _c !== void 0 ? _c : "Lost Element Label", isVisible: isValidBoolean(_element.isVisible, DEFAULT_ELEMENT_PROPS.isVisible), isPlot: isValidBoolean(_element.isPlot, DEFAULT_ELEMENT_PROPS.isPlot), isAnnotative: isValidBoolean(_element.isAnnotative, DEFAULT_ELEMENT_PROPS.isAnnotative), layerId: (_d = _element.layerId) !== null && _d !== void 0 ? _d : null, regionIds: Array.isArray(_element.regionIds) ? _element.regionIds : [], width: restorePrecisionValue(_element.width, elementScope, currentScope, 0), height: restorePrecisionValue(_element.height, elementScope, currentScope, 0), seed: (_e = _element.seed) !== null && _e !== void 0 ? _e : 1, groupIds: (_f = _element.groupIds) !== null && _f !== void 0 ? _f : [], frameId: (_g = _element.frameId) !== null && _g !== void 0 ? _g : null, roundness: restorePrecisionValue(_element.roundness, elementScope, currentScope, DEFAULT_ELEMENT_PROPS.roundness.value), boundElements: (_h = _element.boundElements) !== null && _h !== void 0 ? _h : undefined, updated: (_j = _element.updated) !== null && _j !== void 0 ? _j : getUpdatedTimestamp(), link: _element.link ? normalizeLink(_element.link) : undefined, locked: isValidBooleanValue(_element.locked, false), description: (_k = _element.description) !== null && _k !== void 0 ? _k : undefined, }; if ("customData" in element || "customData" in extra) { base.customData = _element.customData; } const normalizedRaw = getNormalizedDimensions(base); const normalized = { x: restorePrecisionValue(normalizedRaw.x, elementScope, currentScope, 0, true), y: restorePrecisionValue(normalizedRaw.y, elementScope, currentScope, 0, true), width: restorePrecisionValue(normalizedRaw.width, elementScope, currentScope, 0, true), height: restorePrecisionValue(normalizedRaw.height, elementScope, currentScope, 0, true), }; return Object.assign(Object.assign(Object.assign(Object.assign({}, _element), base), normalized), { type: _element.type }); }; const restoreElement = (element, currentScope, restoredBlocks, localState, globalState) => { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0; // Migration: convert deprecated 'diamond' to 'polygon' with 4 sides if (element.type === "diamond") { const migrated = Object.assign(Object.assign({}, element), { type: "polygon", sides: 4 }); return restoreElement(migrated, currentScope, restoredBlocks, localState); } switch (element.type) { case "text": { let fontSize = element.fontSize; let fontFamily = element.fontFamily; if ("font" in element) { try { const fontParts = String(element.font).split(" "); if (fontParts.length === 2) { const [fontSizeStr, fontFamilyName] = fontParts; const parsedSize = parseFloat(fontSizeStr); if (!isNaN(parsedSize) && Number.isFinite(parsedSize)) { fontSize = parsedSize; fontFamily = getFontFamilyByName(fontFamilyName); } } } catch (error) { console.error("Failed to parse legacy font value:", error); fontSize = DEFAULT_FONT_SIZE; fontFamily = DEFAULT_FONT_FAMILY; } } const text = (typeof element.text === "string" && element.text) || ""; const lineHeight = (element.lineHeight && Number.isFinite(element.lineHeight) ? element.lineHeight : detectLineHeight(element)); const restoredLineSpacing = restoreLineSpacing(element.lineSpacing, lineHeight, currentScope); const textElementScope = isValidElementScopeValue(element.scope, globalState === null || globalState === void 0 ? void 0 : globalState.mainScope); const finalFontSize = restorePrecisionValue(fontSize, textElementScope, currentScope, DEFAULT_FONT_SIZE); const restoredTextElement = restoreElementWithProperties(element, { // DucTextElement specific text, dynamic: restoreTextDynamicParts(element.dynamic), autoResize: isValidBooleanValue(element.autoResize, true), containerId: (_a = element.containerId) !== null && _a !== void 0 ? _a : null, originalText: element.originalText || text, // DucTextStyle specific isLtr: isValidBooleanValue(element.isLtr, true), fontFamily, bigFontFamily: isValidString(element.bigFontFamily, "sans-serif"), textAlign: isValidTextAlignValue(element.textAlign), verticalAlign: isValidVerticalAlignValue(element.verticalAlign), lineHeight: lineHeight, lineSpacing: restoredLineSpacing, obliqueAngle: isValidRadianValue(element.obliqueAngle, 0), fontSize: finalFontSize, paperTextHeight: element.paperTextHeight ? restorePrecisionValue(element.paperTextHeight, textElementScope, currentScope) : undefined, widthFactor: typeof element.widthFactor === "number" ? element.widthFactor : 1, isUpsideDown: isValidBooleanValue(element.isUpsideDown, false), isBackwards: isValidBooleanValue(element.isBackwards, false), }, localState); // if empty text, mark as deleted. We keep in array // for data integrity purposes (collab etc.) if (!text && !isValidBooleanValue(element.isDeleted, false)) { element = Object.assign(Object.assign({}, element), { originalText: text, isDeleted: true }); element = bumpVersion(element); } return restoredTextElement; } case "freedraw": { const elementScope = isValidElementScopeValue(element.scope, globalState === null || globalState === void 0 ? void 0 : globalState.mainScope); const points = restoreElementPoints({ points: element.points, x: element.x, y: element.y, width: element.width, height: element.height, elementScope, currentScope, }); return restoreElementWithProperties(element, { points, size: restorePrecisionValue(element.size, elementScope, currentScope, DEFAULT_FREEDRAW_ELEMENT.size), lastCommittedPoint: element.lastCommittedPoint ? restorePoint(element.lastCommittedPoint, elementScope, currentScope) : null, simulatePressure: isValidBoolean(element.simulatePressure, false), pressures: Array.isArray(element.pressures) ? element.pressures : [], thinning: isValidPercentageValue(element.thinning, DEFAULT_FREEDRAW_ELEMENT.thinning, true), smoothing: isValidPercentageValue(element.smoothing, DEFAULT_FREEDRAW_ELEMENT.smoothing), streamline: isValidPercentageValue(element.streamline, DEFAULT_FREEDRAW_ELEMENT.streamline), easing: isValidFreeDrawEasingValue(element.easing), svgPath: isValidString(element.svgPath) || null, start: restoreFreeDrawEnds(element.start), end: restoreFreeDrawEnds(element.end), }, localState); } case "image": return restoreElementWithProperties(element, { status: isValidImageStatusValue(element.status), fileId: element.fileId, scaleFlip: isValidImageScaleValue(element.scaleFlip), crop: element.crop || null, filter: element.filter || null, }, localState); case "line": { // Don't normalize points if there are bindings const hasBindings = !!(element.startBinding || element.endBinding); const elementScope = isValidElementScopeValue(element.scope, globalState === null || globalState === void 0 ? void 0 : globalState.mainScope); const { points, lines } = restoreLinearElementPointsAndLines({ points: element.points, lines: element.lines, x: element.x, y: element.y, width: element.width, height: element.height, elementScope, currentScope, skipNormalization: hasBindings, }); const isEditing = false; // TODO: Handle editingLinearElement properly let finalPoints = points; // Ensure we have at least 2 points for visibility check if (finalPoints.length < 2 && !isEditing) { const elementWidth = restorePrecisionValue(element.width, elementScope, currentScope, 0); const elementHeight = restorePrecisionValue(element.height, elementScope, currentScope, 0); finalPoints = [ { x: getPrecisionValueFromRaw(0, elementScope, currentScope), y: getPrecisionValueFromRaw(0, elementScope, currentScope), mirroring: undefined, }, { x: elementWidth, y: elementHeight, mirroring: undefined, }, ]; } // Handle bindings, with special case for head-only bindings const startBinding = element.startBinding; const endBinding = element.endBinding; // Process bindings const processedStartBinding = startBinding && startBinding.head !== undefined && (!startBinding.elementId || startBinding.elementId === "") ? createHeadOnlyBinding(startBinding.head, restoredBlocks, currentScope) : startBinding ? repairBinding(element, startBinding, currentScope, restoredBlocks) : null; const processedEndBinding = endBinding && endBinding.head !== undefined && (!endBinding.elementId || endBinding.elementId === "") ? createHeadOnlyBinding(endBinding.head, restoredBlocks, currentScope) : endBinding ? repairBinding(element, endBinding, currentScope, restoredBlocks) : null; // Create the base restored element const sizeFromPoints = !hasBindings && getSizeFromPoints(finalPoints.map(getScopedBezierPointFromDucPoint)); let restoredElement = restoreElementWithProperties(element, Object.assign({ points: finalPoints, lines, lastCommittedPoint: element.lastCommittedPoint ? restorePoint(element.lastCommittedPoint, element.scope, currentScope) : null, startBinding: processedStartBinding, wipeoutBelow: isValidBooleanValue(element.wipeoutBelow, false), endBinding: processedEndBinding, x: element.x, y: element.y }, (!sizeFromPoints ? {} : { width: getPrecisionValueFromScoped(sizeFromPoints.width, element.scope, currentScope), height: getPrecisionValueFromScoped(sizeFromPoints.height, element.scope, currentScope), })), localState); const { points: updatedPoints, lines: updatedLines } = mergeOverlappingPoints(finalPoints, lines, LINE_CONFIRM_THRESHOLD); restoredElement = Object.assign(Object.assign({}, restoredElement), { points: updatedPoints, lines: updatedLines }); // Validate path overrides after we have the final points and lines const validatedPathOverrides = validatePathOverrides(element.pathOverrides, restoredElement, elementScope, currentScope); return Object.assign(Object.assign({}, restoredElement), { pathOverrides: validatedPathOverrides }); } case "arrow": { // since arrow is deprecated, we convert it to line return restoreElement(Object.assign(Object.assign({}, element), { type: "line", wipeoutBelow: false }), currentScope, restoredBlocks, localState); } case "ellipse": { const ratio = isValidPercentageValue(element.ratio, DEFAULT_ELLIPSE_ELEMENT.ratio); const startAngle = isValidCutAngleValue(element.startAngle, DEFAULT_ELLIPSE_ELEMENT.startAngle); const endAngle = isValidCutAngleValue(element.endAngle, DEFAULT_ELLIPSE_ELEMENT.endAngle); const showAuxCrosshair = (_b = element.showAuxCrosshair) !== null && _b !== void 0 ? _b : DEFAULT_ELLIPSE_ELEMENT.showAuxCrosshair; return restoreElementWithProperties(element, { ratio, startAngle, endAngle, showAuxCrosshair, }, localState); } case "rectangle": case "embeddable": return restoreElementWithProperties(element, {}, localState); case "polygon": { const sides = isValidPolygonSides(element.sides); return restoreElementWithProperties(Object.assign(Object.assign({}, element), { sides }), {}, localState); } // Stack-like elements case "frame": { const frameElement = element; return restoreElementWithProperties(frameElement, Object.assign({}, restoreStackElementProperties(frameElement, currentScope)), localState); } case "plot": { const plotElement = element; const elementScope = isValidElementScopeValue(plotElement.scope, globalState === null || globalState === void 0 ? void 0 : globalState.mainScope); // Restore base stack element properties (label, visibility, etc.) const stackProperties = restoreStackElementProperties(plotElement, currentScope); // Restore layout-specific properties const layout = { margins: { top: restorePrecisionValue((_d = (_c = plotElement.layout) === null || _c === void 0 ? void 0 : _c.margins) === null || _d === void 0 ? void 0 : _d.top, elementScope, currentScope, 0), right: restorePrecisionValue((_f = (_e = plotElement.layout) === null || _e === void 0 ? void 0 : _e.margins) === null || _f === void 0 ? void 0 : _f.right, elementScope, currentScope, 0), bottom: restorePrecisionValue((_h = (_g = plotElement.layout) === null || _g === void 0 ? void 0 : _g.margins) === null || _h === void 0 ? void 0 : _h.bottom, elementScope, currentScope, 0), left: restorePrecisionValue((_k = (_j = plotElement.layout) === null || _j === void 0 ? void 0 : _j.margins) === null || _k === void 0 ? void 0 : _k.left, elementScope, currentScope, 0), }, }; return restoreElementWithProperties(plotElement, Object.assign(Object.assign({}, stackProperties), { layout }), localState, globalState); } case "viewport": { const viewportElement = element; const elementScope = isValidElementScopeValue(viewportElement.scope, globalState === null || globalState === void 0 ? void 0 : globalState.mainScope); // A viewport has both linear and stack properties const { points, lines } = restoreLinearElementPointsAndLines({ points: viewportElement.points, lines: viewportElement.lines, x: viewportElement.x, y: viewportElement.y, width: viewportElement.width, height: viewportElement.height, elementScope, currentScope, skipNormalization: false, // Viewports are typically non-binding polygons }); const stackProperties = restoreStackElementProperties(viewportElement, currentScope); const defaults = getDefaultLocalState(); const zoomValue = getNormalizedZoom(isFiniteNumber((_l = localState === null || localState === void 0 ? void 0 : localState.zoom) === null || _l === void 0 ? void 0 : _l.value) ? localState.zoom.value : defaults.zoom.value); const scopedZoom = getScopedZoomValue(zoomValue, currentScope); const view = { scrollX: restorePrecisionValue((_m = viewportElement.view) === null || _m === void 0 ? void 0 : _m.scrollX, NEUTRAL_SCOPE, currentScope, 0), scrollY: restorePrecisionValue((_o = viewportElement.view) === null || _o === void 0 ? void 0 : _o.scrollY, NEUTRAL_SCOPE, currentScope, 0), zoom: { value: zoomValue, scoped: scopedZoom, scaled: getScaledZoomValueForScope(scopedZoom, currentScope), }, twistAngle: isValidRadianValue((_p = viewportElement.view) === null || _p === void 0 ? void 0 : _p.twistAngle, 0), centerPoint: restorePoint((_q = viewportElement.view) === null || _q === void 0 ? void 0 : _q.centerPoint, NEUTRAL_SCOPE, currentScope), // Assuming centerPoint is always present scope: isValidElementScopeValue((_r = viewportElement.view) === null || _r === void 0 ? void 0 : _r.scope, globalState === null || globalState === void 0 ? void 0 : globalState.mainScope), }; return restoreElementWithProperties(viewportElement, Object.assign(Object.assign({}, stackProperties), { points, lines, view, scale: (typeof viewportElement.scale === "number" ? viewportElement.scale : 1), shadePlot: isValidEnumValue(viewportElement.shadePlot, VIEWPORT_SHADE_PLOT, VIEWPORT_SHADE_PLOT.AS_DISPLAYED), frozenGroupIds: Array.isArray(viewportElement.frozenGroupIds) ? viewportElement.frozenGroupIds : [], // viewport-specific style scaleIndicatorVisible: isValidBoolean(viewportElement.scaleIndicatorVisible, true) }), localState, globalState); } // Other elements case "pdf": return restoreElementWithProperties(element, { fileId: isValidString(element.fileId) || null }, localState); case "mermaid": return restoreElementWithProperties(element, { source: isValidString(element.source), theme: isValidString(element.theme) || undefined, svgPath: isValidString(element.svgPath) || null, }, localState); // Complex elements requiring specific restore logic case "table": { const tableElement = element; const defaultData = getDefaultTableData(currentScope); return restoreElementWithProperties(tableElement, { columnOrder: Array.isArray(tableElement.columnOrder) ? tableElement.columnOrder : defaultData.columnOrder, rowOrder: Array.isArray(tableElement.rowOrder) ? tableElement.rowOrder : defaultData.rowOrder, columns: restoreTableColumns(tableElement.columns, currentScope, defaultData.columns), rows: restoreTableRows(tableElement.rows, currentScope, defaultData.rows), cells: restoreTableCells(tableElement.cells, defaultData.cells), headerRowCount: typeof tableElement.headerRowCount === "number" ? tableElement.headerRowCount : defaultData.headerRowCount, autoSize: tableElement.autoSize || defaultData.autoSize, // DucTableStyle properties flowDirection: (Object.values(TABLE_FLOW_DIRECTION)).includes(tableElement.flowDirection) ? tableElement.flowDirection : defaultData.flowDirection, headerRowStyle: restoreTableCellStyle(tableElement.headerRowStyle, currentScope, defaultData.headerRowStyle), dataRowStyle: restoreTableCellStyle(tableElement.dataRowStyle, currentScope, defaultData.dataRowStyle), dataColumnStyle: restoreTableCellStyle(tableElement.dataColumnStyle, currentScope, defaultData.dataColumnStyle), }, localState); } case "doc": { const docElement = element; return restoreElementWithProperties(element, Object.assign(Object.assign({}, restoreDocStyleProperties(docElement, currentScope)), { text: isValidString(docElement.text), dynamic: restoreTextDynamicParts(docElement.dynamic), flowDirection: (Object.values(TEXT_FLOW_DIRECTION)).includes(docElement.flowDirection) ? docElement.flowDirection : TEXT_FLOW_DIRECTION.TOP_TO_BOTTOM, columns: restoreTextColumns(docElement.columns, currentScope), autoResize: isValidBooleanValue(docElement.autoResize, true) }), localState); } case "xray": { const xrayElement = element; const elementScope = isValidElementScopeValue(xrayElement.scope, globalState === null || globalState === void 0 ? void 0 : globalState.mainScope); return restoreElementWithProperties(xrayElement, { origin: restorePoint(xrayElement.origin, elementScope, currentScope), direction: restorePoint(xrayElement.direction, elementScope, currentScope), startFromOrigin: isValidBoolean(xrayElement.startFromOrigin, false), color: isValidColor(xrayElement.color, "#FF00FF"), }, localState, globalState); } case "parametric": { const parametricElement = element; let source; if (((_s = parametricElement.source) === null || _s === void 0 ? void 0 : _s.type) === PARAMETRIC_SOURCE_TYPE.FILE) { source = { type: PARAMETRIC_SOURCE_TYPE.FILE, fileId: isValidString((_t = parametricElement.source) === null || _t === void 0 ? void 0 : _t.fileId), }; } else { source = { type: PARAMETRIC_SOURCE_TYPE.CODE, code: isValidString((_u = parametricElement.source) === null || _u === void 0 ? void 0 : _u.code), }; } return restoreElementWithProperties(parametricElement, { source }, localState, globalState); } case "featurecontrolframe": { const fcfElement = element; const elementScope = isValidElementScopeValue(fcfElement.scope, globalState === null || globalState === void 0 ? void 0 : globalState.mainScope); return restoreElementWithProperties(fcfElement, { // Restore style properties textStyle: restoreTextStyle(fcfElement.textStyle, currentScope), layout: { padding: restorePrecisionValue((_v = fcfElement.layout) === null || _v === void 0 ? void 0 : _v.padding, elementScope, currentScope, 2), segmentSpacing: restorePrecisionValue((_w = fcfElement.layout) === null || _w === void 0 ? void 0 : _w.segmentSpacing, elementScope, currentScope, 2), rowSpacing: restorePrecisionValue((_x = fcfElement.layout) === null || _x === void 0 ? void 0 : _x.rowSpacing, elementScope, currentScope, 2), }, symbols: { scale: (_z = (_y = fcfElement.symbols) === null || _y === void 0 ? void 0 : _y.scale) !== null && _z !== void 0 ? _z : 1, }, datumStyle: { bracketStyle: isValidEnumValue((_0 = fcfElement.datumStyle) === null || _0 === void 0 ? void 0 : _0.bracketStyle, DATUM_BRACKET_STYLE, DATUM_BRACKET_STYLE.SQUARE), }, // Restore data properties rows: restoreFcfRows(fcfElement.rows), frameModifiers: restoreFcfFrameModifiers(fcfElement.frameModifiers, elementScope, currentScope), leaderElementId: isValidString(fcfElement.leaderElementId) || null, datumDefinition: restoreFcfDatumDefinition(fcfElement.datumDefinition, elementScope, currentScope, restoredBlocks), }, localState, globalState); } case "leader": { const leaderElement = element; const elementScope = isValidElementScopeValue(leaderElement.scope, globalState === null || globalState === void 0 ? void 0 : globalState.mainScope); // A leader is a linear element const { points, lines } = restoreLinearElementPointsAndLines({ points: leaderElement.points, lines: leaderElement.lines, x: leaderElement.x, y: leaderElement.y, width: leaderElement.width, height: leaderElement.height, elementScope, currentScope, skipNormalization: !!(leaderElement.startBinding || leaderElement.endBinding), }); return restoreElementWithProperties(leaderElement, { points, lines, // DucLeaderStyle properties headsOverride: restoreHeadsOverride(leaderElement.headsOverride, restoredBlocks, elementScope, currentScope), dogleg: restorePrecisionValue(leaderElement.dogleg, elementScope, currentScope, 10), textStyle: restoreTextStyle(leaderElement.textStyle, currentScope), textAttachment: isValidEnumValue(leaderElement.textAttachment, VERTICAL_ALIGN, VERTICAL_ALIGN.TOP), blockAttachment: isValidEnumValue(leaderElement.blockAttachment, BLOCK_ATTACHMENT, BLOCK_ATTACHMENT.CENTER_EXTENTS), // Leader data leaderContent: restoreLeaderContent(leaderElement.leaderContent), contentAnchor: leaderElement.contentAnchor || { x: 0, y: 0 }, }, localState, globalState); } case "dimension": { const dimElement = element; const elementScope = isValidElementScopeValue(dimElement.scope, globalState === null || globalState === void 0 ? void 0 : globalState.mainScope); return restoreElementWithProperties(dimElement, Object.assign(Object.assign({}, restoreDimensionStyle(dimElement, currentScope)), { // Dimension data dimensionType: isValidEnumValue(dimElement.dimensionType, DIMENSION_TYPE, DIMENSION_TYPE.LINEAR), definitionPoints: restoreDimensionDefinitionPoints(dimElement.definitionPoints), obliqueAngle: isValidRadianValue(dimElement.obliqueAngle, 0), ordinateAxis: isValidEnumValue(dimElement.ordinateAxis, AXIS, null), bindings: restoreDimensionBindings(dimElement.bindings, elementScope, currentScope, restoredBlocks), textOverride: isValidString(dimElement.textOverride) || null, textPosition: dimElement.textPosition || null, toleranceOverride: restoreDimensionToleranceStyle(dimElement.toleranceOverride, currentScope), baselineData: dimElement.baselineData ? { baseDimensionId: isValidString(dimElement.baselineData.baseDimensionId), } : undefined, continueData: dimElement.continueData ? { continueFromDimensionId: isValidString(dimElement.continueData.continueFromDimensionId), } : undefined }), localState, globalState); } } return null; }; /** * Restores a point that might have legacy format (x,y as numbers) * @param point - The point to restore * @param elementScope - The scope to use for the precision values * @returns A properly formatted DucPoint */ export const restorePoint = (point, elementScope, currentScope) => { if (!point) { return null; } // Handle x and y conversion const xValue = restorePrecisionValue(point.x, elementScope, currentScope); const yValue = restorePrecisionValue(point.y, elementScope, currentScope); if (!xValue || !yValue) { return null; } // Only keep the new DucPoint structure, ignore legacy properties return { x: xValue, y: yValue, mirroring: isValidBezierMirroringValue(point.mirroring), }; }; const restoreElementPoints = ({ points: oldPoints, x, y, width, height, elementScope, currentScope, }) => { // Convert input parameters to plain numbers if they're precision values const validatedWidth = restorePrecisionValue(width, elementScope, currentScope, 0); const validatedHeight = restorePrecisionValue(height, elementScope, currentScope, 0); // Process the points array with proper conversion let points = []; if (typeof oldPoints === "object" && Array.isArray(oldPoints)) { const migratedPoints = migratePoints(oldPoints); points = migratedPoints .map((point) => { const restoredPoint = restorePoint(point, elementScope, currentScope); if (!restoredPoint) { return null; } return restoredPoint; }) .filter(Boolean); // Ensure we have at least 2 points for linear elements if (points.length === 1) { points.push({ x: validatedWidth, y: validatedHeight, mirroring: undefined, }); } } else { // Default two points points = [ { x: getPrecisionValueFromRaw(0, elementScope, currentScope), y: getPrecisionValueFromRaw(0, elementScope, currentScope), mirroring: undefined, }, { x: validatedWidth, y: validatedHeight, mirroring: undefined, }, ]; } if (points.length === 2 && points[1] && // Should always be true if points.length === 2 points[1].x.value === 0 && points[1].y.value === 0 && (validatedWidth.value !== 0 || validatedHeight.value !== 0)) { points[1] = Object.assign(Object.assign({}, points[1]), { x: validatedWidth, y: validatedHeight }); } return points; }; const restoreLinearElementPointsAndLines = ({ points: oldPoints, lines: oldLines, x, y, width, height, elementScope, currentScope, skipNormalization, }) => { const validatedX = restorePrecisionValue(x, elementScope, currentScope, 0); const validatedY = restorePrecisionValue(y, elementScope, currentScope, 0); const validatedWidth = restorePrecisionValue(width, elementScope, currentScope, 0); const validatedHeight = restorePrecisionValue(height, elementScope, currentScope, 0); let points = restoreElementPoints({ points: oldPoints, x, y, width, height, elementScope, currentScope, }); let lines = oldLines; if (lines === undefined || lines === null) { lines = []; for (let i = 0; i < points.length - 1; i++) { lines.push([ { index: i, handle: null }, { index: i + 1, handle: null }, ]); } } else { lines = lines.map((line) => { return [ { index: line[0].index, handle: line[0].handle ? restorePoint(line[0].handle, elementScope, currentScope) : null, }, { index: line[1].index, handle: line[1].handle ? restorePoint(line[1].handle, elementScope, currentScope) : null, }, ]; }); } if (!skipNormalization && points.length >= 2 && (points[0].x.value !== 0 || points[0].y.value !== 0)) { const normalizedResult = getNormalizedPoints({ points, lines, x: validatedX, y: validatedY, scope: elementScope, }, currentScope); const firstPoint = points[0]; const lastPoint = points[points.length - 1]; points = normalizedResult.points; lines = normalizedResult.lines; if (oldPoints && oldPoints.length === points.length) { if (firstPoint.x.value !== 0 || firstPoint.y.value !== 0) { points[0] = Object.assign({}, points[0]); } if (lastPoint.x.value !== validatedWidth.value || lastPoint.y.value !== validatedHeight.value) { points[points.length - 1] = Object.assign({}, points[points.length - 1]); } } } return { points, lines }; }; /** * Repairs container element's boundElements array by removing duplicates and * fixing containerId of bound elements if not present. Also removes any * bound elements that do not exist in the elements array. * * NOTE mutates elements. */ const repairContainerElement = (container, elementsMap) => { if (container.boundElements) { const boundElements = container.boundElements.slice(); const boundIds = new Set(); container.boundElements = boundElements.reduce((acc, binding) => { const boundElement = elementsMap.get(binding.id); if (boundElement && !boundIds.has(binding.id)) { boundIds.add(binding.id); if (boundElement.isDeleted) { return acc; } acc.push(binding); if (isTextElement(boundElement) && !boundElement.containerId) { boundElement.containerId = container.id; } } return acc; }, []); } }; /** * Repairs target bound element's container's boundElements array, * or removes contaienrId if container does not exist. * * NOTE mutates elements. */ const repairBoundElement = (boundElement, elementsMap) => { const container = boundElement.containerId ? elementsMap.get(boundElement.containerId) : null; if (!container) { boundElement.containerId = null; return; } if (boundElement.isDeleted) { return; } if (container.boundElements && !container.boundElements.find((binding) => binding.id === boundElement.id)) { const boundElements = (container.boundElements || (container.boundElements = [])).slice(); boundElements.push({ type: "text", id: boundElement.id }); container.boundElements = boundElements; } }; /** * Remove an element's frameId if its containing frame is non-existent * * NOTE mutates elements. */ const repairFrameMembership = (element, elementsMap) => { if (element.frameId) { const containingFrame = elementsMap.get(element.frameId); if (!containingFrame) { element.frameId = null; } } }; /** * Assigns z-index values to elements based on their position in the array. * This recreates the original visual stacking that was implied by array order * before explicit z-index support was added. */ export const migrateArrayOrderToZIndex = (elements) => { // Check if we need to do a migration (if any element lacks z-index) const needsMigration = elements.some((el) => el.zIndex === undefined || el.zIndex === null); if (!needsMigration) { return elements; } // Create a new array with explicit z-index values return [...elements].map((element, index) => { // Only update elements that don't already have a valid z-index if (element.zIndex === undefined || element.zIndex === null) { return Object.assign(Object.assign({}, element), { zIndex: index }); } return element; }); }; export const restoreElements = (elements, currentScope, restoredBlocks, opts) => { const existingIds = new Set(); // First restore all elements with their original properties const restored = (elements || []).reduce((elements, element) => { if (element.type !== "selection") { let migratedElement = restoreElement(element, currentScope, restoredBlocks, opts === null || opts === void 0 ? void 0 : opts.localState); // Allow forced pass-through for specific element ids const isPassThrough = Array.isArray(opts === null || opts === void 0 ? void 0 : opts.passThroughElementIds) && migratedElement && migratedElement.id && opts.passThroughElementIds.includes(migratedElement.id); if (migratedElement && (isPassThrough || !isInvisiblySmallElement(migratedElement))) { if (existingIds.has(migratedElement.id)) { migratedElement = Object.assign(Object.assign({}, migratedElement), { id: randomId() }); } existingIds.add(migratedElement.id); elements.push(migratedElement); } } return elements; }, []); let restoredElements = (opts === null || opts === void 0 ? void 0 : opts.syncInvalidIndices) ? opts.syncInvalidIndices(restored, currentScope) : restored; // Migrate array ordering to explicit z-index values if needed restoredElements = migrateArrayOrderToZIndex(restoredElements); if (!(opts === null || opts === void 0 ? void 0 : opts.repairBindings)) { return restoredElements; } const restoredElementsMap = arrayToMap(restoredElements); for (const element of restoredElements) { if (element.frameId) { repairFrameMembership(element, restoredElementsMap); } if (isTextElement(element) && element.containerId) { repairBoundElement(element, restoredElementsMap); } else if (element.boundElements) { repairContainerElement(element, restoredElementsMap); } if (opts.refreshDimensions && isTextElement(element)) { Object.assign(element, refreshTextDimensions(element, getContainerElement(element, restoredElementsMap), restoredElementsMap, currentScope)); } if (isLinearElement(element)) { // Helper function to check if we should keep a binding const shouldKeepBinding = (binding) => { if (!binding) return true; // No binding, nothing to remove // Keep head-only bindings if (binding.head !== undefined && (!binding.elementId || binding.elementId === "")) { return true; } // If binding has an elementId, check if it points to a valid element const elementExists = restoredElementsMap.has(binding.elementId); // For linear-to-linear bindings, ensure the point attribute is valid if (binding.point && elementExists) { const targetElement = restoredElementsMap.get(binding.elementId); if (isLinearElement(targetElement)) { // Check if the point index is within bounds of the target element's points if (binding.point.index >= 0 && binding.point.index < targetElement.points.length && Math.abs(binding.point.offset) <= 1) { return true; } } } return elementExists; }; // Process start binding if (element.startBinding && !shouldKeepBinding(element.startBinding)) { element.startBinding = null; } // Process end binding if (element.endBinding && !shouldKeepBinding(element.endBinding)) { element.endBinding = null; } // Update the bound elements reference for each valid binding // This ensures both sides of the binding relationship are updated const updateBoundElementsRef = (binding) => { var _a; if (!binding || !binding.elementId) return; const targetElement = restoredElementsMap.get(binding.elementId); if (targetElement && !targetElement.isDeleted) { // Add the linear element to the target's boundElements if not already there if (!((_a = targetElement.boundElements) === null || _a === void 0 ? void 0 : _a.some((be) => be.id === element.id))) { const boundElements = targetElement.boundElements || []; targetElement.boundElements = [ ...boundElements, { id: element.id, type: element.type, }, ]; } } }; // Update bound elements references updateBoundElementsRef(element.startBinding); updateBoundElementsRef(element.endBinding); } } return restoredElements; }; export const isValidElementScopeValue = (value, mainScope = NEUTRAL_SCOPE) => { // Only check if the provided value is valid if (value !== undefined && Object.keys(ScaleFactors).includes(value)) { return value; } return mainScope; }; const restoreTextDynamicParts = (dynamic) => { if (!Array.isArray(dynamic)) { return []; } return dynamic .map((part) => ({ tag: isValidString(part.tag), source: part.source, // Assuming source is valid, could be further validated formatting: part.formatting, // Assuming valid cachedValue: isValidString(part.cachedValue), })) .filter((part) => part.tag); }; const restoreLineSpacing = (lineSpacing, legacyLineHeight, currentScope) => { if (lineSpacing && lineSpacing.type && lineSpacing.value) { return { type: isValidLineSpacingTypeValue(lineSpacing.type), value: restorePrecisionValue(lineSpacing.value, currentScope, currentScope, 1.15, true), }; } // Fallback to legacy lineHeight return { type: LINE_SPACING_TYPE.MULTIPLE, value: legacyLineHeight, }; }; const restoreTableCellStyle = (style, currentScope, defaultStyle) => { if (!style || typeof style !== "object") { return defaultStyle; } return Object.assign(Object.assign(Object.assign({}, defaultStyle), style), { alignment: Object.values(TABLE_CELL_ALIGNMENT).includes(style.alignment) ? style.alignment : defaultStyle.alignment }); }; const restoreTableColumns = (columns, currentScope, defaultColumns) => { if (!columns || typeof columns !== "object") { return defaultColumns; } const restored = {}; for (const id in columns) { const col = columns[id]; if (col && typeof col === "object" && isValidString(col.id)) { restored[id] = { id: col.id, width: restorePrecisionValue(col.width, currentScope, currentScope, 100), styleOverrides: col.styleOverrides ? restoreTableCellStyle(col.styleOverrides, currentScope, {}) : undefined, }; } } return restored; }; const restoreTableRows = (rows, currentScope, defaultRows) => { if (!rows || typeof rows !== "object") { return defaultRows; } const restored = {}; for (const id in rows) { const row = rows[id]; if (row && typeof row === "object" && isValidString(row.id)) { restored[id] = { id: row.id, height: restorePrecisionValue(row.height, currentScope, currentScope, 40), styleOverrides: row.styleOverrides ? restoreTableCellStyle(row.styleOverrides, currentScope, {}) : undefined, }; } } return restored; }; const restoreTableCells = (cells, defaultCells) => { if (!cells || typeof cells !== "object") { return defaultCells; } const restored = {}; for (const id in cells) { const cell = cells[id]; if (cell && typeof cell === "object" && isValidString(cell.rowId) && isValidString(cell.columnId)) { restored[id] = { rowId: cell.rowId, columnId: cell.columnId, data: isValidString(cell.data), locked: isValidBooleanValue(cell.locked, false), span: cel