pmx-canvas 0.1.8 → 0.1.10
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 +136 -0
- package/Readme.md +52 -54
- package/dist/canvas/index.js +52 -52
- package/dist/json-render/index.css +1 -1
- package/dist/json-render/index.js +92 -92
- 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/catalog.d.ts +316 -310
- 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 +135 -18
- package/src/cli/index.ts +1 -1
- 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/catalog.ts +9 -0
- package/src/json-render/renderer/index.css +61 -0
- package/src/json-render/renderer/index.tsx +22 -0
- package/src/json-render/server.ts +27 -11
- 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/canvas-validation.ts +9 -2
- package/src/server/diagram-presets.ts +48 -2
- package/src/server/index.ts +45 -6
- package/src/server/server.ts +149 -35
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
|
}
|
package/src/server/server.ts
CHANGED
|
@@ -73,6 +73,7 @@ import {
|
|
|
73
73
|
addCanvasNode,
|
|
74
74
|
addCanvasEdge,
|
|
75
75
|
applyCanvasNodeUpdates,
|
|
76
|
+
buildStructuredNodeUpdate,
|
|
76
77
|
arrangeCanvasNodes,
|
|
77
78
|
clearCanvas,
|
|
78
79
|
createCanvasGraphNode,
|
|
@@ -80,6 +81,7 @@ import {
|
|
|
80
81
|
createCanvasJsonRenderNode,
|
|
81
82
|
deleteCanvasSnapshot,
|
|
82
83
|
executeCanvasBatch,
|
|
84
|
+
fitCanvasView,
|
|
83
85
|
groupCanvasNodes,
|
|
84
86
|
listCanvasSnapshots,
|
|
85
87
|
refreshCanvasWebpageNode,
|
|
@@ -93,6 +95,7 @@ import {
|
|
|
93
95
|
setCanvasContextPins,
|
|
94
96
|
ungroupCanvasNodes,
|
|
95
97
|
validateCanvasNodePatch,
|
|
98
|
+
hasStructuredNodeUpdateFields,
|
|
96
99
|
} from './canvas-operations.js';
|
|
97
100
|
import { validateCanvasLayout } from './canvas-validation.js';
|
|
98
101
|
import { describeCanvasSchema, validateStructuredCanvasPayload } from './canvas-schema.js';
|
|
@@ -109,12 +112,7 @@ import {
|
|
|
109
112
|
import { traceManager } from './trace-manager.js';
|
|
110
113
|
import { buildWebArtifactOnCanvas, resolveWorkspacePath } from './web-artifacts.js';
|
|
111
114
|
import {
|
|
112
|
-
buildGraphSpec,
|
|
113
115
|
buildJsonRenderViewerHtml,
|
|
114
|
-
createJsonRenderNodeData,
|
|
115
|
-
GRAPH_NODE_SIZE,
|
|
116
|
-
JSON_RENDER_NODE_SIZE,
|
|
117
|
-
normalizeAndValidateJsonRenderSpec,
|
|
118
116
|
} from '../json-render/server.js';
|
|
119
117
|
import {
|
|
120
118
|
WEBPAGE_NODE_DEFAULT_SIZE,
|
|
@@ -651,6 +649,97 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
651
649
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
652
650
|
}
|
|
653
651
|
|
|
652
|
+
function pickFiniteNumber(record: Record<string, unknown>, key: string): number | undefined {
|
|
653
|
+
const value = record[key];
|
|
654
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function getRecord(value: unknown): Record<string, unknown> | undefined {
|
|
658
|
+
return isRecord(value) ? value : undefined;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function pickPositiveNumber(record: Record<string, unknown>, key: string): number | undefined {
|
|
662
|
+
const value = pickFiniteNumber(record, key);
|
|
663
|
+
return value !== undefined && value > 0 ? value : undefined;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function normalizeGeometryInput(body: Record<string, unknown>): {
|
|
667
|
+
x?: number;
|
|
668
|
+
y?: number;
|
|
669
|
+
width?: number;
|
|
670
|
+
height?: number;
|
|
671
|
+
position?: { x?: number; y?: number };
|
|
672
|
+
size?: { width?: number; height?: number };
|
|
673
|
+
} {
|
|
674
|
+
const position = getRecord(body.position);
|
|
675
|
+
const size = getRecord(body.size);
|
|
676
|
+
return {
|
|
677
|
+
...(pickFiniteNumber(body, 'x') !== undefined ? { x: pickFiniteNumber(body, 'x') } : {}),
|
|
678
|
+
...(pickFiniteNumber(body, 'y') !== undefined ? { y: pickFiniteNumber(body, 'y') } : {}),
|
|
679
|
+
...(pickFiniteNumber(body, 'width') !== undefined ? { width: pickFiniteNumber(body, 'width') } : {}),
|
|
680
|
+
...(pickFiniteNumber(body, 'height') !== undefined ? { height: pickFiniteNumber(body, 'height') } : {}),
|
|
681
|
+
...(position ? {
|
|
682
|
+
position: {
|
|
683
|
+
...(pickFiniteNumber(position, 'x') !== undefined ? { x: pickFiniteNumber(position, 'x') } : {}),
|
|
684
|
+
...(pickFiniteNumber(position, 'y') !== undefined ? { y: pickFiniteNumber(position, 'y') } : {}),
|
|
685
|
+
},
|
|
686
|
+
} : {}),
|
|
687
|
+
...(size ? {
|
|
688
|
+
size: {
|
|
689
|
+
...(pickFiniteNumber(size, 'width') !== undefined ? { width: pickFiniteNumber(size, 'width') } : {}),
|
|
690
|
+
...(pickFiniteNumber(size, 'height') !== undefined ? { height: pickFiniteNumber(size, 'height') } : {}),
|
|
691
|
+
},
|
|
692
|
+
} : {}),
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function resolveCreateGeometry(body: Record<string, unknown>): {
|
|
697
|
+
x?: number;
|
|
698
|
+
y?: number;
|
|
699
|
+
width?: number;
|
|
700
|
+
height?: number;
|
|
701
|
+
} {
|
|
702
|
+
const geometry = normalizeGeometryInput(body);
|
|
703
|
+
const x = geometry.x ?? geometry.position?.x;
|
|
704
|
+
const y = geometry.y ?? geometry.position?.y;
|
|
705
|
+
const width = geometry.width ?? geometry.size?.width;
|
|
706
|
+
const height = geometry.height ?? geometry.size?.height;
|
|
707
|
+
return {
|
|
708
|
+
...(x !== undefined ? { x } : {}),
|
|
709
|
+
...(y !== undefined ? { y } : {}),
|
|
710
|
+
...(width !== undefined ? { width } : {}),
|
|
711
|
+
...(height !== undefined ? { height } : {}),
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function resolvePatchGeometry(
|
|
716
|
+
body: Record<string, unknown>,
|
|
717
|
+
existing: CanvasNodeState,
|
|
718
|
+
): {
|
|
719
|
+
position?: { x: number; y: number };
|
|
720
|
+
size?: { width: number; height: number };
|
|
721
|
+
} {
|
|
722
|
+
const geometry = normalizeGeometryInput(body);
|
|
723
|
+
const x = geometry.x ?? geometry.position?.x;
|
|
724
|
+
const y = geometry.y ?? geometry.position?.y;
|
|
725
|
+
const width = geometry.width ?? geometry.size?.width;
|
|
726
|
+
const height = geometry.height ?? geometry.size?.height;
|
|
727
|
+
return {
|
|
728
|
+
...(x !== undefined || y !== undefined
|
|
729
|
+
? { position: { x: x ?? existing.position.x, y: y ?? existing.position.y } }
|
|
730
|
+
: {}),
|
|
731
|
+
...(width !== undefined || height !== undefined
|
|
732
|
+
? { size: { width: width ?? existing.size.width, height: height ?? existing.size.height } }
|
|
733
|
+
: {}),
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function parseGraphPayloadData(value: unknown): Array<Record<string, unknown>> | null {
|
|
738
|
+
if (!Array.isArray(value)) return null;
|
|
739
|
+
if (value.some((item) => !isRecord(item))) return null;
|
|
740
|
+
return value as Array<Record<string, unknown>>;
|
|
741
|
+
}
|
|
742
|
+
|
|
654
743
|
function getExtAppNodeCheckpointId(node: CanvasNodeState): string {
|
|
655
744
|
const appCheckpoint = isRecord(node.data.appCheckpoint) ? node.data.appCheckpoint : null;
|
|
656
745
|
const storedCheckpointId = appCheckpoint?.id;
|
|
@@ -1130,9 +1219,11 @@ async function handleCanvasImage(pathname: string): Promise<Response> {
|
|
|
1130
1219
|
const VALID_NODE_TYPES = new Set(['markdown', 'status', 'context', 'ledger', 'trace', 'file', 'image', 'mcp-app', 'webpage', 'group']);
|
|
1131
1220
|
|
|
1132
1221
|
function buildNodeResponse(node: CanvasNodeState): Record<string, unknown> {
|
|
1222
|
+
const serialized = serializeCanvasNode(node);
|
|
1133
1223
|
return {
|
|
1134
1224
|
ok: true,
|
|
1135
|
-
|
|
1225
|
+
node: serialized,
|
|
1226
|
+
...serialized,
|
|
1136
1227
|
};
|
|
1137
1228
|
}
|
|
1138
1229
|
|
|
@@ -1153,15 +1244,15 @@ async function createCanvasWebpageNode(body: Record<string, unknown>): Promise<R
|
|
|
1153
1244
|
const extraData = body.data && typeof body.data === 'object' && !Array.isArray(body.data)
|
|
1154
1245
|
? body.data as Record<string, unknown>
|
|
1155
1246
|
: undefined;
|
|
1247
|
+
const geometry = resolveCreateGeometry(body);
|
|
1156
1248
|
const { id, node } = addCanvasNode({
|
|
1157
1249
|
type: 'webpage',
|
|
1158
1250
|
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
1159
1251
|
content: normalizedUrl,
|
|
1160
1252
|
...(extraData ? { data: extraData } : {}),
|
|
1161
|
-
...
|
|
1162
|
-
...(
|
|
1163
|
-
...(
|
|
1164
|
-
...(typeof body.height === 'number' ? { height: body.height } : { height: WEBPAGE_NODE_DEFAULT_SIZE.height }),
|
|
1253
|
+
...geometry,
|
|
1254
|
+
...(geometry.width === undefined ? { width: WEBPAGE_NODE_DEFAULT_SIZE.width } : {}),
|
|
1255
|
+
...(geometry.height === undefined ? { height: WEBPAGE_NODE_DEFAULT_SIZE.height } : {}),
|
|
1165
1256
|
});
|
|
1166
1257
|
|
|
1167
1258
|
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
@@ -1214,16 +1305,14 @@ async function handleCanvasAddNode(req: Request): Promise<Response> {
|
|
|
1214
1305
|
? body.path
|
|
1215
1306
|
: body.content;
|
|
1216
1307
|
let added: ReturnType<typeof addCanvasNode>;
|
|
1308
|
+
const geometry = resolveCreateGeometry(body);
|
|
1217
1309
|
try {
|
|
1218
1310
|
added = addCanvasNode({
|
|
1219
1311
|
type: type as CanvasNodeState['type'],
|
|
1220
1312
|
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
1221
1313
|
...(typeof content === 'string' ? { content } : {}),
|
|
1222
1314
|
...(extraData ? { data: extraData } : {}),
|
|
1223
|
-
...
|
|
1224
|
-
...(typeof body.y === 'number' ? { y: body.y } : {}),
|
|
1225
|
-
...(typeof body.width === 'number' ? { width: body.width } : {}),
|
|
1226
|
-
...(typeof body.height === 'number' ? { height: body.height } : {}),
|
|
1315
|
+
...geometry,
|
|
1227
1316
|
defaultWidth: 360,
|
|
1228
1317
|
defaultHeight: 200,
|
|
1229
1318
|
fileMode: 'auto',
|
|
@@ -1374,15 +1463,19 @@ async function handleCanvasUpdateNode(nodeId: string, req: Request): Promise<Res
|
|
|
1374
1463
|
if (existing.type === 'webpage' && body.refresh === true) {
|
|
1375
1464
|
return handleCanvasRefreshWebpageNode(nodeId, req);
|
|
1376
1465
|
}
|
|
1377
|
-
const patch: Record<string, unknown> =
|
|
1378
|
-
if (body.position) patch.position = body.position;
|
|
1379
|
-
if (body.size) patch.size = body.size;
|
|
1466
|
+
const patch: Record<string, unknown> = resolvePatchGeometry(body, existing);
|
|
1380
1467
|
if (body.collapsed !== undefined) patch.collapsed = body.collapsed;
|
|
1381
1468
|
if (body.pinned !== undefined) patch.pinned = Boolean(body.pinned);
|
|
1382
1469
|
if (body.dockPosition === null || body.dockPosition === 'left' || body.dockPosition === 'right') {
|
|
1383
1470
|
patch.dockPosition = body.dockPosition;
|
|
1384
1471
|
}
|
|
1385
|
-
if (body
|
|
1472
|
+
if (hasStructuredNodeUpdateFields(body)) {
|
|
1473
|
+
try {
|
|
1474
|
+
patch.data = buildStructuredNodeUpdate(existing, body).data;
|
|
1475
|
+
} catch (error) {
|
|
1476
|
+
return responseJson({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
|
|
1477
|
+
}
|
|
1478
|
+
} else if (body.title !== undefined || body.content !== undefined || body.data || typeof body.arrangeLocked === 'boolean') {
|
|
1386
1479
|
const data = { ...existing.data };
|
|
1387
1480
|
if (body.title !== undefined) {
|
|
1388
1481
|
data.title = String(body.title);
|
|
@@ -1419,7 +1512,8 @@ async function handleCanvasUpdateNode(nodeId: string, req: Request): Promise<Res
|
|
|
1419
1512
|
if (error) return responseJson({ ok: false, error }, 400);
|
|
1420
1513
|
canvasState.updateNode(nodeId, patch as Partial<CanvasNodeState>);
|
|
1421
1514
|
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1422
|
-
|
|
1515
|
+
const updated = canvasState.getNode(nodeId);
|
|
1516
|
+
return responseJson(updated ? buildNodeResponse(updated) : { ok: true, id: nodeId });
|
|
1423
1517
|
}
|
|
1424
1518
|
|
|
1425
1519
|
// ── Arrange nodes ───────────────────────────────────────────
|
|
@@ -1460,6 +1554,22 @@ async function handleCanvasFocus(req: Request): Promise<Response> {
|
|
|
1460
1554
|
return responseJson({ ok: true, focused: nodeId, panned: !noPan });
|
|
1461
1555
|
}
|
|
1462
1556
|
|
|
1557
|
+
async function handleCanvasFit(req: Request): Promise<Response> {
|
|
1558
|
+
const body = await readJson(req);
|
|
1559
|
+
const nodeIds = Array.isArray(body.nodeIds)
|
|
1560
|
+
? body.nodeIds.filter((id): id is string => typeof id === 'string')
|
|
1561
|
+
: undefined;
|
|
1562
|
+
const result = fitCanvasView({
|
|
1563
|
+
...(typeof body.width === 'number' ? { width: body.width } : {}),
|
|
1564
|
+
...(typeof body.height === 'number' ? { height: body.height } : {}),
|
|
1565
|
+
...(typeof body.padding === 'number' ? { padding: body.padding } : {}),
|
|
1566
|
+
...(typeof body.maxScale === 'number' ? { maxScale: body.maxScale } : {}),
|
|
1567
|
+
...(nodeIds ? { nodeIds } : {}),
|
|
1568
|
+
});
|
|
1569
|
+
emitPrimaryWorkbenchEvent('canvas-viewport-update', { viewport: result.viewport });
|
|
1570
|
+
return responseJson(result);
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1463
1573
|
async function handleCanvasBuildWebArtifact(req: Request): Promise<Response> {
|
|
1464
1574
|
const body = await readJson(req);
|
|
1465
1575
|
const title = typeof body.title === 'string' ? body.title.trim() : '';
|
|
@@ -1552,9 +1662,7 @@ async function handleCanvasValidateSpec(req: Request): Promise<Response> {
|
|
|
1552
1662
|
}));
|
|
1553
1663
|
}
|
|
1554
1664
|
|
|
1555
|
-
const data =
|
|
1556
|
-
? body.data.filter((item: unknown) => item && typeof item === 'object') as Array<Record<string, unknown>>
|
|
1557
|
-
: null;
|
|
1665
|
+
const data = parseGraphPayloadData(body.data);
|
|
1558
1666
|
if (!data) {
|
|
1559
1667
|
return responseJson({ ok: false, error: 'Graph validation requires a data array.' }, 400);
|
|
1560
1668
|
}
|
|
@@ -1606,18 +1714,16 @@ async function handleCanvasAddJsonRender(req: Request): Promise<Response> {
|
|
|
1606
1714
|
const title = typeof body.title === 'string' ? body.title.trim() : '';
|
|
1607
1715
|
const rawSpec =
|
|
1608
1716
|
body.spec && typeof body.spec === 'object' && !Array.isArray(body.spec) ? body.spec : body;
|
|
1717
|
+
const geometry = resolveCreateGeometry(body);
|
|
1609
1718
|
|
|
1610
1719
|
try {
|
|
1611
1720
|
const result = createCanvasJsonRenderNode({
|
|
1612
1721
|
...(title ? { title } : {}),
|
|
1613
1722
|
spec: rawSpec,
|
|
1614
|
-
...
|
|
1615
|
-
...(typeof body.y === 'number' ? { y: body.y } : {}),
|
|
1616
|
-
...(typeof body.width === 'number' ? { width: body.width } : {}),
|
|
1617
|
-
...(typeof body.height === 'number' ? { height: body.height } : {}),
|
|
1723
|
+
...geometry,
|
|
1618
1724
|
});
|
|
1619
1725
|
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1620
|
-
return responseJson({
|
|
1726
|
+
return responseJson({ ...buildNodeResponse(result.node), url: result.url, spec: result.spec });
|
|
1621
1727
|
} catch (error) {
|
|
1622
1728
|
const message = error instanceof Error ? error.message : String(error);
|
|
1623
1729
|
return responseJson({ ok: false, error: message }, 400);
|
|
@@ -1628,9 +1734,7 @@ async function handleCanvasAddGraph(req: Request): Promise<Response> {
|
|
|
1628
1734
|
const body = await readJson(req);
|
|
1629
1735
|
const title = typeof body.title === 'string' && body.title.trim() ? body.title.trim() : 'Graph';
|
|
1630
1736
|
const graphType = typeof body.graphType === 'string' ? body.graphType : typeof body.type === 'string' ? body.type : 'line';
|
|
1631
|
-
const data =
|
|
1632
|
-
? body.data.filter((item: unknown) => item && typeof item === 'object') as Array<Record<string, unknown>>
|
|
1633
|
-
: null;
|
|
1737
|
+
const data = parseGraphPayloadData(body.data);
|
|
1634
1738
|
if (!data) {
|
|
1635
1739
|
return responseJson({ ok: false, error: 'Missing required field: data.' }, 400);
|
|
1636
1740
|
}
|
|
@@ -1646,6 +1750,12 @@ async function handleCanvasAddGraph(req: Request): Promise<Response> {
|
|
|
1646
1750
|
const series = Array.isArray(body.series)
|
|
1647
1751
|
? body.series.filter((s: unknown): s is string => typeof s === 'string')
|
|
1648
1752
|
: null;
|
|
1753
|
+
const position = getRecord(body.position);
|
|
1754
|
+
const size = getRecord(body.size);
|
|
1755
|
+
const x = pickFiniteNumber(body, 'x') ?? (position ? pickFiniteNumber(position, 'x') : undefined);
|
|
1756
|
+
const y = pickFiniteNumber(body, 'y') ?? (position ? pickFiniteNumber(position, 'y') : undefined);
|
|
1757
|
+
const width = pickPositiveNumber(body, 'width') ?? (size ? pickPositiveNumber(size, 'width') : undefined);
|
|
1758
|
+
const nodeHeight = pickPositiveNumber(body, 'nodeHeight') ?? (size ? pickPositiveNumber(size, 'height') : undefined);
|
|
1649
1759
|
const result = createCanvasGraphNode({
|
|
1650
1760
|
title,
|
|
1651
1761
|
graphType,
|
|
@@ -1665,13 +1775,13 @@ async function handleCanvasAddGraph(req: Request): Promise<Response> {
|
|
|
1665
1775
|
...(typeof body.barColor === 'string' ? { barColor: body.barColor } : {}),
|
|
1666
1776
|
...(typeof body.lineColor === 'string' ? { lineColor: body.lineColor } : {}),
|
|
1667
1777
|
...(typeof body.height === 'number' ? { height: body.height } : {}),
|
|
1668
|
-
...(
|
|
1669
|
-
...(
|
|
1670
|
-
...(
|
|
1671
|
-
...(
|
|
1778
|
+
...(x !== undefined ? { x } : {}),
|
|
1779
|
+
...(y !== undefined ? { y } : {}),
|
|
1780
|
+
...(width !== undefined ? { width } : {}),
|
|
1781
|
+
...(nodeHeight !== undefined ? { heightPx: nodeHeight } : {}),
|
|
1672
1782
|
});
|
|
1673
1783
|
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1674
|
-
return responseJson({
|
|
1784
|
+
return responseJson({ ...buildNodeResponse(result.node), url: result.url, spec: result.spec });
|
|
1675
1785
|
} catch (error) {
|
|
1676
1786
|
const message = error instanceof Error ? error.message : String(error);
|
|
1677
1787
|
return responseJson({ ok: false, error: message }, 400);
|
|
@@ -3880,6 +3990,10 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
3880
3990
|
return handleCanvasFocus(req);
|
|
3881
3991
|
}
|
|
3882
3992
|
|
|
3993
|
+
if (url.pathname === '/api/canvas/fit' && req.method === 'POST') {
|
|
3994
|
+
return handleCanvasFit(req);
|
|
3995
|
+
}
|
|
3996
|
+
|
|
3883
3997
|
if (url.pathname === '/api/canvas/clear' && req.method === 'POST') {
|
|
3884
3998
|
for (const node of canvasState.getLayout().nodes) {
|
|
3885
3999
|
closeNodeAppSession(node);
|