-
Notifications
You must be signed in to change notification settings - Fork 34
Issues/1546 #474
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: issues/1546
Are you sure you want to change the base?
Issues/1546 #474
Changes from all commits
3adb3a5
0499a49
68e0065
50d1abf
48ff53d
af5dc7a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,182 @@ | ||
| /******************************************************************************** | ||
| * Copyright (c) 2026 EclipseSource and others. | ||
| * | ||
| * This program and the accompanying materials are made available under the | ||
| * terms of the Eclipse Public License v. 2.0 which is available at | ||
| * http://www.eclipse.org/legal/epl-2.0. | ||
| * | ||
| * This Source Code may also be made available under the following Secondary | ||
| * Licenses when the conditions for such availability set forth in the Eclipse | ||
| * Public License v. 2.0 are satisfied: GNU General Public License, version 2 | ||
| * with the GNU Classpath Exception which is available at | ||
| * https://www.gnu.org/software/classpath/license.html. | ||
| * | ||
| * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 | ||
| ********************************************************************************/ | ||
|
|
||
| import { | ||
| Action, | ||
| CommandExecutionContext, | ||
| CommandResult, | ||
| ExportPngMcpAction, | ||
| ExportPngMcpActionResult, | ||
| GModelElement, | ||
| GModelRoot, | ||
| HiddenCommand, | ||
| isExportable, | ||
| isHoverable, | ||
| isSelectable, | ||
| isViewport, | ||
| IVNodePostprocessor, | ||
| TYPES | ||
| } from '@eclipse-glsp/sprotty'; | ||
| import { inject, injectable } from 'inversify'; | ||
| import { VNode } from 'snabbdom'; | ||
| import { GLSPSvgExporter } from '../features/export/glsp-svg-exporter'; | ||
|
|
||
| /** | ||
| * This class extends {@link GLSPSvgExporter} in order to make use of the SVG creation logic. | ||
| * It then uses the SVG string to generate an equivalent PNG instead. | ||
| * | ||
| * This class should not be used for standard SVG generation, but only for generating PNG for MCP purposes. | ||
| */ | ||
| @injectable() | ||
| export class GLSPMcpPngExporter extends GLSPSvgExporter { | ||
| async exportPng(root: GModelRoot, request?: ExportPngMcpAction): Promise<void> { | ||
| if (!request) { | ||
| return; | ||
| } | ||
|
|
||
| return new Promise((resolve, reject) => { | ||
| if (typeof document === 'undefined') { | ||
| reject(new Error('Failed to find document.')); | ||
| return; | ||
| } | ||
|
|
||
| let svgElement = this.findSvgElement(); | ||
| if (!svgElement) { | ||
| reject(new Error('Failed to find SVG element.')); | ||
| return; | ||
| } | ||
|
|
||
| svgElement = this.prepareSvgElement(svgElement, root); | ||
| const serializedSvg = this.createSvg(svgElement, root, request?.options ?? {}, request); | ||
| const svgExport = this.getSvgExport(serializedSvg, svgElement, root); | ||
|
|
||
| const svgBlob = new Blob([svgExport], { type: 'image/svg+xml;charset=utf-8' }); | ||
| const url = URL.createObjectURL(svgBlob); | ||
|
|
||
| const img = new Image(); | ||
|
|
||
| img.onload = async () => { | ||
| try { | ||
| const bitmap = await createImageBitmap(img); | ||
|
|
||
| const aspect = bitmap.width / bitmap.height; | ||
| const width = request?.options?.width ?? 1024; | ||
| const height = width / aspect; | ||
| const offscreen = new OffscreenCanvas(width, height); | ||
| const ctx = offscreen.getContext('2d'); | ||
|
|
||
| if (!ctx) { | ||
| reject(new Error('Failed to get offscreen context.')); | ||
| return; | ||
| } | ||
|
|
||
| ctx.fillStyle = 'white'; | ||
| ctx.fillRect(0, 0, width, height); | ||
| ctx.drawImage(bitmap, 0, 0, width, height); | ||
|
|
||
| const outBlob = await offscreen.convertToBlob({ type: 'image/png' }); | ||
| const reader = new FileReader(); | ||
| reader.onloadend = () => { | ||
| URL.revokeObjectURL(url); | ||
| const result = reader.result as string; | ||
| this.actionDispatcher.dispatch( | ||
| ExportPngMcpActionResult.create( | ||
| result.replace('data:image/png;base64,', ''), | ||
| request.mcpRequestId, | ||
| request.options | ||
| ) | ||
| ); | ||
| resolve(); | ||
| }; | ||
| reader.readAsDataURL(outBlob); | ||
| } catch (err) { | ||
| reject(err); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should make sure to call |
||
| } | ||
| }; | ||
|
|
||
| img.onerror = () => { | ||
| URL.revokeObjectURL(url); | ||
| reject(new Error('Failed to load SVG into image element.')); | ||
| }; | ||
|
|
||
| img.src = url; | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * See sprotty's `ExportSvgCommand` | ||
| */ | ||
| export class ExportPngMcpCommand extends HiddenCommand { | ||
| static readonly KIND = ExportPngMcpAction.KIND; | ||
|
|
||
| constructor(@inject(TYPES.Action) protected action: ExportPngMcpAction) { | ||
| super(); | ||
| } | ||
|
|
||
| execute(context: CommandExecutionContext): CommandResult { | ||
| if (isExportable(context.root)) { | ||
| const root = context.modelFactory.createRoot(context.root); | ||
| if (isExportable(root)) { | ||
| if (isViewport(root)) { | ||
| root.zoom = 1; | ||
| root.scroll = { x: 0, y: 0 }; | ||
| } | ||
| root.index.all().forEach(element => { | ||
| if (isSelectable(element) && element.selected) { | ||
| element.selected = false; | ||
| } | ||
| if (isHoverable(element) && element.hoverFeedback) { | ||
| element.hoverFeedback = false; | ||
| } | ||
| }); | ||
| return { | ||
| model: root, | ||
| modelChanged: true, | ||
| cause: this.action | ||
| }; | ||
| } | ||
| } | ||
| return { | ||
| model: context.root, | ||
| modelChanged: false | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * See sprotty's `ExportSvgPostprocessor` | ||
| */ | ||
| @injectable() | ||
| export class ExportPngMcpPostprocessor implements IVNodePostprocessor { | ||
| protected root: GModelRoot; | ||
|
|
||
| @inject(GLSPMcpPngExporter) | ||
| protected pngExporter: GLSPMcpPngExporter; | ||
|
|
||
| decorate(vnode: VNode, element: GModelElement): VNode { | ||
| if (element instanceof GModelRoot) { | ||
| this.root = element; | ||
| } | ||
| return vnode; | ||
| } | ||
|
|
||
| postUpdate(cause?: Action): void { | ||
| if (this.root && cause !== undefined && cause.kind === ExportPngMcpAction.KIND) { | ||
| this.pngExporter.exportPng(this.root, cause as ExportPngMcpAction); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| /******************************************************************************** | ||
| * Copyright (c) 2026 EclipseSource and others. | ||
| * | ||
| * This program and the accompanying materials are made available under the | ||
| * terms of the Eclipse Public License v. 2.0 which is available at | ||
| * http://www.eclipse.org/legal/epl-2.0. | ||
| * | ||
| * This Source Code may also be made available under the following Secondary | ||
| * Licenses when the conditions for such availability set forth in the Eclipse | ||
| * Public License v. 2.0 are satisfied: GNU General Public License, version 2 | ||
| * with the GNU Classpath Exception which is available at | ||
| * https://www.gnu.org/software/classpath/license.html. | ||
| * | ||
| * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 | ||
| ********************************************************************************/ | ||
|
|
||
| import { | ||
| Command, | ||
| CommandExecutionContext, | ||
| CommandReturn, | ||
| GetSelectionMcpAction, | ||
| GetSelectionMcpResultAction, | ||
| IActionDispatcher, | ||
| isSelectable, | ||
| toArray, | ||
| TYPES | ||
| } from '@eclipse-glsp/sprotty'; | ||
| import { inject, injectable } from 'inversify'; | ||
|
|
||
| @injectable() | ||
| export class GetSelectionMcpCommand extends Command { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this might need to be a SystemCommand so that we do not pollute the undo/redo stack. |
||
| static readonly KIND = GetSelectionMcpAction.KIND; | ||
|
|
||
| @inject(TYPES.IActionDispatcher) | ||
| protected actionDispatcher: IActionDispatcher; | ||
|
|
||
| constructor(@inject(TYPES.Action) protected readonly action: GetSelectionMcpAction) { | ||
| super(); | ||
| } | ||
|
|
||
| execute(context: CommandExecutionContext): CommandReturn { | ||
| const selection = context.root.index | ||
| .all() | ||
| .filter(e => isSelectable(e) && e.selected) | ||
| .map(e => e.id); | ||
| const result = GetSelectionMcpResultAction.create(toArray(selection), this.action.mcpRequestId); | ||
| this.actionDispatcher.dispatch(result); | ||
| return { model: context.root, modelChanged: false }; | ||
| } | ||
|
|
||
| undo(context: CommandExecutionContext): CommandReturn { | ||
| return { model: context.root, modelChanged: false }; | ||
| } | ||
|
|
||
| redo(context: CommandExecutionContext): CommandReturn { | ||
| return { model: context.root, modelChanged: false }; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| /******************************************************************************** | ||
| * Copyright (c) 2026 EclipseSource and others. | ||
| * | ||
| * This program and the accompanying materials are made available under the | ||
| * terms of the Eclipse Public License v. 2.0 which is available at | ||
| * http://www.eclipse.org/legal/epl-2.0. | ||
| * | ||
| * This Source Code may also be made available under the following Secondary | ||
| * Licenses when the conditions for such availability set forth in the Eclipse | ||
| * Public License v. 2.0 are satisfied: GNU General Public License, version 2 | ||
| * with the GNU Classpath Exception which is available at | ||
| * https://www.gnu.org/software/classpath/license.html. | ||
| * | ||
| * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 | ||
| ********************************************************************************/ | ||
|
|
||
| import { configureCommand, FeatureModule, TYPES } from '@eclipse-glsp/sprotty'; | ||
| import { ExportPngMcpCommand, ExportPngMcpPostprocessor, GLSPMcpPngExporter } from './mcp-export-png'; | ||
| import { GetSelectionMcpCommand } from './mcp-get-selection'; | ||
|
|
||
| export const mcpModule = new FeatureModule( | ||
| (bind, _unbind, isBound) => { | ||
| configureCommand({ bind, isBound }, GetSelectionMcpCommand); | ||
|
|
||
| bind(ExportPngMcpPostprocessor).toSelf().inSingletonScope(); | ||
| bind(TYPES.HiddenVNodePostprocessor).toService(ExportPngMcpPostprocessor); | ||
| configureCommand({ bind, isBound }, ExportPngMcpCommand); | ||
| bind(GLSPMcpPngExporter).toSelf().inSingletonScope(); | ||
| }, | ||
| { featureId: Symbol('mcp') } | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should make sure to call
URL.revokeObjectURL(url)here as well.