@stocksharp/diagram 0.4.0 → 0.5.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 (40) hide show
  1. package/README.md +42 -0
  2. package/dist/esm/canvas-renderer.js +91 -16
  3. package/dist/esm/canvas-renderer.js.map +1 -1
  4. package/dist/esm/diagram/context-menu.js +272 -0
  5. package/dist/esm/diagram/context-menu.js.map +1 -0
  6. package/dist/esm/diagram/stocksharp-diagram.js +83 -5
  7. package/dist/esm/diagram/stocksharp-diagram.js.map +1 -1
  8. package/dist/esm/draw-surface.js +2 -0
  9. package/dist/esm/draw-surface.js.map +1 -0
  10. package/dist/esm/i18n.js.map +1 -1
  11. package/dist/esm/index.js.map +1 -1
  12. package/dist/esm/svg-surface.js +341 -0
  13. package/dist/esm/svg-surface.js.map +1 -0
  14. package/dist/ssdiagram.js +879 -164
  15. package/dist/ssdiagram.js.map +4 -4
  16. package/dist/types/canvas-renderer.d.ts +12 -0
  17. package/dist/types/canvas-renderer.d.ts.map +1 -1
  18. package/dist/types/diagram/api.d.ts +36 -2
  19. package/dist/types/diagram/api.d.ts.map +1 -1
  20. package/dist/types/diagram/context-menu.d.ts +31 -0
  21. package/dist/types/diagram/context-menu.d.ts.map +1 -0
  22. package/dist/types/diagram/stocksharp-diagram.d.ts +11 -5
  23. package/dist/types/diagram/stocksharp-diagram.d.ts.map +1 -1
  24. package/dist/types/draw-surface.d.ts +52 -0
  25. package/dist/types/draw-surface.d.ts.map +1 -0
  26. package/dist/types/i18n.d.ts +4 -0
  27. package/dist/types/i18n.d.ts.map +1 -1
  28. package/dist/types/index.d.ts +2 -2
  29. package/dist/types/index.d.ts.map +1 -1
  30. package/dist/types/svg-surface.d.ts +74 -0
  31. package/dist/types/svg-surface.d.ts.map +1 -0
  32. package/package.json +1 -1
  33. package/src/canvas-renderer.ts +103 -16
  34. package/src/diagram/api.ts +46 -2
  35. package/src/diagram/context-menu.ts +298 -0
  36. package/src/diagram/stocksharp-diagram.ts +93 -6
  37. package/src/draw-surface.ts +61 -0
  38. package/src/i18n.ts +7 -0
  39. package/src/index.ts +8 -0
  40. package/src/svg-surface.ts +430 -0
@@ -27,11 +27,15 @@ import {
27
27
  type PortModel,
28
28
  } from '../canvas-renderer.js';
29
29
  import { StockSharpCatalog } from './catalog.js';
30
+ import { ContextMenuView } from './context-menu.js';
30
31
  import { EventEmitter } from './event-emitter.js';
