@stocksharp/diagram 0.4.1 → 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 +1 -1
  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 +5 -0
  40. package/src/svg-surface.ts +430 -0
package/README.md CHANGED
@@ -188,6 +188,48 @@ Socket input is reported separately through `portClicked`, with
188
188
  never starts a wire. `contextMenuRequested` also includes the exact port when
189
189
  the menu was opened over a socket.
190
190
 
191
+ ### The context menu
192
+
193
+ The control has no menu widget: right-click suppresses the browser's own menu
194
+ and emits `contextMenuRequested` with page coordinates, whatever was hit, and
195
+ the menu contents already resolved. Draw it however the host draws menus, then
196
+ send the choice back through `executeContextCommand()`.
197
+
198
+ `commands` is a tree in display order. An entry with a `group` is a submenu;
199
+ anything else is a command. `enabled` is computed for both — a submenu is off
200
+ when every item inside it is:
201
+
202
+ ```ts
203
+ diagram.on('contextMenuRequested', ({ x, y, commands }) => {
204
+ showMenu(x, y, commands.map((item) => 'group' in item
205
+ ? { label: t(item.group), enabled: item.enabled, items: item.commands }
206
+ : { label: t(item.command), enabled: item.enabled }));
207
+ });
208
+
209
+ menu.on('pick', (command) => diagram.executeContextCommand(command));
210
+ ```
211
+
212
+ `undo`, `redo`, `cut`, `copy`, `paste` and `delete` are carried out by the
213
+ control itself. The rest are requests it cannot answer alone: `open`,
214
+ `properties` and `help` raise `nodeOpen` / `nodeProperties` / `nodeHelp`, and
215
+ the `export` submenu raises `exportRequested` with the chosen format. Nothing
216
+ is produced until the host acts on it — only the host knows where the result
217
+ should go, and with which options:
218
+
219
+ ```ts
220
+ diagram.on('exportRequested', ({ format }) => {
221
+ if (format === 'document') return download('strategy.json', diagram.saveDocument());
222
+ const options = { scope: 'content', padding: 40 };
223
+ if (format === 'svg') return download('strategy.svg', diagram.takeSvg(options));
224
+ diagram.takeScreenshot(options).toBlob((blob) => download('strategy.png', blob));
225
+ });
226
+ ```
227
+
228
+ Export is offered whenever the diagram has a node, including in read-only mode,
229
+ because it only reads. Menu labels come from the host's i18n bundle
230
+ (`ctxExportAs`, `ctxExportDocument`, `ctxExportPng`, `ctxExportSvg`, and the
231
+ keys named after the other commands).
232
+
191
233
  Runtime failures flash the node border before leaving it red. Errors found
192
234
  while loading a scheme use a red background. Hovering either state shows the
193
235
  full error text in a tooltip:
@@ -1,3 +1,4 @@
1
+ import { SvgSurface } from './svg-surface.js';
1
2
  // Internal dependency-free canvas renderer used by StockSharpDiagram.
2
3
  //
3
4
  // Dependency-free, pure 2D canvas. Demonstrates the hard parts: typed
@@ -1100,6 +1101,65 @@ export class Diagram {
1100
1101
  copyContext.drawImage(this.canvas, 0, 0);
1101
1102
  return copy;
1102
1103
  }
