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.
269 lines (268 loc) • 19.6 kB
JavaScript
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
import { BLOCK_ATTACHMENT, COLUMN_TYPE, DATUM_BRACKET_STYLE, IMAGE_STATUS, LINE_SPACING_TYPE, PARAMETRIC_SOURCE_TYPE, STACKED_TEXT_ALIGN, TEXT_FLOW_DIRECTION, VERTICAL_ALIGN, VIEWPORT_SHADE_PLOT, } from "../../flatbuffers/duc";
import { getUpdatedTimestamp, getZoom } from "..";
import { DEFAULT_ELEMENT_PROPS, DEFAULT_ELLIPSE_ELEMENT, DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, DEFAULT_FREEDRAW_ELEMENT, DEFAULT_POLYGON_SIDES, DEFAULT_TEXT_ALIGN, DEFAULT_VERTICAL_ALIGN, } from "../constants";
import { getDefaultStackProperties, getDefaultTableData, getDefaultTextStyle } from "./";
import { getFontString, getTextElementPositionOffsets, measureText, } from "./textElement";
import { randomId, randomInteger } from "../math/random";
import { normalizeText } from "../normalize";
import { getPrecisionValueFromRaw } from "../../technical/scopes";
export const newElementWith = (element, updates,
/** pass `true` to always regenerate */
force = false) => {
let didChange = false;
for (const key in updates) {
const value = updates[key];
if (typeof value !== "undefined") {
if (element[key] === value &&
// if object, always update because its attrs could have changed
(typeof value !== "object" || value === null)) {
continue;
}
didChange = true;
}
}
if (!didChange && !force) {
return element;
}
return Object.assign(Object.assign(Object.assign({}, element), updates), { updated: getUpdatedTimestamp(), version: element.version + 1, versionNonce: randomInteger() });
};
const _newElementBase = (type, currentScope, _a) => {
var _b, _c;
var { x, y, scope = currentScope, zIndex, index = DEFAULT_ELEMENT_PROPS.index, label, isVisible = DEFAULT_ELEMENT_PROPS.isVisible, isPlot = DEFAULT_ELEMENT_PROPS.isPlot, isAnnotative = DEFAULT_ELEMENT_PROPS.isAnnotative, stroke = [DEFAULT_ELEMENT_PROPS.stroke], background = [DEFAULT_ELEMENT_PROPS.background], opacity = DEFAULT_ELEMENT_PROPS.opacity, width = DEFAULT_ELEMENT_PROPS.width, height = DEFAULT_ELEMENT_PROPS.height, angle = DEFAULT_ELEMENT_PROPS.angle, groupIds = DEFAULT_ELEMENT_PROPS.groupIds, regionIds = [], frameId = DEFAULT_ELEMENT_PROPS.frameId, layerId = null, roundness = DEFAULT_ELEMENT_PROPS.roundness, boundElements = DEFAULT_ELEMENT_PROPS.boundElements, link = DEFAULT_ELEMENT_PROPS.link, locked = DEFAULT_ELEMENT_PROPS.locked, description = null } = _a, rest = __rest(_a, ["x", "y", "scope", "zIndex", "index", "label", "isVisible", "isPlot", "isAnnotative", "stroke", "background", "opacity", "width", "height", "angle", "groupIds", "regionIds", "frameId", "layerId", "roundness", "boundElements", "link", "locked", "description"]);
// assign type to guard against excess properties
const element = {
id: rest.id || randomId(),
type,
x,
y,
width,
height,
index,
isVisible,
angle,
stroke,
background,
opacity,
groupIds,
frameId,
roundness,
label,
scope,
seed: (_b = rest.seed) !== null && _b !== void 0 ? _b : randomInteger(),
version: rest.version || 1,
versionNonce: (_c = rest.versionNonce) !== null && _c !== void 0 ? _c : 0,
isDeleted: false,
boundElements,
updated: getUpdatedTimestamp(),
link,
locked,
zIndex,
description,
customData: rest.customData,
isPlot,
isAnnotative,
regionIds,
layerId,
};
return element;
};
export const newElement = (currentScope, opts) => _newElementBase(opts.type, currentScope, opts);
export const newEmbeddableElement = (currentScope, opts) => {
return Object.assign(Object.assign({}, opts), _newElementBase(opts.type, currentScope, opts));
};
export const newFrameElement = (currentScope, opts) => (Object.assign(Object.assign(Object.assign({}, _newElementBase("frame", currentScope, opts)), getDefaultStackProperties()), { type: "frame", clip: false, labelVisible: true, standardOverride: null }));
export const newPlotElement = (currentScope, opts) => (Object.assign(Object.assign(Object.assign({}, _newElementBase("plot", currentScope, opts)), getDefaultStackProperties()), { type: "plot", clip: false, labelVisible: true, standardOverride: null, layout: {
margins: {
top: getPrecisionValueFromRaw(25, currentScope, currentScope),
right: getPrecisionValueFromRaw(25, currentScope, currentScope),
bottom: getPrecisionValueFromRaw(25, currentScope, currentScope),
left: getPrecisionValueFromRaw(25, currentScope, currentScope),
}
} }));
export const newViewportElement = (currentScope, opts) => {
var _a, _b, _c;
return (Object.assign(Object.assign(Object.assign({}, _newElementBase("viewport", currentScope, opts)), getDefaultStackProperties()), { type: "viewport", points: [], lines: [], pathOverrides: [], lastCommittedPoint: null, startBinding: null, endBinding: null, standardOverride: null, view: {
scrollX: getPrecisionValueFromRaw(0, currentScope, currentScope),
scrollY: getPrecisionValueFromRaw(0, currentScope, currentScope),
zoom: getZoom((_a = opts.zoom) !== null && _a !== void 0 ? _a : 1, (_b = opts.mainScope) !== null && _b !== void 0 ? _b : currentScope, (_c = opts.scopeExponentThreshold) !== null && _c !== void 0 ? _c : 2),
twistAngle: 0,
centerPoint: {
x: getPrecisionValueFromRaw(0, currentScope, currentScope),
y: getPrecisionValueFromRaw(0, currentScope, currentScope),
},
scope: currentScope,
}, scale: 1, shadePlot: VIEWPORT_SHADE_PLOT.AS_DISPLAYED, frozenGroupIds: [], scaleIndicatorVisible: true }));
};
export const newEllipseElement = (currentScope, opts) => {
return Object.assign(Object.assign({}, _newElementBase(opts.type, currentScope, opts)), { ratio: opts.ratio || DEFAULT_ELLIPSE_ELEMENT.ratio, startAngle: opts.startAngle || DEFAULT_ELLIPSE_ELEMENT.startAngle, endAngle: opts.endAngle || DEFAULT_ELLIPSE_ELEMENT.endAngle, showAuxCrosshair: opts.showAuxCrosshair || DEFAULT_ELLIPSE_ELEMENT.showAuxCrosshair });
};
export const newPolygonElement = (currentScope, opts) => {
return Object.assign(Object.assign({}, _newElementBase(opts.type, currentScope, opts)), { sides: opts.sides || DEFAULT_POLYGON_SIDES });
};
export const newTextElement = (currentScope, opts) => {
var _a, _b, _c, _d, _e, _f;
const scope = (_a = opts.scope) !== null && _a !== void 0 ? _a : currentScope;
const fontFamily = opts.fontFamily || DEFAULT_FONT_FAMILY;
const fontSize = opts.fontSize || getPrecisionValueFromRaw(DEFAULT_FONT_SIZE, scope, currentScope);
const lineHeight = opts.lineHeight || 1.2;
const text = normalizeText(opts.text);
const metrics = measureText(text, getFontString({ fontFamily, fontSize }), lineHeight, currentScope);
const textAlign = opts.textAlign || DEFAULT_TEXT_ALIGN;
const verticalAlign = opts.verticalAlign || DEFAULT_VERTICAL_ALIGN;
const offsets = getTextElementPositionOffsets({ textAlign, verticalAlign }, metrics);
const x = getPrecisionValueFromRaw(opts.x.value - offsets.x, scope, currentScope);
const y = getPrecisionValueFromRaw(opts.y.value - offsets.y, scope, currentScope);
return Object.assign(Object.assign({}, _newElementBase("text", currentScope, Object.assign(Object.assign({}, opts), { x, y }))), { type: "text", text,
fontSize,
fontFamily,
textAlign,
verticalAlign, width: getPrecisionValueFromRaw(metrics.width, scope, currentScope), height: getPrecisionValueFromRaw(metrics.height, scope, currentScope), containerId: opts.containerId || null, originalText: (_b = opts.originalText) !== null && _b !== void 0 ? _b : text, autoResize: (_c = opts.autoResize) !== null && _c !== void 0 ? _c : true, lineHeight,
// DucTextStyle properties
isLtr: (_d = opts.isLtr) !== null && _d !== void 0 ? _d : true, bigFontFamily: opts.bigFontFamily || "sans-serif", lineSpacing: opts.lineSpacing || { type: LINE_SPACING_TYPE.MULTIPLE, value: lineHeight }, obliqueAngle: opts.obliqueAngle || 0, paperTextHeight: opts.paperTextHeight, widthFactor: opts.widthFactor || 1, isUpsideDown: (_e = opts.isUpsideDown) !== null && _e !== void 0 ? _e : false, isBackwards: (_f = opts.isBackwards) !== null && _f !== void 0 ? _f : false, dynamic: opts.dynamic || [] });
};
export const newFreeDrawElement = (currentScope, opts) => {
var _a, _b, _c, _d;
const scope = (_a = opts.scope) !== null && _a !== void 0 ? _a : currentScope;
return Object.assign(Object.assign({}, _newElementBase("freedraw", currentScope, opts)), { type: "freedraw", points: opts.points || [], size: opts.size || getPrecisionValueFromRaw(DEFAULT_FREEDRAW_ELEMENT.size, scope, currentScope), pressures: opts.pressures || [], simulatePressure: opts.simulatePressure, thinning: (_b = opts.thinning) !== null && _b !== void 0 ? _b : DEFAULT_FREEDRAW_ELEMENT.thinning, smoothing: (_c = opts.smoothing) !== null && _c !== void 0 ? _c : DEFAULT_FREEDRAW_ELEMENT.smoothing, streamline: (_d = opts.streamline) !== null && _d !== void 0 ? _d : DEFAULT_FREEDRAW_ELEMENT.streamline, easing: opts.easing || DEFAULT_FREEDRAW_ELEMENT.easing, lastCommittedPoint: null, start: opts.start || null, end: opts.end || null, svgPath: null });
};
export const newLinearElement = (currentScope, opts) => {
var _a;
return (Object.assign(Object.assign({}, _newElementBase("line", currentScope, opts)), { type: "line", points: opts.points || [], lines: opts.lines || [], pathOverrides: opts.pathOverrides || [], lastCommittedPoint: null, startBinding: null, endBinding: null, wipeoutBelow: (_a = opts.wipeoutBelow) !== null && _a !== void 0 ? _a : false }));
};
export const newArrowElement = (currentScope, opts) => {
var _a;
return (Object.assign(Object.assign({}, _newElementBase("arrow", currentScope, opts)), { type: "arrow", points: opts.points || [], lines: opts.lines || [], pathOverrides: opts.pathOverrides || [], lastCommittedPoint: null, startBinding: null, endBinding: null, elbowed: (_a = opts.elbowed) !== null && _a !== void 0 ? _a : true }));
};
export const newImageElement = (currentScope, opts) => {
var _a, _b, _c;
return (Object.assign(Object.assign({}, _newElementBase("image", currentScope, opts)), { type: "image", status: (_a = opts.status) !== null && _a !== void 0 ? _a : IMAGE_STATUS.PENDING, fileId: (_b = opts.fileId) !== null && _b !== void 0 ? _b : null, scaleFlip: (_c = opts.scaleFlip) !== null && _c !== void 0 ? _c : [1, 1], crop: null, filter: null }));
};
export const newTableElement = (currentScope, opts) => (Object.assign(Object.assign(Object.assign({}, _newElementBase("table", currentScope, opts)), getDefaultTableData(currentScope)), { type: "table" }));
export const newDocElement = (currentScope, opts) => {
var _a, _b, _c, _d;
return (Object.assign(Object.assign({}, _newElementBase("doc", currentScope, opts)), { type: "doc", text: opts.text || "", dynamic: opts.dynamic || [], flowDirection: opts.flowDirection || TEXT_FLOW_DIRECTION.TOP_TO_BOTTOM, columns: opts.columns || { type: COLUMN_TYPE.NO_COLUMNS, definitions: [], autoHeight: true }, autoResize: (_a = opts.autoResize) !== null && _a !== void 0 ? _a : true,
// DucDocStyle properties
isLtr: (_b = opts.isLtr) !== null && _b !== void 0 ? _b : true, fontFamily: opts.fontFamily || DEFAULT_FONT_FAMILY, bigFontFamily: opts.bigFontFamily || "sans-serif", textAlign: opts.textAlign || DEFAULT_TEXT_ALIGN, verticalAlign: opts.verticalAlign || DEFAULT_VERTICAL_ALIGN, lineHeight: opts.lineHeight || 1.2, lineSpacing: opts.lineSpacing || { type: LINE_SPACING_TYPE.MULTIPLE, value: 1.2 }, obliqueAngle: opts.obliqueAngle || 0, fontSize: opts.fontSize || getPrecisionValueFromRaw(DEFAULT_FONT_SIZE, currentScope, currentScope), paperTextHeight: opts.paperTextHeight, widthFactor: opts.widthFactor || 1, isUpsideDown: (_c = opts.isUpsideDown) !== null && _c !== void 0 ? _c : false, isBackwards: (_d = opts.isBackwards) !== null && _d !== void 0 ? _d : false, paragraph: opts.paragraph || { firstLineIndent: getPrecisionValueFromRaw(0, currentScope, currentScope), hangingIndent: getPrecisionValueFromRaw(0, currentScope, currentScope), leftIndent: getPrecisionValueFromRaw(0, currentScope, currentScope), rightIndent: getPrecisionValueFromRaw(0, currentScope, currentScope), spaceBefore: getPrecisionValueFromRaw(0, currentScope, currentScope), spaceAfter: getPrecisionValueFromRaw(0, currentScope, currentScope), tabStops: [] }, stackFormat: opts.stackFormat || { autoStack: false, stackChars: [], properties: { upperScale: 0.7, lowerScale: 0.7, alignment: STACKED_TEXT_ALIGN.CENTER } } }));
};
export const newPdfElement = (currentScope, opts) => (Object.assign(Object.assign({}, _newElementBase("pdf", currentScope, opts)), { type: "pdf", fileId: null }));
export const newMermaidElement = (currentScope, opts) => (Object.assign(Object.assign({}, _newElementBase("mermaid", currentScope, opts)), { type: "mermaid", source: "", theme: undefined, svgPath: null }));
export const newXRayElement = (currentScope, opts) => (Object.assign(Object.assign({}, _newElementBase("xray", currentScope, opts)), { type: "xray", origin: { x: getPrecisionValueFromRaw(0, currentScope, currentScope), y: getPrecisionValueFromRaw(0, currentScope, currentScope) }, direction: { x: getPrecisionValueFromRaw(1, currentScope, currentScope), y: getPrecisionValueFromRaw(0, currentScope, currentScope) }, startFromOrigin: false, color: '#FF00FF' }));
export const newLeaderElement = (currentScope, opts) => {
var _a, _b;
return Object.assign(Object.assign({}, _newElementBase("leader", currentScope, opts)), { type: "leader", points: [], lines: [], pathOverrides: [], lastCommittedPoint: null, startBinding: null, endBinding: null, headsOverride: undefined, dogleg: getPrecisionValueFromRaw(10, currentScope, currentScope), textStyle: opts.textStyle || getDefaultTextStyle(currentScope), textAttachment: opts.textAttachment || VERTICAL_ALIGN.TOP, blockAttachment: opts.blockAttachment || BLOCK_ATTACHMENT.CENTER_EXTENTS, leaderContent: (_a = opts.leaderContent) !== null && _a !== void 0 ? _a : null, contentAnchor: (_b = opts.contentAnchor) !== null && _b !== void 0 ? _b : {
x: 0,
y: 0,
} });
};
export const newDimensionElement = (currentScope, opts) => (Object.assign(Object.assign({}, _newElementBase("dimension", currentScope, opts)), { type: 'dimension' }));
export const newFeatureControlFrameElement = (currentScope, opts) => {
return Object.assign(Object.assign({}, _newElementBase("featurecontrolframe", currentScope, opts)), { type: "featurecontrolframe", rows: [], leaderElementId: null, textStyle: getDefaultTextStyle(currentScope), layout: {
padding: getPrecisionValueFromRaw(4, currentScope, currentScope),
segmentSpacing: getPrecisionValueFromRaw(4, currentScope, currentScope),
rowSpacing: getPrecisionValueFromRaw(2, currentScope, currentScope),
}, symbols: {
scale: 1,
}, datumStyle: {
bracketStyle: DATUM_BRACKET_STYLE.SQUARE
} });
};
export const newBlockInstanceElement = (currentScope, opts) => {
var _a, _b, _c;
return (Object.assign(Object.assign({}, _newElementBase("blockinstance", currentScope, opts)), { type: "blockinstance", blockId: opts.blockId, elementOverrides: (_a = opts.elementOverrides) !== null && _a !== void 0 ? _a : {}, attributeValues: (_b = opts.attributeValues) !== null && _b !== void 0 ? _b : {}, duplicationArray: (_c = opts.duplicationArray) !== null && _c !== void 0 ? _c : null }));
};
export const newParametricElement = (currentScope, opts) => (Object.assign(Object.assign({}, _newElementBase("parametric", currentScope, opts)), { type: 'parametric', source: { type: PARAMETRIC_SOURCE_TYPE.CODE, code: "" } }));
// Simplified deep clone for the purpose of cloning DucElement.
//
// Only clones plain objects and arrays. Doesn't clone Date, RegExp, Map, Set,
// Typed arrays and other non-null objects.
//
// Adapted from https://github.com/lukeed/klona
//
// The reason for `deepCopyElement()` wrapper is type safety (only allow
// passing DucElement as the top-level argument).
const _deepCopyElement = (val, depth = 0) => {
// only clone non-primitives
if (val == null || typeof val !== "object") {
return val;
}
const objectType = Object.prototype.toString.call(val);
if (objectType === "[object Object]") {
const tmp = typeof val.constructor === "function"
? Object.create(Object.getPrototypeOf(val))
: {};
for (const key in val) {
if (val.hasOwnProperty(key)) {
// don't copy non-serializable objects like these caches. They'll be
// populated when the element is rendered.
if (depth === 0 && (key === "shape" || key === "canvas")) {
continue;
}
tmp[key] = _deepCopyElement(val[key], depth + 1);
}
}
return tmp;
}
if (Array.isArray(val)) {
let k = val.length;
const arr = new Array(k);
while (k--) {
arr[k] = _deepCopyElement(val[k], depth + 1);
}
return arr;
}
// we're not cloning non-array & non-plain-object objects because we
// don't support them on excalidraw elements yet. If we do, we need to make
// sure we start cloning them, so let's warn about it.
if (import.meta.env.DEV) {
if (objectType !== "[object Object]" &&
objectType !== "[object Array]" &&
objectType.startsWith("[object ")) {
console.warn(`_deepCloneElement: unexpected object type ${objectType}. This value will not be cloned!`);
}
}
return val;
};
/**
* Clones DucElement data structure. Does not regenerate id, nonce, or
* any value. The purpose is to to break object references for immutability
* reasons, whenever we want to keep the original element, but ensure it's not
* mutated.
*
* Only clones plain objects and arrays. Doesn't clone Date, RegExp, Map, Set,
* Typed arrays and other non-null objects.
*/
export const deepCopyElement = (val) => {
return _deepCopyElement(val);
};
/**
* utility wrapper to generate new id. In test env it reuses the old + postfix
* for test assertions.
*/
export const regenerateId = (
/** supply null if no previous id exists */
previousId) => {
// if (isTestEnv() && previousId) {
// let nextId = `${previousId}_copy`;
// // `window.h` may not be defined in some unit tests
// if (
// window.h?.app
// ?.getSceneElementsIncludingDeleted()
// .find((el: DucElement) => el.id === nextId)
// ) {
// nextId += "_copy";
// }
// return nextId;
// }
return randomId();
};