@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.
- package/README.md +42 -0
- package/dist/esm/canvas-renderer.js +91 -16
- package/dist/esm/canvas-renderer.js.map +1 -1
- package/dist/esm/diagram/context-menu.js +272 -0
- package/dist/esm/diagram/context-menu.js.map +1 -0
- package/dist/esm/diagram/stocksharp-diagram.js +83 -5
- package/dist/esm/diagram/stocksharp-diagram.js.map +1 -1
- package/dist/esm/draw-surface.js +2 -0
- package/dist/esm/draw-surface.js.map +1 -0
- package/dist/esm/i18n.js.map +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/svg-surface.js +341 -0
- package/dist/esm/svg-surface.js.map +1 -0
- package/dist/ssdiagram.js +879 -164
- package/dist/ssdiagram.js.map +4 -4
- package/dist/types/canvas-renderer.d.ts +12 -0
- package/dist/types/canvas-renderer.d.ts.map +1 -1
- package/dist/types/diagram/api.d.ts +36 -2
- package/dist/types/diagram/api.d.ts.map +1 -1
- package/dist/types/diagram/context-menu.d.ts +31 -0
- package/dist/types/diagram/context-menu.d.ts.map +1 -0
- package/dist/types/diagram/stocksharp-diagram.d.ts +11 -5
- package/dist/types/diagram/stocksharp-diagram.d.ts.map +1 -1
- package/dist/types/draw-surface.d.ts +52 -0
- package/dist/types/draw-surface.d.ts.map +1 -0
- package/dist/types/i18n.d.ts +4 -0
- package/dist/types/i18n.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/svg-surface.d.ts +74 -0
- package/dist/types/svg-surface.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/canvas-renderer.ts +103 -16
- package/src/diagram/api.ts +46 -2
- package/src/diagram/context-menu.ts +298 -0
- package/src/diagram/stocksharp-diagram.ts +93 -6
- package/src/draw-surface.ts +61 -0
- package/src/i18n.ts +7 -0
- package/src/index.ts +8 -0
- 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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
|
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,
|