@stocksharp/diagram 0.2.3 → 0.4.0

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.
Files changed (68) hide show
  1. package/dist/esm/canvas-renderer.js +79 -28
  2. package/dist/esm/canvas-renderer.js.map +1 -1
  3. package/dist/esm/core/action-registry.js +24 -0
  4. package/dist/esm/core/action-registry.js.map +1 -1
  5. package/dist/esm/core/document.js +134 -28
  6. package/dist/esm/core/document.js.map +1 -1
  7. package/dist/esm/core/json.js +17 -0
  8. package/dist/esm/core/json.js.map +1 -0
  9. package/dist/esm/core/model.js +8 -0
  10. package/dist/esm/core/model.js.map +1 -1
  11. package/dist/esm/core/state.js +16 -2
  12. package/dist/esm/core/state.js.map +1 -1
  13. package/dist/esm/diagram/catalog.js +17 -5
  14. package/dist/esm/diagram/catalog.js.map +1 -1
  15. package/dist/esm/diagram/event-emitter.js +10 -0
  16. package/dist/esm/diagram/event-emitter.js.map +1 -1
  17. package/dist/esm/diagram/stocksharp-diagram.js +58 -56
  18. package/dist/esm/diagram/stocksharp-diagram.js.map +1 -1
  19. package/dist/esm/diagram/types.js +2 -1
  20. package/dist/esm/diagram/types.js.map +1 -1
  21. package/dist/esm/embed.js +121 -38
  22. package/dist/esm/embed.js.map +1 -1
  23. package/dist/esm/index.js +1 -1
  24. package/dist/esm/index.js.map +1 -1
  25. package/dist/ssdiagram.js +380 -150
  26. package/dist/ssdiagram.js.map +4 -4
  27. package/dist/types/canvas-renderer.d.ts +11 -6
  28. package/dist/types/canvas-renderer.d.ts.map +1 -1
  29. package/dist/types/core/action-registry.d.ts +7 -0
  30. package/dist/types/core/action-registry.d.ts.map +1 -1
  31. package/dist/types/core/document.d.ts +1 -1
  32. package/dist/types/core/document.d.ts.map +1 -1
  33. package/dist/types/core/json.d.ts +11 -0
  34. package/dist/types/core/json.d.ts.map +1 -0
  35. package/dist/types/core/model.d.ts +45 -2
  36. package/dist/types/core/model.d.ts.map +1 -1
  37. package/dist/types/core/state.d.ts +24 -1
  38. package/dist/types/core/state.d.ts.map +1 -1
  39. package/dist/types/diagram/api.d.ts +6 -6
  40. package/dist/types/diagram/api.d.ts.map +1 -1
  41. package/dist/types/diagram/catalog.d.ts +2 -1
  42. package/dist/types/diagram/catalog.d.ts.map +1 -1
  43. package/dist/types/diagram/event-emitter.d.ts +6 -1
  44. package/dist/types/diagram/event-emitter.d.ts.map +1 -1
  45. package/dist/types/diagram/palette.d.ts +1 -1
  46. package/dist/types/diagram/palette.d.ts.map +1 -1
  47. package/dist/types/diagram/stocksharp-diagram.d.ts +29 -22
  48. package/dist/types/diagram/stocksharp-diagram.d.ts.map +1 -1
  49. package/dist/types/diagram/types.d.ts +4 -4
  50. package/dist/types/diagram/types.d.ts.map +1 -1
  51. package/dist/types/embed.d.ts.map +1 -1
  52. package/dist/types/index.d.ts +3 -3
  53. package/dist/types/index.d.ts.map +1 -1
  54. package/package.json +2 -2
  55. package/src/canvas-renderer.ts +84 -29
  56. package/src/core/action-registry.ts +25 -0
  57. package/src/core/document.ts +147 -31
  58. package/src/core/json.ts +16 -0
  59. package/src/core/model.ts +55 -2
  60. package/src/core/state.ts +41 -3
  61. package/src/diagram/api.ts +6 -5
  62. package/src/diagram/catalog.ts +18 -6
  63. package/src/diagram/event-emitter.ts +12 -1
  64. package/src/diagram/palette.ts +1 -1
  65. package/src/diagram/stocksharp-diagram.ts +79 -76
  66. package/src/diagram/types.ts +6 -5
  67. package/src/embed.ts +140 -39
  68. package/src/index.ts +4 -0