1104
+ const layout = this.exportLayout(scope, options);
1105
+ const { pixelRatio, exportScale, padding, bounds, width, height, pixelWidth, pixelHeight } = layout;
1106
+ const output = document.createElement('canvas');
1107
+ output.width = pixelWidth;
1108
+ output.height = pixelHeight;
1109
+ output.style.width = `${width}px`;
1110
+ output.style.height = `${height}px`;
1111
+ const outputContext = output.getContext('2d');
1112
+ if (outputContext === null)
1113
+ throw new Error('ssdiagram: screenshot 2d context unavailable');
1114
+ const previous = {
1115
+ ctx: this.ctx,
1116
+ width: this.width,
1117
+ height: this.height,
1118
+ dpr: this.dpr,
1119
+ scale: this.scale,
1120
+ offX: this.offX,
1121
+ offY: this.offY,
1122
+ overviewVisible: this.overviewVisible,
1123
+ background: this.opts.background,
1124
+ };
1125
+ try {
1126
+ this.ctx = outputContext;
1127
+ this.width = width;
1128
+ this.height = height;
1129
+ this.dpr = pixelRatio;
1130
+ this.scale = exportScale;
1131
+ this.offX = scope === 'content' && bounds !== null ? padding - bounds.minX * exportScale : previous.offX;
1132
+ this.offY = scope === 'content' && bounds !== null ? padding - bounds.minY * exportScale : previous.offY;
1133
+ this.overviewVisible = options.includeOverview ?? (scope === 'viewport' && previous.overviewVisible);
1134
+ if (options.background !== undefined)
1135
+ this.opts.background = options.background;
1136
+ outputContext.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
1137
+ this.draw({
1138
+ grid: options.includeGrid ?? true,
1139
+ overview: this.overviewVisible,
1140
+ selection: options.includeSelection ?? scope === 'viewport',
1141
+ runtime: options.includeRuntimeState ?? true,
1142
+ transient: false,
1143
+ });
1144
+ }
1145
+ finally {
1146
+ this.ctx = previous.ctx;
1147
+ this.width = previous.width;
1148
+ this.height = previous.height;
1149
+ this.dpr = previous.dpr;
1150
+ this.scale = previous.scale;
1151
+ this.offX = previous.offX;
1152
+ this.offY = previous.offY;
1153
+ this.overviewVisible = previous.overviewVisible;
1154
+ this.opts.background = previous.background;
1155
+ }
1156
+ return output;
1157
+ }
1158
+ // Everything an export needs to know about size and placement, for either output. Shared on
1159
+ // purpose: a raster and a vector export of the same scope must frame the picture identically,
1160
+ // and two copies of this arithmetic would eventually disagree about padding or where the origin
1161
+ // sits. A block comment here would be lifted onto the next public member in the .d.ts.
1162
+ exportLayout(scope, options) {
1103
1163
  const pixelRatio = this.positiveScreenshotNumber(options.pixelRatio ?? this.dpr, 'pixelRatio');
1104
1164
  const exportScale = scope === 'content'
1105
1165
  ? this.positiveScreenshotNumber(options.scale ?? 1, 'scale')
@@ -1119,14 +1179,33 @@ export class Diagram {
1119
1179
  if (pixelWidth > 16384 || pixelHeight > 16384 || pixelWidth * pixelHeight > 268435456) {
1120
1180
  throw new RangeError(`ssdiagram: screenshot is too large (${pixelWidth}x${pixelHeight})`);
1121
1181
  }
1122
- const output = document.createElement('canvas');
1123
- output.width = pixelWidth;
1124
- output.height = pixelHeight;
1125
- output.style.width = `${width}px`;
1126
- output.style.height = `${height}px`;
1127
- const outputContext = output.getContext('2d');
1128
- if (outputContext === null)
1129
- throw new Error('ssdiagram: screenshot 2d context unavailable');
1182
+ return { pixelRatio, exportScale, padding, bounds, width, height, pixelWidth, pixelHeight };
1183
+ }
1184
+ /**
1185
+ * The same picture takeScreenshot produces, as SVG.
1186
+ *
1187
+ * It is the renderer that draws it -- the surface underneath simply records instead of paints --
1188
+ * so the vector output cannot drift from what the screen and the raster export show. Text keeps
1189
+ * the on-screen metrics by measuring through a real 2D context.
1190
+ *
1191
+ * Scale and pixelRatio still frame the picture (they decide the viewBox), but nothing is
1192
+ * resampled: the result is resolution-independent, which is the point of asking for it.
1193
+ */
1194
+ takeSvg(options = {}) {
1195
+ if (this.destroyed)
1196
+ throw new Error('ssdiagram: cannot export a destroyed diagram');
1197
+ const scope = options.scope ?? 'viewport';
1198
+ if (scope !== 'viewport' && scope !== 'content') {
1199
+ throw new RangeError(`ssdiagram: unsupported screenshot scope "${String(scope)}"`);
1200
+ }
1201
+ const { exportScale, padding, bounds, width, height } = this.exportLayout(scope, options);
1202
+ const metrics = this.canvas.getContext('2d');
1203
+ const surface = new SvgSurface({
1204
+ width,
1205
+ height,
1206
+ background: options.background ?? this.opts.background ?? '#1b1b1f',
1207
+ metrics: metrics ?? undefined,
1208
+ });
1130
1209
  const previous = {
1131
1210
  ctx: this.ctx,
1132
1211
  width: this.width,
@@ -1136,20 +1215,17 @@ export class Diagram {
1136
1215
  offX: this.offX,
1137
1216
  offY: this.offY,
1138
1217
  overviewVisible: this.overviewVisible,
1139
- background: this.opts.background,
1140
1218
  };
1141
1219
  try {
1142
- this.ctx = outputContext;
1220
+ this.ctx = surface;
1143
1221
  this.width = width;
1144
1222
  this.height = height;
1145
- this.dpr = pixelRatio;
1223
+ // 1: an SVG has no device pixels to multiply by, and the viewBox carries the size.
1224
+ this.dpr = 1;
1146
1225
  this.scale = exportScale;
1147
1226
  this.offX = scope === 'content' && bounds !== null ? padding - bounds.minX * exportScale : previous.offX;
1148
1227
  this.offY = scope === 'content' && bounds !== null ? padding - bounds.minY * exportScale : previous.offY;
1149
1228
  this.overviewVisible = options.includeOverview ?? (scope === 'viewport' && previous.overviewVisible);
1150
- if (options.background !== undefined)
1151
- this.opts.background = options.background;
1152
- outputContext.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
1153
1229
  this.draw({
1154
1230
  grid: options.includeGrid ?? true,
1155
1231
  overview: this.overviewVisible,
@@ -1167,9 +1243,8 @@ export class Diagram {
1167
1243
  this.offX = previous.offX;
1168
1244
  this.offY = previous.offY;
1169
1245
  this.overviewVisible = previous.overviewVisible;
1170
- this.opts.background = previous.background;
1171
1246
  }
1172
- return output;
1247
+ return surface.toSvg();
1173
1248
  }
1174
1249
  positiveScreenshotNumber(value, name) {
1175
1250
  if (!Number.isFinite(value) || value <= 0) {