diff --git a/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts b/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts index ad0e53e2..bfb31452 100644 --- a/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts +++ b/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts @@ -10,6 +10,7 @@ import { LayoutMethod } from "../layout/layoutMethod"; import { LayoutModelAction } from "../layout/command"; import { SaveJsonFileAction } from "../serialize/saveJsonFile"; import { SaveDfdAndDdFileAction } from "../serialize/saveDfdAndDdFile"; +import { SaveImageAction } from "../serialize/image"; /** * Provides possible actions for the command palette. @@ -39,7 +40,7 @@ export class WebEditorCommandPaletteActionProvider implements ICommandPaletteAct [ new LabeledAction("Save diagram as JSON", [SaveJsonFileAction.create()], "json"), new LabeledAction("Save diagram as DFD and DD", [SaveDfdAndDdFileAction.create()], "coffee"), - //new LabeledAction("Save viewport as image", [SaveImageAction.create()], "device-camera"), + new LabeledAction("Save viewport as image", [SaveImageAction.create()], "device-camera"), ], "save", ), diff --git a/frontend/webEditor/src/serialize/di.config.ts b/frontend/webEditor/src/serialize/di.config.ts index 493081b5..a23f9034 100644 --- a/frontend/webEditor/src/serialize/di.config.ts +++ b/frontend/webEditor/src/serialize/di.config.ts @@ -9,6 +9,7 @@ import { SaveJsonFileCommand } from "./saveJsonFile"; import { SaveDfdAndDdFileCommand } from "./saveDfdAndDdFile"; import { AnalyzeCommand } from "./analyze"; import { LoadFromUrlCommand } from "./LoadUrl"; +import { SaveImageCommand } from "./image"; import { JsonDropHandler, LoadDroppedFileCommand } from "./dropListener"; export const serializeModule = new ContainerModule((bind, unbind, isBound, rebind) => { @@ -21,6 +22,7 @@ export const serializeModule = new ContainerModule((bind, unbind, isBound, rebin configureCommand(context, SaveJsonFileCommand); configureCommand(context, SaveDfdAndDdFileCommand); configureCommand(context, AnalyzeCommand); + configureCommand(context, SaveImageCommand); configureCommand(context, LoadDroppedFileCommand); bind(TYPES.MouseListener).to(JsonDropHandler); diff --git a/frontend/webEditor/src/serialize/image.ts b/frontend/webEditor/src/serialize/image.ts new file mode 100644 index 00000000..b6bf57ee --- /dev/null +++ b/frontend/webEditor/src/serialize/image.ts @@ -0,0 +1,108 @@ +import { Command, CommandExecutionContext, CommandReturn, TYPES } from "sprotty"; +import themeCss from "../assets/theme.css?raw"; +import elementCss from "../diagram/style.css?raw"; +import { Action } from "sprotty-protocol"; +import { inject } from "inversify"; +import { FileName } from "../fileName/fileName"; + +export namespace SaveImageAction { + export const KIND = "save-image"; + + export function create(): Action { + return { + kind: KIND, + }; + } +} + +export class SaveImageCommand extends Command { + static readonly KIND = SaveImageAction.KIND; + + constructor( + @inject(TYPES.Action) _: Action, + @inject(FileName) private readonly fileName: FileName, + ) { + super(); + } + + execute(context: CommandExecutionContext): CommandReturn { + const root = document.getElementById("sprotty_root"); + if (!root) return context.root; + const firstChild = root.children[0]; + if (!firstChild) return context.root; + const innerSvg = firstChild.innerHTML; + /* The result svg will render (0,0) as the top left corner of the svg. + * We calculate the minimum translation of all children. + * We then offset the whole svg by this opposite of this amount. + */ + const minTranslate = { x: Infinity, y: Infinity }; + for (const child of firstChild.children) { + const childTranslate = this.getMinTranslate(child as HTMLElement); + minTranslate.x = Math.min(minTranslate.x, childTranslate.x); + minTranslate.y = Math.min(minTranslate.y, childTranslate.y); + } + const svg = `${innerSvg}`; + + const blob = new Blob([svg], { type: "image/svg+xml" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = this.fileName.getName() + ".svg"; + link.click(); + + return context.root; + } + undo(context: CommandExecutionContext): CommandReturn { + return context.root; + } + redo(context: CommandExecutionContext): CommandReturn { + return context.root; + } + + /** + * Gets the minimum translation of an element relative to the svg. + * This is done by recursively getting the translation of all child elements + * @param e the element to get the translation from + * @param parentOffset Offset of the containing element + * @returns Minimum absolute offset of any child element relative to the svg + */ + private getMinTranslate( + e: HTMLElement, + parentOffset: { x: number; y: number } = { x: 0, y: 0 }, + ): { x: number; y: number } { + const myTranslate = this.getTranslate(e, parentOffset); + const minTranslate = myTranslate; + + const children = e.children; + for (const child of children) { + const childTranslate = this.getMinTranslate(child as HTMLElement, myTranslate); + minTranslate.x = Math.min(minTranslate.x, childTranslate.x); + minTranslate.y = Math.min(minTranslate.y, childTranslate.y); + } + return minTranslate; + } + + /** + * Calculates the absolute translation of an element relative to the svg. + * If the element has no translation, the offset of the parent is returned. + * @param e the element to get the translation from + * @param parentOffset Offset of the containing element + * @returns Offset of the child relative to the svg + */ + private getTranslate( + e: HTMLElement, + parentOffset: { x: number; y: number } = { x: 0, y: 0 }, + ): { x: number; y: number } { + const transform = e.getAttribute("transform"); + if (!transform) return parentOffset; + const translateMatch = transform.match(/translate\(([^)]+)\)/); + if (!translateMatch) return parentOffset; + const translate = translateMatch[1].match(/(-?[0-9.]+)(?:, | |,)(-?[0-9.]+)/); + if (!translate) return parentOffset; + const x = parseFloat(translate[1]); + const y = parseFloat(translate[2]); + const newX = x + parentOffset.x; + const newY = y + parentOffset.y; + return { x: newX, y: newY }; + } +}