mirror of
https://gitee.com/antv/g6.git
synced 2024-12-01 19:28:39 +08:00
feat(test): config integration test env and add modular canvas (#5374)
* feat(test): config integration test env * feat(runtime): add modular canvas * refactor(test): update snapshot naming rule * chore(runtime): adjust variable naming * chore(test): adjust folder naming * refactor(runtime): remove unnecessary variable usage
This commit is contained in:
parent
d48787e345
commit
4e11baeb51
@ -18,6 +18,12 @@ module.exports = {
|
||||
sourceType: 'script',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/__tests__/**'],
|
||||
rules: {
|
||||
'jsdoc/require-jsdoc': 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
|
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -37,5 +37,5 @@ jobs:
|
||||
with:
|
||||
name: snapshots
|
||||
path: |
|
||||
packages/g6/tests/integration/snapshots/**/*-actual.svg
|
||||
packages/g6/__tests__/integration/snapshots/**/*-actual.svg
|
||||
retention-days: 1
|
||||
|
4
packages/g6/.gitignore
vendored
4
packages/g6/.gitignore
vendored
@ -1,2 +1,2 @@
|
||||
tests/integration/snapshots/**/*-actual.*
|
||||
tests/integration/snapshots/**/*-diff.*
|
||||
__tests__/integration/snapshots/**/*-actual.*
|
||||
__tests__/integration/snapshots/**/*-diff.*
|
1
packages/g6/__tests__/demo/index.ts
Normal file
1
packages/g6/__tests__/demo/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './static';
|
44
packages/g6/__tests__/demo/static/canvas.ts
Normal file
44
packages/g6/__tests__/demo/static/canvas.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { Circle, Rect } from '@antv/g';
|
||||
import { Canvas } from '../../../src/runtime/canvas';
|
||||
import type { TestCase } from '../types';
|
||||
|
||||
export const layeredCanvas: TestCase = async (context) => {
|
||||
const {
|
||||
container,
|
||||
width,
|
||||
height,
|
||||
renderer,
|
||||
canvas = new Canvas({
|
||||
width,
|
||||
height,
|
||||
container,
|
||||
renderer,
|
||||
}),
|
||||
} = context;
|
||||
|
||||
const circle = new Circle({
|
||||
style: {
|
||||
cx: 50,
|
||||
cy: 50,
|
||||
r: 25,
|
||||
fill: 'pink',
|
||||
},
|
||||
});
|
||||
|
||||
const rect = new Rect({
|
||||
style: {
|
||||
x: 100,
|
||||
y: 100,
|
||||
width: 50,
|
||||
height: 50,
|
||||
fill: 'purple',
|
||||
},
|
||||
});
|
||||
|
||||
await canvas.init();
|
||||
|
||||
canvas.appendChild(circle);
|
||||
canvas.appendChild(rect);
|
||||
|
||||
return;
|
||||
};
|
1
packages/g6/__tests__/demo/static/index.ts
Normal file
1
packages/g6/__tests__/demo/static/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './canvas';
|
19
packages/g6/__tests__/demo/types.ts
Normal file
19
packages/g6/__tests__/demo/types.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import type { IRenderer } from '@antv/g';
|
||||
import type { Canvas } from '../../src/runtime/canvas';
|
||||
import type { CanvasLayer } from '../../src/types/canvas';
|
||||
|
||||
type TestCaseContext = {
|
||||
canvas?: Canvas;
|
||||
container: HTMLElement;
|
||||
renderer?: (layer: CanvasLayer) => IRenderer;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export type TestCase = {
|
||||
(context: TestCaseContext): Promise<void>;
|
||||
only?: boolean;
|
||||
skip?: boolean;
|
||||
preprocess?: () => Promise<void>;
|
||||
postprocess?: () => Promise<void>;
|
||||
};
|
@ -0,0 +1,48 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="500"
|
||||
height="500"
|
||||
style="background: transparent; position: absolute; outline: none;"
|
||||
color-interpolation-filters="sRGB"
|
||||
tabindex="1"
|
||||
>
|
||||
<defs />
|
||||
<g id="g-svg-camera" transform="matrix(1,0,0,1,0,0)">
|
||||
<g
|
||||
id="g-root"
|
||||
fill="none"
|
||||
stroke="none"
|
||||
visibility="visible"
|
||||
font-size="16px"
|
||||
font-family="sans-serif"
|
||||
font-style="normal"
|
||||
font-weight="normal"
|
||||
font-variant="normal"
|
||||
text-anchor="left"
|
||||
stroke-dashoffset="0px"
|
||||
transform="matrix(1,0,0,1,0,0)"
|
||||
>
|
||||
<g transform="matrix(1,0,0,1,50,50)">
|
||||
<circle
|
||||
id="g-svg-5"
|
||||
fill="rgba(255,192,203,1)"
|
||||
transform="translate(-25,-25)"
|
||||
cx="25"
|
||||
cy="25"
|
||||
stroke="none"
|
||||
r="25px"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,100,100)">
|
||||
<path
|
||||
id="g-svg-6"
|
||||
fill="rgba(128,0,128,1)"
|
||||
d="M 0,0 l 50,0 l 0,50 l-50 0 z"
|
||||
stroke="none"
|
||||
width="50px"
|
||||
height="50px"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
33
packages/g6/__tests__/integration/static.spec.ts
Normal file
33
packages/g6/__tests__/integration/static.spec.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import * as staticCases from '../demo/static';
|
||||
import { createNodeGCanvas } from './utils/create-node-g-canvas';
|
||||
import { getCases } from './utils/get-cases';
|
||||
import { sleep } from './utils/sleep';
|
||||
import './utils/use-snapshot-matchers';
|
||||
|
||||
describe('static', () => {
|
||||
const cases = getCases(staticCases);
|
||||
|
||||
for (const [name, testCase] of cases) {
|
||||
it(`[static]: ${name}`, async () => {
|
||||
const canvas = createNodeGCanvas();
|
||||
|
||||
try {
|
||||
const { preprocess, postprocess } = testCase;
|
||||
|
||||
await preprocess?.();
|
||||
await testCase({
|
||||
container: document.getElementById('container')!,
|
||||
width: 500,
|
||||
height: 500,
|
||||
canvas,
|
||||
});
|
||||
await postprocess?.();
|
||||
|
||||
await expect(canvas).toMatchSVGSnapshot(`${__dirname}/snapshots/static`, name);
|
||||
} finally {
|
||||
canvas.destroy();
|
||||
await sleep(50);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
@ -0,0 +1,29 @@
|
||||
import { resetEntityCounter } from '@antv/g';
|
||||
import { Plugin as DragAndDropPlugin } from '@antv/g-plugin-dragndrop';
|
||||
import { Renderer as SVGRenderer } from '@antv/g-svg';
|
||||
import { Canvas } from '../../../src/runtime/canvas';
|
||||
import { OffscreenCanvasContext } from './offscreen-canvas-context';
|
||||
|
||||
export function createNodeGCanvas(dom?: HTMLDivElement, width = 500, height = 500) {
|
||||
const container = dom || document.createElement('div');
|
||||
container.style.width = `${width}px`;
|
||||
container.style.height = `${height}px`;
|
||||
|
||||
resetEntityCounter();
|
||||
const offscreenNodeCanvas = {
|
||||
getContext: () => context,
|
||||
} as unknown as HTMLCanvasElement;
|
||||
const context = new OffscreenCanvasContext(offscreenNodeCanvas);
|
||||
|
||||
const renderer = new SVGRenderer();
|
||||
renderer.registerPlugin(new DragAndDropPlugin({ dragstartDistanceThreshold: 10 }));
|
||||
return new Canvas({
|
||||
container,
|
||||
width,
|
||||
height,
|
||||
renderer: () => new SVGRenderer(),
|
||||
// @ts-expect-error offscreenCanvas is not in the type definition
|
||||
document: container.ownerDocument,
|
||||
offscreenCanvas: offscreenNodeCanvas,
|
||||
});
|
||||
}
|
12
packages/g6/__tests__/integration/utils/get-cases.ts
Normal file
12
packages/g6/__tests__/integration/utils/get-cases.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import type { TestCase } from '../../demo/types';
|
||||
|
||||
export function getCases(module: Record<string, TestCase>) {
|
||||
const cases = Object.entries(module);
|
||||
const only = cases.filter(([, { only = false }]) => only);
|
||||
if (only.length !== 0 && process.env.CI === 'true') throw new Error('Cannot run only tests in CI');
|
||||
return (only.length !== 0 ? only : cases)
|
||||
.filter(([, { skip = false }]) => !skip)
|
||||
.map(([name, testCase]) => [kebabCase(name), testCase] as [string, TestCase]);
|
||||
}
|
||||
|
||||
const kebabCase = (str: string) => str.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
|
@ -0,0 +1,128 @@
|
||||
// Computed as round(measureText(text).width * 10) at 10px system-ui. For
|
||||
// characters that are not represented in this map, we’d ideally want to use a
|
||||
// weighted average of what we expect to see. But since we don’t really know
|
||||
// what that is, using “e” seems reasonable.
|
||||
const defaultWidthMap = {
|
||||
a: 56,
|
||||
b: 63,
|
||||
c: 57,
|
||||
d: 63,
|
||||
e: 58,
|
||||
f: 37,
|
||||
g: 62,
|
||||
h: 60,
|
||||
i: 26,
|
||||
j: 26,
|
||||
k: 55,
|
||||
l: 26,
|
||||
m: 88,
|
||||
n: 60,
|
||||
o: 60,
|
||||
p: 62,
|
||||
q: 62,
|
||||
r: 39,
|
||||
s: 54,
|
||||
t: 38,
|
||||
u: 60,
|
||||
v: 55,
|
||||
w: 79,
|
||||
x: 54,
|
||||
y: 55,
|
||||
z: 55,
|
||||
A: 69,
|
||||
B: 67,
|
||||
C: 73,
|
||||
D: 74,
|
||||
E: 61,
|
||||
F: 58,
|
||||
G: 76,
|
||||
H: 75,
|
||||
I: 28,
|
||||
J: 55,
|
||||
K: 67,
|
||||
L: 58,
|
||||
M: 89,
|
||||
N: 75,
|
||||
O: 78,
|
||||
P: 65,
|
||||
Q: 78,
|
||||
R: 67,
|
||||
S: 65,
|
||||
T: 65,
|
||||
U: 75,
|
||||
V: 69,
|
||||
W: 98,
|
||||
X: 69,
|
||||
Y: 67,
|
||||
Z: 67,
|
||||
0: 64,
|
||||
1: 48,
|
||||
2: 62,
|
||||
3: 64,
|
||||
4: 66,
|
||||
5: 63,
|
||||
6: 65,
|
||||
7: 58,
|
||||
8: 65,
|
||||
9: 65,
|
||||
' ': 29,
|
||||
'!': 32,
|
||||
'"': 49,
|
||||
"'": 31,
|
||||
'(': 39,
|
||||
')': 39,
|
||||
',': 31,
|
||||
'-': 48,
|
||||
'.': 31,
|
||||
'/': 32,
|
||||
':': 31,
|
||||
';': 31,
|
||||
'?': 52,
|
||||
'‘': 31,
|
||||
'’': 31,
|
||||
'“': 47,
|
||||
'”': 47,
|
||||
'…': 82,
|
||||
};
|
||||
|
||||
export function measureText(text: string, fontSize: number) {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
sum += ((defaultWidthMap[text[i]] ?? 100) * fontSize) / 100;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
export class OffscreenCanvasContext {
|
||||
private fontSize: number;
|
||||
|
||||
constructor(public canvas: HTMLCanvasElement) {}
|
||||
|
||||
set font(font: string) {
|
||||
// `${fontStyle} ${fontVariant} ${fontWeight} ${fontSizeString}
|
||||
const [, , , fontSizeString] = font.split(' ');
|
||||
const fontSize = parseFloat(fontSizeString.replace('px', ''));
|
||||
this.fontSize = fontSize;
|
||||
}
|
||||
|
||||
fillRect() {}
|
||||
fillText() {}
|
||||
getImageData(sx: number, sy: number, sw: number, sh: number) {
|
||||
return {
|
||||
// ignore ascent and descent
|
||||
data: new Uint8ClampedArray(sw * sh * 4).fill(0),
|
||||
};
|
||||
}
|
||||
|
||||
measureText(text: string): TextMetrics {
|
||||
return {
|
||||
width: measureText(text, this.fontSize),
|
||||
actualBoundingBoxAscent: 0,
|
||||
actualBoundingBoxDescent: 0,
|
||||
actualBoundingBoxLeft: 0,
|
||||
actualBoundingBoxRight: 0,
|
||||
fontBoundingBoxAscent: 0,
|
||||
fontBoundingBoxDescent: 0,
|
||||
};
|
||||
}
|
||||
}
|
5
packages/g6/__tests__/integration/utils/sleep.ts
Normal file
5
packages/g6/__tests__/integration/utils/sleep.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export function sleep(n: number) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, n);
|
||||
});
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
import { Canvas } from '@antv/g';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { format } from 'prettier';
|
||||
import xmlserializer from 'xmlserializer';
|
||||
import { sleep } from './sleep';
|
||||
|
||||
export type ToMatchSVGSnapshotOptions = {
|
||||
fileFormat?: string;
|
||||
keepSVGElementId?: boolean;
|
||||
};
|
||||
const formatSVG = (svg: string, keepSVGElementId: boolean) => {
|
||||
return (keepSVGElementId ? svg : svg.replace(/id="[^"]*"/g, '').replace(/clip-path="[^"]*"/g, '')).replace(
|
||||
'\r\n',
|
||||
'\n',
|
||||
);
|
||||
};
|
||||
|
||||
// @see https://jestjs.io/docs/26.x/expect#expectextendmatchers
|
||||
export async function toMatchSVGSnapshot(
|
||||
gCanvas: Canvas | Canvas[],
|
||||
dir: string,
|
||||
name: string,
|
||||
options: ToMatchSVGSnapshotOptions = {},
|
||||
): Promise<{ message: () => string; pass: boolean }> {
|
||||
await sleep(300);
|
||||
|
||||
const { fileFormat = 'svg', keepSVGElementId = true } = options;
|
||||
const namePath = path.join(dir, name);
|
||||
const actualPath = path.join(dir, `${name}-actual.${fileFormat}`);
|
||||
const expectedPath = path.join(dir, `${name}.${fileFormat}`);
|
||||
const gCanvases = Array.isArray(gCanvas) ? gCanvas : [gCanvas];
|
||||
|
||||
let actual: string = '';
|
||||
|
||||
// Clone <svg>
|
||||
const svg = (gCanvases[0].getContextService().getDomElement() as unknown as SVGElement).cloneNode(true) as SVGElement;
|
||||
const gRoot = svg.querySelector('#g-root');
|
||||
|
||||
gCanvases.slice(1).forEach((gCanvas) => {
|
||||
const dom = (gCanvas.getContextService().getDomElement() as unknown as SVGElement).cloneNode(true) as SVGElement;
|
||||
|
||||
gRoot?.append(...(dom.querySelector('#g-root')?.childNodes || []));
|
||||
});
|
||||
|
||||
actual += svg
|
||||
? formatSVG(
|
||||
await format(xmlserializer.serializeToString(svg as any), {
|
||||
parser: 'babel',
|
||||
}),
|
||||
keepSVGElementId,
|
||||
)
|
||||
: 'null';
|
||||
|
||||
// Remove ';' after format by babel.
|
||||
if (actual !== 'null') actual = actual.slice(0, -2);
|
||||
|
||||
try {
|
||||
console.log('actual', dir, fs.existsSync(dir));
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
if (!fs.existsSync(expectedPath)) {
|
||||
if (process.env.CI === 'true') {
|
||||
throw new Error(`Please generate golden image for ${namePath}`);
|
||||
}
|
||||
console.warn(`! generate ${namePath}`);
|
||||
fs.writeFileSync(expectedPath, actual);
|
||||
return {
|
||||
message: () => `generate ${namePath}`,
|
||||
pass: true,
|
||||
};
|
||||
} else {
|
||||
const expected = fs.readFileSync(expectedPath, {
|
||||
encoding: 'utf8',
|
||||
flag: 'r',
|
||||
});
|
||||
if (actual === expected) {
|
||||
if (fs.existsSync(actualPath)) fs.unlinkSync(actualPath);
|
||||
return {
|
||||
message: () => `match ${namePath}`,
|
||||
pass: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Perverse actual file.
|
||||
if (actual) fs.writeFileSync(actualPath, actual);
|
||||
return {
|
||||
message: () => `mismatch ${namePath}`,
|
||||
pass: false,
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
return {
|
||||
message: () => `${e}`,
|
||||
pass: false,
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import { toMatchSVGSnapshot, ToMatchSVGSnapshotOptions } from './to-match-svg-snapshot';
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace jest {
|
||||
interface Matchers<R> {
|
||||
toMatchSVGSnapshot(dir: string, name: string, options?: ToMatchSVGSnapshotOptions): Promise<R>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect.extend({
|
||||
toMatchSVGSnapshot,
|
||||
});
|
@ -63,8 +63,10 @@
|
||||
"@antv/g-plugin-control": "^1.9.17",
|
||||
"@antv/g-svg": "^1.10.21",
|
||||
"@antv/g-webgl": "^1.9.29",
|
||||
"@types/xmlserializer": "^0.6.6",
|
||||
"stats.js": "^0.17.0",
|
||||
"vite": "^5.0.10"
|
||||
"vite": "^5.0.10",
|
||||
"xmlserializer": "^0.6.1"
|
||||
},
|
||||
"limit-size": [
|
||||
{
|
||||
|
216
packages/g6/src/runtime/canvas.ts
Normal file
216
packages/g6/src/runtime/canvas.ts
Normal file
@ -0,0 +1,216 @@
|
||||
import type {
|
||||
Cursor,
|
||||
DataURLOptions,
|
||||
DisplayObject,
|
||||
CanvasConfig as GCanvasConfig,
|
||||
IRenderer,
|
||||
PointLike,
|
||||
} from '@antv/g';
|
||||
import { Canvas as GCanvas } from '@antv/g';
|
||||
import { Renderer as CanvasRenderer } from '@antv/g-canvas';
|
||||
import { Plugin as DragNDropPlugin } from '@antv/g-plugin-dragndrop';
|
||||
import { createDOM, isFunction } from '@antv/util';
|
||||
import type { CanvasOptions } from '../spec/canvas';
|
||||
import type { CanvasLayer } from '../types/canvas';
|
||||
|
||||
export interface CanvasConfig
|
||||
extends Pick<GCanvasConfig, 'container' | 'devicePixelRatio' | 'width' | 'height' | 'background' | 'cursor'> {
|
||||
renderer?: CanvasOptions['renderer'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated this canvas will be replace by layered canvas
|
||||
*/
|
||||
export class Canvas {
|
||||
protected config: CanvasConfig;
|
||||
|
||||
public background: GCanvas;
|
||||
public main: GCanvas;
|
||||
public label: GCanvas;
|
||||
public transient: GCanvas;
|
||||
public transientLabel: GCanvas;
|
||||
|
||||
public get canvas() {
|
||||
return {
|
||||
main: this.main,
|
||||
label: this.label,
|
||||
transient: this.transient,
|
||||
transientLabel: this.transientLabel,
|
||||
background: this.background,
|
||||
};
|
||||
}
|
||||
|
||||
public renderers: Record<CanvasLayer, IRenderer>;
|
||||
|
||||
constructor(config: CanvasConfig) {
|
||||
this.config = config;
|
||||
const { renderer: getRenderer, ...restConfig } = config;
|
||||
const names: CanvasLayer[] = ['main', 'label', 'transient', 'transientLabel', 'background'];
|
||||
|
||||
const renderers = names.map((name) => {
|
||||
const renderer = isFunction(getRenderer) ? getRenderer?.(name) : new CanvasRenderer();
|
||||
|
||||
renderer.registerPlugin(
|
||||
new DragNDropPlugin({
|
||||
isDocumentDraggable: true,
|
||||
isDocumentDroppable: true,
|
||||
dragstartDistanceThreshold: 10,
|
||||
dragstartTimeThreshold: 100,
|
||||
}),
|
||||
);
|
||||
|
||||
if (name !== 'main') {
|
||||
renderer.unregisterPlugin(renderer.getPlugin('dom-interaction'));
|
||||
}
|
||||
|
||||
this[name] = new GCanvas({
|
||||
renderer,
|
||||
supportsMutipleCanvasesInOneContainer: true,
|
||||
...restConfig,
|
||||
});
|
||||
|
||||
return [name, renderer];
|
||||
});
|
||||
|
||||
this.renderers = Object.fromEntries(renderers);
|
||||
|
||||
this.init().then(() => {
|
||||
Object.entries(this.canvas).forEach(([name, canvas]) => {
|
||||
const domElement = canvas.getContextService().getDomElement() as unknown as HTMLElement;
|
||||
|
||||
domElement.style.position = 'absolute';
|
||||
domElement.style.outline = 'none';
|
||||
domElement.tabIndex = 1;
|
||||
|
||||
if (name !== 'main') domElement.style.pointerEvents = 'none';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public init() {
|
||||
return Promise.all(Object.values(this.canvas).map((canvas) => canvas.ready));
|
||||
}
|
||||
|
||||
public getRendererType(layer: CanvasLayer = 'main') {
|
||||
const plugins = this.renderers[layer].getPlugins();
|
||||
|
||||
for (const plugin of plugins) {
|
||||
if (plugin.name === 'canvas-renderer') return 'canvas';
|
||||
if (plugin.name === 'svg-renderer') return 'svg';
|
||||
if (plugin.name === 'device-renderer') return 'gpu';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
public get context() {
|
||||
return this.main.context;
|
||||
}
|
||||
|
||||
public getDevice() {
|
||||
// @ts-expect-error deviceRendererPlugin is private
|
||||
return this.main.context?.deviceRendererPlugin?.getDevice();
|
||||
}
|
||||
|
||||
public getConfig() {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
public setCursor(cursor: Cursor) {
|
||||
Object.values(this.canvas).forEach((canvas) => {
|
||||
canvas.setCursor(cursor);
|
||||
});
|
||||
}
|
||||
|
||||
public resize(width: number, height: number) {
|
||||
Object.values(this.canvas).forEach((canvas) => {
|
||||
canvas.resize(width, height);
|
||||
});
|
||||
}
|
||||
|
||||
public getCamera() {
|
||||
return this.main.getCamera();
|
||||
}
|
||||
|
||||
public appendChild<T extends DisplayObject>(child: T): T {
|
||||
const layer = child.style?.$layer || 'main';
|
||||
|
||||
return this[layer].appendChild(child);
|
||||
}
|
||||
|
||||
public getContextService() {
|
||||
return this.main.getContextService();
|
||||
}
|
||||
|
||||
public viewport2Client(viewport: PointLike) {
|
||||
return this.main.viewport2Client(viewport);
|
||||
}
|
||||
|
||||
public viewport2Canvas(viewport: PointLike) {
|
||||
return this.main.viewport2Canvas(viewport);
|
||||
}
|
||||
|
||||
public client2Viewport(client: PointLike) {
|
||||
return this.main.client2Viewport(client);
|
||||
}
|
||||
|
||||
public canvas2Viewport(canvas: PointLike) {
|
||||
return this.main.canvas2Viewport(canvas);
|
||||
}
|
||||
|
||||
public async toDataURL(options?: Partial<DataURLOptions>) {
|
||||
const devicePixelRatio = window.devicePixelRatio || 1;
|
||||
const { width, height, renderer: getRenderer } = this.config;
|
||||
|
||||
const container: HTMLElement = createDOM('<div id="virtual-image"></div>');
|
||||
|
||||
const offscreenCanvas = new GCanvas({
|
||||
width,
|
||||
height,
|
||||
renderer: getRenderer('main'),
|
||||
devicePixelRatio,
|
||||
container,
|
||||
background: this.background.getConfig().background,
|
||||
});
|
||||
|
||||
await offscreenCanvas.ready;
|
||||
|
||||
offscreenCanvas.appendChild(this.background.getRoot().cloneNode(true));
|
||||
offscreenCanvas.appendChild(this.main.getRoot().cloneNode(true));
|
||||
|
||||
// Handle label canvas
|
||||
const label = this.label.getRoot().cloneNode(true);
|
||||
const originCanvasPosition = offscreenCanvas.viewport2Canvas({ x: 0, y: 0 });
|
||||
const currentCanvasPosition = this.main.viewport2Canvas({ x: 0, y: 0 });
|
||||
label.translate([
|
||||
currentCanvasPosition.x - originCanvasPosition.x,
|
||||
currentCanvasPosition.y - originCanvasPosition.y,
|
||||
]);
|
||||
label.scale(1 / this.main.getCamera().getZoom());
|
||||
offscreenCanvas.appendChild(label);
|
||||
|
||||
offscreenCanvas.appendChild(this.transient.getRoot().cloneNode(true));
|
||||
|
||||
const camera = this.main.getCamera();
|
||||
const offscreenCamera = offscreenCanvas.getCamera();
|
||||
offscreenCamera.setZoom(camera.getZoom());
|
||||
offscreenCamera.setPosition(camera.getPosition());
|
||||
offscreenCamera.setFocalPoint(camera.getFocalPoint());
|
||||
|
||||
const contextService = offscreenCanvas.getContextService();
|
||||
|
||||
return contextService.toDataURL(options);
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
Object.values(this.canvas).forEach((canvas) => {
|
||||
const camera = canvas.getCamera();
|
||||
// @ts-expect-error landmark is private
|
||||
if (camera.landmarks?.length) {
|
||||
camera.cancelLandmarkAnimation();
|
||||
}
|
||||
|
||||
canvas.destroy();
|
||||
});
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
root: './tests',
|
||||
root: './__tests__',
|
||||
server: {
|
||||
port: 8080,
|
||||
open: '/',
|
||||
|
Loading…
Reference in New Issue
Block a user