html-overlay-node 0.1.9 → 0.1.11
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/example.json +9 -9
- package/dist/html-overlay-node.es.js +1000 -321
- package/dist/html-overlay-node.es.js.map +1 -1
- package/dist/html-overlay-node.umd.js +1 -1
- package/dist/html-overlay-node.umd.js.map +1 -1
- package/dist/img/favicon.svg +1 -0
- package/index.css +436 -232
- package/package.json +1 -1
- package/readme.md +143 -440
- package/src/core/Graph.js +34 -5
- package/src/core/Runner.js +188 -54
- package/src/index.js +29 -26
- package/src/interact/Controller.js +35 -6
- package/src/nodes/core.js +55 -77
- package/src/nodes/logic.js +51 -48
- package/src/nodes/math.js +23 -8
- package/src/nodes/util.js +238 -131
- package/src/nodes/value.js +87 -102
- package/src/render/CanvasRenderer.js +465 -285
- package/src/render/HtmlOverlay.js +65 -3
- package/src/render/hitTest.js +5 -2
- package/src/ui/HelpOverlay.js +158 -0
- package/src/ui/PropertyPanel.css +58 -27
- package/src/ui/PropertyPanel.js +441 -268
|
@@ -342,11 +342,12 @@ class Graph {
|
|
|
342
342
|
const available = Array.from(this.registry.types.keys()).join(", ") || "none";
|
|
343
343
|
throw new Error(`Unknown node type: "${type}". Available types: ${available}`);
|
|
344
344
|
}
|
|
345
|
+
const height = opts.height || ((_a = def.size) == null ? void 0 : _a.h) || this._calculateDefaultNodeHeight(def);
|
|
345
346
|
const node = new Node({
|
|
346
347
|
type,
|
|
347
348
|
title: def.title,
|
|
348
|
-
width: (
|
|
349
|
-
height
|
|
349
|
+
width: opts.width || ((_b = def.size) == null ? void 0 : _b.w) || 140,
|
|
350
|
+
height,
|
|
350
351
|
...opts
|
|
351
352
|
});
|
|
352
353
|
for (const i of def.inputs || []) node.addInput(i.name, i.datatype, i.portType || "data");
|
|
@@ -488,8 +489,12 @@ class Graph {
|
|
|
488
489
|
}
|
|
489
490
|
fromJSON(json) {
|
|
490
491
|
var _a, _b, _c;
|
|
491
|
-
this.clear();
|
|
492
|
+
this.nodes.clear();
|
|
493
|
+
this.edges.clear();
|
|
492
494
|
for (const nd of json.nodes) {
|
|
495
|
+
const def = (_b = (_a = this.registry) == null ? void 0 : _a.types) == null ? void 0 : _b.get(nd.type);
|
|
496
|
+
const minH = def ? this._calculateDefaultNodeHeight(def) : 60;
|
|
497
|
+
const height = nd.h !== void 0 ? nd.h : minH;
|
|
493
498
|
const node = new Node({
|
|
494
499
|
id: nd.id,
|
|
495
500
|
type: nd.type,
|
|
@@ -497,9 +502,8 @@ class Graph {
|
|
|
497
502
|
x: nd.x,
|
|
498
503
|
y: nd.y,
|
|
499
504
|
width: nd.w,
|
|
500
|
-
height
|
|
505
|
+
height
|
|
501
506
|
});
|
|
502
|
-
const def = (_b = (_a = this.registry) == null ? void 0 : _a.types) == null ? void 0 : _b.get(nd.type);
|
|
503
507
|
if (def == null ? void 0 : def.onCreate) {
|
|
504
508
|
def.onCreate(node);
|
|
505
509
|
}
|
|
@@ -525,6 +529,21 @@ class Graph {
|
|
|
525
529
|
(_c = this.hooks) == null ? void 0 : _c.emit("graph:deserialize", json);
|
|
526
530
|
return this;
|
|
527
531
|
}
|
|
532
|
+
_calculateDefaultNodeHeight(def) {
|
|
533
|
+
var _a, _b;
|
|
534
|
+
const inCount = ((_a = def.inputs) == null ? void 0 : _a.length) || 0;
|
|
535
|
+
const outCount = ((_b = def.outputs) == null ? void 0 : _b.length) || 0;
|
|
536
|
+
const maxPorts = Math.max(inCount, outCount);
|
|
537
|
+
const headerHeight = 26;
|
|
538
|
+
const portSpacing = 20;
|
|
539
|
+
if (def.html) {
|
|
540
|
+
const lastPortBottom = maxPorts > 0 ? 50 + (maxPorts - 1) * portSpacing : 26;
|
|
541
|
+
return Math.max(lastPortBottom + 50, 90);
|
|
542
|
+
}
|
|
543
|
+
const padding = 8;
|
|
544
|
+
let h = headerHeight + padding + maxPorts * portSpacing + padding;
|
|
545
|
+
return Math.max(h, 40);
|
|
546
|
+
}
|
|
528
547
|
}
|
|
529
548
|
function portRect(node, port, idx, dir) {
|
|
530
549
|
const {
|
|
@@ -538,8 +557,10 @@ function portRect(node, port, idx, dir) {
|
|
|
538
557
|
w: node.size.width,
|
|
539
558
|
h: node.size.height
|
|
540
559
|
};
|
|
541
|
-
const headerHeight =
|
|
542
|
-
const
|
|
560
|
+
const headerHeight = 26;
|
|
561
|
+
const padding = 8;
|
|
562
|
+
const portSpacing = 20;
|
|
563
|
+
const y = ny + headerHeight + padding + idx * portSpacing + portSpacing / 2;
|
|
543
564
|
const portWidth = 12;
|
|
544
565
|
const portHeight = 12;
|
|
545
566
|
if (dir === "in") {
|
|
@@ -562,32 +583,21 @@ const _CanvasRenderer = class _CanvasRenderer {
|
|
|
562
583
|
this.edgeStyle = edgeStyle;
|
|
563
584
|
this.theme = Object.assign(
|
|
564
585
|
{
|
|
565
|
-
bg: "#
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
title: "#1f1f24",
|
|
574
|
-
// Darker header
|
|
575
|
-
text: "#e4e4e7",
|
|
576
|
-
// Softer white
|
|
577
|
-
textMuted: "#a1a1aa",
|
|
578
|
-
// Muted text
|
|
579
|
-
port: "#6366f1",
|
|
580
|
-
// Indigo for data ports
|
|
586
|
+
bg: "#0e0e16",
|
|
587
|
+
grid: "#1c1c2c",
|
|
588
|
+
node: "rgba(22, 22, 34, 0.9)",
|
|
589
|
+
nodeBorder: "rgba(255, 255, 255, 0.08)",
|
|
590
|
+
title: "rgba(28, 28, 42, 0.95)",
|
|
591
|
+
text: "#f5f5f7",
|
|
592
|
+
textMuted: "#8e8eaf",
|
|
593
|
+
port: "#4f46e5",
|
|
581
594
|
portExec: "#10b981",
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
//
|
|
585
|
-
edgeActive: "#8b5cf6",
|
|
586
|
-
// Purple for active
|
|
595
|
+
edge: "rgba(255, 255, 255, 0.12)",
|
|
596
|
+
edgeActive: "#34c38f",
|
|
597
|
+
// green for active edge animation
|
|
587
598
|
accent: "#6366f1",
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
// Brighter accent
|
|
599
|
+
accentBright: "#818cf8",
|
|
600
|
+
accentGlow: "rgba(99, 102, 241, 0.25)"
|
|
591
601
|
},
|
|
592
602
|
theme
|
|
593
603
|
);
|
|
@@ -609,10 +619,6 @@ const _CanvasRenderer = class _CanvasRenderer {
|
|
|
609
619
|
this.offsetY = offsetY;
|
|
610
620
|
(_a = this._onTransformChange) == null ? void 0 : _a.call(this);
|
|
611
621
|
}
|
|
612
|
-
/**
|
|
613
|
-
* Set callback to be called when transform changes (zoom/pan)
|
|
614
|
-
* @param {Function} callback - Function to call on transform change
|
|
615
|
-
*/
|
|
616
622
|
setTransformChangeCallback(callback) {
|
|
617
623
|
this._onTransformChange = callback;
|
|
618
624
|
}
|
|
@@ -656,7 +662,7 @@ const _CanvasRenderer = class _CanvasRenderer {
|
|
|
656
662
|
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
657
663
|
}
|
|
658
664
|
// ── Drawing ────────────────────────────────────────────────────────────────
|
|
659
|
-
_drawArrowhead(x1, y1, x2, y2, size =
|
|
665
|
+
_drawArrowhead(x1, y1, x2, y2, size = 8) {
|
|
660
666
|
const { ctx } = this;
|
|
661
667
|
const s = size / this.scale;
|
|
662
668
|
const ang = Math.atan2(y2 - y1, x2 - x1);
|
|
@@ -667,21 +673,14 @@ const _CanvasRenderer = class _CanvasRenderer {
|
|
|
667
673
|
ctx.closePath();
|
|
668
674
|
ctx.fill();
|
|
669
675
|
}
|
|
670
|
-
_drawScreenText(text, lx, ly, {
|
|
671
|
-
fontPx = 12,
|
|
672
|
-
color = this.theme.text,
|
|
673
|
-
align = "left",
|
|
674
|
-
baseline = "alphabetic",
|
|
675
|
-
dpr = 1
|
|
676
|
-
// 추후 devicePixelRatio 도입
|
|
677
|
-
} = {}) {
|
|
676
|
+
_drawScreenText(text, lx, ly, { fontPx = 11, color = this.theme.text, align = "left", baseline = "alphabetic" } = {}) {
|
|
678
677
|
const { ctx } = this;
|
|
679
678
|
const { x: sx, y: sy } = this.worldToScreen(lx, ly);
|
|
680
679
|
ctx.save();
|
|
681
680
|
this._resetTransform();
|
|
682
681
|
const px = Math.round(sx) + 0.5;
|
|
683
682
|
const py = Math.round(sy) + 0.5;
|
|
684
|
-
ctx.font = `${fontPx * this.scale}px system-ui`;
|
|
683
|
+
ctx.font = `${fontPx * this.scale}px "Inter", system-ui, sans-serif`;
|
|
685
684
|
ctx.fillStyle = color;
|
|
686
685
|
ctx.textAlign = align;
|
|
687
686
|
ctx.textBaseline = baseline;
|
|
@@ -694,39 +693,51 @@ const _CanvasRenderer = class _CanvasRenderer {
|
|
|
694
693
|
ctx.fillStyle = theme.bg;
|
|
695
694
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
696
695
|
this._applyTransform();
|
|
697
|
-
ctx.strokeStyle = this._rgba(theme.grid, 0.35);
|
|
698
|
-
ctx.lineWidth = 1 / scale;
|
|
699
|
-
const base = 20;
|
|
700
|
-
const step = base;
|
|
701
696
|
const x0 = -offsetX / scale;
|
|
702
697
|
const y0 = -offsetY / scale;
|
|
703
698
|
const x1 = (canvas.width - offsetX) / scale;
|
|
704
699
|
const y1 = (canvas.height - offsetY) / scale;
|
|
705
|
-
const
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
700
|
+
const minorStep = 24;
|
|
701
|
+
const majorStep = 120;
|
|
702
|
+
const minorR = 1 / scale;
|
|
703
|
+
const majorR = 1.5 / scale;
|
|
704
|
+
const startX = Math.floor(x0 / minorStep) * minorStep;
|
|
705
|
+
const startY = Math.floor(y0 / minorStep) * minorStep;
|
|
706
|
+
ctx.fillStyle = this._rgba(theme.grid, 0.7);
|
|
707
|
+
for (let gx = startX; gx <= x1; gx += minorStep) {
|
|
708
|
+
for (let gy = startY; gy <= y1; gy += minorStep) {
|
|
709
|
+
const isMajorX = Math.round(gx / majorStep) * majorStep === Math.round(gx);
|
|
710
|
+
const isMajorY = Math.round(gy / majorStep) * majorStep === Math.round(gy);
|
|
711
|
+
if (isMajorX && isMajorY) continue;
|
|
712
|
+
ctx.beginPath();
|
|
713
|
+
ctx.arc(gx, gy, minorR, 0, Math.PI * 2);
|
|
714
|
+
ctx.fill();
|
|
715
|
+
}
|
|
711
716
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
717
|
+
const majorStartX = Math.floor(x0 / majorStep) * majorStep;
|
|
718
|
+
const majorStartY = Math.floor(y0 / majorStep) * majorStep;
|
|
719
|
+
ctx.fillStyle = this._rgba(theme.grid, 1);
|
|
720
|
+
for (let gx = majorStartX; gx <= x1; gx += majorStep) {
|
|
721
|
+
for (let gy = majorStartY; gy <= y1; gy += majorStep) {
|
|
722
|
+
ctx.beginPath();
|
|
723
|
+
ctx.arc(gx, gy, majorR, 0, Math.PI * 2);
|
|
724
|
+
ctx.fill();
|
|
725
|
+
}
|
|
715
726
|
}
|
|
716
|
-
ctx.stroke();
|
|
717
727
|
this._resetTransform();
|
|
718
728
|
}
|
|
719
729
|
draw(graph, {
|
|
720
730
|
selection = /* @__PURE__ */ new Set(),
|
|
721
731
|
tempEdge = null,
|
|
722
|
-
running = false,
|
|
723
732
|
time = performance.now(),
|
|
724
|
-
|
|
725
|
-
|
|
733
|
+
activeNodes = /* @__PURE__ */ new Set(),
|
|
734
|
+
// Now explicitly passing active nodes
|
|
726
735
|
activeEdges = /* @__PURE__ */ new Set(),
|
|
727
|
-
|
|
736
|
+
activeEdgeTimes = /* @__PURE__ */ new Map(),
|
|
737
|
+
drawEdges = true,
|
|
738
|
+
loopActiveEdges = false
|
|
728
739
|
} = {}) {
|
|
729
|
-
var _a, _b, _c, _d
|
|
740
|
+
var _a, _b, _c, _d;
|
|
730
741
|
graph.updateWorldTransforms();
|
|
731
742
|
this.drawGrid();
|
|
732
743
|
const { ctx, theme } = this;
|
|
@@ -741,40 +752,48 @@ const _CanvasRenderer = class _CanvasRenderer {
|
|
|
741
752
|
}
|
|
742
753
|
}
|
|
743
754
|
if (drawEdges) {
|
|
744
|
-
ctx.lineWidth =
|
|
745
|
-
let dashArray = null;
|
|
746
|
-
let dashOffset = 0;
|
|
747
|
-
if (running) {
|
|
748
|
-
const speed = 120;
|
|
749
|
-
const phase = time / 1e3 * speed / this.scale % _CanvasRenderer.FONT_SIZE;
|
|
750
|
-
dashArray = [6 / this.scale, 6 / this.scale];
|
|
751
|
-
dashOffset = -phase;
|
|
752
|
-
}
|
|
755
|
+
ctx.lineWidth = 2.5 / this.scale;
|
|
753
756
|
for (const e of graph.edges.values()) {
|
|
754
|
-
const shouldAnimate = activeEdges && activeEdges.size > 0 && activeEdges.has(e.id);
|
|
755
|
-
if (running && shouldAnimate && dashArray) {
|
|
756
|
-
ctx.setLineDash(dashArray);
|
|
757
|
-
ctx.lineDashOffset = dashOffset;
|
|
758
|
-
} else {
|
|
759
|
-
ctx.setLineDash([]);
|
|
760
|
-
ctx.lineDashOffset = 0;
|
|
761
|
-
}
|
|
762
757
|
const isActive = activeEdges && activeEdges.has(e.id);
|
|
763
758
|
if (isActive) {
|
|
764
|
-
ctx.
|
|
759
|
+
ctx.save();
|
|
760
|
+
ctx.shadowColor = this.theme.edgeActive;
|
|
761
|
+
ctx.shadowBlur = 8 / this.scale;
|
|
762
|
+
ctx.strokeStyle = this.theme.edgeActive;
|
|
765
763
|
ctx.lineWidth = 3 / this.scale;
|
|
764
|
+
ctx.setLineDash([]);
|
|
765
|
+
this._drawEdge(graph, e);
|
|
766
|
+
ctx.restore();
|
|
767
|
+
(activeEdgeTimes == null ? void 0 : activeEdgeTimes.get(e.id)) ?? time;
|
|
768
|
+
const flowSpeed = this.theme.flowSpeed || 150;
|
|
769
|
+
edgeLen / flowSpeed * 1e3;
|
|
770
|
+
const dotT = loopActiveEdges ? time / 1e3 * (flowSpeed / edgeLen) % 1 : time / 1e3 * 1.2 % 1;
|
|
771
|
+
const dotPos = this._getEdgeDotPosition(graph, e, dotT);
|
|
772
|
+
if (dotPos) {
|
|
773
|
+
ctx.save();
|
|
774
|
+
ctx.fillStyle = "#ffffff";
|
|
775
|
+
ctx.shadowColor = this.theme.edgeActive;
|
|
776
|
+
ctx.shadowBlur = 10 / this.scale;
|
|
777
|
+
ctx.beginPath();
|
|
778
|
+
ctx.arc(dotPos.x, dotPos.y, 3 / this.scale, 0, Math.PI * 2);
|
|
779
|
+
ctx.fill();
|
|
780
|
+
ctx.restore();
|
|
781
|
+
}
|
|
766
782
|
} else {
|
|
783
|
+
ctx.setLineDash([]);
|
|
767
784
|
ctx.strokeStyle = theme.edge;
|
|
768
|
-
ctx.lineWidth =
|
|
785
|
+
ctx.lineWidth = 2.5 / this.scale;
|
|
786
|
+
this._drawEdge(graph, e);
|
|
769
787
|
}
|
|
770
|
-
this._drawEdge(graph, e);
|
|
771
788
|
}
|
|
772
789
|
}
|
|
773
790
|
if (tempEdge) {
|
|
774
791
|
const a = this.screenToWorld(tempEdge.x1, tempEdge.y1);
|
|
775
792
|
const b = this.screenToWorld(tempEdge.x2, tempEdge.y2);
|
|
776
793
|
const prevDash = this.ctx.getLineDash();
|
|
777
|
-
this.ctx.setLineDash([
|
|
794
|
+
this.ctx.setLineDash([5 / this.scale, 5 / this.scale]);
|
|
795
|
+
this.ctx.strokeStyle = this._rgba(this.theme.accentBright, 0.7);
|
|
796
|
+
this.ctx.lineWidth = 2.5 / this.scale;
|
|
778
797
|
let ptsForArrow = null;
|
|
779
798
|
if (this.edgeStyle === "line") {
|
|
780
799
|
this._drawLine(a.x, a.y, b.x, b.y);
|
|
@@ -795,9 +814,8 @@ const _CanvasRenderer = class _CanvasRenderer {
|
|
|
795
814
|
if (ptsForArrow && ptsForArrow.length >= 2) {
|
|
796
815
|
const p1 = ptsForArrow[ptsForArrow.length - 2];
|
|
797
816
|
const p2 = ptsForArrow[ptsForArrow.length - 1];
|
|
798
|
-
this.ctx.fillStyle = this.theme.
|
|
799
|
-
this.
|
|
800
|
-
this._drawArrowhead(p1.x, p1.y, p2.x, p2.y, 12);
|
|
817
|
+
this.ctx.fillStyle = this.theme.accentBright;
|
|
818
|
+
this._drawArrowhead(p1.x, p1.y, p2.x, p2.y, 10);
|
|
801
819
|
}
|
|
802
820
|
}
|
|
803
821
|
for (const n of graph.nodes.values()) {
|
|
@@ -805,21 +823,21 @@ const _CanvasRenderer = class _CanvasRenderer {
|
|
|
805
823
|
const sel = selection.has(n.id);
|
|
806
824
|
const def = (_d = (_c = this.registry) == null ? void 0 : _c.types) == null ? void 0 : _d.get(n.type);
|
|
807
825
|
const hasHtmlOverlay = !!(def == null ? void 0 : def.html);
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
826
|
+
this._drawNode(n, sel, !hasHtmlOverlay ? true : false);
|
|
827
|
+
if (def == null ? void 0 : def.onDraw) {
|
|
828
|
+
def.onDraw(n, { ctx, theme, renderer: this });
|
|
811
829
|
}
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
for (const n of graph.nodes.values()) {
|
|
815
|
-
if (n.type !== "core/Group") {
|
|
816
|
-
const def = (_f = (_e = this.registry) == null ? void 0 : _e.types) == null ? void 0 : _f.get(n.type);
|
|
817
|
-
const hasHtmlOverlay = !!(def == null ? void 0 : def.html);
|
|
818
830
|
if (hasHtmlOverlay) {
|
|
819
831
|
this._drawPorts(n);
|
|
820
832
|
}
|
|
821
833
|
}
|
|
822
834
|
}
|
|
835
|
+
if (activeNodes.size > 0) {
|
|
836
|
+
for (const nodeId of activeNodes) {
|
|
837
|
+
const node = graph.nodes.get(nodeId);
|
|
838
|
+
if (node) this._drawActiveNodeBorder(node, time);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
823
841
|
this._resetTransform();
|
|
824
842
|
}
|
|
825
843
|
_rgba(hex, a) {
|
|
@@ -832,40 +850,58 @@ const _CanvasRenderer = class _CanvasRenderer {
|
|
|
832
850
|
return `rgba(${r},${g},${b},${a})`;
|
|
833
851
|
}
|
|
834
852
|
_drawNode(node, selected, skipPorts = false) {
|
|
853
|
+
var _a, _b;
|
|
835
854
|
const { ctx, theme } = this;
|
|
836
|
-
const r =
|
|
855
|
+
const r = 2;
|
|
837
856
|
const { x, y, w, h } = node.computed;
|
|
838
|
-
|
|
857
|
+
const headerH = 26;
|
|
858
|
+
const typeDef = (_b = (_a = this.registry) == null ? void 0 : _a.types) == null ? void 0 : _b.get(node.type);
|
|
859
|
+
const categoryColor = node.color || (typeDef == null ? void 0 : typeDef.color) || theme.accent;
|
|
860
|
+
if (selected) {
|
|
839
861
|
ctx.save();
|
|
840
|
-
ctx.shadowColor = "rgba(
|
|
862
|
+
ctx.shadowColor = "rgba(255,255,255,0.3)";
|
|
841
863
|
ctx.shadowBlur = 8 / this.scale;
|
|
842
|
-
ctx.
|
|
843
|
-
ctx.
|
|
844
|
-
|
|
845
|
-
ctx
|
|
864
|
+
ctx.strokeStyle = "#ffffff";
|
|
865
|
+
ctx.lineWidth = 1.5 / this.scale;
|
|
866
|
+
const pad = 8 / this.scale;
|
|
867
|
+
roundRect(ctx, x - pad, y - pad, w + pad * 2, h + pad * 2, r + pad);
|
|
868
|
+
ctx.stroke();
|
|
846
869
|
ctx.restore();
|
|
847
870
|
}
|
|
871
|
+
ctx.save();
|
|
872
|
+
ctx.shadowColor = "rgba(0,0,0,0.7)";
|
|
873
|
+
ctx.shadowBlur = 20 / this.scale;
|
|
874
|
+
ctx.shadowOffsetY = 6 / this.scale;
|
|
848
875
|
ctx.fillStyle = theme.node;
|
|
849
|
-
ctx
|
|
850
|
-
ctx.
|
|
876
|
+
roundRect(ctx, x, y, w, h, r);
|
|
877
|
+
ctx.fill();
|
|
878
|
+
ctx.restore();
|
|
879
|
+
ctx.fillStyle = theme.node;
|
|
880
|
+
ctx.strokeStyle = selected ? "rgba(255,255,255,0.4)" : theme.nodeBorder;
|
|
881
|
+
ctx.lineWidth = 1 / this.scale;
|
|
851
882
|
roundRect(ctx, x, y, w, h, r);
|
|
852
883
|
ctx.fill();
|
|
853
884
|
ctx.stroke();
|
|
854
885
|
ctx.fillStyle = theme.title;
|
|
855
|
-
roundRect(ctx, x, y, w,
|
|
886
|
+
roundRect(ctx, x, y, w, headerH, { tl: r, tr: r, br: 0, bl: 0 });
|
|
856
887
|
ctx.fill();
|
|
857
|
-
ctx.
|
|
858
|
-
ctx.
|
|
888
|
+
ctx.save();
|
|
889
|
+
ctx.globalCompositeOperation = "source-atop";
|
|
890
|
+
ctx.fillStyle = categoryColor;
|
|
891
|
+
ctx.globalAlpha = 0.25;
|
|
892
|
+
ctx.fillRect(x, y, w, headerH);
|
|
893
|
+
ctx.restore();
|
|
894
|
+
ctx.strokeStyle = selected ? "rgba(255,255,255,0.2)" : this._rgba(theme.nodeBorder, 0.6);
|
|
895
|
+
ctx.lineWidth = 1 / this.scale;
|
|
859
896
|
ctx.beginPath();
|
|
860
|
-
ctx.moveTo(x +
|
|
861
|
-
ctx.lineTo(x + w
|
|
862
|
-
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
|
|
863
|
-
ctx.lineTo(x + w, y + 24);
|
|
864
|
-
ctx.moveTo(x, y + 24);
|
|
865
|
-
ctx.lineTo(x, y + r);
|
|
866
|
-
ctx.quadraticCurveTo(x, y, x + r, y);
|
|
897
|
+
ctx.moveTo(x, y + headerH);
|
|
898
|
+
ctx.lineTo(x + w, y + headerH);
|
|
867
899
|
ctx.stroke();
|
|
868
|
-
|
|
900
|
+
ctx.fillStyle = categoryColor;
|
|
901
|
+
ctx.beginPath();
|
|
902
|
+
ctx.roundRect(x, y, w, 2.5 / this.scale, { tl: r, tr: r, br: 0, bl: 0 });
|
|
903
|
+
ctx.fill();
|
|
904
|
+
this._drawScreenText(node.title, x + 10, y + headerH / 2, {
|
|
869
905
|
fontPx: _CanvasRenderer.FONT_SIZE,
|
|
870
906
|
color: theme.text,
|
|
871
907
|
baseline: "middle",
|
|
@@ -876,97 +912,165 @@ const _CanvasRenderer = class _CanvasRenderer {
|
|
|
876
912
|
const rct = portRect(node, p, i, "in");
|
|
877
913
|
const cx = rct.x + rct.w / 2;
|
|
878
914
|
const cy = rct.y + rct.h / 2;
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
ctx.stroke();
|
|
888
|
-
} else {
|
|
889
|
-
ctx.fillStyle = theme.port;
|
|
890
|
-
ctx.strokeStyle = "rgba(99, 102, 241, 0.3)";
|
|
891
|
-
ctx.lineWidth = 2 / this.scale;
|
|
892
|
-
ctx.beginPath();
|
|
893
|
-
ctx.arc(cx, cy, 5, 0, Math.PI * 2);
|
|
894
|
-
ctx.fill();
|
|
895
|
-
ctx.stroke();
|
|
915
|
+
this._drawPortShape(cx, cy, p.portType);
|
|
916
|
+
if (p.name) {
|
|
917
|
+
this._drawScreenText(p.name, cx + 10, cy, {
|
|
918
|
+
fontPx: 10,
|
|
919
|
+
color: theme.textMuted,
|
|
920
|
+
baseline: "middle",
|
|
921
|
+
align: "left"
|
|
922
|
+
});
|
|
896
923
|
}
|
|
897
924
|
});
|
|
898
925
|
node.outputs.forEach((p, i) => {
|
|
899
926
|
const rct = portRect(node, p, i, "out");
|
|
900
927
|
const cx = rct.x + rct.w / 2;
|
|
901
928
|
const cy = rct.y + rct.h / 2;
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
ctx.stroke();
|
|
911
|
-
} else {
|
|
912
|
-
ctx.fillStyle = theme.port;
|
|
913
|
-
ctx.strokeStyle = "rgba(99, 102, 241, 0.3)";
|
|
914
|
-
ctx.lineWidth = 2 / this.scale;
|
|
915
|
-
ctx.beginPath();
|
|
916
|
-
ctx.arc(cx, cy, 5, 0, Math.PI * 2);
|
|
917
|
-
ctx.fill();
|
|
918
|
-
ctx.stroke();
|
|
929
|
+
this._drawPortShape(cx, cy, p.portType);
|
|
930
|
+
if (p.name) {
|
|
931
|
+
this._drawScreenText(p.name, cx - 10, cy, {
|
|
932
|
+
fontPx: 10,
|
|
933
|
+
color: theme.textMuted,
|
|
934
|
+
baseline: "middle",
|
|
935
|
+
align: "right"
|
|
936
|
+
});
|
|
919
937
|
}
|
|
920
938
|
});
|
|
921
939
|
}
|
|
922
|
-
|
|
940
|
+
_drawActiveNodeBorder(node, time) {
|
|
941
|
+
const { ctx, theme } = this;
|
|
942
|
+
const { x, y, w, h } = node.computed;
|
|
943
|
+
const r = node.radius || 12;
|
|
944
|
+
const speed = 30;
|
|
945
|
+
const dashLen = 6;
|
|
946
|
+
const gapLen = 6;
|
|
947
|
+
ctx.save();
|
|
948
|
+
ctx.shadowColor = theme.accentBright || "#7080d8";
|
|
949
|
+
ctx.shadowBlur = (12 + Math.sin(time / 200) * 4) / this.scale;
|
|
950
|
+
ctx.strokeStyle = theme.accentBright || "#7080d8";
|
|
951
|
+
ctx.lineWidth = 3 / this.scale;
|
|
952
|
+
ctx.setLineDash([dashLen / this.scale, gapLen / this.scale]);
|
|
953
|
+
ctx.lineDashOffset = -(time / 1e3) * speed / this.scale;
|
|
954
|
+
ctx.beginPath();
|
|
955
|
+
const pad = 2 / this.scale;
|
|
956
|
+
this._roundRect(ctx, x - pad, y - pad, w + pad * 2, h + pad * 2, r + pad);
|
|
957
|
+
ctx.stroke();
|
|
958
|
+
ctx.restore();
|
|
959
|
+
}
|
|
960
|
+
// Internal helper for rounded rectangles if not using the browser's native one
|
|
961
|
+
_roundRect(ctx, x, y, w, h, r) {
|
|
962
|
+
if (typeof r === "number") {
|
|
963
|
+
r = { tl: r, tr: r, br: r, bl: r };
|
|
964
|
+
}
|
|
965
|
+
ctx.beginPath();
|
|
966
|
+
ctx.moveTo(x + r.tl, y);
|
|
967
|
+
ctx.lineTo(x + w - r.tr, y);
|
|
968
|
+
ctx.quadraticCurveTo(x + w, y, x + w, y + r.tr);
|
|
969
|
+
ctx.lineTo(x + w, y + h - r.br);
|
|
970
|
+
ctx.quadraticCurveTo(x + w, y + h, x + w - r.br, y + h);
|
|
971
|
+
ctx.lineTo(x + r.bl, y + h);
|
|
972
|
+
ctx.quadraticCurveTo(x, y + h, x, y + h - r.bl);
|
|
973
|
+
ctx.lineTo(x, y + r.tl);
|
|
974
|
+
ctx.quadraticCurveTo(x, y, x + r.tl, y);
|
|
975
|
+
ctx.closePath();
|
|
976
|
+
}
|
|
977
|
+
_drawPortShape(cx, cy, portType) {
|
|
923
978
|
const { ctx, theme } = this;
|
|
979
|
+
if (portType === "exec") {
|
|
980
|
+
const s = 5 / this.scale;
|
|
981
|
+
ctx.save();
|
|
982
|
+
ctx.fillStyle = theme.portExec;
|
|
983
|
+
ctx.strokeStyle = this._rgba(theme.portExec, 0.4);
|
|
984
|
+
ctx.lineWidth = 2 / this.scale;
|
|
985
|
+
ctx.beginPath();
|
|
986
|
+
ctx.moveTo(cx, cy - s);
|
|
987
|
+
ctx.lineTo(cx + s, cy);
|
|
988
|
+
ctx.lineTo(cx, cy + s);
|
|
989
|
+
ctx.lineTo(cx - s, cy);
|
|
990
|
+
ctx.closePath();
|
|
991
|
+
ctx.fill();
|
|
992
|
+
ctx.stroke();
|
|
993
|
+
ctx.restore();
|
|
994
|
+
} else {
|
|
995
|
+
ctx.save();
|
|
996
|
+
ctx.strokeStyle = this._rgba(theme.port, 0.35);
|
|
997
|
+
ctx.lineWidth = 3 / this.scale;
|
|
998
|
+
ctx.beginPath();
|
|
999
|
+
ctx.arc(cx, cy, 5 / this.scale, 0, Math.PI * 2);
|
|
1000
|
+
ctx.stroke();
|
|
1001
|
+
ctx.fillStyle = theme.port;
|
|
1002
|
+
ctx.beginPath();
|
|
1003
|
+
ctx.arc(cx, cy, 3.5 / this.scale, 0, Math.PI * 2);
|
|
1004
|
+
ctx.fill();
|
|
1005
|
+
ctx.restore();
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
_drawPorts(node) {
|
|
924
1009
|
node.inputs.forEach((p, i) => {
|
|
925
1010
|
const rct = portRect(node, p, i, "in");
|
|
926
1011
|
const cx = rct.x + rct.w / 2;
|
|
927
1012
|
const cy = rct.y + rct.h / 2;
|
|
928
|
-
|
|
929
|
-
const portSize = 8;
|
|
930
|
-
ctx.fillStyle = theme.portExec;
|
|
931
|
-
ctx.strokeStyle = "rgba(16, 185, 129, 0.3)";
|
|
932
|
-
ctx.lineWidth = 2 / this.scale;
|
|
933
|
-
ctx.beginPath();
|
|
934
|
-
ctx.roundRect(cx - portSize / 2, cy - portSize / 2, portSize, portSize, 2);
|
|
935
|
-
ctx.fill();
|
|
936
|
-
ctx.stroke();
|
|
937
|
-
} else {
|
|
938
|
-
ctx.fillStyle = theme.port;
|
|
939
|
-
ctx.strokeStyle = "rgba(99, 102, 241, 0.3)";
|
|
940
|
-
ctx.lineWidth = 2 / this.scale;
|
|
941
|
-
ctx.beginPath();
|
|
942
|
-
ctx.arc(cx, cy, 5, 0, Math.PI * 2);
|
|
943
|
-
ctx.fill();
|
|
944
|
-
}
|
|
1013
|
+
this._drawPortShape(cx, cy, p.portType);
|
|
945
1014
|
});
|
|
946
1015
|
node.outputs.forEach((p, i) => {
|
|
947
1016
|
const rct = portRect(node, p, i, "out");
|
|
948
1017
|
const cx = rct.x + rct.w / 2;
|
|
949
1018
|
const cy = rct.y + rct.h / 2;
|
|
950
|
-
|
|
951
|
-
const portSize = 8;
|
|
952
|
-
ctx.fillStyle = theme.portExec;
|
|
953
|
-
ctx.strokeStyle = "rgba(16, 185, 129, 0.3)";
|
|
954
|
-
ctx.lineWidth = 2 / this.scale;
|
|
955
|
-
ctx.beginPath();
|
|
956
|
-
ctx.roundRect(cx - portSize / 2, cy - portSize / 2, portSize, portSize, 2);
|
|
957
|
-
ctx.fill();
|
|
958
|
-
ctx.stroke();
|
|
959
|
-
} else {
|
|
960
|
-
ctx.fillStyle = theme.port;
|
|
961
|
-
ctx.strokeStyle = "rgba(99, 102, 241, 0.3)";
|
|
962
|
-
ctx.lineWidth = 2 / this.scale;
|
|
963
|
-
ctx.beginPath();
|
|
964
|
-
ctx.arc(cx, cy, 5, 0, Math.PI * 2);
|
|
965
|
-
ctx.fill();
|
|
966
|
-
ctx.stroke();
|
|
967
|
-
}
|
|
1019
|
+
this._drawPortShape(cx, cy, p.portType);
|
|
968
1020
|
});
|
|
969
1021
|
}
|
|
1022
|
+
/** Selection border for HTML overlay nodes, drawn on the edge canvas */
|
|
1023
|
+
_drawHtmlSelectionBorder(node) {
|
|
1024
|
+
const { ctx } = this;
|
|
1025
|
+
const { x, y, w, h } = node.computed;
|
|
1026
|
+
const r = 2;
|
|
1027
|
+
const pad = 2.5 / this.scale;
|
|
1028
|
+
ctx.save();
|
|
1029
|
+
ctx.shadowColor = "rgba(255,255,255,0.3)";
|
|
1030
|
+
ctx.shadowBlur = 8 / this.scale;
|
|
1031
|
+
ctx.strokeStyle = "#ffffff";
|
|
1032
|
+
ctx.lineWidth = 1.5 / this.scale;
|
|
1033
|
+
roundRect(ctx, x - pad, y - pad, w + pad * 2, h + pad * 2, r);
|
|
1034
|
+
ctx.stroke();
|
|
1035
|
+
ctx.restore();
|
|
1036
|
+
}
|
|
1037
|
+
/** Rotating dashed border drawn on the edge canvas for executing nodes */
|
|
1038
|
+
_drawActiveNodeBorder(node, time) {
|
|
1039
|
+
const { ctx } = this;
|
|
1040
|
+
const { x, y, w, h } = node.computed;
|
|
1041
|
+
const r = 2;
|
|
1042
|
+
const pad = 8 / this.scale;
|
|
1043
|
+
const dashLen = 8 / this.scale;
|
|
1044
|
+
const gapLen = 6 / this.scale;
|
|
1045
|
+
const offset = -(time / 1e3) * (50 / this.scale);
|
|
1046
|
+
ctx.save();
|
|
1047
|
+
ctx.setLineDash([dashLen, gapLen]);
|
|
1048
|
+
ctx.lineDashOffset = offset;
|
|
1049
|
+
ctx.strokeStyle = "rgba(74,176,217,0.9)";
|
|
1050
|
+
ctx.lineWidth = 2 / this.scale;
|
|
1051
|
+
ctx.shadowColor = "#4ab0d9";
|
|
1052
|
+
ctx.shadowBlur = 6 / this.scale;
|
|
1053
|
+
roundRect(ctx, x - pad, y - pad, w + pad * 2, h + pad * 2, r + pad);
|
|
1054
|
+
ctx.stroke();
|
|
1055
|
+
ctx.restore();
|
|
1056
|
+
}
|
|
1057
|
+
/** Approximate arc length of an edge in world coordinates */
|
|
1058
|
+
_getEdgeLength(graph, e) {
|
|
1059
|
+
const from = graph.nodes.get(e.fromNode);
|
|
1060
|
+
const to = graph.nodes.get(e.toNode);
|
|
1061
|
+
if (!from || !to) return 200;
|
|
1062
|
+
const iOut = from.outputs.findIndex((p) => p.id === e.fromPort);
|
|
1063
|
+
const iIn = to.inputs.findIndex((p) => p.id === e.toPort);
|
|
1064
|
+
const pr1 = portRect(from, null, iOut, "out");
|
|
1065
|
+
const pr2 = portRect(to, null, iIn, "in");
|
|
1066
|
+
const x1 = pr1.x + pr1.w / 2, y1 = pr1.y + pr1.h / 2;
|
|
1067
|
+
const x2 = pr2.x + pr2.w / 2, y2 = pr2.y + pr2.h / 2;
|
|
1068
|
+
if (this.edgeStyle === "orthogonal") {
|
|
1069
|
+
return Math.abs(x2 - x1) + Math.abs(y2 - y1);
|
|
1070
|
+
}
|
|
1071
|
+
const chord = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
|
|
1072
|
+
return this.edgeStyle === "bezier" ? chord * 1.4 : chord;
|
|
1073
|
+
}
|
|
970
1074
|
_drawEdge(graph, e) {
|
|
971
1075
|
const from = graph.nodes.get(e.fromNode);
|
|
972
1076
|
const to = graph.nodes.get(e.toNode);
|
|
@@ -975,7 +1079,8 @@ const _CanvasRenderer = class _CanvasRenderer {
|
|
|
975
1079
|
const iIn = to.inputs.findIndex((p) => p.id === e.toPort);
|
|
976
1080
|
const pr1 = portRect(from, null, iOut, "out");
|
|
977
1081
|
const pr2 = portRect(to, null, iIn, "in");
|
|
978
|
-
const x1 = pr1.x + pr1.w / 2, y1 = pr1.y + pr1.h / 2
|
|
1082
|
+
const x1 = pr1.x + pr1.w / 2, y1 = pr1.y + pr1.h / 2;
|
|
1083
|
+
const x2 = pr2.x + pr2.w / 2, y2 = pr2.y + pr2.h / 2;
|
|
979
1084
|
if (this.edgeStyle === "line") {
|
|
980
1085
|
this._drawLine(x1, y1, x2, y2);
|
|
981
1086
|
} else if (this.edgeStyle === "orthogonal") {
|
|
@@ -984,6 +1089,32 @@ const _CanvasRenderer = class _CanvasRenderer {
|
|
|
984
1089
|
this._drawCurve(x1, y1, x2, y2);
|
|
985
1090
|
}
|
|
986
1091
|
}
|
|
1092
|
+
_getEdgeDotPosition(graph, e, t) {
|
|
1093
|
+
const from = graph.nodes.get(e.fromNode);
|
|
1094
|
+
const to = graph.nodes.get(e.toNode);
|
|
1095
|
+
if (!from || !to) return null;
|
|
1096
|
+
const iOut = from.outputs.findIndex((p) => p.id === e.fromPort);
|
|
1097
|
+
const iIn = to.inputs.findIndex((p) => p.id === e.toPort);
|
|
1098
|
+
const pr1 = portRect(from, null, iOut, "out");
|
|
1099
|
+
const pr2 = portRect(to, null, iIn, "in");
|
|
1100
|
+
const x1 = pr1.x + pr1.w / 2, y1 = pr1.y + pr1.h / 2;
|
|
1101
|
+
const x2 = pr2.x + pr2.w / 2, y2 = pr2.y + pr2.h / 2;
|
|
1102
|
+
if (this.edgeStyle === "bezier") {
|
|
1103
|
+
const dx = Math.max(40, Math.abs(x2 - x1) * 0.4);
|
|
1104
|
+
return cubicBezierPoint(x1, y1, x1 + dx, y1, x2 - dx, y2, x2, y2, t);
|
|
1105
|
+
} else if (this.edgeStyle === "orthogonal") {
|
|
1106
|
+
const midX = (x1 + x2) / 2;
|
|
1107
|
+
const pts = [
|
|
1108
|
+
{ x: x1, y: y1 },
|
|
1109
|
+
{ x: midX, y: y1 },
|
|
1110
|
+
{ x: midX, y: y2 },
|
|
1111
|
+
{ x: x2, y: y2 }
|
|
1112
|
+
];
|
|
1113
|
+
return polylinePoint(pts, t);
|
|
1114
|
+
} else {
|
|
1115
|
+
return { x: x1 + (x2 - x1) * t, y: y1 + (y2 - y1) * t };
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
987
1118
|
_drawLine(x1, y1, x2, y2) {
|
|
988
1119
|
const { ctx } = this;
|
|
989
1120
|
ctx.beginPath();
|
|
@@ -1000,15 +1131,12 @@ const _CanvasRenderer = class _CanvasRenderer {
|
|
|
1000
1131
|
}
|
|
1001
1132
|
_drawOrthogonal(x1, y1, x2, y2) {
|
|
1002
1133
|
const midX = (x1 + x2) / 2;
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
{ x: x2, y: y2 }
|
|
1010
|
-
];
|
|
1011
|
-
}
|
|
1134
|
+
const pts = [
|
|
1135
|
+
{ x: x1, y: y1 },
|
|
1136
|
+
{ x: midX, y: y1 },
|
|
1137
|
+
{ x: midX, y: y2 },
|
|
1138
|
+
{ x: x2, y: y2 }
|
|
1139
|
+
];
|
|
1012
1140
|
const { ctx } = this;
|
|
1013
1141
|
const prevJoin = ctx.lineJoin, prevCap = ctx.lineCap;
|
|
1014
1142
|
ctx.lineJoin = "round";
|
|
@@ -1026,45 +1154,74 @@ const _CanvasRenderer = class _CanvasRenderer {
|
|
|
1026
1154
|
ctx.bezierCurveTo(x1 + dx, y1, x2 - dx, y2, x2, y2);
|
|
1027
1155
|
ctx.stroke();
|
|
1028
1156
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1157
|
+
drawEdgesOnly(graph, {
|
|
1158
|
+
activeEdges = /* @__PURE__ */ new Set(),
|
|
1159
|
+
activeEdgeTimes = /* @__PURE__ */ new Map(),
|
|
1160
|
+
activeNodes = /* @__PURE__ */ new Set(),
|
|
1161
|
+
selection = /* @__PURE__ */ new Set(),
|
|
1162
|
+
time = performance.now(),
|
|
1163
|
+
tempEdge = null,
|
|
1164
|
+
loopActiveEdges = false
|
|
1165
|
+
} = {}) {
|
|
1166
|
+
var _a, _b;
|
|
1035
1167
|
this._resetTransform();
|
|
1036
1168
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
1037
1169
|
this._applyTransform();
|
|
1038
1170
|
const { ctx, theme } = this;
|
|
1039
|
-
let dashArray = null;
|
|
1040
|
-
let dashOffset = 0;
|
|
1041
|
-
if (running || activeEdges.size > 0) {
|
|
1042
|
-
const speed = 120;
|
|
1043
|
-
const phase = time / 1e3 * speed / this.scale % 12;
|
|
1044
|
-
dashArray = [6 / this.scale, 6 / this.scale];
|
|
1045
|
-
dashOffset = -phase;
|
|
1046
|
-
}
|
|
1047
|
-
ctx.lineWidth = 1.5 / this.scale;
|
|
1048
|
-
ctx.strokeStyle = theme.edge;
|
|
1049
1171
|
for (const e of graph.edges.values()) {
|
|
1050
|
-
const isActive = activeEdges
|
|
1051
|
-
if (isActive
|
|
1052
|
-
ctx.
|
|
1053
|
-
ctx.
|
|
1054
|
-
ctx.
|
|
1172
|
+
const isActive = activeEdges.has(e.id);
|
|
1173
|
+
if (isActive) {
|
|
1174
|
+
ctx.save();
|
|
1175
|
+
ctx.shadowColor = theme.edgeActive;
|
|
1176
|
+
ctx.shadowBlur = 6 / this.scale;
|
|
1177
|
+
ctx.strokeStyle = theme.edgeActive;
|
|
1055
1178
|
ctx.lineWidth = 3 / this.scale;
|
|
1179
|
+
ctx.setLineDash([]);
|
|
1180
|
+
this._drawEdge(graph, e);
|
|
1181
|
+
ctx.restore();
|
|
1182
|
+
const flowSpeed = this.theme.flowSpeed || 150;
|
|
1183
|
+
const activationTime = activeEdgeTimes.get(e.id) ?? time;
|
|
1184
|
+
const edgeLen2 = Math.max(50, this._getEdgeLength(graph, e));
|
|
1185
|
+
const duration = edgeLen2 / flowSpeed * 1e3;
|
|
1186
|
+
const rawT = (time - activationTime) / duration;
|
|
1187
|
+
const dotT = loopActiveEdges ? time / 1e3 * (flowSpeed / edgeLen2) % 1 : Math.min(1, rawT);
|
|
1188
|
+
const dotPos = this._getEdgeDotPosition(graph, e, dotT);
|
|
1189
|
+
if (dotPos) {
|
|
1190
|
+
ctx.save();
|
|
1191
|
+
ctx.fillStyle = this._rgba(theme.edgeActive, 0.9);
|
|
1192
|
+
ctx.shadowColor = theme.edgeActive;
|
|
1193
|
+
ctx.shadowBlur = 8 / this.scale;
|
|
1194
|
+
ctx.beginPath();
|
|
1195
|
+
ctx.arc(dotPos.x, dotPos.y, 2.5 / this.scale, 0, Math.PI * 2);
|
|
1196
|
+
ctx.fill();
|
|
1197
|
+
ctx.restore();
|
|
1198
|
+
}
|
|
1056
1199
|
} else {
|
|
1057
1200
|
ctx.setLineDash([]);
|
|
1058
1201
|
ctx.strokeStyle = theme.edge;
|
|
1059
|
-
ctx.lineWidth =
|
|
1202
|
+
ctx.lineWidth = 2.5 / this.scale;
|
|
1203
|
+
this._drawEdge(graph, e);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
for (const nodeId of selection) {
|
|
1207
|
+
const node = graph.nodes.get(nodeId);
|
|
1208
|
+
if (!node) continue;
|
|
1209
|
+
const def = (_b = (_a = this.registry) == null ? void 0 : _a.types) == null ? void 0 : _b.get(node.type);
|
|
1210
|
+
if (def == null ? void 0 : def.html) this._drawHtmlSelectionBorder(node);
|
|
1211
|
+
}
|
|
1212
|
+
if (activeNodes.size > 0) {
|
|
1213
|
+
for (const nodeId of activeNodes) {
|
|
1214
|
+
const node = graph.nodes.get(nodeId);
|
|
1215
|
+
if (node) this._drawActiveNodeBorder(node, time);
|
|
1060
1216
|
}
|
|
1061
|
-
this._drawEdge(graph, e);
|
|
1062
1217
|
}
|
|
1063
1218
|
if (tempEdge) {
|
|
1064
1219
|
const a = this.screenToWorld(tempEdge.x1, tempEdge.y1);
|
|
1065
1220
|
const b = this.screenToWorld(tempEdge.x2, tempEdge.y2);
|
|
1066
1221
|
const prevDash = this.ctx.getLineDash();
|
|
1067
|
-
this.ctx.setLineDash([
|
|
1222
|
+
this.ctx.setLineDash([5 / this.scale, 5 / this.scale]);
|
|
1223
|
+
this.ctx.strokeStyle = this._rgba(this.theme.accentBright, 0.7);
|
|
1224
|
+
this.ctx.lineWidth = 2.5 / this.scale;
|
|
1068
1225
|
let ptsForArrow = null;
|
|
1069
1226
|
if (this.edgeStyle === "line") {
|
|
1070
1227
|
this._drawLine(a.x, a.y, b.x, b.y);
|
|
@@ -1085,15 +1242,14 @@ const _CanvasRenderer = class _CanvasRenderer {
|
|
|
1085
1242
|
if (ptsForArrow && ptsForArrow.length >= 2) {
|
|
1086
1243
|
const p1 = ptsForArrow[ptsForArrow.length - 2];
|
|
1087
1244
|
const p2 = ptsForArrow[ptsForArrow.length - 1];
|
|
1088
|
-
this.ctx.fillStyle = this.theme.
|
|
1089
|
-
this.
|
|
1090
|
-
this._drawArrowhead(p1.x, p1.y, p2.x, p2.y, 12);
|
|
1245
|
+
this.ctx.fillStyle = this.theme.accentBright;
|
|
1246
|
+
this._drawArrowhead(p1.x, p1.y, p2.x, p2.y, 10);
|
|
1091
1247
|
}
|
|
1092
1248
|
}
|
|
1093
1249
|
this._resetTransform();
|
|
1094
1250
|
}
|
|
1095
1251
|
};
|
|
1096
|
-
__publicField(_CanvasRenderer, "FONT_SIZE",
|
|
1252
|
+
__publicField(_CanvasRenderer, "FONT_SIZE", 11);
|
|
1097
1253
|
__publicField(_CanvasRenderer, "SELECTED_NODE_COLOR", "#6cf");
|
|
1098
1254
|
let CanvasRenderer = _CanvasRenderer;
|
|
1099
1255
|
function roundRect(ctx, x, y, w, h, r = 6) {
|
|
@@ -1110,6 +1266,38 @@ function roundRect(ctx, x, y, w, h, r = 6) {
|
|
|
1110
1266
|
ctx.quadraticCurveTo(x, y, x + r.tl, y);
|
|
1111
1267
|
ctx.closePath();
|
|
1112
1268
|
}
|
|
1269
|
+
function cubicBezierPoint(x0, y0, x1, y1, x2, y2, x3, y3, t) {
|
|
1270
|
+
const mt = 1 - t;
|
|
1271
|
+
return {
|
|
1272
|
+
x: mt * mt * mt * x0 + 3 * mt * mt * t * x1 + 3 * mt * t * t * x2 + t * t * t * x3,
|
|
1273
|
+
y: mt * mt * mt * y0 + 3 * mt * mt * t * y1 + 3 * mt * t * t * y2 + t * t * t * y3
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
function polylinePoint(pts, t) {
|
|
1277
|
+
let totalLen = 0;
|
|
1278
|
+
const lens = [];
|
|
1279
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
1280
|
+
const dx = pts[i + 1].x - pts[i].x;
|
|
1281
|
+
const dy = pts[i + 1].y - pts[i].y;
|
|
1282
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
1283
|
+
lens.push(len);
|
|
1284
|
+
totalLen += len;
|
|
1285
|
+
}
|
|
1286
|
+
if (totalLen === 0) return pts[0];
|
|
1287
|
+
let target = t * totalLen;
|
|
1288
|
+
let accum = 0;
|
|
1289
|
+
for (let i = 0; i < lens.length; i++) {
|
|
1290
|
+
if (accum + lens[i] >= target) {
|
|
1291
|
+
const segT = lens[i] > 0 ? (target - accum) / lens[i] : 0;
|
|
1292
|
+
return {
|
|
1293
|
+
x: pts[i].x + (pts[i + 1].x - pts[i].x) * segT,
|
|
1294
|
+
y: pts[i].y + (pts[i + 1].y - pts[i].y) * segT
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
accum += lens[i];
|
|
1298
|
+
}
|
|
1299
|
+
return pts[pts.length - 1];
|
|
1300
|
+
}
|
|
1113
1301
|
function findEdgeId(graph, a, b, c, d) {
|
|
1114
1302
|
for (const [id, e] of graph.edges) {
|
|
1115
1303
|
if (e.fromNode === a && e.fromPort === b && e.toNode === c && e.toPort === d)
|
|
@@ -1232,6 +1420,9 @@ const _Controller = class _Controller {
|
|
|
1232
1420
|
this.gDragging = null;
|
|
1233
1421
|
this.gResizing = null;
|
|
1234
1422
|
this.boxSelecting = null;
|
|
1423
|
+
this.activeEdges = /* @__PURE__ */ new Set();
|
|
1424
|
+
this.activeEdgeTimes = /* @__PURE__ */ new Map();
|
|
1425
|
+
this.activeNodes = /* @__PURE__ */ new Set();
|
|
1235
1426
|
this.snapToGrid = true;
|
|
1236
1427
|
this.gridSize = 20;
|
|
1237
1428
|
this._cursor = "default";
|
|
@@ -1243,6 +1434,16 @@ const _Controller = class _Controller {
|
|
|
1243
1434
|
this._onContextMenuEvt = this._onContextMenu.bind(this);
|
|
1244
1435
|
this._onDblClickEvt = this._onDblClick.bind(this);
|
|
1245
1436
|
this._bindEvents();
|
|
1437
|
+
this.hooks.on("runner:step-updated", ({ activeNodeId, activeEdgeIds = [] }) => {
|
|
1438
|
+
this.activeNodes = activeNodeId ? /* @__PURE__ */ new Set([activeNodeId]) : /* @__PURE__ */ new Set();
|
|
1439
|
+
this.activeEdges = new Set(activeEdgeIds);
|
|
1440
|
+
this.activeEdgeTimes.clear();
|
|
1441
|
+
const now = performance.now();
|
|
1442
|
+
for (const edgeId of activeEdgeIds) {
|
|
1443
|
+
this.activeEdgeTimes.set(edgeId, now);
|
|
1444
|
+
}
|
|
1445
|
+
this.render();
|
|
1446
|
+
});
|
|
1246
1447
|
}
|
|
1247
1448
|
destroy() {
|
|
1248
1449
|
const c = this.renderer.canvas;
|
|
@@ -1534,7 +1735,8 @@ const _Controller = class _Controller {
|
|
|
1534
1735
|
const dx = w.x - this.resizing.startX;
|
|
1535
1736
|
const dy = w.y - this.resizing.startY;
|
|
1536
1737
|
const minW = _Controller.MIN_NODE_WIDTH;
|
|
1537
|
-
const
|
|
1738
|
+
const maxPorts = Math.max(n.inputs.length, n.outputs.length);
|
|
1739
|
+
const minH = maxPorts > 0 ? Math.max(_Controller.MIN_NODE_HEIGHT, 42 + maxPorts * 20) : _Controller.MIN_NODE_HEIGHT;
|
|
1538
1740
|
n.size.width = Math.max(minW, this.resizing.startW + dx);
|
|
1539
1741
|
n.size.height = Math.max(minH, this.resizing.startH + dy);
|
|
1540
1742
|
(_a = this.hooks) == null ? void 0 : _a.emit("node:resize", n);
|
|
@@ -1792,17 +1994,22 @@ const _Controller = class _Controller {
|
|
|
1792
1994
|
this.graph.updateWorldTransforms();
|
|
1793
1995
|
this.render();
|
|
1794
1996
|
}
|
|
1795
|
-
render() {
|
|
1997
|
+
render(time = performance.now()) {
|
|
1796
1998
|
var _a;
|
|
1797
1999
|
const tEdge = this.renderTempEdge();
|
|
2000
|
+
const runner = this.graph.runner;
|
|
2001
|
+
const isStepMode = !!runner && runner.executionMode === "step";
|
|
1798
2002
|
this.renderer.draw(this.graph, {
|
|
1799
2003
|
selection: this.selection,
|
|
1800
2004
|
tempEdge: null,
|
|
1801
2005
|
// Don't draw temp edge on background
|
|
1802
2006
|
boxSelecting: this.boxSelecting,
|
|
1803
2007
|
activeEdges: this.activeEdges || /* @__PURE__ */ new Set(),
|
|
1804
|
-
|
|
2008
|
+
activeEdgeTimes: this.activeEdgeTimes,
|
|
2009
|
+
drawEdges: !this.edgeRenderer,
|
|
1805
2010
|
// Only draw edges here if no separate edge renderer
|
|
2011
|
+
time,
|
|
2012
|
+
loopActiveEdges: isStepMode
|
|
1806
2013
|
});
|
|
1807
2014
|
(_a = this.htmlOverlay) == null ? void 0 : _a.draw(this.graph, this.selection);
|
|
1808
2015
|
if (this.edgeRenderer) {
|
|
@@ -1810,11 +2017,13 @@ const _Controller = class _Controller {
|
|
|
1810
2017
|
edgeCtx.clearRect(0, 0, this.edgeRenderer.canvas.width, this.edgeRenderer.canvas.height);
|
|
1811
2018
|
this.edgeRenderer._applyTransform();
|
|
1812
2019
|
this.edgeRenderer.drawEdgesOnly(this.graph, {
|
|
1813
|
-
activeEdges: this.activeEdges
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
2020
|
+
activeEdges: this.activeEdges,
|
|
2021
|
+
activeEdgeTimes: this.activeEdgeTimes,
|
|
2022
|
+
activeNodes: this.activeNodes,
|
|
2023
|
+
selection: this.selection,
|
|
2024
|
+
time,
|
|
2025
|
+
tempEdge: tEdge,
|
|
2026
|
+
loopActiveEdges: isStepMode
|
|
1818
2027
|
});
|
|
1819
2028
|
this.edgeRenderer._resetTransform();
|
|
1820
2029
|
}
|
|
@@ -2208,12 +2417,14 @@ class Runner {
|
|
|
2208
2417
|
this._raf = null;
|
|
2209
2418
|
this._last = 0;
|
|
2210
2419
|
this.cyclesPerFrame = Math.max(1, cyclesPerFrame | 0);
|
|
2420
|
+
this.executionMode = "run";
|
|
2421
|
+
this.activePlan = null;
|
|
2422
|
+
this.activeStepIndex = -1;
|
|
2423
|
+
this.stepCache = /* @__PURE__ */ new Map();
|
|
2211
2424
|
}
|
|
2212
|
-
// 외부에서 실행 중인지 확인
|
|
2213
2425
|
isRunning() {
|
|
2214
2426
|
return this.running;
|
|
2215
2427
|
}
|
|
2216
|
-
// 실행 도중에도 CPS 변경 가능
|
|
2217
2428
|
setCyclesPerFrame(n) {
|
|
2218
2429
|
this.cyclesPerFrame = Math.max(1, n | 0);
|
|
2219
2430
|
}
|
|
@@ -2246,29 +2457,23 @@ class Runner {
|
|
|
2246
2457
|
}
|
|
2247
2458
|
}
|
|
2248
2459
|
/**
|
|
2249
|
-
* Execute connected nodes once from a starting node
|
|
2250
|
-
*
|
|
2251
|
-
* @param {string} startNodeId - ID of the node to start from
|
|
2252
|
-
* @param {number} dt - Delta time
|
|
2460
|
+
* Execute connected nodes once from a starting node.
|
|
2461
|
+
* Returns execEdgeOrder: exec edges in the order they were traversed.
|
|
2253
2462
|
*/
|
|
2254
2463
|
runOnce(startNodeId, dt = 0) {
|
|
2255
|
-
console.log("[Runner.runOnce] Starting exec flow from node:", startNodeId);
|
|
2256
|
-
const executedNodes = [];
|
|
2257
2464
|
const allConnectedNodes = /* @__PURE__ */ new Set();
|
|
2258
|
-
const
|
|
2465
|
+
const execEdgeOrder = [];
|
|
2466
|
+
const runCache = /* @__PURE__ */ new Map();
|
|
2467
|
+
const queue = [{ nodeId: startNodeId, fromEdgeId: null }];
|
|
2259
2468
|
const visited = /* @__PURE__ */ new Set();
|
|
2260
2469
|
while (queue.length > 0) {
|
|
2261
|
-
const currentNodeId = queue.shift();
|
|
2470
|
+
const { nodeId: currentNodeId, fromEdgeId } = queue.shift();
|
|
2262
2471
|
if (visited.has(currentNodeId)) continue;
|
|
2263
2472
|
visited.add(currentNodeId);
|
|
2473
|
+
if (fromEdgeId) execEdgeOrder.push(fromEdgeId);
|
|
2264
2474
|
const node = this.graph.nodes.get(currentNodeId);
|
|
2265
|
-
if (!node)
|
|
2266
|
-
console.warn(`[Runner.runOnce] Node not found: ${currentNodeId}`);
|
|
2267
|
-
continue;
|
|
2268
|
-
}
|
|
2269
|
-
executedNodes.push(currentNodeId);
|
|
2475
|
+
if (!node) continue;
|
|
2270
2476
|
allConnectedNodes.add(currentNodeId);
|
|
2271
|
-
console.log(`[Runner.runOnce] Executing: ${node.title} (${node.type})`);
|
|
2272
2477
|
for (const input of node.inputs) {
|
|
2273
2478
|
if (input.portType === "data") {
|
|
2274
2479
|
for (const edge of this.graph.edges.values()) {
|
|
@@ -2276,32 +2481,154 @@ class Runner {
|
|
|
2276
2481
|
const sourceNode = this.graph.nodes.get(edge.fromNode);
|
|
2277
2482
|
if (sourceNode && !allConnectedNodes.has(edge.fromNode)) {
|
|
2278
2483
|
allConnectedNodes.add(edge.fromNode);
|
|
2279
|
-
this.
|
|
2484
|
+
this._executeNodeWithCache(edge.fromNode, dt, runCache);
|
|
2280
2485
|
}
|
|
2281
2486
|
}
|
|
2282
2487
|
}
|
|
2283
2488
|
}
|
|
2284
2489
|
}
|
|
2285
|
-
this.
|
|
2286
|
-
const
|
|
2287
|
-
|
|
2490
|
+
this._executeNodeWithCache(currentNodeId, dt, runCache);
|
|
2491
|
+
const execOutputPorts = node.outputs.filter((p) => p.portType === "exec");
|
|
2492
|
+
for (const execOutput of execOutputPorts) {
|
|
2493
|
+
for (const edge of this.graph.edges.values()) {
|
|
2494
|
+
if (edge.fromNode === currentNodeId && edge.fromPort === execOutput.id) {
|
|
2495
|
+
queue.push({ nodeId: edge.toNode, fromEdgeId: edge.id });
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2288
2499
|
}
|
|
2289
|
-
console.log("[Runner.runOnce] Executed nodes:", executedNodes.length);
|
|
2290
2500
|
const connectedEdges = /* @__PURE__ */ new Set();
|
|
2291
2501
|
for (const edge of this.graph.edges.values()) {
|
|
2292
2502
|
if (allConnectedNodes.has(edge.fromNode) && allConnectedNodes.has(edge.toNode)) {
|
|
2293
2503
|
connectedEdges.add(edge.id);
|
|
2294
2504
|
}
|
|
2295
2505
|
}
|
|
2296
|
-
|
|
2297
|
-
|
|
2506
|
+
return { connectedNodes: allConnectedNodes, connectedEdges, execEdgeOrder };
|
|
2507
|
+
}
|
|
2508
|
+
setExecutionMode(mode) {
|
|
2509
|
+
this.executionMode = mode;
|
|
2510
|
+
if (mode === "run") this.resetStepping();
|
|
2511
|
+
}
|
|
2512
|
+
resetStepping() {
|
|
2513
|
+
var _a, _b;
|
|
2514
|
+
this.activePlan = null;
|
|
2515
|
+
this.activeStepIndex = -1;
|
|
2516
|
+
this.stepCache.clear();
|
|
2517
|
+
(_b = (_a = this.hooks) == null ? void 0 : _a.emit) == null ? void 0 : _b.call(_a, "runner:step-updated", { activeNodeId: null });
|
|
2518
|
+
}
|
|
2519
|
+
buildPlan(startNodeId) {
|
|
2520
|
+
const plan = [];
|
|
2521
|
+
const allConnectedNodes = /* @__PURE__ */ new Set();
|
|
2522
|
+
const queue = [{ nodeId: startNodeId, fromEdgeId: null }];
|
|
2523
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2524
|
+
while (queue.length > 0) {
|
|
2525
|
+
const { nodeId: currentNodeId, fromEdgeId } = queue.shift();
|
|
2526
|
+
if (visited.has(currentNodeId)) continue;
|
|
2527
|
+
visited.add(currentNodeId);
|
|
2528
|
+
allConnectedNodes.add(currentNodeId);
|
|
2529
|
+
const node = this.graph.nodes.get(currentNodeId);
|
|
2530
|
+
if (!node) continue;
|
|
2531
|
+
const dataDeps = [];
|
|
2532
|
+
for (const input of node.inputs) {
|
|
2533
|
+
if (input.portType === "data") {
|
|
2534
|
+
for (const edge of this.graph.edges.values()) {
|
|
2535
|
+
if (edge.toNode === currentNodeId && edge.toPort === input.id) {
|
|
2536
|
+
const srcId = edge.fromNode;
|
|
2537
|
+
if (!allConnectedNodes.has(srcId)) {
|
|
2538
|
+
allConnectedNodes.add(srcId);
|
|
2539
|
+
dataDeps.push(srcId);
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
const incomingEdges = [];
|
|
2546
|
+
for (const edge of this.graph.edges.values()) {
|
|
2547
|
+
if (edge.toNode === currentNodeId) {
|
|
2548
|
+
incomingEdges.push(edge.id);
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
plan.push({ nodeId: currentNodeId, fromEdgeId, incomingEdges, dataDeps });
|
|
2552
|
+
const execOutputs = node.outputs.filter((p) => p.portType === "exec");
|
|
2553
|
+
for (const execOutput of execOutputs) {
|
|
2554
|
+
for (const edge of this.graph.edges.values()) {
|
|
2555
|
+
if (edge.fromNode === currentNodeId && edge.fromPort === execOutput.id) {
|
|
2556
|
+
queue.push({ nodeId: edge.toNode, fromEdgeId: edge.id });
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
return plan;
|
|
2562
|
+
}
|
|
2563
|
+
startStepping(startNodeId) {
|
|
2564
|
+
var _a, _b;
|
|
2565
|
+
this.stepCache.clear();
|
|
2566
|
+
this.activePlan = this.buildPlan(startNodeId);
|
|
2567
|
+
this.activeStepIndex = 0;
|
|
2568
|
+
const step = this.activePlan[0];
|
|
2569
|
+
(_b = (_a = this.hooks) == null ? void 0 : _a.emit) == null ? void 0 : _b.call(_a, "runner:step-updated", {
|
|
2570
|
+
activeNodeId: step == null ? void 0 : step.nodeId,
|
|
2571
|
+
activeEdgeIds: (step == null ? void 0 : step.incomingEdges) || []
|
|
2572
|
+
});
|
|
2573
|
+
this.start();
|
|
2574
|
+
}
|
|
2575
|
+
executeNextStep() {
|
|
2576
|
+
var _a, _b, _c, _d;
|
|
2577
|
+
if (!this.activePlan || this.activeStepIndex < 0 || this.activeStepIndex >= this.activePlan.length) {
|
|
2578
|
+
this.resetStepping();
|
|
2579
|
+
return null;
|
|
2580
|
+
}
|
|
2581
|
+
const step = this.activePlan[this.activeStepIndex];
|
|
2582
|
+
for (const depId of step.dataDeps) {
|
|
2583
|
+
this._executeNodeWithCache(depId, 0, this.stepCache);
|
|
2584
|
+
}
|
|
2585
|
+
this._executeNodeWithCache(step.nodeId, 0, this.stepCache);
|
|
2586
|
+
this.activeStepIndex++;
|
|
2587
|
+
if (this.activeStepIndex < this.activePlan.length) {
|
|
2588
|
+
const nextStep = this.activePlan[this.activeStepIndex];
|
|
2589
|
+
(_b = (_a = this.hooks) == null ? void 0 : _a.emit) == null ? void 0 : _b.call(_a, "runner:step-updated", {
|
|
2590
|
+
activeNodeId: nextStep.nodeId,
|
|
2591
|
+
activeEdgeIds: nextStep.incomingEdges || []
|
|
2592
|
+
});
|
|
2593
|
+
} else {
|
|
2594
|
+
(_d = (_c = this.hooks) == null ? void 0 : _c.emit) == null ? void 0 : _d.call(_c, "runner:step-updated", { activeNodeId: null });
|
|
2595
|
+
this.resetStepping();
|
|
2596
|
+
}
|
|
2597
|
+
return step.nodeId;
|
|
2598
|
+
}
|
|
2599
|
+
/** Execute a node using a shared run-local output cache for reliable data passing. */
|
|
2600
|
+
_executeNodeWithCache(nodeId, dt, runCache) {
|
|
2601
|
+
var _a, _b;
|
|
2602
|
+
const node = this.graph.nodes.get(nodeId);
|
|
2603
|
+
if (!node) return;
|
|
2604
|
+
const def = this.registry.types.get(node.type);
|
|
2605
|
+
if (!(def == null ? void 0 : def.onExecute)) return;
|
|
2606
|
+
try {
|
|
2607
|
+
def.onExecute(node, {
|
|
2608
|
+
dt,
|
|
2609
|
+
graph: this.graph,
|
|
2610
|
+
getInput: (portName) => {
|
|
2611
|
+
const p = node.inputs.find((i) => i.name === portName) || node.inputs[0];
|
|
2612
|
+
if (!p) return void 0;
|
|
2613
|
+
for (const edge of this.graph.edges.values()) {
|
|
2614
|
+
if (edge.toNode === nodeId && edge.toPort === p.id) {
|
|
2615
|
+
const key = `${edge.fromNode}:${edge.fromPort}`;
|
|
2616
|
+
return runCache.has(key) ? runCache.get(key) : this.graph._curBuf().get(key);
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
return void 0;
|
|
2620
|
+
},
|
|
2621
|
+
setOutput: (portName, value) => {
|
|
2622
|
+
const p = node.outputs.find((o) => o.name === portName) || node.outputs[0];
|
|
2623
|
+
if (p) {
|
|
2624
|
+
runCache.set(`${node.id}:${p.id}`, value);
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
});
|
|
2628
|
+
} catch (err) {
|
|
2629
|
+
(_b = (_a = this.hooks) == null ? void 0 : _a.emit) == null ? void 0 : _b.call(_a, "error", err);
|
|
2630
|
+
}
|
|
2298
2631
|
}
|
|
2299
|
-
/**
|
|
2300
|
-
* Find all nodes connected via exec outputs
|
|
2301
|
-
* Supports multiple connections from a single exec output
|
|
2302
|
-
* @param {string} nodeId - Current node ID
|
|
2303
|
-
* @returns {string[]} Array of next node IDs
|
|
2304
|
-
*/
|
|
2305
2632
|
findAllNextExecNodes(nodeId) {
|
|
2306
2633
|
const node = this.graph.nodes.get(nodeId);
|
|
2307
2634
|
if (!node) return [];
|
|
@@ -2317,11 +2644,6 @@ class Runner {
|
|
|
2317
2644
|
}
|
|
2318
2645
|
return nextNodes;
|
|
2319
2646
|
}
|
|
2320
|
-
/**
|
|
2321
|
-
* Execute a single node
|
|
2322
|
-
* @param {string} nodeId - Node ID to execute
|
|
2323
|
-
* @param {number} dt - Delta time
|
|
2324
|
-
*/
|
|
2325
2647
|
executeNode(nodeId, dt) {
|
|
2326
2648
|
var _a, _b;
|
|
2327
2649
|
const node = this.graph.nodes.get(nodeId);
|
|
@@ -2360,7 +2682,9 @@ class Runner {
|
|
|
2360
2682
|
const dtMs = this._last ? t - this._last : 0;
|
|
2361
2683
|
this._last = t;
|
|
2362
2684
|
const dt = dtMs / 1e3;
|
|
2363
|
-
|
|
2685
|
+
if (this.executionMode === "run") {
|
|
2686
|
+
this.step(this.cyclesPerFrame, dt);
|
|
2687
|
+
}
|
|
2364
2688
|
(_b2 = (_a2 = this.hooks) == null ? void 0 : _a2.emit) == null ? void 0 : _b2.call(_a2, "runner:tick", {
|
|
2365
2689
|
time: t,
|
|
2366
2690
|
dt,
|
|
@@ -2419,7 +2743,7 @@ class HtmlOverlay {
|
|
|
2419
2743
|
const header = document.createElement("div");
|
|
2420
2744
|
header.className = "node-header";
|
|
2421
2745
|
Object.assign(header.style, {
|
|
2422
|
-
height: "
|
|
2746
|
+
height: "26px",
|
|
2423
2747
|
flexShrink: "0",
|
|
2424
2748
|
display: "flex",
|
|
2425
2749
|
alignItems: "center",
|
|
@@ -2494,6 +2818,7 @@ class HtmlOverlay {
|
|
|
2494
2818
|
}
|
|
2495
2819
|
seen.add(node.id);
|
|
2496
2820
|
}
|
|
2821
|
+
this._drawStepOverlay(graph);
|
|
2497
2822
|
for (const [id, el] of this.nodes) {
|
|
2498
2823
|
if (!seen.has(id)) {
|
|
2499
2824
|
el.remove();
|
|
@@ -2501,6 +2826,59 @@ class HtmlOverlay {
|
|
|
2501
2826
|
}
|
|
2502
2827
|
}
|
|
2503
2828
|
}
|
|
2829
|
+
_drawStepOverlay(graph) {
|
|
2830
|
+
const runner = graph.runner;
|
|
2831
|
+
if (!runner || runner.executionMode !== "step" || !runner.activePlan) {
|
|
2832
|
+
if (this._stepBtn) {
|
|
2833
|
+
this._stepBtn.style.display = "none";
|
|
2834
|
+
}
|
|
2835
|
+
return;
|
|
2836
|
+
}
|
|
2837
|
+
const nextStep = runner.activePlan[runner.activeStepIndex];
|
|
2838
|
+
if (!nextStep) {
|
|
2839
|
+
if (this._stepBtn) this._stepBtn.style.display = "none";
|
|
2840
|
+
return;
|
|
2841
|
+
}
|
|
2842
|
+
const node = graph.nodes.get(nextStep.nodeId);
|
|
2843
|
+
if (!node) return;
|
|
2844
|
+
if (!this._stepBtn) {
|
|
2845
|
+
this._stepBtn = document.createElement("button");
|
|
2846
|
+
this._stepBtn.className = "step-play-button";
|
|
2847
|
+
this._stepBtn.innerHTML = "▶";
|
|
2848
|
+
Object.assign(this._stepBtn.style, {
|
|
2849
|
+
position: "absolute",
|
|
2850
|
+
zIndex: "100",
|
|
2851
|
+
width: "20px",
|
|
2852
|
+
height: "20px",
|
|
2853
|
+
borderRadius: "4px",
|
|
2854
|
+
border: "none",
|
|
2855
|
+
background: "transparent",
|
|
2856
|
+
color: "white",
|
|
2857
|
+
fontSize: "12px",
|
|
2858
|
+
cursor: "pointer",
|
|
2859
|
+
display: "flex",
|
|
2860
|
+
alignItems: "center",
|
|
2861
|
+
justifyContent: "center",
|
|
2862
|
+
// boxShadow: "0 2px 6px rgba(0,0,0,0.3)",
|
|
2863
|
+
pointerEvents: "auto",
|
|
2864
|
+
transition: "transform 0.1s, background 0.2s"
|
|
2865
|
+
});
|
|
2866
|
+
this._stepBtn.addEventListener("mouseover", () => {
|
|
2867
|
+
this._stepBtn.style.transform = "scale(1)";
|
|
2868
|
+
});
|
|
2869
|
+
this._stepBtn.addEventListener("mouseout", () => {
|
|
2870
|
+
this._stepBtn.style.transform = "scale(1)";
|
|
2871
|
+
});
|
|
2872
|
+
this._stepBtn.addEventListener("click", (e) => {
|
|
2873
|
+
e.stopPropagation();
|
|
2874
|
+
runner.executeNextStep();
|
|
2875
|
+
});
|
|
2876
|
+
this.container.appendChild(this._stepBtn);
|
|
2877
|
+
}
|
|
2878
|
+
this._stepBtn.style.display = "flex";
|
|
2879
|
+
this._stepBtn.style.left = `${node.computed.x + node.computed.w - 26}px`;
|
|
2880
|
+
this._stepBtn.style.top = `${node.computed.y + 2}px`;
|
|
2881
|
+
}
|
|
2504
2882
|
/**
|
|
2505
2883
|
* Sync container transform with renderer state (lightweight update)
|
|
2506
2884
|
* Called when zoom/pan occurs without needing full redraw
|
|
@@ -2635,10 +3013,44 @@ class PropertyPanel {
|
|
|
2635
3013
|
this.hooks = hooks;
|
|
2636
3014
|
this.registry = registry;
|
|
2637
3015
|
this.render = render;
|
|
3016
|
+
this._def = null;
|
|
2638
3017
|
this.panel = null;
|
|
2639
3018
|
this.currentNode = null;
|
|
2640
3019
|
this.isVisible = false;
|
|
3020
|
+
this._selfUpdating = false;
|
|
2641
3021
|
this._createPanel();
|
|
3022
|
+
this._bindHooks();
|
|
3023
|
+
}
|
|
3024
|
+
_bindHooks() {
|
|
3025
|
+
var _a, _b, _c, _d, _e, _f;
|
|
3026
|
+
(_a = this.hooks) == null ? void 0 : _a.on("edge:create", () => {
|
|
3027
|
+
if (this._canRefresh()) this._renderContent();
|
|
3028
|
+
});
|
|
3029
|
+
(_b = this.hooks) == null ? void 0 : _b.on("edge:delete", () => {
|
|
3030
|
+
if (this._canRefresh()) this._renderContent();
|
|
3031
|
+
});
|
|
3032
|
+
(_c = this.hooks) == null ? void 0 : _c.on("node:updated", (node) => {
|
|
3033
|
+
var _a2;
|
|
3034
|
+
if (this._canRefresh() && ((_a2 = this.currentNode) == null ? void 0 : _a2.id) === (node == null ? void 0 : node.id) && !this._selfUpdating) {
|
|
3035
|
+
this._renderContent();
|
|
3036
|
+
}
|
|
3037
|
+
});
|
|
3038
|
+
(_d = this.hooks) == null ? void 0 : _d.on("node:move", (node) => {
|
|
3039
|
+
var _a2;
|
|
3040
|
+
if (this._canRefresh() && ((_a2 = this.currentNode) == null ? void 0 : _a2.id) === (node == null ? void 0 : node.id)) {
|
|
3041
|
+
this._updatePositionFields();
|
|
3042
|
+
}
|
|
3043
|
+
});
|
|
3044
|
+
(_e = this.hooks) == null ? void 0 : _e.on("runner:tick", () => {
|
|
3045
|
+
if (this._canRefresh()) this._updateLiveValues();
|
|
3046
|
+
});
|
|
3047
|
+
(_f = this.hooks) == null ? void 0 : _f.on("runner:stop", () => {
|
|
3048
|
+
if (this._canRefresh()) this._updateLiveValues();
|
|
3049
|
+
});
|
|
3050
|
+
}
|
|
3051
|
+
_canRefresh() {
|
|
3052
|
+
if (!this.isVisible || !this.currentNode) return false;
|
|
3053
|
+
return !this.panel.querySelector("[data-field]:focus");
|
|
2642
3054
|
}
|
|
2643
3055
|
_createPanel() {
|
|
2644
3056
|
this.panel = document.createElement("div");
|
|
@@ -2668,8 +3080,10 @@ class PropertyPanel {
|
|
|
2668
3080
|
});
|
|
2669
3081
|
}
|
|
2670
3082
|
open(node) {
|
|
3083
|
+
var _a, _b;
|
|
2671
3084
|
if (!node) return;
|
|
2672
3085
|
this.currentNode = node;
|
|
3086
|
+
this._def = ((_b = (_a = this.registry) == null ? void 0 : _a.types) == null ? void 0 : _b.get(node.type)) || null;
|
|
2673
3087
|
this.isVisible = true;
|
|
2674
3088
|
this._renderContent();
|
|
2675
3089
|
this.panel.style.display = "block";
|
|
@@ -2705,9 +3119,9 @@ class PropertyPanel {
|
|
|
2705
3119
|
</div>
|
|
2706
3120
|
</div>
|
|
2707
3121
|
</div>
|
|
2708
|
-
|
|
3122
|
+
|
|
2709
3123
|
<div class="section">
|
|
2710
|
-
<div class="section-title">Position & Size</div>
|
|
3124
|
+
<div class="section-title">Position & Size</div>
|
|
2711
3125
|
<div class="section-body">
|
|
2712
3126
|
<div class="field-row">
|
|
2713
3127
|
<div class="field">
|
|
@@ -2731,16 +3145,92 @@ class PropertyPanel {
|
|
|
2731
3145
|
</div>
|
|
2732
3146
|
</div>
|
|
2733
3147
|
</div>
|
|
2734
|
-
|
|
3148
|
+
|
|
3149
|
+
${this._renderConnections(node)}
|
|
2735
3150
|
${this._renderPorts(node)}
|
|
3151
|
+
${this._renderLiveValues(node)}
|
|
2736
3152
|
${this._renderState(node)}
|
|
2737
|
-
|
|
3153
|
+
|
|
2738
3154
|
<div class="panel-actions">
|
|
2739
3155
|
<button class="btn-secondary panel-close-btn">Close</button>
|
|
2740
3156
|
</div>
|
|
2741
3157
|
`;
|
|
2742
3158
|
this._attachInputListeners();
|
|
2743
3159
|
}
|
|
3160
|
+
_renderConnections(node) {
|
|
3161
|
+
const edges = [...this.graph.edges.values()];
|
|
3162
|
+
const incoming = edges.filter((e) => e.toNode === node.id);
|
|
3163
|
+
const outgoing = edges.filter((e) => e.fromNode === node.id);
|
|
3164
|
+
if (!incoming.length && !outgoing.length) return "";
|
|
3165
|
+
const edgeLabel = (e, dir) => {
|
|
3166
|
+
const otherId = dir === "in" ? e.fromNode : e.toNode;
|
|
3167
|
+
const other = this.graph.nodes.get(otherId);
|
|
3168
|
+
return `<div class="port-item">
|
|
3169
|
+
<span class="port-icon data"></span>
|
|
3170
|
+
<span class="port-name" style="font-size:10px;color:#5a5a78;">${(other == null ? void 0 : other.title) ?? otherId}</span>
|
|
3171
|
+
</div>`;
|
|
3172
|
+
};
|
|
3173
|
+
return `
|
|
3174
|
+
<div class="section">
|
|
3175
|
+
<div class="section-title">Connections</div>
|
|
3176
|
+
<div class="section-body">
|
|
3177
|
+
${incoming.length ? `
|
|
3178
|
+
<div class="port-group">
|
|
3179
|
+
<div class="port-group-title">Incoming (${incoming.length})</div>
|
|
3180
|
+
${incoming.map((e) => edgeLabel(e, "in")).join("")}
|
|
3181
|
+
</div>` : ""}
|
|
3182
|
+
${outgoing.length ? `
|
|
3183
|
+
<div class="port-group">
|
|
3184
|
+
<div class="port-group-title">Outgoing (${outgoing.length})</div>
|
|
3185
|
+
${outgoing.map((e) => edgeLabel(e, "out")).join("")}
|
|
3186
|
+
</div>` : ""}
|
|
3187
|
+
</div>
|
|
3188
|
+
</div>
|
|
3189
|
+
`;
|
|
3190
|
+
}
|
|
3191
|
+
_renderLiveValues(node) {
|
|
3192
|
+
var _a, _b;
|
|
3193
|
+
const cur = (_b = (_a = this.graph) == null ? void 0 : _a._curBuf) == null ? void 0 : _b.call(_a);
|
|
3194
|
+
if (!cur) return "";
|
|
3195
|
+
const lines = [];
|
|
3196
|
+
for (const input of node.inputs) {
|
|
3197
|
+
`${node.id}:${input.id}`;
|
|
3198
|
+
for (const edge of this.graph.edges.values()) {
|
|
3199
|
+
if (edge.toNode === node.id && edge.toPort === input.id) {
|
|
3200
|
+
const upKey = `${edge.fromNode}:${edge.fromPort}`;
|
|
3201
|
+
const val = cur.get(upKey);
|
|
3202
|
+
if (val !== void 0) {
|
|
3203
|
+
lines.push(`<div class="port-item">
|
|
3204
|
+
<span class="port-icon data"></span>
|
|
3205
|
+
<span class="port-name">↳ ${input.name}</span>
|
|
3206
|
+
<span class="port-type" style="color:var(--color-primary);background:rgba(99,102,241,0.1);">${JSON.stringify(val)}</span>
|
|
3207
|
+
</div>`);
|
|
3208
|
+
}
|
|
3209
|
+
break;
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
for (const output of node.outputs) {
|
|
3214
|
+
const key = `${node.id}:${output.id}`;
|
|
3215
|
+
const val = cur.get(key);
|
|
3216
|
+
if (val !== void 0) {
|
|
3217
|
+
lines.push(`<div class="port-item">
|
|
3218
|
+
<span class="port-icon exec" style="background:#10b981;"></span>
|
|
3219
|
+
<span class="port-name">↳ ${output.name}</span>
|
|
3220
|
+
<span class="port-type" style="color:#10b981;background:rgba(16,185,129,0.1);">${JSON.stringify(val)}</span>
|
|
3221
|
+
</div>`);
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
if (!lines.length) return "";
|
|
3225
|
+
return `
|
|
3226
|
+
<div class="section">
|
|
3227
|
+
<div class="section-title">Live Values</div>
|
|
3228
|
+
<div class="section-body">
|
|
3229
|
+
${lines.join("")}
|
|
3230
|
+
</div>
|
|
3231
|
+
</div>
|
|
3232
|
+
`;
|
|
3233
|
+
}
|
|
2744
3234
|
_renderPorts(node) {
|
|
2745
3235
|
if (!node.inputs.length && !node.outputs.length) return "";
|
|
2746
3236
|
return `
|
|
@@ -2759,7 +3249,6 @@ class PropertyPanel {
|
|
|
2759
3249
|
`).join("")}
|
|
2760
3250
|
</div>
|
|
2761
3251
|
` : ""}
|
|
2762
|
-
|
|
2763
3252
|
${node.outputs.length ? `
|
|
2764
3253
|
<div class="port-group">
|
|
2765
3254
|
<div class="port-group-title">Outputs (${node.outputs.length})</div>
|
|
@@ -2777,38 +3266,56 @@ class PropertyPanel {
|
|
|
2777
3266
|
`;
|
|
2778
3267
|
}
|
|
2779
3268
|
_renderState(node) {
|
|
2780
|
-
if (!node.state
|
|
3269
|
+
if (!node.state) return "";
|
|
3270
|
+
const entries = Object.entries(node.state).filter(([key, value]) => {
|
|
3271
|
+
if (key.startsWith("_")) return false;
|
|
3272
|
+
const t = typeof value;
|
|
3273
|
+
return t === "string" || t === "number" || t === "boolean";
|
|
3274
|
+
});
|
|
3275
|
+
if (!entries.length) return "";
|
|
3276
|
+
const fieldHtml = ([key, value]) => {
|
|
3277
|
+
if (typeof value === "boolean") {
|
|
3278
|
+
return `
|
|
3279
|
+
<div class="field">
|
|
3280
|
+
<label>${key}</label>
|
|
3281
|
+
<select data-field="state.${key}">
|
|
3282
|
+
<option value="true"${value ? " selected" : ""}>true</option>
|
|
3283
|
+
<option value="false"${!value ? " selected" : ""}>false</option>
|
|
3284
|
+
</select>
|
|
3285
|
+
</div>`;
|
|
3286
|
+
}
|
|
3287
|
+
return `
|
|
3288
|
+
<div class="field">
|
|
3289
|
+
<label>${key}</label>
|
|
3290
|
+
<input type="${typeof value === "number" ? "number" : "text"}"
|
|
3291
|
+
data-field="state.${key}"
|
|
3292
|
+
value="${value}" />
|
|
3293
|
+
</div>`;
|
|
3294
|
+
};
|
|
2781
3295
|
return `
|
|
2782
3296
|
<div class="section">
|
|
2783
3297
|
<div class="section-title">State</div>
|
|
2784
3298
|
<div class="section-body">
|
|
2785
|
-
${
|
|
2786
|
-
<div class="field">
|
|
2787
|
-
<label>${key}</label>
|
|
2788
|
-
<input
|
|
2789
|
-
type="${typeof value === "number" ? "number" : "text"}"
|
|
2790
|
-
data-field="state.${key}"
|
|
2791
|
-
value="${value}"
|
|
2792
|
-
/>
|
|
2793
|
-
</div>
|
|
2794
|
-
`).join("")}
|
|
3299
|
+
${entries.map(fieldHtml).join("")}
|
|
2795
3300
|
</div>
|
|
2796
3301
|
</div>
|
|
2797
3302
|
`;
|
|
2798
3303
|
}
|
|
2799
3304
|
_attachInputListeners() {
|
|
2800
|
-
|
|
2801
|
-
|
|
3305
|
+
var _a;
|
|
3306
|
+
this.panel.querySelectorAll("[data-field]").forEach((input) => {
|
|
2802
3307
|
input.addEventListener("change", () => {
|
|
3308
|
+
this._selfUpdating = true;
|
|
2803
3309
|
this._handleFieldChange(input.dataset.field, input.value);
|
|
3310
|
+
this._selfUpdating = false;
|
|
2804
3311
|
});
|
|
2805
3312
|
});
|
|
2806
|
-
this.panel.querySelector(".panel-close-btn").addEventListener("click", () => {
|
|
3313
|
+
(_a = this.panel.querySelector(".panel-close-btn")) == null ? void 0 : _a.addEventListener("click", () => {
|
|
2807
3314
|
this.close();
|
|
2808
3315
|
});
|
|
2809
3316
|
}
|
|
2810
3317
|
_handleFieldChange(field, value) {
|
|
2811
|
-
var _a;
|
|
3318
|
+
var _a, _b;
|
|
2812
3319
|
const node = this.currentNode;
|
|
2813
3320
|
if (!node) return;
|
|
2814
3321
|
switch (field) {
|
|
@@ -2832,21 +3339,192 @@ class PropertyPanel {
|
|
|
2832
3339
|
default:
|
|
2833
3340
|
if (field.startsWith("state.")) {
|
|
2834
3341
|
const key = field.substring(6);
|
|
2835
|
-
if (node.state) {
|
|
2836
|
-
const
|
|
2837
|
-
|
|
3342
|
+
if (node.state && key in node.state) {
|
|
3343
|
+
const orig = node.state[key];
|
|
3344
|
+
if (typeof orig === "boolean") {
|
|
3345
|
+
node.state[key] = value === "true";
|
|
3346
|
+
} else if (typeof orig === "number") {
|
|
3347
|
+
node.state[key] = parseFloat(value);
|
|
3348
|
+
} else {
|
|
3349
|
+
node.state[key] = value;
|
|
3350
|
+
}
|
|
2838
3351
|
}
|
|
2839
3352
|
}
|
|
2840
3353
|
}
|
|
2841
3354
|
(_a = this.hooks) == null ? void 0 : _a.emit("node:updated", node);
|
|
2842
|
-
|
|
2843
|
-
|
|
3355
|
+
(_b = this.render) == null ? void 0 : _b.call(this);
|
|
3356
|
+
}
|
|
3357
|
+
/** Lightweight update of position fields only (no full re-render) */
|
|
3358
|
+
_updatePositionFields() {
|
|
3359
|
+
const node = this.currentNode;
|
|
3360
|
+
if (!node) return;
|
|
3361
|
+
const xEl = this.panel.querySelector('[data-field="x"]');
|
|
3362
|
+
const yEl = this.panel.querySelector('[data-field="y"]');
|
|
3363
|
+
if (xEl) xEl.value = Math.round(node.computed.x);
|
|
3364
|
+
if (yEl) yEl.value = Math.round(node.computed.y);
|
|
3365
|
+
}
|
|
3366
|
+
/** Lightweight in-place update of the Live Values section */
|
|
3367
|
+
_updateLiveValues() {
|
|
3368
|
+
var _a, _b;
|
|
3369
|
+
const node = this.currentNode;
|
|
3370
|
+
if (!node) return;
|
|
3371
|
+
const cur = (_b = (_a = this.graph) == null ? void 0 : _a._curBuf) == null ? void 0 : _b.call(_a);
|
|
3372
|
+
if (!cur) return;
|
|
3373
|
+
let section = this.panel.querySelector(".live-values-section");
|
|
3374
|
+
const newHtml = this._renderLiveValues(node);
|
|
3375
|
+
if (!newHtml) {
|
|
3376
|
+
if (section) section.remove();
|
|
3377
|
+
return;
|
|
3378
|
+
}
|
|
3379
|
+
const wrapper = document.createElement("div");
|
|
3380
|
+
wrapper.innerHTML = newHtml;
|
|
3381
|
+
const newSection = wrapper.firstElementChild;
|
|
3382
|
+
newSection.classList.add("live-values-section");
|
|
3383
|
+
if (section) {
|
|
3384
|
+
section.replaceWith(newSection);
|
|
3385
|
+
} else {
|
|
3386
|
+
this.panel.querySelectorAll(".section");
|
|
3387
|
+
const actions = this.panel.querySelector(".panel-actions");
|
|
3388
|
+
if (actions) {
|
|
3389
|
+
actions.before(newSection);
|
|
3390
|
+
} else {
|
|
3391
|
+
this.panel.querySelector(".panel-content").appendChild(newSection);
|
|
3392
|
+
}
|
|
2844
3393
|
}
|
|
2845
3394
|
}
|
|
2846
3395
|
destroy() {
|
|
2847
|
-
|
|
2848
|
-
|
|
3396
|
+
var _a;
|
|
3397
|
+
(_a = this.panel) == null ? void 0 : _a.remove();
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
class HelpOverlay {
|
|
3401
|
+
constructor(container, options = {}) {
|
|
3402
|
+
this.container = container;
|
|
3403
|
+
this.options = {
|
|
3404
|
+
shortcuts: options.shortcuts || this._getDefaultShortcuts(),
|
|
3405
|
+
onToggle: options.onToggle || null
|
|
3406
|
+
};
|
|
3407
|
+
this.isVisible = false;
|
|
3408
|
+
this.overlay = null;
|
|
3409
|
+
this.toggleBtn = null;
|
|
3410
|
+
this._createElements();
|
|
3411
|
+
this._bindEvents();
|
|
3412
|
+
}
|
|
3413
|
+
_getDefaultShortcuts() {
|
|
3414
|
+
return [
|
|
3415
|
+
{
|
|
3416
|
+
group: "Selection",
|
|
3417
|
+
items: [
|
|
3418
|
+
{ label: "Select node", key: "Click" },
|
|
3419
|
+
{ label: "Multi-select", key: "Shift+Click" },
|
|
3420
|
+
{ label: "Box select", key: "Ctrl+Drag" }
|
|
3421
|
+
]
|
|
3422
|
+
},
|
|
3423
|
+
{
|
|
3424
|
+
group: "Edit",
|
|
3425
|
+
items: [
|
|
3426
|
+
{ label: "Delete", key: "Del" },
|
|
3427
|
+
{ label: "Undo", key: "Ctrl+Z" },
|
|
3428
|
+
{ label: "Redo", key: "Ctrl+Y" }
|
|
3429
|
+
]
|
|
3430
|
+
},
|
|
3431
|
+
{
|
|
3432
|
+
group: "Group & Align",
|
|
3433
|
+
items: [
|
|
3434
|
+
{ label: "Create group", key: "Ctrl+G" },
|
|
3435
|
+
{ label: "Align horizontal", key: "A" },
|
|
3436
|
+
{ label: "Align vertical", key: "Shift+A" }
|
|
3437
|
+
]
|
|
3438
|
+
},
|
|
3439
|
+
{
|
|
3440
|
+
group: "View",
|
|
3441
|
+
items: [
|
|
3442
|
+
{ label: "Toggle snap", key: "G" },
|
|
3443
|
+
{ label: "Pan", key: "Mid+Drag" },
|
|
3444
|
+
{ label: "Zoom", key: "Scroll" },
|
|
3445
|
+
{ label: "Context menu", key: "RClick" }
|
|
3446
|
+
]
|
|
3447
|
+
}
|
|
3448
|
+
];
|
|
3449
|
+
}
|
|
3450
|
+
_createElements() {
|
|
3451
|
+
this.toggleBtn = document.createElement("div");
|
|
3452
|
+
this.toggleBtn.id = "helpToggle";
|
|
3453
|
+
this.toggleBtn.title = "단축키 (?)";
|
|
3454
|
+
this.toggleBtn.textContent = "?";
|
|
3455
|
+
this.container.appendChild(this.toggleBtn);
|
|
3456
|
+
this.overlay = document.createElement("div");
|
|
3457
|
+
this.overlay.id = "helpOverlay";
|
|
3458
|
+
const sectionsHtml = this.options.shortcuts.map(
|
|
3459
|
+
(group) => `
|
|
3460
|
+
<h4>${group.group}</h4>
|
|
3461
|
+
${group.items.map(
|
|
3462
|
+
(item) => `
|
|
3463
|
+
<div class="shortcut-item">
|
|
3464
|
+
<span>${item.label}</span>
|
|
3465
|
+
<span class="shortcut-key">${item.key}</span>
|
|
3466
|
+
</div>
|
|
3467
|
+
`
|
|
3468
|
+
).join("")}
|
|
3469
|
+
`
|
|
3470
|
+
).join("");
|
|
3471
|
+
this.overlay.innerHTML = `
|
|
3472
|
+
<h3>
|
|
3473
|
+
<span>Keyboard Shortcuts</span>
|
|
3474
|
+
<button class="close-btn" id="helpClose" title="Close">×</button>
|
|
3475
|
+
</h3>
|
|
3476
|
+
${sectionsHtml}
|
|
3477
|
+
`;
|
|
3478
|
+
this.container.appendChild(this.overlay);
|
|
3479
|
+
}
|
|
3480
|
+
_bindEvents() {
|
|
3481
|
+
this.toggleBtn.addEventListener("click", () => this.toggle());
|
|
3482
|
+
const closeBtn = this.overlay.querySelector("#helpClose");
|
|
3483
|
+
if (closeBtn) {
|
|
3484
|
+
closeBtn.addEventListener("click", (e) => {
|
|
3485
|
+
e.stopPropagation();
|
|
3486
|
+
this.close();
|
|
3487
|
+
});
|
|
2849
3488
|
}
|
|
3489
|
+
document.addEventListener("mousedown", (e) => {
|
|
3490
|
+
if (this.isVisible) {
|
|
3491
|
+
if (!this.overlay.contains(e.target) && !this.toggleBtn.contains(e.target)) {
|
|
3492
|
+
this.close();
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
});
|
|
3496
|
+
window.addEventListener("keydown", (e) => {
|
|
3497
|
+
if (e.key === "?" || e.shiftKey && e.key === "/") {
|
|
3498
|
+
if (!["INPUT", "TEXTAREA", "SELECT"].includes(document.activeElement.tagName)) {
|
|
3499
|
+
e.preventDefault();
|
|
3500
|
+
this.toggle();
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
if (e.key === "Escape" && this.isVisible) {
|
|
3504
|
+
this.close();
|
|
3505
|
+
}
|
|
3506
|
+
});
|
|
3507
|
+
}
|
|
3508
|
+
toggle() {
|
|
3509
|
+
if (this.isVisible) this.close();
|
|
3510
|
+
else this.open();
|
|
3511
|
+
}
|
|
3512
|
+
open() {
|
|
3513
|
+
this.isVisible = true;
|
|
3514
|
+
this.overlay.classList.add("visible");
|
|
3515
|
+
this.toggleBtn.classList.add("active");
|
|
3516
|
+
if (this.options.onToggle) this.options.onToggle(true);
|
|
3517
|
+
}
|
|
3518
|
+
close() {
|
|
3519
|
+
this.isVisible = false;
|
|
3520
|
+
this.overlay.classList.remove("visible");
|
|
3521
|
+
this.toggleBtn.classList.remove("active");
|
|
3522
|
+
if (this.options.onToggle) this.options.onToggle(false);
|
|
3523
|
+
}
|
|
3524
|
+
destroy() {
|
|
3525
|
+
var _a, _b;
|
|
3526
|
+
(_a = this.toggleBtn) == null ? void 0 : _a.remove();
|
|
3527
|
+
(_b = this.overlay) == null ? void 0 : _b.remove();
|
|
2850
3528
|
}
|
|
2851
3529
|
}
|
|
2852
3530
|
function setupDefaultContextMenu(contextMenu, { controller, graph, hooks }) {
|
|
@@ -2925,6 +3603,8 @@ function createGraphEditor(target, {
|
|
|
2925
3603
|
showMinimap = true,
|
|
2926
3604
|
enablePropertyPanel = true,
|
|
2927
3605
|
propertyPanelContainer = null,
|
|
3606
|
+
enableHelp = true,
|
|
3607
|
+
helpShortcuts = null,
|
|
2928
3608
|
setupDefaultContextMenu: setupDefaultContextMenu$1 = true,
|
|
2929
3609
|
setupContextMenu = null,
|
|
2930
3610
|
plugins = []
|
|
@@ -3058,43 +3738,31 @@ function createGraphEditor(target, {
|
|
|
3058
3738
|
propertyPanel.open(node);
|
|
3059
3739
|
});
|
|
3060
3740
|
}
|
|
3741
|
+
let helpOverlay = null;
|
|
3742
|
+
if (enableHelp) {
|
|
3743
|
+
helpOverlay = new HelpOverlay(container, {
|
|
3744
|
+
shortcuts: helpShortcuts
|
|
3745
|
+
});
|
|
3746
|
+
}
|
|
3061
3747
|
const runner = new Runner({ graph, registry, hooks });
|
|
3062
3748
|
graph.runner = runner;
|
|
3063
3749
|
graph.controller = controller;
|
|
3064
3750
|
hooks.on("runner:tick", ({ time, dt }) => {
|
|
3065
|
-
|
|
3066
|
-
selection: controller.selection,
|
|
3067
|
-
tempEdge: controller.connecting ? controller.renderTempEdge() : null,
|
|
3068
|
-
// 필요시 helper
|
|
3069
|
-
running: true,
|
|
3070
|
-
time,
|
|
3071
|
-
dt
|
|
3072
|
-
});
|
|
3073
|
-
htmlOverlay.draw(graph, controller.selection);
|
|
3751
|
+
controller.render(time);
|
|
3074
3752
|
});
|
|
3075
3753
|
hooks.on("runner:start", () => {
|
|
3076
|
-
|
|
3077
|
-
selection: controller.selection,
|
|
3078
|
-
tempEdge: controller.connecting ? controller.renderTempEdge() : null,
|
|
3079
|
-
running: true,
|
|
3080
|
-
time: performance.now(),
|
|
3081
|
-
dt: 0
|
|
3082
|
-
});
|
|
3083
|
-
htmlOverlay.draw(graph, controller.selection);
|
|
3754
|
+
controller.render(performance.now());
|
|
3084
3755
|
});
|
|
3085
3756
|
hooks.on("runner:stop", () => {
|
|
3086
|
-
|
|
3087
|
-
selection: controller.selection,
|
|
3088
|
-
tempEdge: controller.connecting ? controller.renderTempEdge() : null,
|
|
3089
|
-
running: false,
|
|
3090
|
-
time: performance.now(),
|
|
3091
|
-
dt: 0
|
|
3092
|
-
});
|
|
3093
|
-
htmlOverlay.draw(graph, controller.selection);
|
|
3757
|
+
controller.render(performance.now());
|
|
3094
3758
|
});
|
|
3095
3759
|
hooks.on("node:updated", () => {
|
|
3096
3760
|
controller.render();
|
|
3097
3761
|
});
|
|
3762
|
+
hooks.on("graph:deserialize", () => {
|
|
3763
|
+
renderer.setTransform({ scale: 1, offsetX: 0, offsetY: 0 });
|
|
3764
|
+
controller.render();
|
|
3765
|
+
});
|
|
3098
3766
|
if (setupDefaultContextMenu$1) {
|
|
3099
3767
|
setupDefaultContextMenu(contextMenu, { controller, graph, hooks });
|
|
3100
3768
|
}
|
|
@@ -3140,6 +3808,8 @@ function createGraphEditor(target, {
|
|
|
3140
3808
|
},
|
|
3141
3809
|
graph,
|
|
3142
3810
|
renderer,
|
|
3811
|
+
edgeRenderer,
|
|
3812
|
+
// Expose edge renderer for style changes
|
|
3143
3813
|
controller,
|
|
3144
3814
|
// Expose controller for snap-to-grid access
|
|
3145
3815
|
runner,
|
|
@@ -3158,6 +3828,14 @@ function createGraphEditor(target, {
|
|
|
3158
3828
|
render: () => controller.render(),
|
|
3159
3829
|
start: () => runner.start(),
|
|
3160
3830
|
stop: () => runner.stop(),
|
|
3831
|
+
setEdgeStyle: (style) => {
|
|
3832
|
+
renderer.setEdgeStyle(style);
|
|
3833
|
+
edgeRenderer.setEdgeStyle(style);
|
|
3834
|
+
},
|
|
3835
|
+
setExecutionMode: (mode) => {
|
|
3836
|
+
runner.setExecutionMode(mode);
|
|
3837
|
+
controller.render();
|
|
3838
|
+
},
|
|
3161
3839
|
destroy: () => {
|
|
3162
3840
|
runner.stop();
|
|
3163
3841
|
ro.disconnect();
|
|
@@ -3166,6 +3844,7 @@ function createGraphEditor(target, {
|
|
|
3166
3844
|
contextMenu.destroy();
|
|
3167
3845
|
if (propertyPanel) propertyPanel.destroy();
|
|
3168
3846
|
if (minimap) minimap.destroy();
|
|
3847
|
+
if (helpOverlay) helpOverlay.destroy();
|
|
3169
3848
|
}
|
|
3170
3849
|
};
|
|
3171
3850
|
if (autorun) runner.start();
|