pmx-canvas 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,6 +22,7 @@ import { searchNodes } from './spatial-analysis.js';
22
22
  import { getCanvasNodeTitle, serializeCanvasNode, type SerializedCanvasNode } from './canvas-serialization.js';
23
23
  import {
24
24
  buildGraphSpec,
25
+ buildGraphConfig,
25
26
  createJsonRenderNodeData,
26
27
  GRAPH_NODE_SIZE,
27
28
  inferJsonRenderNodeTitle,
@@ -42,6 +43,33 @@ import { buildExcalidrawRestoreCheckpointToolInput, ensureExcalidrawCheckpointId
42
43
  export type CanvasArrangeMode = 'grid' | 'column' | 'flow';
43
44
  export type CanvasPinMode = 'set' | 'add' | 'remove';
44
45
 
46
+ export interface CanvasFitViewOptions {
47
+ width?: number;
48
+ height?: number;
49
+ padding?: number;
50
+ maxScale?: number;
51
+ nodeIds?: string[];
52
+ }
53
+
54
+ export interface CanvasFitViewResult {
55
+ ok: true;
56
+ viewport: { x: number; y: number; scale: number };
57
+ nodeCount: number;
58
+ bounds: { x: number; y: number; width: number; height: number } | null;
59
+ }
60
+
61
+ export interface CanvasGraphNodeUpdateInput extends Partial<GraphNodeInput> {
62
+ spec?: unknown;
63
+ type?: string;
64
+ }
65
+
66
+ export interface CanvasStructuredNodeUpdateInput extends Omit<CanvasGraphNodeUpdateInput, 'data'> {
67
+ content?: unknown;
68
+ data?: unknown;
69
+ arrangeLocked?: unknown;
70
+ chartHeight?: unknown;
71
+ }
72
+
45
73
  interface CanvasAddNodeInput {
46
74
  type: CanvasNodeState['type'];
47
75
  title?: string;
@@ -84,6 +112,246 @@ function isRecord(value: unknown): value is Record<string, unknown> {
84
112
  return value !== null && typeof value === 'object' && !Array.isArray(value);
85
113
  }
86
114
 
115
+ function positiveNumber(value: number | undefined, fallback: number): number {
116
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : fallback;
117
+ }
118
+
119
+ function pickString(record: Record<string, unknown>, key: string): string | undefined {
120
+ const value = record[key];
121
+ return typeof value === 'string' && value.trim().length > 0 ? value : undefined;
122
+ }
123
+
124
+ function pickNumber(record: Record<string, unknown>, key: string): number | undefined {
125
+ const value = record[key];
126
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
127
+ }
128
+
129
+ function pickStringArray(record: Record<string, unknown>, key: string): string[] | undefined {
130
+ const value = record[key];
131
+ if (!Array.isArray(value)) return undefined;
132
+ const strings = value.filter((item): item is string => typeof item === 'string' && item.trim().length > 0);
133
+ return strings.length > 0 ? strings : undefined;
134
+ }
135
+
136
+ function pickGraphData(record: Record<string, unknown>, key: string): Array<Record<string, unknown>> | undefined {
137
+ const value = record[key];
138
+ if (!Array.isArray(value)) return undefined;
139
+ const rows = value.filter((item): item is Record<string, unknown> => isRecord(item));
140
+ return rows.length === value.length ? rows : undefined;
141
+ }
142
+
143
+ function pickAggregate(record: Record<string, unknown>, key: string): GraphNodeInput['aggregate'] | undefined {
144
+ const value = record[key];
145
+ return value === 'sum' || value === 'count' || value === 'avg' ? value : undefined;
146
+ }
147
+
148
+ function isJsonRenderSpecLike(value: unknown): boolean {
149
+ return isRecord(value) && typeof value.root === 'string' && isRecord(value.elements);
150
+ }
151
+
152
+ function isGraphPayloadLike(value: unknown): value is Record<string, unknown> {
153
+ return isRecord(value) && !isJsonRenderSpecLike(value) && (
154
+ Array.isArray(value.data) || typeof value.graphType === 'string'
155
+ );
156
+ }
157
+
158
+ function hasGraphUpdateFields(input: Record<string, unknown>): boolean {
159
+ return input.graphType !== undefined ||
160
+ input.type !== undefined ||
161
+ Array.isArray(input.data) ||
162
+ input.xKey !== undefined ||
163
+ input.yKey !== undefined ||
164
+ input.zKey !== undefined ||
165
+ input.nameKey !== undefined ||
166
+ input.valueKey !== undefined ||
167
+ input.axisKey !== undefined ||
168
+ input.metrics !== undefined ||
169
+ input.series !== undefined ||
170
+ input.barKey !== undefined ||
171
+ input.lineKey !== undefined ||
172
+ input.aggregate !== undefined ||
173
+ input.color !== undefined ||
174
+ input.barColor !== undefined ||
175
+ input.lineColor !== undefined ||
176
+ input.chartHeight !== undefined;
177
+ }
178
+
179
+ function graphUpdateInput(input: CanvasStructuredNodeUpdateInput): CanvasGraphNodeUpdateInput {
180
+ const data = pickGraphData(input as Record<string, unknown>, 'data');
181
+ const {
182
+ data: _data,
183
+ content: _content,
184
+ arrangeLocked: _arrangeLocked,
185
+ chartHeight,
186
+ ...graphFields
187
+ } = input;
188
+ return {
189
+ ...graphFields,
190
+ ...(data ? { data } : {}),
191
+ ...(typeof chartHeight === 'number' ? { height: chartHeight } : {}),
192
+ };
193
+ }
194
+
195
+ function mergeNodeDataFields(
196
+ base: Record<string, unknown>,
197
+ input: CanvasStructuredNodeUpdateInput,
198
+ ): Record<string, unknown> {
199
+ return {
200
+ ...base,
201
+ ...(isRecord(input.data) ? input.data : {}),
202
+ ...(typeof input.arrangeLocked === 'boolean' ? { arrangeLocked: input.arrangeLocked } : {}),
203
+ };
204
+ }
205
+
206
+ export function hasStructuredNodeUpdateFields(input: Record<string, unknown>): boolean {
207
+ return input.spec !== undefined || hasGraphUpdateFields(input);
208
+ }
209
+
210
+ export function buildStructuredNodeUpdate(
211
+ node: CanvasNodeState,
212
+ input: CanvasStructuredNodeUpdateInput,
213
+ ): { data: Record<string, unknown> } {
214
+ const inputRecord = input as Record<string, unknown>;
215
+ const hasSpec = inputRecord.spec !== undefined;
216
+ const hasGraphFields = hasGraphUpdateFields(inputRecord);
217
+
218
+ if (node.type === 'json-render') {
219
+ if (hasGraphFields) {
220
+ throw new Error(`Graph update fields can only be used with graph nodes, not ${node.type} nodes.`);
221
+ }
222
+ if (!hasSpec) {
223
+ throw new Error('json-render structured updates require a spec.');
224
+ }
225
+ return {
226
+ data: mergeNodeDataFields(buildJsonRenderNodeUpdate(node, {
227
+ ...(typeof input.title === 'string' ? { title: input.title } : {}),
228
+ spec: input.spec,
229
+ }).data, input),
230
+ };
231
+ }
232
+
233
+ if (node.type === 'graph') {
234
+ return {
235
+ data: mergeNodeDataFields(buildGraphNodeUpdate(node, graphUpdateInput(input)).data, input),
236
+ };
237
+ }
238
+
239
+ throw new Error(`Structured spec and graph updates can only be used with json-render or graph nodes, not ${node.type} nodes.`);
240
+ }
241
+
242
+ function graphConfigToInput(config: Record<string, unknown>, fallbackTitle: string): GraphNodeInput | null {
243
+ const data = pickGraphData(config, 'data');
244
+ if (!data) return null;
245
+ return {
246
+ title: pickString(config, 'title') ?? fallbackTitle,
247
+ graphType: pickString(config, 'graphType') ?? 'line',
248
+ data,
249
+ ...(pickString(config, 'xKey') ? { xKey: pickString(config, 'xKey') } : {}),
250
+ ...(pickString(config, 'yKey') ? { yKey: pickString(config, 'yKey') } : {}),
251
+ ...(pickString(config, 'zKey') ? { zKey: pickString(config, 'zKey') } : {}),
252
+ ...(pickString(config, 'nameKey') ? { nameKey: pickString(config, 'nameKey') } : {}),
253
+ ...(pickString(config, 'valueKey') ? { valueKey: pickString(config, 'valueKey') } : {}),
254
+ ...(pickString(config, 'axisKey') ? { axisKey: pickString(config, 'axisKey') } : {}),
255
+ ...(pickStringArray(config, 'metrics') ? { metrics: pickStringArray(config, 'metrics') } : {}),
256
+ ...(pickStringArray(config, 'series') ? { series: pickStringArray(config, 'series') } : {}),
257
+ ...(pickString(config, 'barKey') ? { barKey: pickString(config, 'barKey') } : {}),
258
+ ...(pickString(config, 'lineKey') ? { lineKey: pickString(config, 'lineKey') } : {}),
259
+ ...(pickAggregate(config, 'aggregate') ? { aggregate: pickAggregate(config, 'aggregate') } : {}),
260
+ ...(pickString(config, 'color') ? { color: pickString(config, 'color') } : {}),
261
+ ...(pickString(config, 'barColor') ? { barColor: pickString(config, 'barColor') } : {}),
262
+ ...(pickString(config, 'lineColor') ? { lineColor: pickString(config, 'lineColor') } : {}),
263
+ ...(pickNumber(config, 'height') !== undefined ? { height: pickNumber(config, 'height') } : {}),
264
+ };
265
+ }
266
+
267
+ function mergeGraphInput(source: Record<string, unknown>, fallback: GraphNodeInput | null): GraphNodeInput {
268
+ const data = pickGraphData(source, 'data') ?? fallback?.data;
269
+ if (!data) throw new Error('Graph update requires a data array, either in the update payload or the existing graphConfig.');
270
+ return {
271
+ title: pickString(source, 'title') ?? fallback?.title ?? 'Graph',
272
+ graphType: pickString(source, 'graphType') ?? pickString(source, 'type') ?? fallback?.graphType ?? 'line',
273
+ data,
274
+ ...((pickString(source, 'xKey') ?? fallback?.xKey) ? { xKey: pickString(source, 'xKey') ?? fallback?.xKey } : {}),
275
+ ...((pickString(source, 'yKey') ?? fallback?.yKey) ? { yKey: pickString(source, 'yKey') ?? fallback?.yKey } : {}),
276
+ ...((pickString(source, 'zKey') ?? fallback?.zKey) ? { zKey: pickString(source, 'zKey') ?? fallback?.zKey } : {}),
277
+ ...((pickString(source, 'nameKey') ?? fallback?.nameKey) ? { nameKey: pickString(source, 'nameKey') ?? fallback?.nameKey } : {}),
278
+ ...((pickString(source, 'valueKey') ?? fallback?.valueKey) ? { valueKey: pickString(source, 'valueKey') ?? fallback?.valueKey } : {}),
279
+ ...((pickString(source, 'axisKey') ?? fallback?.axisKey) ? { axisKey: pickString(source, 'axisKey') ?? fallback?.axisKey } : {}),
280
+ ...((pickStringArray(source, 'metrics') ?? fallback?.metrics) ? { metrics: pickStringArray(source, 'metrics') ?? fallback?.metrics } : {}),
281
+ ...((pickStringArray(source, 'series') ?? fallback?.series) ? { series: pickStringArray(source, 'series') ?? fallback?.series } : {}),
282
+ ...((pickString(source, 'barKey') ?? fallback?.barKey) ? { barKey: pickString(source, 'barKey') ?? fallback?.barKey } : {}),
283
+ ...((pickString(source, 'lineKey') ?? fallback?.lineKey) ? { lineKey: pickString(source, 'lineKey') ?? fallback?.lineKey } : {}),
284
+ ...((pickAggregate(source, 'aggregate') ?? fallback?.aggregate) ? { aggregate: pickAggregate(source, 'aggregate') ?? fallback?.aggregate } : {}),
285
+ ...((pickString(source, 'color') ?? fallback?.color) ? { color: pickString(source, 'color') ?? fallback?.color } : {}),
286
+ ...((pickString(source, 'barColor') ?? fallback?.barColor) ? { barColor: pickString(source, 'barColor') ?? fallback?.barColor } : {}),
287
+ ...((pickString(source, 'lineColor') ?? fallback?.lineColor) ? { lineColor: pickString(source, 'lineColor') ?? fallback?.lineColor } : {}),
288
+ ...((pickNumber(source, 'height') ?? fallback?.height) !== undefined ? { height: pickNumber(source, 'height') ?? fallback?.height } : {}),
289
+ };
290
+ }
291
+
292
+ export function buildJsonRenderNodeUpdate(
293
+ node: CanvasNodeState,
294
+ input: { title?: string; spec: unknown },
295
+ ): { data: Record<string, unknown>; spec: JsonRenderSpec } {
296
+ if (node.type !== 'json-render') throw new Error(`Node "${node.id}" is not a json-render node.`);
297
+ const spec = normalizeAndValidateJsonRenderSpec(input.spec);
298
+ const title = input.title?.trim() || inferJsonRenderNodeTitle(spec);
299
+ return {
300
+ spec,
301
+ data: {
302
+ ...node.data,
303
+ ...createJsonRenderNodeData(node.id, title, spec, { viewerType: 'json-render' }),
304
+ },
305
+ };
306
+ }
307
+
308
+ export function buildGraphNodeUpdate(
309
+ node: CanvasNodeState,
310
+ input: CanvasGraphNodeUpdateInput,
311
+ ): { data: Record<string, unknown>; spec: JsonRenderSpec; graphConfig: Record<string, unknown> } {
312
+ if (node.type !== 'graph') throw new Error(`Node "${node.id}" is not a graph node.`);
313
+ const currentConfig = isRecord(node.data.graphConfig) ? node.data.graphConfig : {};
314
+ const fallbackTitle = typeof node.data.title === 'string' ? node.data.title : 'Graph';
315
+ const fallback = graphConfigToInput(currentConfig, fallbackTitle);
316
+ const source = isGraphPayloadLike(input.spec)
317
+ ? input.spec
318
+ : Object.fromEntries(
319
+ Object.entries(input).filter(([key, value]) => key !== 'spec' && value !== undefined),
320
+ );
321
+
322
+ if (input.spec !== undefined && !isGraphPayloadLike(input.spec)) {
323
+ const spec = normalizeAndValidateJsonRenderSpec(input.spec);
324
+ const title = input.title?.trim() || fallbackTitle;
325
+ return {
326
+ spec,
327
+ graphConfig: currentConfig,
328
+ data: {
329
+ ...node.data,
330
+ ...createJsonRenderNodeData(node.id, title, spec, {
331
+ viewerType: 'graph',
332
+ graphConfig: currentConfig,
333
+ }),
334
+ },
335
+ };
336
+ }
337
+
338
+ const graphInput = mergeGraphInput(source, fallback);
339
+ const spec = buildGraphSpec(graphInput);
340
+ const graphConfig = buildGraphConfig(graphInput);
341
+ const title = graphInput.title?.trim() || 'Graph';
342
+ return {
343
+ spec,
344
+ graphConfig,
345
+ data: {
346
+ ...node.data,
347
+ ...createJsonRenderNodeData(node.id, title, spec, {
348
+ viewerType: 'graph',
349
+ graphConfig,
350
+ }),
351
+ },
352
+ };
353
+ }
354
+
87
355
  function getStoredExcalidrawCheckpointId(node: CanvasNodeState): string | null {
88
356
  const appCheckpoint = isRecord(node.data.appCheckpoint) ? node.data.appCheckpoint : null;
89
357
  const checkpointId = appCheckpoint?.id;
@@ -1033,26 +1301,7 @@ export function createCanvasGraphNode(
1033
1301
  dockPosition: null,
1034
1302
  data: createJsonRenderNodeData(id, title, spec, {
1035
1303
  viewerType: 'graph',
1036
- graphConfig: {
1037
- title,
1038
- graphType: input.graphType,
1039
- data: input.data,
1040
- ...(input.xKey ? { xKey: input.xKey } : {}),
1041
- ...(input.yKey ? { yKey: input.yKey } : {}),
1042
- ...(input.zKey ? { zKey: input.zKey } : {}),
1043
- ...(input.nameKey ? { nameKey: input.nameKey } : {}),
1044
- ...(input.valueKey ? { valueKey: input.valueKey } : {}),
1045
- ...(input.axisKey ? { axisKey: input.axisKey } : {}),
1046
- ...(input.metrics?.length ? { metrics: input.metrics } : {}),
1047
- ...(input.series?.length ? { series: input.series } : {}),
1048
- ...(input.barKey ? { barKey: input.barKey } : {}),
1049
- ...(input.lineKey ? { lineKey: input.lineKey } : {}),
1050
- ...(input.aggregate ? { aggregate: input.aggregate } : {}),
1051
- ...(input.color ? { color: input.color } : {}),
1052
- ...(input.barColor ? { barColor: input.barColor } : {}),
1053
- ...(input.lineColor ? { lineColor: input.lineColor } : {}),
1054
- ...(typeof input.height === 'number' ? { height: input.height } : {}),
1055
- },
1304
+ graphConfig: buildGraphConfig(input),
1056
1305
  }),
1057
1306
  };
1058
1307
 
@@ -1060,6 +1309,46 @@ export function createCanvasGraphNode(
1060
1309
  return { id, url: String(node.data.url), spec, node };
1061
1310
  }
1062
1311
 
1312
+ export function fitCanvasView(options: CanvasFitViewOptions = {}): CanvasFitViewResult {
1313
+ const width = positiveNumber(options.width, 1440);
1314
+ const height = positiveNumber(options.height, 900);
1315
+ const padding = positiveNumber(options.padding, 60);
1316
+ const maxScale = positiveNumber(options.maxScale, 1);
1317
+ const nodeIdFilter = options.nodeIds && options.nodeIds.length > 0 ? new Set(options.nodeIds) : null;
1318
+ const targetNodes = canvasState.getLayout().nodes.filter((node) => !nodeIdFilter || nodeIdFilter.has(node.id));
1319
+
1320
+ if (targetNodes.length === 0) {
1321
+ const viewport = canvasState.viewport;
1322
+ return { ok: true, viewport, nodeCount: 0, bounds: null };
1323
+ }
1324
+
1325
+ let minX = Number.POSITIVE_INFINITY;
1326
+ let minY = Number.POSITIVE_INFINITY;
1327
+ let maxX = Number.NEGATIVE_INFINITY;
1328
+ let maxY = Number.NEGATIVE_INFINITY;
1329
+ for (const node of targetNodes) {
1330
+ minX = Math.min(minX, node.position.x);
1331
+ minY = Math.min(minY, node.position.y);
1332
+ maxX = Math.max(maxX, node.position.x + node.size.width);
1333
+ maxY = Math.max(maxY, node.position.y + node.size.height);
1334
+ }
1335
+
1336
+ const bounds = { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
1337
+ const worldWidth = Math.max(1, bounds.width + padding * 2);
1338
+ const worldHeight = Math.max(1, bounds.height + padding * 2);
1339
+ const scale = Math.min(maxScale, width / worldWidth, height / worldHeight);
1340
+ const centerX = minX + bounds.width / 2;
1341
+ const centerY = minY + bounds.height / 2;
1342
+ const viewport = {
1343
+ x: width / 2 - centerX * scale,
1344
+ y: height / 2 - centerY * scale,
1345
+ scale,
1346
+ };
1347
+
1348
+ canvasState.setViewport(viewport);
1349
+ return { ok: true, viewport: canvasState.viewport, nodeCount: targetNodes.length, bounds };
1350
+ }
1351
+
1063
1352
  function isPlainRecord(value: unknown): value is Record<string, unknown> {
1064
1353
  return !!value && typeof value === 'object' && !Array.isArray(value);
1065
1354
  }
@@ -439,6 +439,8 @@ export function describeCanvasSchema(): {
439
439
  'canvas_build_web_artifact',
440
440
  'canvas_open_mcp_app',
441
441
  'canvas_create_group',
442
+ 'canvas_update_node',
443
+ 'canvas_fit_view',
442
444
  'canvas_describe_schema',
443
445
  'canvas_validate_spec',
444
446
  ],
@@ -16,6 +16,8 @@ import {
16
16
  createCanvasGraphNode,
17
17
  createCanvasGroup,
18
18
  createCanvasJsonRenderNode,
19
+ buildStructuredNodeUpdate,
20
+ fitCanvasView,
19
21
  deleteCanvasSnapshot,
20
22
  executeCanvasBatch,
21
23
  groupCanvasNodes,
@@ -30,6 +32,7 @@ import {
30
32
  setCanvasContextPins,
31
33
  ungroupCanvasNodes,
32
34
  validateCanvasNodePatch,
35
+ hasStructuredNodeUpdateFields,
33
36
  } from './canvas-operations.js';
34
37
  import { validateCanvasLayout } from './canvas-validation.js';
35
38
  import { describeCanvasSchema, validateStructuredCanvasPayload } from './canvas-schema.js';
@@ -51,8 +54,6 @@ import {
51
54
  } from './diagram-presets.js';
52
55
  import {
53
56
  buildGraphSpec,
54
- buildJsonRenderViewerHtml,
55
- createJsonRenderNodeData,
56
57
  GRAPH_NODE_SIZE,
57
58
  JSON_RENDER_NODE_SIZE,
58
59
  normalizeAndValidateJsonRenderSpec,
@@ -221,15 +222,41 @@ export class PmxCanvas extends EventEmitter {
221
222
  return result;
222
223
  }
223
224
 
224
- updateNode(id: string, patch: Partial<CanvasNodeState>): void {
225
+ updateNode(id: string, patch: Partial<CanvasNodeState> & Record<string, unknown>): void {
226
+ const existing = canvasState.getNode(id);
227
+ if (!existing) return;
228
+ const resolvedPatch: Partial<CanvasNodeState> = {};
229
+ if (patch.position) resolvedPatch.position = patch.position;
230
+ if (patch.size) resolvedPatch.size = patch.size;
231
+ if (patch.collapsed !== undefined) resolvedPatch.collapsed = patch.collapsed;
232
+ if (patch.pinned !== undefined) resolvedPatch.pinned = patch.pinned;
233
+ if (patch.dockPosition !== undefined) resolvedPatch.dockPosition = patch.dockPosition;
234
+
235
+ if (hasStructuredNodeUpdateFields(patch)) {
236
+ resolvedPatch.data = buildStructuredNodeUpdate(existing, patch).data;
237
+ } else if (
238
+ patch.data !== undefined ||
239
+ patch.title !== undefined ||
240
+ patch.content !== undefined ||
241
+ typeof patch.arrangeLocked === 'boolean'
242
+ ) {
243
+ resolvedPatch.data = {
244
+ ...existing.data,
245
+ ...(patch.data && typeof patch.data === 'object' && !Array.isArray(patch.data) ? patch.data : {}),
246
+ ...(typeof patch.title === 'string' ? { title: patch.title } : {}),
247
+ ...(typeof patch.content === 'string' ? { content: patch.content } : {}),
248
+ ...(typeof patch.arrangeLocked === 'boolean' ? { arrangeLocked: patch.arrangeLocked } : {}),
249
+ };
250
+ }
251
+
225
252
  const error = validateCanvasNodePatch({
226
- ...(patch.position ? { position: patch.position } : {}),
227
- ...(patch.size ? { size: patch.size } : {}),
253
+ ...(resolvedPatch.position ? { position: resolvedPatch.position } : {}),
254
+ ...(resolvedPatch.size ? { size: resolvedPatch.size } : {}),
228
255
  });
229
256
  if (error) {
230
257
  throw new Error(error);
231
258
  }
232
- canvasState.updateNode(id, patch);
259
+ canvasState.updateNode(id, resolvedPatch);
233
260
  emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
234
261
  }
235
262
 
@@ -344,6 +371,18 @@ export class PmxCanvas extends EventEmitter {
344
371
  return { focused: id, panned: !noPan };
345
372
  }
346
373
 
374
+ fitView(options?: {
375
+ width?: number;
376
+ height?: number;
377
+ padding?: number;
378
+ maxScale?: number;
379
+ nodeIds?: string[];
380
+ }): ReturnType<typeof fitCanvasView> {
381
+ const result = fitCanvasView(options);
382
+ emitPrimaryWorkbenchEvent('canvas-viewport-update', { viewport: result.viewport });
383
+ return result;
384
+ }
385
+
347
386
  getLayout(): CanvasLayout {
348
387
  return canvasState.getLayout();
349
388
  }