@@ -5,6 +5,8 @@
5
5
  // selection, delete, zoom/pan, load/save round-trip.
6
6
  import { createDiagramDocument, parseDiagramDocument } from './core/document.js';
7
7
  import { DiagramCommandHistory } from './core/history.js';
8
+ import { setJsonKey } from './core/json.js';
9
+ import { toPortDynamicMode } from './core/model.js';
8
10
  import { cloneDiagramRuntimeState, createDiagramNodeRuntimeState, createDiagramPortRuntimeState, createDiagramRuntimeState, createEditableDiagramPermissions, createReadOnlyDiagramPermissions, } from './core/state.js';
9
11
  const SCREEN_RENDER_OPTIONS = {
10
12
  grid: true,
@@ -26,7 +28,7 @@ function copyJsonObject(value) {
26
28
  return {};
27
29
  const result = {};
28
30
  for (const [key, item] of Object.entries(value))
29
- result[key] = copyJsonValue(item);
31
+ setJsonKey(result, key, copyJsonValue(item));
30
32
  return result;
31
33
  }
32
34
  function copyParameters(value) {
@@ -65,7 +67,7 @@ export class PortModel {
65
67
  this.maxLinks = typeof init.maxLinks === 'number' ? init.maxLinks : 0;
66
68
  this.availableTypes = [...(init.availableTypes ?? [])];
67
69
  this.isDynamic = init.isDynamic ?? false;
68
- this.dynamicMode = init.dynamicMode ?? '';
70
+ this.dynamicMode = toPortDynamicMode(init.dynamicMode);
69
71
  this.isSibling = init.isSibling ?? false;
70
72
  this.metadata = copyJsonObject(init.metadata);
71
73
  }
@@ -141,11 +143,12 @@ export class NodeModel {
141
143
  }
142
144
  }
143
145
  export class LinkModel {
144
- constructor(from, fromPort, to, toPort, id = '', metadata) {
146
+ constructor(from, fromPort, to, toPort, id = '', metadata, style = 'solid') {
145
147
  this.from = from;
146
148
  this.fromPort = fromPort;
147
149
  this.to = to;
148
150
  this.toPort = toPort;
151
+ this.style = style;
149
152
  this.id = id;
150
153
  this.metadata = copyJsonObject(metadata);
151
154
  }
@@ -157,14 +160,13 @@ export class LinkModel {
157
160
  fromPort: this.fromPort,
158
161
  to: this.to,
159
162
  toPort: this.toPort,
163
+ style: this.style,
160
164
  metadata: copyJsonObject(this.metadata),
161
165
  };
162
166
  }
163
167
  }
164
- const HEADER_H = 22;
165
168
  const PORT_R = 6;
166
169
  const PORT_ROW_H = 20;
167
- const NODE_PAD = 10;
168
170
  const RELINK_HANDLE_PX = 8; // WPF relink adornment is an 8x8 diamond
169
171
  const RELINK_DRAG_THRESHOLD_PX = 4;
170
172
  const PORT_SQ = 9; // socket square size — sits outside the node
@@ -219,6 +221,7 @@ export class Diagram {
219
221
  constructor(opts) {
220
222
  this.nodes = [];
221
223
  this.links = [];
224
+ this.zones = [];
222
225
  this.idSeq = 1;
223
226
  this.linkSeq = 1;
224
227
  this.documentMetadata = {};
@@ -241,8 +244,6 @@ export class Diagram {
241
244
  this.dragNode = null;
242
245
  this.dragStart = []; // group-drag origin
243
246
  this.dragAnchor = { wx: 0, wy: 0 };
244
- this.dragDX = 0;
245
- this.dragDY = 0;
246
247
  this.panning = false;
247
248
  this.panX = 0;
248
249
  this.panY = 0;
@@ -567,7 +568,7 @@ export class Diagram {
567
568
  if (!validation.allowed)
568
569
  return false;
569
570
  }
570
- const link = new LinkModel(init.from, init.fromPort, init.to, init.toPort, init.id ?? this.nextLinkId(), init.metadata);
571
+ const link = new LinkModel(init.from, init.fromPort, init.to, init.toPort, init.id ?? this.nextLinkId(), init.metadata, init.style ?? 'solid');
571
572
  if (this.links.some((l) => l.id === link.id))
572
573
  return false;
573
574
  this.links.splice(clamp(Math.trunc(index), 0, this.links.length), 0, link);
@@ -894,7 +895,7 @@ export class Diagram {
894
895
  if (value === undefined)
895
896
  delete node.paramValues[name];
896
897
  else
897
- node.paramValues[name] = value;
898
+ setJsonKey(node.paramValues, name, value);
898
899
  });
899
900
  }
900
901
  setShowNodeMessages(show) {
@@ -977,7 +978,7 @@ export class Diagram {
977
978
  if (this.links.some((existing) => existing.id === id)) {
978
979
  throw new Error(`ssdiagram: duplicate link id "${id}"`);
979
980
  }
980
- const model = new LinkModel(link.from, link.fromPort, link.to, link.toPort, id, link.metadata);
981
+ const model = new LinkModel(link.from, link.fromPort, link.to, link.toPort, id, link.metadata, link.style ?? 'solid');
981
982
  if (!this.links.some((existing) => existing.key() === model.key()))
982
983
  this.links.push(model);
983
984
  }
@@ -985,7 +986,7 @@ export class Diagram {
985
986
  if (node.loadError.length === 0)
986
987
  continue;
987
988
  const state = createDiagramNodeRuntimeState();
988
- state.error = { kind: 'load', message: node.loadError, pulse: ++this.runtimePulse };
989
+ state.errors.load = { kind: 'load', message: node.loadError, pulse: ++this.runtimePulse };
989
990
  this.runtimeState.nodes[node.id] = state;
990
991
  }
991
992
  if (Object.keys(this.runtimeState.nodes).length > 0)
@@ -1009,8 +1010,10 @@ export class Diagram {
1009
1010
  fromPort: link.from.portId,
1010
1011
  to: link.to.nodeId,
1011
1012
  toPort: link.to.portId,
1013
+ style: link.style,
1012
1014
  metadata: link.metadata,
1013
1015
  })));
