UNPKG

@tldraw/editor

Version:

tldraw infinite canvas SDK (editor).

366 lines (365 loc) • 13.6 kB
"use strict"; 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