31
32
  import type {
32
33
  DiagramEvents,
33
34
  DiagramClipboard,
34
35
  ContextCommand,
36
+ ContextCommandGroup,
37
+ ContextCommandState,
38
+ ContextMenuItemState,
35
39
  DiagramLoadOptions,
36
40
  DiagramFullscreenLabels,
37
41
  DiagramGridSettings,
@@ -58,8 +62,28 @@ interface ContextActionContext {
58
62
  selection: DiagramSelection;
59
63
  nodes: DiagramNode[];
60
64
  links: Link[];
65
+ /** The whole diagram, not the selection: export offers the picture as a whole. */
66
+ document: DiagramDocument;
61
67
  }
62
68
 
69
+ type ContextMenuEntry = ContextCommand | { group: ContextCommandGroup; commands: readonly ContextCommand[] };
70
+
71
+ /**
72
+ * Menu order, and the only place the nesting is declared. The registry is flat because
73
+ * every command must be runnable by id, so the shape of the menu cannot live there.
74
+ * A command missing from this list would be executable and never offered; the test suite
75
+ * walks both and refuses to let the two drift.
76
+ */
77
+ const CONTEXT_MENU: readonly ContextMenuEntry[] = [
78
+ 'undo', 'redo',
79
+ 'cut', 'copy', 'paste',
80
+ 'open',
81
+ 'delete',
82
+ { group: 'export', commands: ['exportDocument', 'exportPng', 'exportSvg'] },
83
+ 'properties',
84
+ 'help',
85
+ ];
86
+
63
87
  export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
64
88
  private readonly div: HTMLElement;
65
89
  private readonly catalog: StockSharpCatalog;
@@ -79,6 +103,7 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
79
103
  private fullscreenButtonVisible = true;
80
104
  private fullscreenLabels: DiagramFullscreenLabels = { enter: 'Enter fullscreen', exit: 'Exit fullscreen' };
81
105
  private readonly contextActions = new DiagramActionRegistry<ContextCommand, ContextActionContext>();
106
+ private contextMenu: ContextMenuView | null = null;
82
107
 
83
108
  constructor(options: DiagramOptions) {
84
109
  super();
@@ -98,6 +123,7 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
98
123
  this.prepareFullscreenButtonHost();
99
124
  this.fullscreenButton = this.createFullscreenButton();
100
125
  this.updateFullscreenButton();
126
+ this.setContextMenuEnabled(options.showContextMenu ?? true);
101
127
  this.registerContextActions();
102
128
  this.bindCanvasEvents();
103
129
  this.disposables.push(this.catalog.on('portTypesChanged', () => this.applySocketTheme()));
@@ -372,6 +398,11 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
372
398
  return this.canvas.takeScreenshot(options);
373
399
  }
374
400
 
401
+ /** The same picture as takeScreenshot, as an SVG document. */
402
+ takeSvg(options: DiagramScreenshotOptions = {}): string {
403
+ return this.canvas.takeSvg(options);
404
+ }
405
+
375
406
  getSelection(): DiagramSnapshot<DiagramSelection> {
376
407
  return this.canvas.getSelection();
377
408
  }
@@ -509,11 +540,42 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
509
540
  return fallback !== null && this.canvas.pasteDocument(fallback).length > 0;
510
541
  }
511
542
 
512
- getContextCommands(): Array<{ command: ContextCommand; enabled: boolean }> {
513
- return this.contextActions.states(this.contextActionContext()).map(({ id, enabled }) => ({
514
- command: id,
515
- enabled,
516
- }));
543
+ /**
544
+ * Turns the built-in menu on or off. Off leaves `contextMenuRequested` untouched, so a
545
+ * host that draws its own is only trading one menu for another, never losing the event.
546
+ */
547
+ setContextMenuEnabled(enabled: boolean): void {
548
+ if (enabled === (this.contextMenu !== null)) return;
549
+ if (!enabled) {
550
+ this.contextMenu?.destroy();
551
+ this.contextMenu = null;
552
+ return;
553
+ }
554
+ this.contextMenu = new ContextMenuView({
555
+ container: this.div,
556
+ // Through the public entry point, so the built-in menu is exactly as privileged
557
+ // as a host's own -- same guards, same contextCommand event.
558
+ execute: (command) => { this.executeContextCommand(command); },
559
+ });
560
+ }
561
+
562
+ isContextMenuEnabled(): boolean {
563
+ return this.contextMenu !== null;
564
+ }
565
+
566
+ getContextCommands(): ContextMenuItemState[] {
567
+ const context = this.contextActionContext();
568
+ const state = (command: ContextCommand): ContextCommandState => ({
569
+ command,
570
+ enabled: this.contextActions.canExecute(command, context),
571
+ });
572
+ return CONTEXT_MENU.map((entry) => {
573
+ if (typeof entry === 'string') return state(entry);
574
+ const commands = entry.commands.map(state);
575
+ // A submenu that opens onto nothing but greyed items is itself dead, and
576
+ // saying so here spares every host from working the same thing out again.
577
+ return { group: entry.group, enabled: commands.some(({ enabled }) => enabled), commands };
578
+ });
517
579
  }
518
580
 
519
581
  executeContextCommand(command: ContextCommand): boolean {
@@ -588,6 +650,8 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
588
650
  destroy(): void {
589
651
  if (this.destroyed) return;
590
652
  this.destroyed = true;
653
+ this.contextMenu?.destroy();
654
+ this.contextMenu = null;
591
655
  for (const dispose of this.disposables.splice(0).reverse()) dispose();
592
656
  this.canvas.destroy();
593
657
  }
@@ -752,6 +816,7 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
752
816
  }),