1016
+ this.zones = document.zones.map((zone) => ({ ...zone, metadata: copyJsonObject(zone.metadata) }));
1014
1017
  this.documentMetadata = copyJsonObject(document.metadata);
1015
1018
  }
1016
1019
  saveDocument() {
@@ -1020,8 +1023,10 @@ export class Diagram {
1020
1023
  id: link.id,
1021
1024
  from: { nodeId: link.from, portId: link.fromPort },
1022
1025
  to: { nodeId: link.to, portId: link.toPort },
1026
+ style: link.style,
1023
1027
  metadata: link.metadata,
1024
1028
  })),
1029
+ zones: this.zones.map((zone) => ({ ...zone, metadata: copyJsonObject(zone.metadata) })),
1025
1030
  metadata: this.documentMetadata,
1026
1031
  });
1027
1032
  }
@@ -1190,14 +1195,16 @@ export class Diagram {
1190
1195
  else if (previous.globalError?.pulse !== this.runtimeState.globalError.pulse) {
1191
1196
  this.globalErrorFlashStart = performance.now();
1192
1197
  }
1193
- this.runtimePulse = Math.max(this.runtimePulse, this.runtimeState.globalError?.pulse ?? 0, ...Object.values(this.runtimeState.nodes).map((node) => node.error?.pulse ?? 0));
1198
+ this.runtimePulse = Math.max(this.runtimePulse, this.runtimeState.globalError?.pulse ?? 0, ...Object.values(this.runtimeState.nodes)
1199
+ .flatMap((node) => [node.errors.runtime?.pulse ?? 0, node.errors.load?.pulse ?? 0]));
1194
1200
  for (const node of this.nodes) {
1195
- const error = this.runtimeState.nodes[node.id]?.error ?? null;
1196
- const previousError = previous.nodes[node.id]?.error ?? null;
1197
- node.runtimeError = error?.kind === 'runtime' ? error.message : '';
1198
- node.loadError = error?.kind === 'load' ? error.message : '';
1199
- if (error?.kind === 'runtime') {
1200
- if (previousError?.kind !== 'runtime' || previousError.pulse !== error.pulse) {
1201
+ const errors = this.runtimeState.nodes[node.id]?.errors;
1202
+ const previousRuntime = previous.nodes[node.id]?.errors.runtime;
1203
+ const runtimeError = errors?.runtime;
1204
+ node.runtimeError = runtimeError?.message ?? '';
1205
+ node.loadError = errors?.load?.message ?? '';
1206
+ if (runtimeError !== undefined) {
1207
+ if (previousRuntime === undefined || previousRuntime.pulse !== runtimeError.pulse) {
1201
1208
  node.errorFlashStart = performance.now();
1202
1209
  }
1203
1210
  }
@@ -1257,7 +1264,14 @@ export class Diagram {
1257
1264
  const state = this.getRuntimeState();
1258
1265
  const nodeState = state.nodes[id] ?? createDiagramNodeRuntimeState();
1259
1266
  state.nodes[id] = nodeState;
1260
- nodeState.error = message.length === 0 ? null : { kind, message, pulse: ++this.runtimePulse };
1267
+ // Only this kind's slot is written, so an error of the other kind that is
1268
+ // already on the node survives.
1269
+ if (message.length === 0)
1270
+ delete nodeState.errors[kind];
1271
+ else if (kind === 'load')
1272
+ nodeState.errors.load = { kind, message, pulse: ++this.runtimePulse };
1273
+ else
1274
+ nodeState.errors.runtime = { kind, message, pulse: ++this.runtimePulse };
1261
1275
  this.runtimeState = state;
1262
1276
  this.emitRuntimeStateChanged();
1263
1277
  return true;
@@ -1274,12 +1288,11 @@ export class Diagram {
1274
1288
  node.loadError = '';
1275
1289
  const state = this.getRuntimeState();
1276
1290
  const nodeState = state.nodes[id];
1277
- if (nodeState !== undefined && (kind === undefined || nodeState.error?.kind === kind)) {
1278
- nodeState.error = node.runtimeError.length > 0
1279
- ? { kind: 'runtime', message: node.runtimeError, pulse: ++this.runtimePulse }
1280
- : node.loadError.length > 0
1281
- ? { kind: 'load', message: node.loadError, pulse: ++this.runtimePulse }
1282
- : null;
1291
+ if (nodeState !== undefined) {
1292
+ if (kind === undefined || kind === 'runtime')
1293
+ delete nodeState.errors.runtime;
1294
+ if (kind === undefined || kind === 'load')
1295
+ delete nodeState.errors.load;
1283
1296
  }
1284
1297
  this.runtimeState = state;
1285
1298
  this.emitRuntimeStateChanged();
@@ -2382,7 +2395,10 @@ export class Diagram {
2382
2395
  if (options.grid)
2383
2396
  this.drawGrid();
2384
2397
  ctx.setTransform(this.dpr * this.scale, 0, 0, this.dpr * this.scale, this.dpr * this.offX, this.dpr * (this.offY + introDy));
2385
- // Links first. Each link jumps over the verticals of the links
2398
+ // Zones sit under everything: they say where things are, and a place that painted over
2399
+ // its occupants would be worse than no place at all.
2400
+ this.drawZones();
2401
+ // Links next. Each link jumps over the verticals of the links
2386
2402
  // drawn before it (deterministic bridge — Link.JumpOver parity).
2387
2403
  const prior = []; // segments of links already drawn
2388
2404
  const hoveredNode = this.hoverPort?.node ?? this.hoverNode;
@@ -2404,7 +2420,7 @@ export class Diagram {
2404
2420
  const color = state === 'sel' ? this.selectionColor() : state === 'hov' ? this.linkHoverColor() : baseColor;
2405
2421
  const width = state === 'sel' ? 3 : state === 'hov' ? 2.6 : 2;
2406
2422
  const pts = this.routeLink(a, b, new Set([l.from, l.to]));
2407
- this.strokeRoute(pts, color, width, prior);
2423
+ this.strokeRoute(pts, color, width, prior, l.style);
2408
2424
  this.drawArrow(pts, color);
2409
2425
  for (let k = 1; k < pts.length; k += 1) {
2410
2426
  const [x1, y1] = pts[k - 1];
@@ -2571,6 +2587,37 @@ export class Diagram {
2571
2587
  }
2572
2588
  return lines.length > 0 ? lines : [''];
2573
2589
  }
2590
+ drawZones() {
2591
+ if (this.zones.length === 0)
2592
+ return;
2593
+ const ctx = this.ctx;
2594
+ for (const zone of this.zones) {
2595
+ const tint = zone.color.length > 0 ? zone.color : (this.opts.zoneColor ?? '#8a8f98');
2596
+ ctx.save();
2597
+ roundRect(ctx, zone.x, zone.y, zone.width, zone.height, 10);
2598
+ ctx.globalAlpha = 0.12;
2599
+ ctx.fillStyle = tint;
2600
+ ctx.fill();
2601
+ ctx.globalAlpha = 0.5;
2602
+ ctx.setLineDash([10, 6]);
2603
+ ctx.lineWidth = 1.5;
2604
+ ctx.strokeStyle = tint;
2605
+ ctx.stroke();
2606
+ ctx.restore();
2607
+ if (zone.name.length === 0)
2608
+ continue;
2609
+ ctx.save();
2610
+ ctx.globalAlpha = 0.75;
2611
+ ctx.fillStyle = tint;
2612
+ ctx.font = '600 12px Segoe UI, Tahoma, sans-serif';
2613
+ ctx.textAlign = 'left';
2614
+ ctx.textBaseline = 'top';
2615
+ // Inside the top-left corner: a caption outside the box would collide with whatever
2616
+ // sits above it, and the corner is the one spot a rectangle always has to spare.
2617
+ ctx.fillText(zone.name, zone.x + 12, zone.y + 9, Math.max(0, zone.width - 24));
2618
+ ctx.restore();
2619
+ }
2620
+ }
2574
2621
  drawGrid() {
2575
2622
  const ctx = this.ctx;
2576
2623
  const step = this.gridSize * this.scale;
@@ -2712,10 +2759,13 @@ export class Diagram {
2712
2759
  // Symmetric jump-over: any segment of THIS link hops the perpendicular
2713
2760
  // segments of links drawn before it (H over earlier V, V over earlier
2714
2761
  // H). Exactly one bridge per real crossing, consistent everywhere.
2715
- strokeRoute(pts, color, width, prior) {
2762
+ strokeRoute(pts, color, width, prior, style = 'solid') {
2716
2763
  const ctx = this.ctx;
2717
2764
  ctx.strokeStyle = color;
2718
2765
  ctx.lineWidth = width;
2766
+ // The dash is measured in world units, so it keeps its rhythm as the canvas is zoomed.
2767
+ if (style === 'dashed')
2768
+ ctx.setLineDash([9, 6]);
2719
2769
  ctx.lineJoin = 'miter';
2720
2770
  ctx.lineCap = 'butt';
2721
2771
  ctx.beginPath();
@@ -2775,6 +2825,7 @@ export class Diagram {
2775
2825
  }
2776
2826
  }
2777
2827
  ctx.stroke();
2828
+ ctx.setLineDash([]);
2778
2829
  }
2779
2830
  drawArrow(pts, color) {
2780
2831
  const n = pts.length;