diff --git a/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx b/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx index 9560b7286..150ba5ed7 100644 --- a/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx +++ b/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx @@ -70,7 +70,7 @@ export class Ellipse extends TLDrawShapeUtil { rx={rx} ry={ry} stroke="none" - fill={style.isFilled ? styles.fill : 'transparent'} + fill={style.isFilled ? styles.fill : 'none'} pointerEvents="all" /> { y={+styles.strokeWidth / 2} width={Math.max(0, size[0] - strokeWidth)} height={Math.max(0, size[1] - strokeWidth)} - fill={style.isFilled ? styles.fill : 'transparent'} + fill={style.isFilled ? styles.fill : 'none'} stroke="none" pointerEvents="all" /> diff --git a/packages/tldraw/src/state/tldr.ts b/packages/tldraw/src/state/tldr.ts index 331d70517..6e0e81800 100644 --- a/packages/tldraw/src/state/tldr.ts +++ b/packages/tldraw/src/state/tldr.ts @@ -809,6 +809,38 @@ export class TLDR { return Array.from(bindingsToUpdate.values()) } + static copyStringToClipboard = (string: string) => { + try { + navigator.clipboard.writeText(string) + } catch (e) { + const textarea = document.createElement('textarea') + textarea.setAttribute('position', 'fixed') + textarea.setAttribute('top', '0') + textarea.setAttribute('readonly', 'true') + textarea.setAttribute('contenteditable', 'true') + textarea.style.position = 'fixed' + textarea.value = string + document.body.appendChild(textarea) + textarea.focus() + textarea.select() + + try { + const range = document.createRange() + range.selectNodeContents(textarea) + const sel = window.getSelection() + if (sel) { + sel.removeAllRanges() + sel.addRange(range) + textarea.setSelectionRange(0, textarea.value.length) + } + } catch (err) { + null // Could not copy to clipboard + } finally { + document.body.removeChild(textarea) + } + } + } + /* -------------------------------------------------- */ /* Assertions */ /* -------------------------------------------------- */ diff --git a/packages/tldraw/src/state/tlstate.ts b/packages/tldraw/src/state/tlstate.ts index c772e25dd..44fcf8cf7 100644 --- a/packages/tldraw/src/state/tlstate.ts +++ b/packages/tldraw/src/state/tlstate.ts @@ -711,20 +711,79 @@ export class TLDrawState extends StateManager { return this } - copyAsSvg = () => { - // TODO - return '' + /** + * Copy one or more shapes as SVG. + * @param ids The ids of the shapes to copy. + * @param pageId The page from which to copy the shapes. + * @returns A string containing the JSON. + */ + copyAsSvg = (ids = this.selectedIds, pageId = this.currentPageId) => { + if (ids.length === 0) return + + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg') + + ids.forEach((id) => { + const elm = document.getElementById(id) + if (elm) { + const clone = elm?.cloneNode(true) + svg.appendChild(clone) + } + }) + + const shapes = ids.map((id) => this.getShape(id, pageId)) + const bounds = Utils.getCommonBounds(shapes.map(TLDR.getBounds)) + const padding = 16 + + // Resize the element to the bounding box + svg.setAttribute( + 'viewBox', + [ + bounds.minX - padding, + bounds.minY - padding, + bounds.width + padding * 2, + bounds.height + padding * 2, + ].join(' ') + ) + + svg.setAttribute('width', String(bounds.width)) + + svg.setAttribute('height', String(bounds.height)) + + const s = new XMLSerializer() + + const svgString = s + .serializeToString(svg) + .replaceAll(' ', '') + .replaceAll(/((\s|")[0-9]*\.[0-9]{2})([0-9]*)(\b|"|\))/g, '$1') + + TLDR.copyStringToClipboard(svgString) + + return svgString } - copyAsJson = () => { - // TODO - return {} + /** + * Copy one or more shapes as JSON + * @param ids The ids of the shapes to copy. + * @param pageId The page from which to copy the shapes. + * @returns A string containing the JSON. + */ + copyAsJson = (ids = this.selectedIds, pageId = this.currentPageId) => { + const shapes = ids.map((id) => this.getShape(id, pageId)) + const json = JSON.stringify(shapes, null, 2) + TLDR.copyStringToClipboard(json) + return json } /* -------------------------------------------------- */ /* Sessions */ /* -------------------------------------------------- */ + /** + * Start a new session. + * @param session The new session + * @param args arguments of the session's start method. + * @returns this + */ startSession(session: T, ...args: ParametersExceptFirst): this { this.session = session @@ -749,6 +808,11 @@ export class TLDrawState extends StateManager { return this.setStatus(session.status) } + /** + * Update the current session. + * @param args The arguments of the current session's update method. + * @returns this + */ updateSession(...args: ParametersExceptFirst): this { const { session } = this if (!session) return this @@ -757,6 +821,11 @@ export class TLDrawState extends StateManager { return this.patchState(patch, `session:update:${session.id}`) } + /** + * Cancel the current session. + * @param args The arguments of the current session's cancel method. + * @returns this + */ cancelSession(...args: ParametersExceptFirst): this { const { session } = this if (!session) return this @@ -808,6 +877,11 @@ export class TLDrawState extends StateManager { ) } + /** + * Complete the current session. + * @param args The arguments of the current session's complete method. + * @returns this + */ completeSession(...args: ParametersExceptFirst) { const { session } = this @@ -931,11 +1005,17 @@ export class TLDrawState extends StateManager { /* Selection */ /* -------------------------------------------------- */ + /** + * Clear the selection history (undo/redo stack for selection). + */ private clearSelectHistory() { this.selectHistory.pointer = 0 this.selectHistory.stack = [this.selectedIds] } + /** + * Adds a selection to the selection history (undo/redo stack for selection). + */ private addToSelectHistory(ids: string[]) { if (this.selectHistory.pointer < this.selectHistory.stack.length) { this.selectHistory.stack = this.selectHistory.stack.slice(0, this.selectHistory.pointer + 1)