From 4da229dc42e97f2c54a52897f499113d8df2bb60 Mon Sep 17 00:00:00 2001 From: Samuel Gratzl Date: Sun, 17 Sep 2023 11:32:51 -0400 Subject: [PATCH 1/2] fix: support export full=true option closes #25 --- samples/annotations.html | 1 + samples/annotations.ts | 14 +++++++++ src/LayersPlugin.ts | 61 +++++++++++++++------------------------ src/elements/edges.ts | 10 +++++-- src/elements/nodes.ts | 5 ++-- src/layers/ABaseLayer.ts | 3 +- src/layers/CanvasLayer.ts | 17 ++++------- src/layers/interfaces.ts | 9 +----- src/layers/public.ts | 6 +++- 9 files changed, 62 insertions(+), 64 deletions(-) diff --git a/samples/annotations.html b/samples/annotations.html index 5a8594d..1fcd9d1 100644 --- a/samples/annotations.html +++ b/samples/annotations.html @@ -12,6 +12,7 @@ +
diff --git a/samples/annotations.ts b/samples/annotations.ts index 9bd3368..0c32f98 100644 --- a/samples/annotations.ts +++ b/samples/annotations.ts @@ -74,4 +74,18 @@ namespace Annotations { a.click(); }); }); + document.getElementById('png2')?.addEventListener('click', () => { + layers + .png({ + output: 'blob-promise', + ignoreUnsupportedLayerOrder: true, + full: true, + }) + .then((r) => { + const url = URL.createObjectURL(r); + const a = document.getElementById('url') as HTMLAnchorElement; + a.href = url; + a.click(); + }); + }); } diff --git a/src/LayersPlugin.ts b/src/LayersPlugin.ts index c892f45..5e7fe4f 100644 --- a/src/LayersPlugin.ts +++ b/src/LayersPlugin.ts @@ -337,24 +337,6 @@ export default class LayersPlugin { ); } - const renderer = ( - this.cy as unknown as { - renderer(): { - bufferCanvasImage( - o: cy.ExportJpgStringOptions | cy.ExportJpgBlobOptions | cy.ExportJpgBlobPromiseOptions - ): HTMLCanvasElement; - }; - } - ).renderer(); - - const bg = options.bg; - - const canvas = renderer.bufferCanvasImage({ ...options, bg: undefined }); - - const width = canvas.width; - const height = canvas.height; - const ctx = canvas.getContext('2d')!; - const before = layers .slice(0, nodeIndex) .reverse() @@ -364,27 +346,30 @@ export default class LayersPlugin { .slice(nodeIndex + 1) .filter((d) => d.supportsRender() && d !== this.dragLayer && d !== this.selectBoxLayer); - const scale = options.scale ?? 1; - - const hint = { scale, width, height, full: options.full ?? false }; - - ctx.globalCompositeOperation = 'destination-over'; - for (const l of before) { - l.renderInto(ctx, hint); - } - - ctx.globalCompositeOperation = 'source-over'; - for (const l of after) { - l.renderInto(ctx, hint); - } - - if (bg) { - ctx.globalCompositeOperation = 'destination-over'; - ctx.fillStyle = bg; - ctx.rect(0, 0, width, height); - ctx.fill(); - } + const renderer = ( + this.cy as unknown as { + renderer(): { + bufferCanvasImage( + o: cy.ExportJpgStringOptions | cy.ExportJpgBlobOptions | cy.ExportJpgBlobPromiseOptions + ): HTMLCanvasElement; + drawElements(ctx: CanvasRenderingContext2D, elems: cy.Collection): void; + }; + } + ).renderer(); + const drawElements = renderer.drawElements; + // patch with all levels + renderer.drawElements = function (ctx, elems) { + for (const l of before) { + l.renderInto(ctx); + } + drawElements.call(this, ctx, elems); + for (const l of after) { + l.renderInto(ctx); + } + }; + const canvas = renderer.bufferCanvasImage(options); + renderer.drawElements = drawElements; return canvas; } diff --git a/src/elements/edges.ts b/src/elements/edges.ts index dd0ed02..2360b06 100644 --- a/src/elements/edges.ts +++ b/src/elements/edges.ts @@ -1,5 +1,6 @@ import type cy from 'cytoscape'; import type { ICanvasLayer, IPoint } from '../layers'; +import type { IRenderHint } from '../public'; import { ICallbackRemover, registerCallback } from './utils'; import { IElementLayerOptions, defaultElementLayerOptions } from './common'; @@ -71,7 +72,7 @@ export function renderPerEdge( layer.cy.on('add remove', o.selector, revaluateAndUpdateOnce); } - const renderer = (ctx: CanvasRenderingContext2D) => { + const renderer = (ctx: CanvasRenderingContext2D, hint: IRenderHint) => { if (o.queryEachTime) { edges = reevaluateCollection(edges); } @@ -90,7 +91,12 @@ export function renderPerEdge( impl && impl.startX != null && impl.startY != null ? { x: impl.startX, y: impl.startY } : edge.sourceEndpoint(); const t = impl && impl.endX != null && impl.endY != null ? { x: impl.endX, y: impl.endY } : edge.targetEndpoint(); - if (o.checkBounds && o.checkBoundsPointCount > 0 && !anyVisible(layer, s, t, o.checkBoundsPointCount)) { + if ( + !hint.forExport && + o.checkBounds && + o.checkBoundsPointCount > 0 && + !anyVisible(layer, s, t, o.checkBoundsPointCount) + ) { return; } if (impl && impl.pathCache) { diff --git a/src/elements/nodes.ts b/src/elements/nodes.ts index d064517..4690ed1 100644 --- a/src/elements/nodes.ts +++ b/src/elements/nodes.ts @@ -1,5 +1,6 @@ import type cy from 'cytoscape'; import type { ICanvasLayer, IHTMLLayer, ISVGLayer, ILayer } from '../layers'; +import type { IRenderHint } from '../public'; import { SVG_NS } from '../layers/SVGLayer'; import { matchNodes, registerCallback, ICallbackRemover, IMatchOptions } from './utils'; import { IElementLayerOptions, defaultElementLayerOptions } from './common'; @@ -140,7 +141,7 @@ export function renderPerNode( if (layer.type === 'canvas') { const oCanvas = o as INodeCanvasLayerOption; - const renderer = (ctx: CanvasRenderingContext2D) => { + const renderer = (ctx: CanvasRenderingContext2D, hint: IRenderHint) => { const t = ctx.getTransform(); if (o.queryEachTime) { nodes = reevaluateCollection(nodes); @@ -150,7 +151,7 @@ export function renderPerNode( return; } const bb = node.boundingBox(o.boundingBox); - if (oCanvas.checkBounds && !layer.inVisibleBounds(bb)) { + if (!hint.forExport && oCanvas.checkBounds && !layer.inVisibleBounds(bb)) { return; } if (oCanvas.position === 'top-left') { diff --git a/src/layers/ABaseLayer.ts b/src/layers/ABaseLayer.ts index f83761d..ffc0e7a 100644 --- a/src/layers/ABaseLayer.ts +++ b/src/layers/ABaseLayer.ts @@ -7,7 +7,6 @@ import type { IHTMLStaticLayer, ISVGStaticLayer, ICanvasStaticLayer, - IRenderHint, } from './interfaces'; import type cy from 'cytoscape'; import type { ICanvasLayerOptions, ISVGLayerOptions, IHTMLLayerOptions } from './public'; @@ -39,7 +38,7 @@ export abstract class ABaseLayer implements IMoveAbleLayer { return false; } - renderInto(_ctx: CanvasRenderingContext2D, _hint: IRenderHint): void { + renderInto(_ctx: CanvasRenderingContext2D): void { // dummy } diff --git a/src/layers/CanvasLayer.ts b/src/layers/CanvasLayer.ts index e8a81f3..6bab3e3 100644 --- a/src/layers/CanvasLayer.ts +++ b/src/layers/CanvasLayer.ts @@ -1,11 +1,4 @@ -import type { - ICanvasLayer, - ILayerElement, - ILayerImpl, - IRenderFunction, - ICanvasStaticLayer, - IRenderHint, -} from './interfaces'; +import type { ICanvasLayer, ILayerElement, ILayerImpl, IRenderFunction, ICanvasStaticLayer } from './interfaces'; import { layerStyle, stopClicks } from './utils'; import { ABaseLayer, ILayerAdapter } from './ABaseLayer'; import type { ICanvasLayerOptions } from './public'; @@ -86,7 +79,7 @@ export class CanvasBaseLayer extends ABaseLayer implements ILayerImpl { ctx.scale(this.transform.zoom * scale, this.transform.zoom * scale); for (const r of this.callbacks) { - r(ctx); + r(ctx, {}); } ctx.restore(); @@ -96,8 +89,10 @@ export class CanvasBaseLayer extends ABaseLayer implements ILayerImpl { return true; } - renderInto(ctx: CanvasRenderingContext2D, hint: IRenderHint): void { - this.drawImpl(ctx, hint.scale); + renderInto(ctx: CanvasRenderingContext2D): void { + for (const r of this.callbacks) { + r(ctx, { forExport: true }); + } } resize(width: number, height: number) { diff --git a/src/layers/interfaces.ts b/src/layers/interfaces.ts index abfc5fc..069be51 100644 --- a/src/layers/interfaces.ts +++ b/src/layers/interfaces.ts @@ -6,13 +6,6 @@ export interface ILayerElement { __cy_layer: ILayer & ILayerImpl; } -export interface IRenderHint { - scale: number; - width: number; - height: number; - full: boolean; -} - export interface ILayerImpl { readonly root: HTMLElement | SVGElement; resize(width: number, height: number): void; @@ -20,5 +13,5 @@ export interface ILayerImpl { setViewport(tx: number, ty: number, zoom: number): void; supportsRender(): boolean; - renderInto(ctx: CanvasRenderingContext2D, hint: IRenderHint): void; + renderInto(ctx: CanvasRenderingContext2D): void; } diff --git a/src/layers/public.ts b/src/layers/public.ts index fe86887..54828a9 100644 --- a/src/layers/public.ts +++ b/src/layers/public.ts @@ -5,8 +5,12 @@ export interface IPoint { y: number; } +export interface IRenderHint { + forExport?: boolean; +} + export interface IRenderFunction { - (ctx: CanvasRenderingContext2D): void; + (ctx: CanvasRenderingContext2D, hint: IRenderHint): void; } export interface IDOMUpdateFunction { From 3d4406c1ad114441f59dda4bd17f8365662835ec Mon Sep 17 00:00:00 2001 From: Samuel Gratzl Date: Sun, 17 Sep 2023 11:36:11 -0400 Subject: [PATCH 2/2] fix: imports --- src/elements/edges.ts | 3 +-- src/elements/nodes.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/elements/edges.ts b/src/elements/edges.ts index 2360b06..7354f7f 100644 --- a/src/elements/edges.ts +++ b/src/elements/edges.ts @@ -1,6 +1,5 @@ import type cy from 'cytoscape'; -import type { ICanvasLayer, IPoint } from '../layers'; -import type { IRenderHint } from '../public'; +import type { ICanvasLayer, IPoint, IRenderHint } from '../layers'; import { ICallbackRemover, registerCallback } from './utils'; import { IElementLayerOptions, defaultElementLayerOptions } from './common'; diff --git a/src/elements/nodes.ts b/src/elements/nodes.ts index 4690ed1..9988671 100644 --- a/src/elements/nodes.ts +++ b/src/elements/nodes.ts @@ -1,6 +1,5 @@ import type cy from 'cytoscape'; -import type { ICanvasLayer, IHTMLLayer, ISVGLayer, ILayer } from '../layers'; -import type { IRenderHint } from '../public'; +import type { ICanvasLayer, IHTMLLayer, ISVGLayer, ILayer, IRenderHint } from '../layers'; import { SVG_NS } from '../layers/SVGLayer'; import { matchNodes, registerCallback, ICallbackRemover, IMatchOptions } from './utils'; import { IElementLayerOptions, defaultElementLayerOptions } from './common';