753
817
  this.canvas.on('undoStackChanged', (state) => this.emit('undoStackChanged', state)),
754
818
  this.canvas.on('contextMenu', ({ x, y, node, link, port }) => {
819
+ const commands = this.getContextCommands();
755
820
  this.emit('contextMenuRequested', {
756
821
  x,
757
822
  y,
@@ -759,8 +824,11 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
759
824
  link: link === null ? null : this.fromCanvasLink(link),
760
825
  port: port === null ? null : this.fromCanvasPort(port.port),
761
826
  portDirection: port?.port.direction ?? null,
762
- commands: this.getContextCommands(),
827
+ commands,
763
828
  });
829
+ // After the event, so a host that draws its own menu has already been told
830
+ // and can have switched this one off in the handler.
831
+ this.contextMenu?.show(x, y, commands);
764
832
  }),
765
833
  );
766
834
  }
@@ -800,6 +868,24 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
800
868
  && (selection.nodeIds.length > 0 || selection.linkIds.length > 0),
801
869
  execute: () => this.canvas.deleteSelection(),
802
870
  },
871
+ // Export asks rather than does. Where the result goes -- a download, an upload,
872
+ // a clipboard, a print preview -- is the host's decision, and so is which options
873
+ // to render with, so the control reports the request and stops there.
874
+ // Availability follows the diagram, not the selection: all three export the whole
875
+ // picture, and an empty diagram has none to give. They stay on in read-only mode,
876
+ // which forbids changing the diagram, not looking at it.
877
+ exportDocument: {
878
+ canExecute: ({ document }) => document.nodes.length > 0,
879
+ execute: () => this.emit('exportRequested', { format: 'document' }),
880
+ },
881
+ exportPng: {
882
+ canExecute: ({ document }) => document.nodes.length > 0,
883
+ execute: () => this.emit('exportRequested', { format: 'png' }),
884
+ },
885
+ exportSvg: {
886
+ canExecute: ({ document }) => document.nodes.length > 0,
887
+ execute: () => this.emit('exportRequested', { format: 'svg' }),
888
+ },
803
889
  properties: {
804
890
  canExecute: ({ nodes }) => nodes.length > 0,
805
891
  execute: ({ nodes }) => this.emit('nodeProperties', { nodes }),
@@ -818,6 +904,7 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
818
904
  const linkIds = new Set(selection.linkIds);
819
905
  return {
820
906
  selection,
907
+ document,
821
908
  nodes: document.nodes
822
909
  .filter((node) => nodeIds.has(node.id))
823
910
  .map((node) => this.fromCanvasInit(node)),
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The drawing operations the renderer actually uses, and nothing more.
3
+ *
4
+ * The renderer paints straight onto a 2D context, which means the picture only ever exists as
5
+ * pixels: a screenshot can be taken, but there is no vector form to export. Naming the surface it
6
+ * draws against is what makes a second backend possible without a second renderer -- one that emits
7
+ * SVG. Two renderers would be two truths about how a node looks, and they would drift.
8
+ *
9
+ * The member list is deliberately a subset of CanvasRenderingContext2D with identical names and
10
+ * semantics. TypeScript matches types structurally, so the browser's own context satisfies this
11
+ * without a wrapper: the on-screen path keeps calling the real thing directly, at no cost and with
12
+ * no behaviour to re-verify.
13
+ */
14
+ export interface DrawSurface {
15
+ // ----- state -----
16
+ fillStyle: string | CanvasGradient | CanvasPattern;
17
+ strokeStyle: string | CanvasGradient | CanvasPattern;
18
+ lineWidth: number;
19
+ lineJoin: CanvasLineJoin;
20
+ lineCap: CanvasLineCap;
21
+ globalAlpha: number;
22
+ font: string;
23
+ textAlign: CanvasTextAlign;
24
+ textBaseline: CanvasTextBaseline;
25
+
26
+ save(): void;
27
+ restore(): void;
28
+ setLineDash(segments: readonly number[]): void;
29
+
30
+ // ----- transform -----
31
+ setTransform(a: number, b: number, c: number, d: number, e: number, f: number): void;
32
+ translate(x: number, y: number): void;
33
+ rotate(angle: number): void;
34
+
35
+ // ----- paths -----
36
+ beginPath(): void;
37
+ closePath(): void;
38
+ moveTo(x: number, y: number): void;
39
+ lineTo(x: number, y: number): void;
40
+ arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, counterclockwise?: boolean): void;
41
+ arcTo(x1: number, y1: number, x2: number, y2: number, radius: number): void;
42
+ fill(): void;
43
+ stroke(): void;
44
+ clip(): void;
45
+
46
+ // ----- rectangles -----
47
+ clearRect(x: number, y: number, w: number, h: number): void;
48
+ fillRect(x: number, y: number, w: number, h: number): void;
49
+ strokeRect(x: number, y: number, w: number, h: number): void;
50
+
51
+ // ----- text -----
52
+ fillText(text: string, x: number, y: number, maxWidth?: number): void;
53
+ /**
54
+ * Only `width` is read by the renderer. An SVG backend has no font engine of its own and
55
+ * borrows a real 2D context for this, so text lands in exactly the same place in both outputs.
56
+ */
57
+ measureText(text: string): { width: number };
58
+
59
+ // ----- images -----
60
+ drawImage(image: CanvasImageSource, dx: number, dy: number, dw: number, dh: number): void;
61
+ }
package/src/i18n.ts CHANGED
@@ -95,6 +95,13 @@ export interface DesignerI18n {
95
95
  copy?: string;
96
96
  paste?: string;
97
97
  ctxOpen?: string;
98
+ // Submenu title, and deliberately not `ctxExport` -- that one is the solution
99
+ // explorer's "export this strategy", a different action that a host may well word
100
+ // differently. Reads as its own item too: "Export as > SVG image".
101
+ ctxExportAs?: string;
102
+ ctxExportDocument?: string;
103
+ ctxExportPng?: string;
104
+ ctxExportSvg?: string;
98
105
  properties?: string;
99
106
  ctxHelp?: string;
100
107
  collapse?: string;
package/src/index.ts CHANGED
@@ -32,6 +32,9 @@ export type {
32
32
  DiagramDocumentPort,
33
33
  DiagramDocumentPortInput,
34
34
  DiagramDocumentVersion,
35
+ DiagramDocumentZone,
36
+ DiagramDocumentZoneInput,
37
+ DiagramLinkStyle,
35
38
  DiagramParameterSchema,
36
39
  JsonObject,
37
40
  JsonPrimitive,
@@ -84,9 +87,14 @@ export {
84
87
 
85
88
  export type {
86
89
  ContextCommand,
90
+ ContextCommandGroup,
91
+ ContextCommandGroupState,
87
92
  ContextCommandPayload,
88
93
  ContextCommandState,
94
+ ContextMenuItemState,
89
95
  ContextMenuRequestedPayload,
96
+ ExportFormat,
97
+ ExportRequestedPayload,
90
98
  DiagramEvents,
91
99
  DiagramClipboard,
92
100
  DiagramGridSettings,