@stocksharp/diagram 1.2.1 → 1.2.3
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/dist/esm/canvas-renderer.js +105 -35
- package/dist/esm/canvas-renderer.js.map +1 -1
- package/dist/esm/diagram/context-menu.js +1 -1
- package/dist/esm/diagram/context-menu.js.map +1 -1
- package/dist/esm/diagram/palette.js +5 -0
- package/dist/esm/diagram/palette.js.map +1 -1
- package/dist/esm/diagram/stocksharp-diagram.js +10 -0
- package/dist/esm/diagram/stocksharp-diagram.js.map +1 -1
- package/dist/ssdiagram-headless.js +81 -28
- package/dist/ssdiagram-headless.js.map +2 -2
- package/dist/ssdiagram.js +93 -28
- package/dist/ssdiagram.js.map +2 -2
- package/dist/types/canvas-renderer.d.ts +5 -0
- package/dist/types/canvas-renderer.d.ts.map +1 -1
- package/dist/types/diagram/palette.d.ts.map +1 -1
- package/dist/types/diagram/stocksharp-diagram.d.ts +8 -0
- package/dist/types/diagram/stocksharp-diagram.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/canvas-renderer.ts +103 -32
- package/src/diagram/context-menu.ts +1 -1
- package/src/diagram/palette.ts +4 -0
- package/src/diagram/stocksharp-diagram.ts +11 -0
|
@@ -50,6 +50,12 @@ function isWildcardPortType(type) {
|
|
|
50
50
|
|| normalized === 'system.object'
|
|
51
51
|
|| normalized.startsWith('system.object,');
|
|
52
52
|
}
|
|
53
|
+
// The empty socket a grow-on-connect input spawns its siblings from. The scheme records it
|
|
54
|
+
// first, but it accepts nothing until something is wired into it, so it belongs under the
|
|
55
|
+
// sockets that do carry data.
|
|
56
|
+
function isGrowAnchor(port) {
|
|
57
|
+
return port.isDynamic && !port.isSibling;
|
|
58
|
+
}
|
|
53
59
|
function arePortTypesCompatible(fromType, toType, availableTypes) {
|
|
54
60
|
if (isWildcardPortType(fromType) || isWildcardPortType(toType))
|
|
55
61
|
return true;
|
|
@@ -262,6 +268,8 @@ export class Diagram {
|
|
|
262
268
|
this.hoverPort = null;
|
|
263
269
|
this.hoverNode = null;
|
|
264
270
|
this.hoveredLink = null;
|
|
271
|
+
// Type feeding each wired input, keyed by node and port, rebuilt for every frame.
|
|
272
|
+
this.portFeed = new Map();
|
|
265
273
|
this.tipTimer = null;
|
|
266
274
|
this.tipShow = false;
|
|
267
275
|
this.tipTarget = null;
|
|
@@ -315,8 +323,21 @@ export class Diagram {
|
|
|
315
323
|
throw new Error('ssdiagram: 2d context unavailable');
|
|
316
324
|
this.ctx = ctx;
|
|
317
325
|
this.resize(this.host.clientWidth || 800, this.host.clientHeight || 480);
|
|
326
|
+
this.observeHostSize();
|
|
318
327
|
this.bind();
|
|
319
328
|
}
|
|
329
|
+
/// The control creates the canvas, so it keeps it the size of the element it was mounted in.
|
|
330
|
+
/// A host that lays its panels out after construction - a docking shell, a tab that opens
|
|
331
|
+
/// hidden - measures nothing at that point, and the fallback size above would then outlive
|
|
332
|
+
/// the layout: a strip of the panel that draws nothing and answers no clicks. resize()
|
|
333
|
+
/// ignores anything smaller than two pixels, so a panel switched away from keeps its view.
|
|
334
|
+
observeHostSize() {
|
|
335
|
+
if (typeof ResizeObserver === 'undefined')
|
|
336
|
+
return;
|
|
337
|
+
const observer = new ResizeObserver(() => this.resize(this.host.clientWidth, this.host.clientHeight));
|
|
338
|
+
observer.observe(this.host);
|
|
339
|
+
this.domDisposables.push(() => observer.disconnect());
|
|
340
|
+
}
|
|
320
341
|
// ---- events -----------------------------------------------------
|
|
321
342
|
on(ev, h) {
|
|
322
343
|
let set = this.handlers.get(ev);
|
|
@@ -1628,8 +1649,11 @@ export class Diagram {
|
|
|
1628
1649
|
// node border).
|
|
1629
1650
|
const off = PORT_SQ / 2;
|
|
1630
1651
|
const place = (ports, x) => {
|
|
1631
|
-
const
|
|
1632
|
-
|
|
1652
|
+
const stack = ports.some(isGrowAnchor)
|
|
1653
|
+
? [...ports.filter((p) => !isGrowAnchor(p)), ...ports.filter(isGrowAnchor)]
|
|
1654
|
+
: ports;
|
|
1655
|
+
const startY = n.y + (n.h - stack.length * PORT_ROW_H) / 2 + PORT_ROW_H / 2;
|
|
1656
|
+
stack.forEach((p, i) => { p.cx = x; p.cy = startY + i * PORT_ROW_H; });
|
|
1633
1657
|
};
|
|
1634
1658
|
place(n.inPorts, n.x - off);
|
|
1635
1659
|
place(n.outPorts, n.x + n.w + off);
|
|
@@ -1642,6 +1666,29 @@ export class Diagram {
|
|
|
1642
1666
|
toWorld(sx, sy) {
|
|
1643
1667
|
return [(sx - this.offX) / this.scale, (sy - this.offY) / this.scale];
|
|
1644
1668
|
}
|
|
1669
|
+
/// Type of the output wired into each connected input. First link wins, which is the one
|
|
1670
|
+
/// drawn first, so a socket accepting several links agrees with the wire on top.
|
|
1671
|
+
feedTypes() {
|
|
1672
|
+
const feed = new Map();
|
|
1673
|
+
for (const l of this.links) {
|
|
1674
|
+
const from = this.findNode(l.from)?.outPorts.find((p) => p.id === l.fromPort);
|
|
1675
|
+
if (from === undefined || isWildcardPortType(from.type))
|
|
1676
|
+
continue;
|
|
1677
|
+
const to = this.findNode(l.to)?.inPorts.find((p) => p.id === l.toPort);
|
|
1678
|
+
if (to === undefined || feed.has(to))
|
|
1679
|
+
continue;
|
|
1680
|
+
feed.set(to, from.type);
|
|
1681
|
+
}
|
|
1682
|
+
return feed;
|
|
1683
|
+
}
|
|
1684
|
+
/// Colour type of a socket: its own, unless it is a wildcard input with something wired in.
|
|
1685
|
+
/// A wildcard promises to take whatever arrives, so once something has arrived the socket
|
|
1686
|
+
/// shows what that is. An untyped socket ('') promises nothing and keeps its neutral fill.
|
|
1687
|
+
portFillType(p) {
|
|
1688
|
+
if (p.direction !== 'in' || p.type === '' || !isWildcardPortType(p.type))
|
|
1689
|
+
return p.type;
|
|
1690
|
+
return this.portFeed.get(p) ?? p.type;
|
|
1691
|
+
}
|
|
1645
1692
|
portColor(type) {
|
|
1646
1693
|
const maxL = this.opts.linkMaxLightness;
|
|
1647
1694
|
const light = maxL !== undefined && maxL < 0.5;
|
|
@@ -2018,10 +2065,44 @@ export class Diagram {
|
|
|
2018
2065
|
}
|
|
2019
2066
|
this.lpStart = null;
|
|
2020
2067
|
}
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2068
|
+
/// Ends a node drag the way releasing the pointer does: the whole multi-node move becomes
|
|
2069
|
+
/// one undo step, and every node that actually moved is announced. Nodes are moved in place
|
|
2070
|
+
/// as the pointer travels, so a caller that clears the drag without coming through here
|
|
2071
|
+
/// leaves them where the drag put them with nothing in the history and the host unaware.
|
|
2072
|
+
commitNodeDrag() {
|
|
2073
|
+
if (this.dragNode === null)
|
|
2074
|
+
return;
|
|
2075
|
+
// Capture before/after positions so the whole multi-node
|
|
2076
|
+
// drag is ONE undo step. Skip the action if nothing
|
|
2077
|
+
// actually moved (a "click" that bypassed the threshold).
|
|
2078
|
+
const moves = this.dragStart
|
|
2079
|
+
.map((it) => ({ id: it.n.id, fromX: it.x, fromY: it.y, toX: it.n.x, toY: it.n.y }))
|
|
2080
|
+
.filter((m) => m.fromX !== m.toX || m.fromY !== m.toY);
|
|
2081
|
+
// Announce the same set the undo step records. Emitting for every node the drag
|
|
2082
|
+
// touched reported a move for a plain selection click -- one per selected node --
|
|
2083
|
+
// and a host doing dirty tracking marked the strategy modified for nothing.
|
|
2084
|
+
const moved = new Set(moves.map((m) => m.id));
|
|
2085
|
+
for (const it of this.dragStart) {
|
|
2086
|
+
if (moved.has(it.n.id))
|
|
2087
|
+
this.emit('nodeMoved', { node: it.n });
|
|
2088
|
+
}
|
|
2089
|
+
this.dragNode = null;
|
|
2090
|
+
this.dragStart = [];
|
|
2091
|
+
if (moves.length > 0) {
|
|
2092
|
+
this.record({
|
|
2093
|
+
do: () => { for (const m of moves)
|
|
2094
|
+
this.doMoveNode(m.id, m.toX, m.toY); },
|
|
2095
|
+
undo: () => { for (const m of moves)
|
|
2096
|
+
this.doMoveNode(m.id, m.fromX, m.fromY); },
|
|
2097
|
+
label: 'drag',
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
// Hit-test at screen coords and emit a contextMenu event. Ends whatever gesture was in
|
|
2102
|
+
// flight — once the menu opens we don't want a half-drag racing it — and a node drag ends
|
|
2103
|
+
// properly rather than being dropped, because its nodes have already moved.
|
|
2024
2104
|
fireContextMenu(sx, sy, pageX, pageY) {
|
|
2105
|
+
this.commitNodeDrag();
|
|
2025
2106
|
const [wx, wy] = this.toWorld(sx, sy);
|
|
2026
2107
|
const port = this.portAt(wx, wy);
|
|
2027
2108
|
const node = port?.node ?? this.nodeAt(wx, wy);
|
|
@@ -2044,11 +2125,17 @@ export class Diagram {
|
|
|
2044
2125
|
this.dragNode = null;
|
|
2045
2126
|
this.dragStart = [];
|
|
2046
2127
|
this.rubber = null;
|
|
2047
|
-
this.panning = false;
|
|
2048
2128
|
this.linking = null;
|
|
2049
2129
|
this.relinking = null;
|
|
2050
2130
|
this.relinkCandidate = null;
|
|
2051
2131
|
this.linkSnap = null;
|
|
2132
|
+
// A pan ends here too, and it ends settled: a host that persists the viewport on the
|
|
2133
|
+
// settled event would otherwise keep the position the pan started from.
|
|
2134
|
+
const viewportMoved = this.panning || this.ovDragging;
|
|
2135
|
+
this.panning = false;
|
|
2136
|
+
this.ovDragging = false;
|
|
2137
|
+
if (viewportMoved)
|
|
2138
|
+
this.emitViewChanged(false);
|
|
2052
2139
|
// A menu is about to cover this spot, and it opens under a cursor that has not moved --
|
|
2053
2140
|
// which fires no boundary event, so nothing else would end the hover. Without this the
|
|
2054
2141
|
// tooltip armed by the last pointermove appears 400ms later, through the open menu.
|
|
@@ -2349,33 +2436,7 @@ export class Diagram {
|
|
|
2349
2436
|
const finish = (e) => {
|
|
2350
2437
|
this.cancelLongPress();
|
|
2351
2438
|
this.relinkCandidate = null;
|
|
2352
|
-
|
|
2353
|
-
// Capture before/after positions so the whole multi-node
|
|
2354
|
-
// drag is ONE undo step. Skip the action if nothing
|
|
2355
|
-
// actually moved (a "click" that bypassed the threshold).
|
|
2356
|
-
const moves = this.dragStart
|
|
2357
|
-
.map((it) => ({ id: it.n.id, fromX: it.x, fromY: it.y, toX: it.n.x, toY: it.n.y }))
|
|
2358
|
-
.filter((m) => m.fromX !== m.toX || m.fromY !== m.toY);
|
|
2359
|
-
// Announce the same set the undo step records. Emitting for every node the drag
|
|
2360
|
-
// touched reported a move for a plain selection click -- one per selected node --
|
|
2361
|
-
// and a host doing dirty tracking marked the strategy modified for nothing.
|
|
2362
|
-
const moved = new Set(moves.map((m) => m.id));
|
|
2363
|
-
for (const it of this.dragStart) {
|
|
2364
|
-
if (moved.has(it.n.id))
|
|
2365
|
-
this.emit('nodeMoved', { node: it.n });
|
|
2366
|
-
}
|
|
2367
|
-
this.dragNode = null;
|
|
2368
|
-
this.dragStart = [];
|
|
2369
|
-
if (moves.length > 0) {
|
|
2370
|
-
this.record({
|
|
2371
|
-
do: () => { for (const m of moves)
|
|
2372
|
-
this.doMoveNode(m.id, m.toX, m.toY); },
|
|
2373
|
-
undo: () => { for (const m of moves)
|
|
2374
|
-
this.doMoveNode(m.id, m.fromX, m.fromY); },
|
|
2375
|
-
label: 'drag',
|
|
2376
|
-
});
|
|
2377
|
-
}
|
|
2378
|
-
}
|
|
2439
|
+
this.commitNodeDrag();
|
|
2379
2440
|
if (this.rubber !== null) {
|
|
2380
2441
|
const rx0 = Math.min(this.rubber.x0, this.rubber.x);
|
|
2381
2442
|
const ry0 = Math.min(this.rubber.y0, this.rubber.y);
|
|
@@ -2470,13 +2531,21 @@ export class Diagram {
|
|
|
2470
2531
|
this.zoomToFit();
|
|
2471
2532
|
});
|
|
2472
2533
|
// Desktop right-click → same contextMenu event as touch long-press.
|
|
2473
|
-
|
|
2534
|
+
const openContextMenu = (e) => {
|
|
2474
2535
|
e.preventDefault();
|
|
2475
2536
|
if (!this.permissions.inspect)
|
|
2476
2537
|
return;
|
|
2477
2538
|
const [sx, sy] = localXY(e);
|
|
2478
2539
|
this.cancelLongPress();
|
|
2479
2540
|
this.fireContextMenu(sx, sy, e.clientX, e.clientY);
|
|
2541
|
+
};
|
|
2542
|
+
this.listen(this.canvas, 'contextmenu', openContextMenu);
|
|
2543
|
+
// The control is more than its canvas: its own buttons sit over the corners, and a
|
|
2544
|
+
// right-click on one of those stops at the button, so the browser drew its own menu
|
|
2545
|
+
// there. The canvas keeps its own handler, and this one skips what that already took.
|
|
2546
|
+
this.listen(this.host, 'contextmenu', (e) => {
|
|
2547
|
+
if (e.target !== this.canvas)
|
|
2548
|
+
openContextMenu(e);
|
|
2480
2549
|
});
|
|
2481
2550
|
// Two-finger pinch → uniform zoom around the initial midpoint
|
|
2482
2551
|
// between the fingers. Mobile analog of mouse wheel.
|
|
@@ -2659,6 +2728,7 @@ export class Diagram {
|
|
|
2659
2728
|
}
|
|
2660
2729
|
if (options.transient && (this.linking !== null || this.relinking !== null))
|
|
2661
2730
|
this.drawPendingLink();
|
|
2731
|
+
this.portFeed = this.feedTypes();
|
|
2662
2732
|
for (const n of this.nodes)
|
|
2663
2733
|
this.drawNode(n, options.selection && this.selectedNodes.has(n), options);
|
|
2664
2734
|
if (options.selection && this.permissions.createLinks && this.relinking === null)
|
|
@@ -2960,7 +3030,7 @@ export class Diagram {
|
|
|
2960
3030
|
// Single rounded path filled + stroked (aligned, anti-aliased —
|
|
2961
3031
|
// no half-pixel double edge).
|
|
2962
3032
|
roundRect(ctx, x, y, s, s, r);
|
|
2963
|
-
ctx.fillStyle = this.portColor(p
|
|
3033
|
+
ctx.fillStyle = this.portColor(this.portFillType(p));
|
|
2964
3034
|
ctx.fill();
|
|
2965
3035
|
ctx.lineWidth = magnet ? 1.5 : 1;
|
|
2966
3036
|
ctx.strokeStyle = magnet ? '#ffffff' : 'rgba(12,12,16,0.55)';
|