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.
- package/CHANGELOG.md +65 -0
- package/Readme.md +52 -54
- package/dist/canvas/index.js +61 -61
- package/dist/types/client/canvas/auto-fit.d.ts +5 -0
- package/dist/types/client/nodes/ExtAppFrame.d.ts +2 -0
- package/dist/types/json-render/server.d.ts +1 -0
- package/dist/types/server/canvas-operations.d.ts +49 -0
- package/dist/types/server/index.d.ts +9 -1
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +13 -1
- package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +1 -1
- package/skills/web-artifacts-builder/scripts/bundle-artifact.sh +11 -0
- package/src/cli/agent.ts +109 -14
- package/src/client/canvas/CanvasNode.tsx +5 -7
- package/src/client/canvas/auto-fit.ts +21 -0
- package/src/client/nodes/ExtAppFrame.tsx +4 -2
- package/src/json-render/server.ts +27 -0
- package/src/mcp/server.ts +46 -14
- package/src/server/canvas-operations.ts +309 -20
- package/src/server/canvas-schema.ts +2 -0
- package/src/server/index.ts +45 -6
- package/src/server/server.ts +149 -35
|
@@ -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
|
}
|
package/src/server/index.ts
CHANGED
|
@@ -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
|
-
...(
|
|
227
|
-
...(
|
|
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,
|
|
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
|
}
|