@stocksharp/diagram 0.4.1 → 0.6.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 +105 -2
- package/dist/esm/canvas-renderer.js +116 -36
- package/dist/esm/canvas-renderer.js.map +1 -1
- package/dist/esm/diagram/context-menu.js +319 -0
- package/dist/esm/diagram/context-menu.js.map +1 -0
- package/dist/esm/diagram/stocksharp-diagram.js +121 -9
- 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/embed.js +7 -4
- package/dist/esm/embed.js.map +1 -1
- package/dist/esm/i18n.js +15 -4
- 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 +978 -188
- package/dist/ssdiagram.js.map +4 -4
- package/dist/types/canvas-renderer.d.ts +13 -0
- package/dist/types/canvas-renderer.d.ts.map +1 -1
- package/dist/types/diagram/api.d.ts +42 -2
- package/dist/types/diagram/api.d.ts.map +1 -1
- package/dist/types/diagram/context-menu.d.ts +33 -0
- package/dist/types/diagram/context-menu.d.ts.map +1 -0
- package/dist/types/diagram/stocksharp-diagram.d.ts +18 -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/embed.d.ts.map +1 -1
- package/dist/types/i18n.d.ts +12 -0
- package/dist/types/i18n.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -1
- 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 +120 -27
- package/src/diagram/api.ts +53 -2
- package/src/diagram/context-menu.ts +363 -0
- package/src/diagram/stocksharp-diagram.ts +134 -10
- package/src/draw-surface.ts +61 -0
- package/src/embed.ts +7 -4
- package/src/i18n.ts +41 -6
- package/src/index.ts +10 -0
- package/src/svg-surface.ts +430 -0
package/README.md
CHANGED
|
@@ -154,7 +154,9 @@ missing type. Sites can localize that message through
|
|
|
154
154
|
`data-diagram-missing-element="Missing: {typeId}"` on the host.
|
|
155
155
|
|
|
156
156
|
Every text the embedded control shows comes from the host, so a translated page
|
|
157
|
-
carries no English leftovers
|
|
157
|
+
carries no English leftovers. These attributes are per-instance overrides on top
|
|
158
|
+
of the page's translation bundle (see *Translating the control* below); set them
|
|
159
|
+
when one diagram has to read differently from the rest of the page:
|
|
158
160
|
|
|
159
161
|
| Host attribute | What it renames |
|
|
160
162
|
|---|---|
|
|
@@ -163,7 +165,9 @@ carries no English leftovers:
|
|
|
163
165
|
| `data-diagram-missing-element="Missing: {typeId}"` | the placeholder for an element the palette lacks |
|
|
164
166
|
|
|
165
167
|
The same labels are available to custom integrations as the `fullscreenLabels`
|
|
166
|
-
option and `setFullscreenLabels()` on `StockSharpDiagram`.
|
|
168
|
+
option and `setFullscreenLabels()` on `StockSharpDiagram`. With neither the
|
|
169
|
+
attribute nor the option, the wording comes from the bundle, so a page that has
|
|
170
|
+
already been translated needs no per-diagram markup at all.
|
|
167
171
|
|
|
168
172
|
### Node actions and errors
|
|
169
173
|
|
|
@@ -188,6 +192,105 @@ Socket input is reported separately through `portClicked`, with
|
|
|
188
192
|
never starts a wire. `contextMenuRequested` also includes the exact port when
|
|
189
193
|
the menu was opened over a socket.
|
|
190
194
|
|
|
195
|
+
### The context menu
|
|
196
|
+
|
|
197
|
+
Right-click opens the control's own menu, with an Export submenu. It is on by
|
|
198
|
+
default, because right-click suppresses the browser's menu either way and a
|
|
199
|
+
control that takes one away owes one back. It needs no stylesheet: it is styled
|
|
200
|
+
inline over `--ssdiagram-*` custom properties (`--ssdiagram-menu-background`,
|
|
201
|
+
`--ssdiagram-menu-border`, `--ssdiagram-menu-color`, `--ssdiagram-menu-hover`,
|
|
202
|
+
`--ssdiagram-menu-disabled-color`, `--ssdiagram-menu-font`), falling back to the
|
|
203
|
+
`--ssdiagram-control-*` values the fullscreen button uses. Its class names —
|
|
204
|
+
`ssdiagram-context-menu`, `-item`, `-label`, `-arrow`, `-submenu`, `-separator` —
|
|
205
|
+
are stable if you would rather use CSS.
|
|
206
|
+
|
|
207
|
+
To draw your own instead, pass `showContextMenu: false` (or call
|
|
208
|
+
`setContextMenuEnabled(false)`) and handle `contextMenuRequested`, which is
|
|
209
|
+
emitted either way — and emitted *before* the built-in menu opens, so a handler
|
|
210
|
+
can still switch it off.
|
|
211
|
+
|
|
212
|
+
`commands` is a tree in display order. An entry with a `group` is a submenu;
|
|
213
|
+
anything else is a command. `enabled` is computed for both — a submenu is off
|
|
214
|
+
when every item inside it is:
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
diagram.on('contextMenuRequested', ({ x, y, commands }) => {
|
|
218
|
+
showMenu(x, y, commands.map((item) => 'group' in item
|
|
219
|
+
? { label: t(item.group), enabled: item.enabled, items: item.commands }
|
|
220
|
+
: { label: t(item.command), enabled: item.enabled }));
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
menu.on('pick', (command) => diagram.executeContextCommand(command));
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`undo`, `redo`, `cut`, `copy`, `paste`, `delete` and `overview` are carried out
|
|
227
|
+
by the control itself. `overview` toggles the corner minimap, and reports its
|
|
228
|
+
current state as `checked` on the command so a menu can tick it rather than offer
|
|
229
|
+
a dead show/hide pair. The rest are requests it cannot answer alone: `open`,
|
|
230
|
+
`properties` and `help` raise `nodeOpen` / `nodeProperties` / `nodeHelp`, and
|
|
231
|
+
the `export` submenu raises `exportRequested` with the chosen format. Nothing
|
|
232
|
+
is produced until the host acts on it — only the host knows where the result
|
|
233
|
+
should go, and with which options:
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
diagram.on('exportRequested', ({ format }) => {
|
|
237
|
+
if (format === 'document') return download('strategy.json', diagram.saveDocument());
|
|
238
|
+
const options = { scope: 'content', padding: 40 };
|
|
239
|
+
if (format === 'svg') return download('strategy.svg', diagram.takeSvg(options));
|
|
240
|
+
diagram.takeScreenshot(options).toBlob((blob) => download('strategy.png', blob));
|
|
241
|
+
});
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Export is offered whenever the diagram has a node, including in read-only mode,
|
|
245
|
+
because it only reads.
|
|
246
|
+
|
|
247
|
+
#### Translating the control
|
|
248
|
+
|
|
249
|
+
There is one mechanism, and it is `window.__designerI18n`: an object typed by the
|
|
250
|
+
exported `DesignerI18n`, holding every string the package renders itself — the
|
|
251
|
+
menu, the fullscreen button's tooltip, and the embed failure notes. It is read on
|
|
252
|
+
every lookup, so it can be assigned at any point, before or after the bundle
|
|
253
|
+
loads, and reassigned later to change language. A key that is missing or empty
|
|
254
|
+
falls back to English.
|
|
255
|
+
|
|
256
|
+
Per-instance settings — the `fullscreenLabels` option, `setFullscreenLabels()`,
|
|
257
|
+
and the `data-diagram-*` attributes above — override the bundle for one diagram.
|
|
258
|
+
Resolution is: explicit setting, then bundle, then the built-in English.
|
|
259
|
+
|
|
260
|
+
The context menu is rebuilt on every open and needs nothing after a language
|
|
261
|
+
change. The fullscreen button is drawn once, so call `refreshLabels()` to
|
|
262
|
+
re-read the bundle for it.
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
import type { DesignerI18n } from '@stocksharp/diagram';
|
|
266
|
+
|
|
267
|
+
const labels: DesignerI18n = { cut: 'Вырезать', ctxDelete: 'Удалить', /* … */ };
|
|
268
|
+
(window as unknown as { __designerI18n: DesignerI18n }).__designerI18n = labels;
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
The key is not always the command name, so here is the whole menu:
|
|
272
|
+
|
|
273
|
+
| command | key | English |
|
|
274
|
+
|---|---|---|
|
|
275
|
+
| `undo` / `redo` | `undo` / `redo` | Undo / Redo |
|
|
276
|
+
| `cut` / `copy` / `paste` | `cut` / `copy` / `paste` | Cut / Copy / Paste |
|
|
277
|
+
| `open` | `ctxOpen` | Open |
|
|
278
|
+
| `delete` | `ctxDelete` | Delete |
|
|
279
|
+
| *(the submenu)* | `ctxExportAs` | Export as |
|
|
280
|
+
| `exportDocument` | `ctxExportDocument` | Scheme |
|
|
281
|
+
| `exportPng` / `exportSvg` | `ctxExportPng` / `ctxExportSvg` | PNG image / SVG image |
|
|
282
|
+
| `overview` | `ctxOverview` | Overview |
|
|
283
|
+
| `properties` | `properties` | Properties |
|
|
284
|
+
| `help` | `ctxHelp` | Help |
|
|
285
|
+
| *(fullscreen button)* | `fullscreenEnter` / `fullscreenExit` | Enter / Exit fullscreen |
|
|
286
|
+
| *(embed failures)* | `embedErrorLoad` / `embedErrorEmpty` / `embedErrorDraw` | the three notes shown in place of a diagram |
|
|
287
|
+
| *(embed placeholder)* | `embedMissingElement` | the missing-element tooltip |
|
|
288
|
+
|
|
289
|
+
`ctxDelete` is separate from the solution explorer's `delete` on purpose: that
|
|
290
|
+
one removes a whole strategy, this one the selected nodes and links, and a
|
|
291
|
+
language that declines the object cannot serve both from one key. The same goes
|
|
292
|
+
for `ctxExportAs` against `ctxExport`.
|
|
293
|
+
|
|
191
294
|
Runtime failures flash the node border before leaving it red. Errors found
|
|
192
295
|
while loading a scheme use a red background. Hovering either state shows the
|
|
193
296
|
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
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
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 =
|
|
1220
|
+
this.ctx = surface;
|
|
1143
1221
|
this.width = width;
|
|
1144
1222
|
this.height = height;
|
|
1145
|
-
|
|
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
|
|
1247
|
+
return surface.toSvg();
|
|
1173
1248
|
}
|
|
1174
1249
|
positiveScreenshotNumber(value, name) {
|
|
1175
1250
|
if (!Number.isFinite(value) || value <= 0) {
|
|
@@ -1801,6 +1876,26 @@ export class Diagram {
|
|
|
1801
1876
|
return added.map((node) => node.id);
|
|
1802
1877
|
}
|
|
1803
1878
|
// ---- input ------------------------------------------------------
|
|
1879
|
+
// Ends hover and everything hanging off it: highlight, cursor and the delayed tooltip.
|
|
1880
|
+
clearHover() {
|
|
1881
|
+
if (this.hoverPort !== null)
|
|
1882
|
+
this.emit('portHover', { node: this.hoverPort.node, port: this.hoverPort.port, hovering: false });
|
|
1883
|
+
if (this.hoverNode !== null)
|
|
1884
|
+
this.emit('nodeHover', { node: this.hoverNode, hovering: false });
|
|
1885
|
+
if (this.hoveredLink !== null)
|
|
1886
|
+
this.emit('linkHover', { link: this.hoveredLink, hovering: false });
|
|
1887
|
+
this.hoverPort = null;
|
|
1888
|
+
this.hoverNode = null;
|
|
1889
|
+
this.hoveredLink = null;
|
|
1890
|
+
this.tipTarget = null;
|
|
1891
|
+
this.tipShow = false;
|
|
1892
|
+
if (this.tipTimer !== null) {
|
|
1893
|
+
clearTimeout(this.tipTimer);
|
|
1894
|
+
this.tipTimer = null;
|
|
1895
|
+
}
|
|
1896
|
+
this.canvas.style.cursor = 'default';
|
|
1897
|
+
this.scheduleDraw();
|
|
1898
|
+
}
|
|
1804
1899
|
cancelLongPress() {
|
|
1805
1900
|
if (this.lpTimer !== null) {
|
|
1806
1901
|
clearTimeout(this.lpTimer);
|
|
@@ -1839,7 +1934,10 @@ export class Diagram {
|
|
|
1839
1934
|
this.relinking = null;
|
|
1840
1935
|
this.relinkCandidate = null;
|
|
1841
1936
|
this.linkSnap = null;
|
|
1842
|
-
this
|
|
1937
|
+
// A menu is about to cover this spot, and it opens under a cursor that has not moved --
|
|
1938
|
+
// which fires no boundary event, so nothing else would end the hover. Without this the
|
|
1939
|
+
// tooltip armed by the last pointermove appears 400ms later, through the open menu.
|
|
1940
|
+
this.clearHover();
|
|
1843
1941
|
this.emit('contextMenu', { x: pageX, y: pageY, link, node, port });
|
|
1844
1942
|
}
|
|
1845
1943
|
listen(target, type, listener, options) {
|
|
@@ -2217,25 +2315,7 @@ export class Diagram {
|
|
|
2217
2315
|
this.emitViewChanged(true);
|
|
2218
2316
|
this.scheduleDraw();
|
|
2219
2317
|
}, { passive: false });
|
|
2220
|
-
this.listen(this.canvas, 'pointerleave', () =>
|
|
2221
|
-
if (this.hoverPort !== null)
|
|
2222
|
-
this.emit('portHover', { node: this.hoverPort.node, port: this.hoverPort.port, hovering: false });
|
|
2223
|
-
if (this.hoverNode !== null)
|
|
2224
|
-
this.emit('nodeHover', { node: this.hoverNode, hovering: false });
|
|
2225
|
-
if (this.hoveredLink !== null)
|
|
2226
|
-
this.emit('linkHover', { link: this.hoveredLink, hovering: false });
|
|
2227
|
-
this.hoverPort = null;
|
|
2228
|
-
this.hoverNode = null;
|
|
2229
|
-
this.hoveredLink = null;
|
|
2230
|
-
this.tipTarget = null;
|
|
2231
|
-
this.tipShow = false;
|
|
2232
|
-
if (this.tipTimer !== null) {
|
|
2233
|
-
clearTimeout(this.tipTimer);
|
|
2234
|
-
this.tipTimer = null;
|
|
2235
|
-
}
|
|
2236
|
-
this.canvas.style.cursor = 'default';
|
|
2237
|
-
this.scheduleDraw();
|
|
2238
|
-
});
|
|
2318
|
+
this.listen(this.canvas, 'pointerleave', () => this.clearHover());
|
|
2239
2319
|
this.listen(this.canvas, 'dblclick', (e) => {
|
|
2240
2320
|
const [sx, sy] = localXY(e);
|
|
2241
2321
|
const [wx, wy] = this.toWorld(sx, sy);
|