@tldraw/editor
Version:
tldraw infinite canvas SDK (editor).
366 lines (365 loc) • 13.6 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var getSvgJsx_exports = {};
__export(getSvgJsx_exports, {
getSvgJsx: () => getSvgJsx
});
module.exports = __toCommonJS(getSvgJsx_exports);
var import_jsx_runtime = require("react/jsx-runtime");
var import_state_react = require("@tldraw/state-react");
var import_tlschema = require("@tldraw/tlschema");
var import_utils = require("@tldraw/utils");
var import_react = require("react");
var import_react_dom = require("react-dom");
var import_ErrorBoundary = require("../components/ErrorBoundary");
var import_Shape = require("../components/Shape");
var import_SvgExportContext = require("../editor/types/SvgExportContext");
var import_useEditor = require("../hooks/useEditor");
var import_useEvent = require("../hooks/useEvent");
var import_useSafeId = require("../hooks/useSafeId");
var import_Mat = require("../primitives/Mat");
var import_ExportDelay = require("./ExportDelay");
function getSvgJsx(editor, ids, opts = {}) {
if (!window.document) throw Error("No document");
const {
scale = 1,
// should we include the background in the export? or is it transparent?
background = editor.getInstanceState().exportBackground,
padding = editor.options.defaultSvgPadding,
preserveAspectRatio
} = opts;
const isDarkMode = opts.darkMode ?? editor.user.getIsDarkMode();
const shapeIdsToInclude = editor.getShapeAndDescendantIds(ids);
const renderingShapes = editor.getUnorderedRenderingShapes(false).filter(({ id }) => shapeIdsToInclude.has(id));
let bbox = null;
if (opts.bounds) {
bbox = opts.bounds;
} else {
for (const { id } of renderingShapes) {
const maskedPageBounds = editor.getShapeMaskedPageBounds(id);
if (!maskedPageBounds) continue;
if (bbox) {
bbox.union(maskedPageBounds);
} else {
bbox = maskedPageBounds.clone();
}
}
}
if (!bbox) return;
const singleFrameShapeId = ids.length === 1 && editor.isShapeOfType(editor.getShape(ids[0]), "frame") ? ids[0] : null;
if (!singleFrameShapeId) {
bbox.expandBy(padding);
}
const w = bbox.width * scale;
const h = bbox.height * scale;
try {
document.body.focus?.();
} catch {
}
const exportDelay = new import_ExportDelay.ExportDelay(editor.options.maxExportDelayMs);
const initialEffectPromise = (0, import_utils.promiseWithResolve)();
exportDelay.waitUntil(initialEffectPromise);
const svg = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
SvgExport,
{
editor,
preserveAspectRatio,
scale,
pixelRatio: opts.pixelRatio ?? null,
bbox,
background,
singleFrameShapeId,
isDarkMode,
renderingShapes,
onMount: initialEffectPromise.resolve,
waitUntil: exportDelay.waitUntil
}
);
return { jsx: svg, width: w, height: h, exportDelay };
}
function SvgExport({
editor,
preserveAspectRatio,
scale,
pixelRatio,
bbox,
background,
singleFrameShapeId,
isDarkMode,
renderingShapes,
onMount,
waitUntil
}) {
const masksId = (0, import_useSafeId.useUniqueSafeId)();
const theme = (0, import_tlschema.getDefaultColorTheme)({ isDarkMode });
const stateAtom = (0, import_state_react.useAtom)("export state", { defsById: {}, shapeElements: null });
const { defsById, shapeElements } = (0, import_state_react.useValue)(stateAtom);
const addExportDef = (0, import_useEvent.useEvent)((def) => {
stateAtom.update((state) => {
if ((0, import_utils.hasOwnProperty)(state.defsById, def.key)) return state;
const promise = Promise.resolve(def.getElement());
waitUntil(
promise.then((result) => {
stateAtom.update((state2) => ({
...state2,
defsById: { ...state2.defsById, [def.key]: { pending: false, element: result } }
}));
})
);
return {
...state,
defsById: { ...state.defsById, [def.key]: { pending: true, element: promise } }
};
});
});
const exportContext = (0, import_react.useMemo)(
() => ({
isDarkMode,
waitUntil,
addExportDef,
scale,
pixelRatio,
async resolveAssetUrl(assetId, width) {
const asset = editor.getAsset(assetId);
if (!asset || asset.type !== "image" && asset.type !== "video") return null;
return await editor.resolveAssetUrl(assetId, {
screenScale: scale * (width / asset.props.w),
shouldResolveToOriginal: pixelRatio === null,
dpr: pixelRatio ?? void 0
});
}
}),
[isDarkMode, waitUntil, addExportDef, scale, pixelRatio, editor]
);
const didRenderRef = (0, import_react.useRef)(false);
(0, import_react.useLayoutEffect)(() => {
if (didRenderRef.current) {
throw new Error("SvgExport should only render once - do not use with react strict mode");
}
didRenderRef.current = true;
(async () => {
const shapeDefs = {};
const unorderedShapeElementPromises = renderingShapes.map(
async ({ id, opacity, index, backgroundIndex }) => {
if (id === singleFrameShapeId) return [];
const shape = editor.getShape(id);
if (editor.isShapeOfType(shape, "group")) return [];
const elements = [];
const util = editor.getShapeUtil(shape);
if (util.toSvg || util.toBackgroundSvg) {
const [toSvgResult, toBackgroundSvgResult] = await Promise.all([
util.toSvg?.(shape, exportContext),
util.toBackgroundSvg?.(shape, exportContext)
]);
const pageTransform = editor.getShapePageTransform(shape);
let pageTransformString = pageTransform.toCssString();
let scale2 = 1;
if ("scale" in shape.props) {
if (shape.props.scale !== 1) {
scale2 = shape.props.scale;
pageTransformString = `${pageTransformString} scale(${shape.props.scale}, ${shape.props.scale})`;
}
}
const pageMask = editor.getShapeMask(shape.id);
const shapeMask = pageMask ? import_Mat.Mat.From(import_Mat.Mat.Inverse(pageTransform)).applyToPoints(pageMask) : null;
const shapeMaskId = (0, import_useSafeId.suffixSafeId)(masksId, shape.id);
if (shapeMask) {
shapeDefs[shapeMaskId] = {
pending: false,
element: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("clipPath", { id: shapeMaskId, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"path",
{
d: `M${shapeMask.map(({ x, y }) => `${x / scale2},${y / scale2}`).join("L")}Z`
}
) })
};
}
if (toSvgResult) {
elements.push({
zIndex: index,
element: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"g",
{
transform: pageTransformString,
opacity,
clipPath: pageMask ? `url(#${shapeMaskId})` : void 0,
children: toSvgResult
},
`fg_${shape.id}`
)
});
}
if (toBackgroundSvgResult) {
elements.push({
zIndex: backgroundIndex,
element: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"g",
{
transform: pageTransformString,
opacity,
clipPath: pageMask ? `url(#${shapeMaskId})` : void 0,
children: toBackgroundSvgResult
},
`bg_${shape.id}`
)
});
}
} else {
elements.push({
zIndex: index,
element: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
ForeignObjectShape,
{
shape,
util,
component: import_Shape.InnerShape,
className: "tl-shape",
bbox,
opacity
},
`fg_${shape.id}`
)
});
if (util.backgroundComponent) {
elements.push({
zIndex: backgroundIndex,
element: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
ForeignObjectShape,
{
shape,
util,
component: import_Shape.InnerShapeBackground,
className: "tl-shape tl-shape-background",
bbox,
opacity
},
`bg_${shape.id}`
)
});
}
}
return elements;
}
);
const unorderedShapeElements = (await Promise.all(unorderedShapeElementPromises)).flat();
(0, import_react_dom.flushSync)(() => {
stateAtom.update((state) => ({
...state,
shapeElements: unorderedShapeElements.sort((a, b) => a.zIndex - b.zIndex).map(({ element }) => element),
defsById: { ...state.defsById, ...shapeDefs }
}));
});
})();
}, [bbox, editor, exportContext, masksId, renderingShapes, singleFrameShapeId, stateAtom]);
(0, import_react.useEffect)(() => {
const fontsInUse = /* @__PURE__ */ new Set();
for (const { id } of renderingShapes) {
for (const font of editor.fonts.getShapeFontFaces(id)) {
fontsInUse.add(font);
}
}
for (const font of fontsInUse) {
addExportDef({
key: (0, import_utils.uniqueId)(),
getElement: async () => {
const declaration = await editor.fonts.toEmbeddedCssDeclaration(font);
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { nonce: editor.options.nonce, children: declaration });
}
});
}
}, [editor, renderingShapes, addExportDef]);
(0, import_react.useEffect)(() => {
if (shapeElements === null) return;
onMount();
}, [onMount, shapeElements]);
let backgroundColor = background ? theme.background : "transparent";
if (singleFrameShapeId && background) {
const frameShapeUtil = editor.getShapeUtil("frame");
if (frameShapeUtil?.options.showColors) {
const shape = editor.getShape(singleFrameShapeId);
const color = theme[shape.props.color];
backgroundColor = color.frame.fill;
} else {
backgroundColor = theme.solid;
}
}
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_SvgExportContext.SvgExportContextProvider, { editor, context: exportContext, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
"svg",
{
preserveAspectRatio,
direction: "ltr",
width: bbox.width * scale,
height: bbox.height * scale,
viewBox: `${bbox.minX} ${bbox.minY} ${bbox.width} ${bbox.height}`,
strokeLinecap: "round",
strokeLinejoin: "round",
style: { backgroundColor },
"data-color-mode": isDarkMode ? "dark" : "light",
className: `tl-container tl-theme__force-sRGB ${isDarkMode ? "tl-theme__dark" : "tl-theme__light"}`,
children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("defs", { children: Object.entries(defsById).map(
([key, def]) => def.pending ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react.Fragment, { children: def.element }, key)
) }),
shapeElements
]
}
) });
}
function ForeignObjectShape({
shape,
util,
className,
component: Component,
bbox,
opacity
}) {
const editor = (0, import_useEditor.useEditor)();
const transform = import_Mat.Mat.Translate(-bbox.minX, -bbox.minY).multiply(
editor.getShapePageTransform(shape.id)
);
const bounds = editor.getShapeGeometry(shape.id).bounds;
const width = Math.max(bounds.width, 1);
const height = Math.max(bounds.height, 1);
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ErrorBoundary.ErrorBoundary, { fallback: () => null, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"foreignObject",
{
x: bbox.minX,
y: bbox.minY,
width: bbox.w,
height: bbox.h,
className: "tl-shape-foreign-object tl-export-embed-styles",
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"div",
{
className,
"data-shape-type": shape.type,
style: {
clipPath: editor.getShapeClipPath(shape.id),
transform: transform.toCssString(),
width,
height,
opacity
},
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Component, { shape, util })
}
)
}
) });
}
//# sourceMappingURL=getSvgJsx.js.map