html-overlay-node 0.1.6 → 0.1.10
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 +3 -3
- package/dist/html-overlay-node.es.js +997 -1014
- 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/index.css +391 -232
- package/package.json +9 -8
- package/readme.md +58 -364
- package/src/core/Edge.js +4 -2
- package/src/core/Graph.js +29 -5
- package/src/core/Node.js +27 -11
- package/src/core/Runner.js +201 -211
- package/src/defaults/contextMenu.js +102 -0
- package/src/defaults/index.js +6 -0
- package/src/index.js +85 -793
- package/src/interact/ContextMenu.js +5 -1
- package/src/interact/Controller.js +73 -46
- package/src/nodes/core.js +266 -0
- package/src/nodes/index.js +42 -0
- package/src/nodes/logic.js +60 -0
- package/src/nodes/math.js +99 -0
- package/src/nodes/util.js +176 -0
- package/src/nodes/value.js +100 -0
- package/src/render/CanvasRenderer.js +784 -604
- package/src/render/HtmlOverlay.js +15 -5
- package/src/render/hitTest.js +18 -9
- package/src/ui/HelpOverlay.js +158 -0
- package/src/ui/PropertyPanel.css +58 -27
- package/src/ui/PropertyPanel.js +441 -268
- package/src/utils/utils.js +4 -4
|
@@ -153,27 +153,36 @@ class Node {
|
|
|
153
153
|
* @param {string} [portType="data"] - Port type: "exec" or "data"
|
|
154
154
|
* @returns {Object} The created port
|
|
155
155
|
*/
|
|
156
|
+
/**
|
|
157
|
+
* Recalculate minimum size based on ports
|
|
158
|
+
*/
|
|
159
|
+
_updateMinSize() {
|
|
160
|
+
const HEADER_HEIGHT = 28;
|
|
161
|
+
const PORT_SPACING = 24;
|
|
162
|
+
const BOTTOM_PADDING = 10;
|
|
163
|
+
const inHeight = HEADER_HEIGHT + 10 + this.inputs.length * PORT_SPACING + BOTTOM_PADDING;
|
|
164
|
+
const outHeight = HEADER_HEIGHT + 10 + this.outputs.length * PORT_SPACING + BOTTOM_PADDING;
|
|
165
|
+
const minHeight = Math.max(inHeight, outHeight, 60);
|
|
166
|
+
if (this.size.height < minHeight) {
|
|
167
|
+
this.size.height = minHeight;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
156
170
|
addInput(name, datatype = "any", portType = "data") {
|
|
157
|
-
if (
|
|
158
|
-
throw new Error("Input port name must be a non-empty
|
|
171
|
+
if (typeof name !== "string" || portType === "data" && !name) {
|
|
172
|
+
throw new Error("Input port name must be a string (non-empty for data ports)");
|
|
159
173
|
}
|
|
160
174
|
const port = { id: randomUUID(), name, datatype, portType, dir: "in" };
|
|
161
175
|
this.inputs.push(port);
|
|
176
|
+
this._updateMinSize();
|
|
162
177
|
return port;
|
|
163
178
|
}
|
|
164
|
-
/**
|
|
165
|
-
* Add an output port to this node
|
|
166
|
-
* @param {string} name - Port name
|
|
167
|
-
* @param {string} [datatype="any"] - Data type for the port
|
|
168
|
-
* @param {string} [portType="data"] - Port type: "exec" or "data"
|
|
169
|
-
* @returns {Object} The created port
|
|
170
|
-
*/
|
|
171
179
|
addOutput(name, datatype = "any", portType = "data") {
|
|
172
|
-
if (
|
|
173
|
-
throw new Error("Output port name must be a non-empty
|
|
180
|
+
if (typeof name !== "string" || portType === "data" && !name) {
|
|
181
|
+
throw new Error("Output port name must be a string (non-empty for data ports)");
|
|
174
182
|
}
|
|
175
183
|
const port = { id: randomUUID(), name, datatype, portType, dir: "out" };
|
|
176
184
|
this.outputs.push(port);
|
|
185
|
+
this._updateMinSize();
|
|
177
186
|
return port;
|
|
178
187
|
}
|
|
179
188
|
}
|
|
@@ -188,8 +197,8 @@ class Edge {
|
|
|
188
197
|
* @param {string} options.toPort - Target port ID
|
|
189
198
|
*/
|
|
190
199
|
constructor({ id, fromNode, fromPort, toNode, toPort }) {
|
|
191
|
-
if (
|
|
192
|
-
throw new Error("Edge requires fromNode, fromPort, toNode, and toPort");
|
|
200
|
+
if (fromNode == null || fromPort == null || toNode == null || toPort == null) {
|
|
201
|
+
throw new Error("Edge requires fromNode, fromPort, toNode, and toPort (null/undefined not allowed)");
|
|
193
202
|
}
|
|
194
203
|
this.id = id ?? randomUUID();
|
|
195
204
|
this.fromNode = fromNode;
|
|
@@ -333,11 +342,12 @@ class Graph {
|
|
|
333
342
|
const available = Array.from(this.registry.types.keys()).join(", ") || "none";
|
|
334
343
|
throw new Error(`Unknown node type: "${type}". Available types: ${available}`);
|
|
335
344
|
}
|
|
345
|
+
const height = opts.height || ((_a = def.size) == null ? void 0 : _a.h) || this._calculateDefaultNodeHeight(def);
|
|
336
346
|
const node = new Node({
|
|
337
347
|
type,
|
|
338
348
|
title: def.title,
|
|
339
|
-
width: (
|
|
340
|
-
height
|
|
349
|
+
width: opts.width || ((_b = def.size) == null ? void 0 : _b.w) || 140,
|
|
350
|
+
height,
|
|
341
351
|
...opts
|
|
342
352
|
});
|
|
343
353
|
for (const i of def.inputs || []) node.addInput(i.name, i.datatype, i.portType || "data");
|
|
@@ -479,8 +489,12 @@ class Graph {
|
|
|
479
489
|
}
|
|
480
490
|
fromJSON(json) {
|
|
481
491
|
var _a, _b, _c;
|
|
482
|
-
this.clear();
|
|
492
|
+
this.nodes.clear();
|
|
493
|
+
this.edges.clear();
|
|
483
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;
|
|
484
498
|
const node = new Node({
|
|
485
499
|
id: nd.id,
|
|
486
500
|
type: nd.type,
|
|
@@ -488,9 +502,8 @@ class Graph {
|
|
|
488
502
|
x: nd.x,
|
|
489
503
|
y: nd.y,
|
|
490
504
|
width: nd.w,
|
|
491
|
-
height
|
|
505
|
+
height
|
|
492
506
|
});
|
|
493
|
-
const def = (_b = (_a = this.registry) == null ? void 0 : _a.types) == null ? void 0 : _b.get(nd.type);
|
|
494
507
|
if (def == null ? void 0 : def.onCreate) {
|
|
495
508
|
def.onCreate(node);
|
|
496
509
|
}
|
|
@@ -516,19 +529,35 @@ class Graph {
|
|
|
516
529
|
(_c = this.hooks) == null ? void 0 : _c.emit("graph:deserialize", json);
|
|
517
530
|
return this;
|
|
518
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 padding = 8;
|
|
539
|
+
const portSpacing = 20;
|
|
540
|
+
let h = headerHeight + padding + maxPorts * portSpacing + padding;
|
|
541
|
+
if (def.html) h += 16;
|
|
542
|
+
return Math.max(h, 40);
|
|
543
|
+
}
|
|
519
544
|
}
|
|
520
545
|
function portRect(node, port, idx, dir) {
|
|
521
|
-
const {
|
|
546
|
+
const {
|
|
547
|
+
x: nx,
|
|
548
|
+
y: ny,
|
|
549
|
+
w: width,
|
|
550
|
+
h: height
|
|
551
|
+
} = node.computed || {
|
|
522
552
|
x: node.pos.x,
|
|
523
553
|
y: node.pos.y,
|
|
524
554
|
w: node.size.width,
|
|
525
555
|
h: node.size.height
|
|
526
556
|
};
|
|
527
|
-
const
|
|
528
|
-
const
|
|
529
|
-
const
|
|
530
|
-
const
|
|
531
|
-
const y = ny + headerHeight + spacing * (idx + 1);
|
|
557
|
+
const headerHeight = 26;
|
|
558
|
+
const padding = 8;
|
|
559
|
+
const portSpacing = 20;
|
|
560
|
+
const y = ny + headerHeight + padding + idx * portSpacing + portSpacing / 2;
|
|
532
561
|
const portWidth = 12;
|
|
533
562
|
const portHeight = 12;
|
|
534
563
|
if (dir === "in") {
|
|
@@ -551,32 +580,20 @@ const _CanvasRenderer = class _CanvasRenderer {
|
|
|
551
580
|
this.edgeStyle = edgeStyle;
|
|
552
581
|
this.theme = Object.assign(
|
|
553
582
|
{
|
|
554
|
-
bg: "#
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
title: "#1f1f24",
|
|
563
|
-
// Darker header
|
|
564
|
-
text: "#e4e4e7",
|
|
565
|
-
// Softer white
|
|
566
|
-
textMuted: "#a1a1aa",
|
|
567
|
-
// Muted text
|
|
568
|
-
port: "#6366f1",
|
|
569
|
-
// Indigo for data ports
|
|
583
|
+
bg: "#0e0e16",
|
|
584
|
+
grid: "#1c1c2c",
|
|
585
|
+
node: "rgba(22, 22, 34, 0.9)",
|
|
586
|
+
nodeBorder: "rgba(255, 255, 255, 0.08)",
|
|
587
|
+
title: "rgba(28, 28, 42, 0.95)",
|
|
588
|
+
text: "#f5f5f7",
|
|
589
|
+
textMuted: "#8e8eaf",
|
|
590
|
+
port: "#4f46e5",
|
|
570
591
|
portExec: "#10b981",
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
// Neutral edge color
|
|
574
|
-
edgeActive: "#8b5cf6",
|
|
575
|
-
// Purple for active
|
|
592
|
+
edge: "rgba(255, 255, 255, 0.12)",
|
|
593
|
+
edgeActive: "#6366f1",
|
|
576
594
|
accent: "#6366f1",
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
// Brighter accent
|
|
595
|
+
accentBright: "#818cf8",
|
|
596
|
+
accentGlow: "rgba(99, 102, 241, 0.25)"
|
|
580
597
|
},
|
|
581
598
|
theme
|
|
582
599
|
);
|
|
@@ -591,31 +608,33 @@ const _CanvasRenderer = class _CanvasRenderer {
|
|
|
591
608
|
this.canvas.width = w;
|
|
592
609
|
this.canvas.height = h;
|
|
593
610
|
}
|
|
594
|
-
setTransform({
|
|
595
|
-
|
|
596
|
-
offsetX = this.offsetX,
|
|
597
|
-
offsetY = this.offsetY
|
|
598
|
-
} = {}) {
|
|
611
|
+
setTransform({ scale = this.scale, offsetX = this.offsetX, offsetY = this.offsetY } = {}) {
|
|
612
|
+
var _a;
|
|
599
613
|
this.scale = Math.min(this.maxScale, Math.max(this.minScale, scale));
|
|
600
614
|
this.offsetX = offsetX;
|
|
601
615
|
this.offsetY = offsetY;
|
|
616
|
+
(_a = this._onTransformChange) == null ? void 0 : _a.call(this);
|
|
617
|
+
}
|
|
618
|
+
setTransformChangeCallback(callback) {
|
|
619
|
+
this._onTransformChange = callback;
|
|
602
620
|
}
|
|
603
621
|
panBy(dx, dy) {
|
|
622
|
+
var _a;
|
|
604
623
|
this.offsetX += dx;
|
|
605
624
|
this.offsetY += dy;
|
|
625
|
+
(_a = this._onTransformChange) == null ? void 0 : _a.call(this);
|
|
606
626
|
}
|
|
607
627
|
zoomAt(factor, cx, cy) {
|
|
628
|
+
var _a;
|
|
608
629
|
const prev = this.scale;
|
|
609
|
-
const next = Math.min(
|
|
610
|
-
this.maxScale,
|
|
611
|
-
Math.max(this.minScale, prev * factor)
|
|
612
|
-
);
|
|
630
|
+
const next = Math.min(this.maxScale, Math.max(this.minScale, prev * factor));
|
|
613
631
|
if (next === prev) return;
|
|
614
632
|
const wx = (cx - this.offsetX) / prev;
|
|
615
633
|
const wy = (cy - this.offsetY) / prev;
|
|
616
634
|
this.offsetX = cx - wx * next;
|
|
617
635
|
this.offsetY = cy - wy * next;
|
|
618
636
|
this.scale = next;
|
|
637
|
+
(_a = this._onTransformChange) == null ? void 0 : _a.call(this);
|
|
619
638
|
}
|
|
620
639
|
screenToWorld(x, y) {
|
|
621
640
|
return {
|
|
@@ -631,36 +650,30 @@ const _CanvasRenderer = class _CanvasRenderer {
|
|
|
631
650
|
}
|
|
632
651
|
_applyTransform() {
|
|
633
652
|
const { ctx } = this;
|
|
634
|
-
ctx.setTransform(
|
|
653
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
654
|
+
ctx.translate(this.offsetX, this.offsetY);
|
|
655
|
+
ctx.scale(this.scale, this.scale);
|
|
635
656
|
}
|
|
636
657
|
_resetTransform() {
|
|
637
658
|
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
638
659
|
}
|
|
639
660
|
// ── Drawing ────────────────────────────────────────────────────────────────
|
|
640
|
-
_drawArrowhead(x1, y1, x2, y2, size =
|
|
661
|
+
_drawArrowhead(x1, y1, x2, y2, size = 8) {
|
|
641
662
|
const { ctx } = this;
|
|
642
663
|
const s = size / this.scale;
|
|
643
664
|
const ang = Math.atan2(y2 - y1, x2 - x1);
|
|
644
665
|
ctx.beginPath();
|
|
645
666
|
ctx.moveTo(x2, y2);
|
|
646
|
-
ctx.lineTo(
|
|
647
|
-
|
|
648
|
-
y2 - s * Math.sin(ang - Math.PI / 6)
|
|
649
|
-
);
|
|
650
|
-
ctx.lineTo(
|
|
651
|
-
x2 - s * Math.cos(ang + Math.PI / 6),
|
|
652
|
-
y2 - s * Math.sin(ang + Math.PI / 6)
|
|
653
|
-
);
|
|
667
|
+
ctx.lineTo(x2 - s * Math.cos(ang - Math.PI / 6), y2 - s * Math.sin(ang - Math.PI / 6));
|
|
668
|
+
ctx.lineTo(x2 - s * Math.cos(ang + Math.PI / 6), y2 - s * Math.sin(ang + Math.PI / 6));
|
|
654
669
|
ctx.closePath();
|
|
655
670
|
ctx.fill();
|
|
656
671
|
}
|
|
657
672
|
_drawScreenText(text, lx, ly, {
|
|
658
|
-
fontPx =
|
|
673
|
+
fontPx = 11,
|
|
659
674
|
color = this.theme.text,
|
|
660
675
|
align = "left",
|
|
661
|
-
baseline = "alphabetic"
|
|
662
|
-
dpr = 1
|
|
663
|
-
// 추후 devicePixelRatio 도입
|
|
676
|
+
baseline = "alphabetic"
|
|
664
677
|
} = {}) {
|
|
665
678
|
const { ctx } = this;
|
|
666
679
|
const { x: sx, y: sy } = this.worldToScreen(lx, ly);
|
|
@@ -668,7 +681,7 @@ const _CanvasRenderer = class _CanvasRenderer {
|
|
|
668
681
|
this._resetTransform();
|
|
669
682
|
const px = Math.round(sx) + 0.5;
|
|
670
683
|
const py = Math.round(sy) + 0.5;
|
|
671
|
-
ctx.font = `${fontPx * this.scale}px system-ui`;
|
|
684
|
+
ctx.font = `${fontPx * this.scale}px "Inter", system-ui, sans-serif`;
|
|
672
685
|
ctx.fillStyle = color;
|
|
673
686
|
ctx.textAlign = align;
|
|
674
687
|
ctx.textBaseline = baseline;
|
|
@@ -681,38 +694,47 @@ const _CanvasRenderer = class _CanvasRenderer {
|
|
|
681
694
|
ctx.fillStyle = theme.bg;
|
|
682
695
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
683
696
|
this._applyTransform();
|
|
684
|
-
ctx.strokeStyle = this._rgba(theme.grid, 0.35);
|
|
685
|
-
ctx.lineWidth = 1 / scale;
|
|
686
|
-
const base = 20;
|
|
687
|
-
const step = base;
|
|
688
697
|
const x0 = -offsetX / scale;
|
|
689
698
|
const y0 = -offsetY / scale;
|
|
690
699
|
const x1 = (canvas.width - offsetX) / scale;
|
|
691
700
|
const y1 = (canvas.height - offsetY) / scale;
|
|
692
|
-
const
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
701
|
+
const minorStep = 24;
|
|
702
|
+
const majorStep = 120;
|
|
703
|
+
const minorR = 1 / scale;
|
|
704
|
+
const majorR = 1.5 / scale;
|
|
705
|
+
const startX = Math.floor(x0 / minorStep) * minorStep;
|
|
706
|
+
const startY = Math.floor(y0 / minorStep) * minorStep;
|
|
707
|
+
ctx.fillStyle = this._rgba(theme.grid, 0.7);
|
|
708
|
+
for (let gx = startX; gx <= x1; gx += minorStep) {
|
|
709
|
+
for (let gy = startY; gy <= y1; gy += minorStep) {
|
|
710
|
+
const isMajorX = Math.round(gx / majorStep) * majorStep === Math.round(gx);
|
|
711
|
+
const isMajorY = Math.round(gy / majorStep) * majorStep === Math.round(gy);
|
|
712
|
+
if (isMajorX && isMajorY) continue;
|
|
713
|
+
ctx.beginPath();
|
|
714
|
+
ctx.arc(gx, gy, minorR, 0, Math.PI * 2);
|
|
715
|
+
ctx.fill();
|
|
716
|
+
}
|
|
698
717
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
718
|
+
const majorStartX = Math.floor(x0 / majorStep) * majorStep;
|
|
719
|
+
const majorStartY = Math.floor(y0 / majorStep) * majorStep;
|
|
720
|
+
ctx.fillStyle = this._rgba(theme.grid, 1);
|
|
721
|
+
for (let gx = majorStartX; gx <= x1; gx += majorStep) {
|
|
722
|
+
for (let gy = majorStartY; gy <= y1; gy += majorStep) {
|
|
723
|
+
ctx.beginPath();
|
|
724
|
+
ctx.arc(gx, gy, majorR, 0, Math.PI * 2);
|
|
725
|
+
ctx.fill();
|
|
726
|
+
}
|
|
702
727
|
}
|
|
703
|
-
ctx.stroke();
|
|
704
728
|
this._resetTransform();
|
|
705
729
|
}
|
|
706
730
|
draw(graph, {
|
|
707
731
|
selection = /* @__PURE__ */ new Set(),
|
|
708
732
|
tempEdge = null,
|
|
709
|
-
running = false,
|
|
710
733
|
time = performance.now(),
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
activeEdges = /* @__PURE__ */ new Set()
|
|
734
|
+
activeEdges = /* @__PURE__ */ new Set(),
|
|
735
|
+
drawEdges = true
|
|
714
736
|
} = {}) {
|
|
715
|
-
var _a, _b, _c, _d
|
|
737
|
+
var _a, _b, _c, _d;
|
|
716
738
|
graph.updateWorldTransforms();
|
|
717
739
|
this.drawGrid();
|
|
718
740
|
const { ctx, theme } = this;
|
|
@@ -722,43 +744,50 @@ const _CanvasRenderer = class _CanvasRenderer {
|
|
|
722
744
|
if (n.type === "core/Group") {
|
|
723
745
|
const sel = selection.has(n.id);
|
|
724
746
|
const def = (_b = (_a = this.registry) == null ? void 0 : _a.types) == null ? void 0 : _b.get(n.type);
|
|
725
|
-
if (def == null ? void 0 : def.onDraw) def.onDraw(n, { ctx, theme });
|
|
747
|
+
if (def == null ? void 0 : def.onDraw) def.onDraw(n, { ctx, theme, renderer: this });
|
|
726
748
|
else this._drawNode(n, sel);
|
|
727
749
|
}
|
|
728
750
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
751
|
+
if (drawEdges) {
|
|
752
|
+
ctx.lineWidth = 1.5 / this.scale;
|
|
753
|
+
for (const e of graph.edges.values()) {
|
|
754
|
+
const isActive = activeEdges && activeEdges.has(e.id);
|
|
755
|
+
if (isActive) {
|
|
756
|
+
ctx.save();
|
|
757
|
+
ctx.shadowColor = this.theme.edgeActive;
|
|
758
|
+
ctx.shadowBlur = 8 / this.scale;
|
|
759
|
+
ctx.strokeStyle = this.theme.edgeActive;
|
|
760
|
+
ctx.lineWidth = 2 / this.scale;
|
|
761
|
+
ctx.setLineDash([]);
|
|
762
|
+
this._drawEdge(graph, e);
|
|
763
|
+
ctx.restore();
|
|
764
|
+
const dotT = time / 1e3 * 1.2 % 1;
|
|
765
|
+
const dotPos = this._getEdgeDotPosition(graph, e, dotT);
|
|
766
|
+
if (dotPos) {
|
|
767
|
+
ctx.save();
|
|
768
|
+
ctx.fillStyle = "#ffffff";
|
|
769
|
+
ctx.shadowColor = this.theme.edgeActive;
|
|
770
|
+
ctx.shadowBlur = 10 / this.scale;
|
|
771
|
+
ctx.beginPath();
|
|
772
|
+
ctx.arc(dotPos.x, dotPos.y, 3 / this.scale, 0, Math.PI * 2);
|
|
773
|
+
ctx.fill();
|
|
774
|
+
ctx.restore();
|
|
775
|
+
}
|
|
776
|
+
} else {
|
|
777
|
+
ctx.setLineDash([]);
|
|
778
|
+
ctx.strokeStyle = theme.edge;
|
|
779
|
+
ctx.lineWidth = 1.5 / this.scale;
|
|
780
|
+
this._drawEdge(graph, e);
|
|
781
|
+
}
|
|
754
782
|
}
|
|
755
|
-
this._drawEdge(graph, e);
|
|
756
783
|
}
|
|
757
784
|
if (tempEdge) {
|
|
758
785
|
const a = this.screenToWorld(tempEdge.x1, tempEdge.y1);
|
|
759
786
|
const b = this.screenToWorld(tempEdge.x2, tempEdge.y2);
|
|
760
787
|
const prevDash = this.ctx.getLineDash();
|
|
761
|
-
this.ctx.setLineDash([
|
|
788
|
+
this.ctx.setLineDash([5 / this.scale, 5 / this.scale]);
|
|
789
|
+
this.ctx.strokeStyle = this._rgba(this.theme.accentBright, 0.7);
|
|
790
|
+
this.ctx.lineWidth = 1.5 / this.scale;
|
|
762
791
|
let ptsForArrow = null;
|
|
763
792
|
if (this.edgeStyle === "line") {
|
|
764
793
|
this._drawLine(a.x, a.y, b.x, b.y);
|
|
@@ -773,9 +802,8 @@ const _CanvasRenderer = class _CanvasRenderer {
|
|
|
773
802
|
if (ptsForArrow && ptsForArrow.length >= 2) {
|
|
774
803
|
const p1 = ptsForArrow[ptsForArrow.length - 2];
|
|
775
804
|
const p2 = ptsForArrow[ptsForArrow.length - 1];
|
|
776
|
-
this.ctx.fillStyle = this.theme.
|
|
777
|
-
this.
|
|
778
|
-
this._drawArrowhead(p1.x, p1.y, p2.x, p2.y, 12);
|
|
805
|
+
this.ctx.fillStyle = this.theme.accentBright;
|
|
806
|
+
this._drawArrowhead(p1.x, p1.y, p2.x, p2.y, 10);
|
|
779
807
|
}
|
|
780
808
|
}
|
|
781
809
|
for (const n of graph.nodes.values()) {
|
|
@@ -783,14 +811,10 @@ const _CanvasRenderer = class _CanvasRenderer {
|
|
|
783
811
|
const sel = selection.has(n.id);
|
|
784
812
|
const def = (_d = (_c = this.registry) == null ? void 0 : _c.types) == null ? void 0 : _d.get(n.type);
|
|
785
813
|
const hasHtmlOverlay = !!(def == null ? void 0 : def.html);
|
|
786
|
-
this._drawNode(n, sel, hasHtmlOverlay);
|
|
787
|
-
if (def == null ? void 0 : def.onDraw)
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
for (const n of graph.nodes.values()) {
|
|
791
|
-
if (n.type !== "core/Group") {
|
|
792
|
-
const def = (_f = (_e = this.registry) == null ? void 0 : _e.types) == null ? void 0 : _f.get(n.type);
|
|
793
|
-
const hasHtmlOverlay = !!(def == null ? void 0 : def.html);
|
|
814
|
+
this._drawNode(n, sel, !hasHtmlOverlay ? true : false);
|
|
815
|
+
if (def == null ? void 0 : def.onDraw) {
|
|
816
|
+
def.onDraw(n, { ctx, theme, renderer: this });
|
|
817
|
+
}
|
|
794
818
|
if (hasHtmlOverlay) {
|
|
795
819
|
this._drawPorts(n);
|
|
796
820
|
}
|
|
@@ -808,40 +832,58 @@ const _CanvasRenderer = class _CanvasRenderer {
|
|
|
808
832
|
return `rgba(${r},${g},${b},${a})`;
|
|
809
833
|
}
|
|
810
834
|
_drawNode(node, selected, skipPorts = false) {
|
|
835
|
+
var _a, _b;
|
|
811
836
|
const { ctx, theme } = this;
|
|
812
|
-
const r =
|
|
837
|
+
const r = 2;
|
|
813
838
|
const { x, y, w, h } = node.computed;
|
|
814
|
-
|
|
839
|
+
const headerH = 26;
|
|
840
|
+
const typeDef = (_b = (_a = this.registry) == null ? void 0 : _a.types) == null ? void 0 : _b.get(node.type);
|
|
841
|
+
const categoryColor = node.color || (typeDef == null ? void 0 : typeDef.color) || theme.accent;
|
|
842
|
+
if (selected) {
|
|
815
843
|
ctx.save();
|
|
816
|
-
ctx.shadowColor =
|
|
817
|
-
ctx.shadowBlur =
|
|
818
|
-
ctx.
|
|
819
|
-
ctx.
|
|
820
|
-
|
|
821
|
-
ctx
|
|
844
|
+
ctx.shadowColor = theme.accentGlow;
|
|
845
|
+
ctx.shadowBlur = 10 / this.scale;
|
|
846
|
+
ctx.strokeStyle = theme.accentBright;
|
|
847
|
+
ctx.lineWidth = 2 / this.scale;
|
|
848
|
+
const pad = 1.5 / this.scale;
|
|
849
|
+
roundRect(ctx, x - pad, y - pad, w + pad * 2, h + pad * 2, r + pad);
|
|
850
|
+
ctx.stroke();
|
|
822
851
|
ctx.restore();
|
|
823
852
|
}
|
|
853
|
+
ctx.save();
|
|
854
|
+
ctx.shadowColor = "rgba(0,0,0,0.7)";
|
|
855
|
+
ctx.shadowBlur = 20 / this.scale;
|
|
856
|
+
ctx.shadowOffsetY = 6 / this.scale;
|
|
857
|
+
ctx.fillStyle = theme.node;
|
|
858
|
+
roundRect(ctx, x, y, w, h, r);
|
|
859
|
+
ctx.fill();
|
|
860
|
+
ctx.restore();
|
|
824
861
|
ctx.fillStyle = theme.node;
|
|
825
862
|
ctx.strokeStyle = selected ? theme.accentBright : theme.nodeBorder;
|
|
826
|
-
ctx.lineWidth =
|
|
863
|
+
ctx.lineWidth = 1 / this.scale;
|
|
827
864
|
roundRect(ctx, x, y, w, h, r);
|
|
828
865
|
ctx.fill();
|
|
829
866
|
ctx.stroke();
|
|
830
867
|
ctx.fillStyle = theme.title;
|
|
831
|
-
roundRect(ctx, x, y, w,
|
|
868
|
+
roundRect(ctx, x, y, w, headerH, { tl: r, tr: r, br: 0, bl: 0 });
|
|
832
869
|
ctx.fill();
|
|
833
|
-
ctx.
|
|
834
|
-
ctx.
|
|
870
|
+
ctx.save();
|
|
871
|
+
ctx.globalCompositeOperation = "source-atop";
|
|
872
|
+
ctx.fillStyle = categoryColor;
|
|
873
|
+
ctx.globalAlpha = 0.25;
|
|
874
|
+
ctx.fillRect(x, y, w, headerH);
|
|
875
|
+
ctx.restore();
|
|
876
|
+
ctx.strokeStyle = selected ? this._rgba(theme.accentBright, 0.3) : this._rgba(theme.nodeBorder, 0.6);
|
|
877
|
+
ctx.lineWidth = 1 / this.scale;
|
|
835
878
|
ctx.beginPath();
|
|
836
|
-
ctx.moveTo(x +
|
|
837
|
-
ctx.lineTo(x + w
|
|
838
|
-
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
|
|
839
|
-
ctx.lineTo(x + w, y + 24);
|
|
840
|
-
ctx.moveTo(x, y + 24);
|
|
841
|
-
ctx.lineTo(x, y + r);
|
|
842
|
-
ctx.quadraticCurveTo(x, y, x + r, y);
|
|
879
|
+
ctx.moveTo(x, y + headerH);
|
|
880
|
+
ctx.lineTo(x + w, y + headerH);
|
|
843
881
|
ctx.stroke();
|
|
844
|
-
|
|
882
|
+
ctx.fillStyle = categoryColor;
|
|
883
|
+
ctx.beginPath();
|
|
884
|
+
ctx.roundRect(x, y, w, 2.5 / this.scale, { tl: r, tr: r, br: 0, bl: 0 });
|
|
885
|
+
ctx.fill();
|
|
886
|
+
this._drawScreenText(node.title, x + 10, y + headerH / 2, {
|
|
845
887
|
fontPx: _CanvasRenderer.FONT_SIZE,
|
|
846
888
|
color: theme.text,
|
|
847
889
|
baseline: "middle",
|
|
@@ -852,97 +894,111 @@ const _CanvasRenderer = class _CanvasRenderer {
|
|
|
852
894
|
const rct = portRect(node, p, i, "in");
|
|
853
895
|
const cx = rct.x + rct.w / 2;
|
|
854
896
|
const cy = rct.y + rct.h / 2;
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
ctx.stroke();
|
|
864
|
-
} else {
|
|
865
|
-
ctx.fillStyle = theme.port;
|
|
866
|
-
ctx.strokeStyle = "rgba(99, 102, 241, 0.3)";
|
|
867
|
-
ctx.lineWidth = 2 / this.scale;
|
|
868
|
-
ctx.beginPath();
|
|
869
|
-
ctx.arc(cx, cy, 5, 0, Math.PI * 2);
|
|
870
|
-
ctx.fill();
|
|
871
|
-
ctx.stroke();
|
|
897
|
+
this._drawPortShape(cx, cy, p.portType);
|
|
898
|
+
if (p.name) {
|
|
899
|
+
this._drawScreenText(p.name, cx + 10, cy, {
|
|
900
|
+
fontPx: 10,
|
|
901
|
+
color: theme.textMuted,
|
|
902
|
+
baseline: "middle",
|
|
903
|
+
align: "left"
|
|
904
|
+
});
|
|
872
905
|
}
|
|
873
906
|
});
|
|
874
907
|
node.outputs.forEach((p, i) => {
|
|
875
908
|
const rct = portRect(node, p, i, "out");
|
|
876
909
|
const cx = rct.x + rct.w / 2;
|
|
877
910
|
const cy = rct.y + rct.h / 2;
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
ctx.stroke();
|
|
887
|
-
} else {
|
|
888
|
-
ctx.fillStyle = theme.port;
|
|
889
|
-
ctx.strokeStyle = "rgba(99, 102, 241, 0.3)";
|
|
890
|
-
ctx.lineWidth = 2 / this.scale;
|
|
891
|
-
ctx.beginPath();
|
|
892
|
-
ctx.arc(cx, cy, 5, 0, Math.PI * 2);
|
|
893
|
-
ctx.fill();
|
|
894
|
-
ctx.stroke();
|
|
911
|
+
this._drawPortShape(cx, cy, p.portType);
|
|
912
|
+
if (p.name) {
|
|
913
|
+
this._drawScreenText(p.name, cx - 10, cy, {
|
|
914
|
+
fontPx: 10,
|
|
915
|
+
color: theme.textMuted,
|
|
916
|
+
baseline: "middle",
|
|
917
|
+
align: "right"
|
|
918
|
+
});
|
|
895
919
|
}
|
|
896
920
|
});
|
|
897
921
|
}
|
|
898
|
-
|
|
922
|
+
_drawPortShape(cx, cy, portType) {
|
|
899
923
|
const { ctx, theme } = this;
|
|
924
|
+
if (portType === "exec") {
|
|
925
|
+
const s = 5 / this.scale;
|
|
926
|
+
ctx.save();
|
|
927
|
+
ctx.fillStyle = theme.portExec;
|
|
928
|
+
ctx.strokeStyle = this._rgba(theme.portExec, 0.4);
|
|
929
|
+
ctx.lineWidth = 2 / this.scale;
|
|
930
|
+
ctx.beginPath();
|
|
931
|
+
ctx.moveTo(cx, cy - s);
|
|
932
|
+
ctx.lineTo(cx + s, cy);
|
|
933
|
+
ctx.lineTo(cx, cy + s);
|
|
934
|
+
ctx.lineTo(cx - s, cy);
|
|
935
|
+
ctx.closePath();
|
|
936
|
+
ctx.fill();
|
|
937
|
+
ctx.stroke();
|
|
938
|
+
ctx.restore();
|
|
939
|
+
} else {
|
|
940
|
+
ctx.save();
|
|
941
|
+
ctx.strokeStyle = this._rgba(theme.port, 0.35);
|
|
942
|
+
ctx.lineWidth = 3 / this.scale;
|
|
943
|
+
ctx.beginPath();
|
|
944
|
+
ctx.arc(cx, cy, 5 / this.scale, 0, Math.PI * 2);
|
|
945
|
+
ctx.stroke();
|
|
946
|
+
ctx.fillStyle = theme.port;
|
|
947
|
+
ctx.beginPath();
|
|
948
|
+
ctx.arc(cx, cy, 3.5 / this.scale, 0, Math.PI * 2);
|
|
949
|
+
ctx.fill();
|
|
950
|
+
ctx.restore();
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
_drawPorts(node) {
|
|
900
954
|
node.inputs.forEach((p, i) => {
|
|
901
955
|
const rct = portRect(node, p, i, "in");
|
|
902
956
|
const cx = rct.x + rct.w / 2;
|
|
903
957
|
const cy = rct.y + rct.h / 2;
|
|
904
|
-
|
|
905
|
-
const portSize = 8;
|
|
906
|
-
ctx.fillStyle = theme.portExec;
|
|
907
|
-
ctx.strokeStyle = "rgba(16, 185, 129, 0.3)";
|
|
908
|
-
ctx.lineWidth = 2 / this.scale;
|
|
909
|
-
ctx.beginPath();
|
|
910
|
-
ctx.roundRect(cx - portSize / 2, cy - portSize / 2, portSize, portSize, 2);
|
|
911
|
-
ctx.fill();
|
|
912
|
-
ctx.stroke();
|
|
913
|
-
} else {
|
|
914
|
-
ctx.fillStyle = theme.port;
|
|
915
|
-
ctx.strokeStyle = "rgba(99, 102, 241, 0.3)";
|
|
916
|
-
ctx.lineWidth = 2 / this.scale;
|
|
917
|
-
ctx.beginPath();
|
|
918
|
-
ctx.arc(cx, cy, 5, 0, Math.PI * 2);
|
|
919
|
-
ctx.fill();
|
|
920
|
-
}
|
|
958
|
+
this._drawPortShape(cx, cy, p.portType);
|
|
921
959
|
});
|
|
922
960
|
node.outputs.forEach((p, i) => {
|
|
923
961
|
const rct = portRect(node, p, i, "out");
|
|
924
962
|
const cx = rct.x + rct.w / 2;
|
|
925
963
|
const cy = rct.y + rct.h / 2;
|
|
926
|
-
|
|
927
|
-
const portSize = 8;
|
|
928
|
-
ctx.fillStyle = theme.portExec;
|
|
929
|
-
ctx.strokeStyle = "rgba(16, 185, 129, 0.3)";
|
|
930
|
-
ctx.lineWidth = 2 / this.scale;
|
|
931
|
-
ctx.beginPath();
|
|
932
|
-
ctx.roundRect(cx - portSize / 2, cy - portSize / 2, portSize, portSize, 2);
|
|
933
|
-
ctx.fill();
|
|
934
|
-
ctx.stroke();
|
|
935
|
-
} else {
|
|
936
|
-
ctx.fillStyle = theme.port;
|
|
937
|
-
ctx.strokeStyle = "rgba(99, 102, 241, 0.3)";
|
|
938
|
-
ctx.lineWidth = 2 / this.scale;
|
|
939
|
-
ctx.beginPath();
|
|
940
|
-
ctx.arc(cx, cy, 5, 0, Math.PI * 2);
|
|
941
|
-
ctx.fill();
|
|
942
|
-
ctx.stroke();
|
|
943
|
-
}
|
|
964
|
+
this._drawPortShape(cx, cy, p.portType);
|
|
944
965
|
});
|
|
945
966
|
}
|
|
967
|
+
/** Selection border for HTML overlay nodes, drawn on the edge canvas */
|
|
968
|
+
_drawHtmlSelectionBorder(node) {
|
|
969
|
+
const { ctx, theme } = this;
|
|
970
|
+
const { x, y, w, h } = node.computed;
|
|
971
|
+
const r = 2;
|
|
972
|
+
const pad = 1.5 / this.scale;
|
|
973
|
+
ctx.save();
|
|
974
|
+
ctx.shadowColor = theme.accentGlow;
|
|
975
|
+
ctx.shadowBlur = 14 / this.scale;
|
|
976
|
+
ctx.strokeStyle = theme.accentBright;
|
|
977
|
+
ctx.lineWidth = 1.5 / this.scale;
|
|
978
|
+
roundRect(ctx, x - pad, y - pad, w + pad * 2, h + pad * 2, r);
|
|
979
|
+
ctx.stroke();
|
|
980
|
+
ctx.restore();
|
|
981
|
+
}
|
|
982
|
+
/** Rotating dashed border drawn on the edge canvas for executing nodes */
|
|
983
|
+
_drawActiveNodeBorder(node, time) {
|
|
984
|
+
const { ctx, theme } = this;
|
|
985
|
+
const { x, y, w, h } = node.computed;
|
|
986
|
+
const r = 2;
|
|
987
|
+
const pad = 2.5 / this.scale;
|
|
988
|
+
const dashLen = 8 / this.scale;
|
|
989
|
+
const gapLen = 6 / this.scale;
|
|
990
|
+
const offset = -(time / 1e3) * (50 / this.scale);
|
|
991
|
+
ctx.save();
|
|
992
|
+
ctx.setLineDash([dashLen, gapLen]);
|
|
993
|
+
ctx.lineDashOffset = offset;
|
|
994
|
+
ctx.strokeStyle = this._rgba(theme.portExec, 0.9);
|
|
995
|
+
ctx.lineWidth = 1.5 / this.scale;
|
|
996
|
+
ctx.shadowColor = theme.portExec;
|
|
997
|
+
ctx.shadowBlur = 4 / this.scale;
|
|
998
|
+
roundRect(ctx, x - pad, y - pad, w + pad * 2, h + pad * 2, r + pad);
|
|
999
|
+
ctx.stroke();
|
|
1000
|
+
ctx.restore();
|
|
1001
|
+
}
|
|
946
1002
|
_drawEdge(graph, e) {
|
|
947
1003
|
const from = graph.nodes.get(e.fromNode);
|
|
948
1004
|
const to = graph.nodes.get(e.toNode);
|
|
@@ -951,7 +1007,8 @@ const _CanvasRenderer = class _CanvasRenderer {
|
|
|
951
1007
|
const iIn = to.inputs.findIndex((p) => p.id === e.toPort);
|
|
952
1008
|
const pr1 = portRect(from, null, iOut, "out");
|
|
953
1009
|
const pr2 = portRect(to, null, iIn, "in");
|
|
954
|
-
const x1 = pr1.x
|
|
1010
|
+
const x1 = pr1.x + pr1.w / 2, y1 = pr1.y + pr1.h / 2;
|
|
1011
|
+
const x2 = pr2.x + pr2.w / 2, y2 = pr2.y + pr2.h / 2;
|
|
955
1012
|
if (this.edgeStyle === "line") {
|
|
956
1013
|
this._drawLine(x1, y1, x2, y2);
|
|
957
1014
|
} else if (this.edgeStyle === "orthogonal") {
|
|
@@ -960,6 +1017,32 @@ const _CanvasRenderer = class _CanvasRenderer {
|
|
|
960
1017
|
this._drawCurve(x1, y1, x2, y2);
|
|
961
1018
|
}
|
|
962
1019
|
}
|
|
1020
|
+
_getEdgeDotPosition(graph, e, t) {
|
|
1021
|
+
const from = graph.nodes.get(e.fromNode);
|
|
1022
|
+
const to = graph.nodes.get(e.toNode);
|
|
1023
|
+
if (!from || !to) return null;
|
|
1024
|
+
const iOut = from.outputs.findIndex((p) => p.id === e.fromPort);
|
|
1025
|
+
const iIn = to.inputs.findIndex((p) => p.id === e.toPort);
|
|
1026
|
+
const pr1 = portRect(from, null, iOut, "out");
|
|
1027
|
+
const pr2 = portRect(to, null, iIn, "in");
|
|
1028
|
+
const x1 = pr1.x + pr1.w / 2, y1 = pr1.y + pr1.h / 2;
|
|
1029
|
+
const x2 = pr2.x + pr2.w / 2, y2 = pr2.y + pr2.h / 2;
|
|
1030
|
+
if (this.edgeStyle === "bezier") {
|
|
1031
|
+
const dx = Math.max(40, Math.abs(x2 - x1) * 0.4);
|
|
1032
|
+
return cubicBezierPoint(x1, y1, x1 + dx, y1, x2 - dx, y2, x2, y2, t);
|
|
1033
|
+
} else if (this.edgeStyle === "orthogonal") {
|
|
1034
|
+
const midX = (x1 + x2) / 2;
|
|
1035
|
+
const pts = [
|
|
1036
|
+
{ x: x1, y: y1 },
|
|
1037
|
+
{ x: midX, y: y1 },
|
|
1038
|
+
{ x: midX, y: y2 },
|
|
1039
|
+
{ x: x2, y: y2 }
|
|
1040
|
+
];
|
|
1041
|
+
return polylinePoint(pts, t);
|
|
1042
|
+
} else {
|
|
1043
|
+
return { x: x1 + (x2 - x1) * t, y: y1 + (y2 - y1) * t };
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
963
1046
|
_drawLine(x1, y1, x2, y2) {
|
|
964
1047
|
const { ctx } = this;
|
|
965
1048
|
ctx.beginPath();
|
|
@@ -971,21 +1054,17 @@ const _CanvasRenderer = class _CanvasRenderer {
|
|
|
971
1054
|
const { ctx } = this;
|
|
972
1055
|
ctx.beginPath();
|
|
973
1056
|
ctx.moveTo(points[0].x, points[0].y);
|
|
974
|
-
for (let i = 1; i < points.length; i++)
|
|
975
|
-
ctx.lineTo(points[i].x, points[i].y);
|
|
1057
|
+
for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
|
|
976
1058
|
ctx.stroke();
|
|
977
1059
|
}
|
|
978
1060
|
_drawOrthogonal(x1, y1, x2, y2) {
|
|
979
1061
|
const midX = (x1 + x2) / 2;
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
{ x: x2, y: y2 }
|
|
987
|
-
];
|
|
988
|
-
}
|
|
1062
|
+
const pts = [
|
|
1063
|
+
{ x: x1, y: y1 },
|
|
1064
|
+
{ x: midX, y: y1 },
|
|
1065
|
+
{ x: midX, y: y2 },
|
|
1066
|
+
{ x: x2, y: y2 }
|
|
1067
|
+
];
|
|
989
1068
|
const { ctx } = this;
|
|
990
1069
|
const prevJoin = ctx.lineJoin, prevCap = ctx.lineCap;
|
|
991
1070
|
ctx.lineJoin = "round";
|
|
@@ -1003,8 +1082,91 @@ const _CanvasRenderer = class _CanvasRenderer {
|
|
|
1003
1082
|
ctx.bezierCurveTo(x1 + dx, y1, x2 - dx, y2, x2, y2);
|
|
1004
1083
|
ctx.stroke();
|
|
1005
1084
|
}
|
|
1085
|
+
drawEdgesOnly(graph, {
|
|
1086
|
+
activeEdges = /* @__PURE__ */ new Set(),
|
|
1087
|
+
activeEdgeTimes = /* @__PURE__ */ new Map(),
|
|
1088
|
+
activeNodes = /* @__PURE__ */ new Set(),
|
|
1089
|
+
selection = /* @__PURE__ */ new Set(),
|
|
1090
|
+
time = performance.now(),
|
|
1091
|
+
tempEdge = null
|
|
1092
|
+
} = {}) {
|
|
1093
|
+
var _a, _b;
|
|
1094
|
+
this._resetTransform();
|
|
1095
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
1096
|
+
this._applyTransform();
|
|
1097
|
+
const { ctx, theme } = this;
|
|
1098
|
+
for (const e of graph.edges.values()) {
|
|
1099
|
+
const isActive = activeEdges.has(e.id);
|
|
1100
|
+
if (isActive) {
|
|
1101
|
+
ctx.save();
|
|
1102
|
+
ctx.shadowColor = theme.edgeActive;
|
|
1103
|
+
ctx.shadowBlur = 6 / this.scale;
|
|
1104
|
+
ctx.strokeStyle = theme.edgeActive;
|
|
1105
|
+
ctx.lineWidth = 2 / this.scale;
|
|
1106
|
+
ctx.setLineDash([]);
|
|
1107
|
+
this._drawEdge(graph, e);
|
|
1108
|
+
ctx.restore();
|
|
1109
|
+
const activationTime = activeEdgeTimes.get(e.id) ?? time;
|
|
1110
|
+
const dotT = Math.min(1, (time - activationTime) / 620);
|
|
1111
|
+
const dotPos = this._getEdgeDotPosition(graph, e, dotT);
|
|
1112
|
+
if (dotPos) {
|
|
1113
|
+
ctx.save();
|
|
1114
|
+
ctx.fillStyle = this._rgba(theme.edgeActive, 0.9);
|
|
1115
|
+
ctx.shadowColor = theme.edgeActive;
|
|
1116
|
+
ctx.shadowBlur = 8 / this.scale;
|
|
1117
|
+
ctx.beginPath();
|
|
1118
|
+
ctx.arc(dotPos.x, dotPos.y, 2.5 / this.scale, 0, Math.PI * 2);
|
|
1119
|
+
ctx.fill();
|
|
1120
|
+
ctx.restore();
|
|
1121
|
+
}
|
|
1122
|
+
} else {
|
|
1123
|
+
ctx.setLineDash([]);
|
|
1124
|
+
ctx.strokeStyle = theme.edge;
|
|
1125
|
+
ctx.lineWidth = 1.5 / this.scale;
|
|
1126
|
+
this._drawEdge(graph, e);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
for (const nodeId of selection) {
|
|
1130
|
+
const node = graph.nodes.get(nodeId);
|
|
1131
|
+
if (!node) continue;
|
|
1132
|
+
const def = (_b = (_a = this.registry) == null ? void 0 : _a.types) == null ? void 0 : _b.get(node.type);
|
|
1133
|
+
if (def == null ? void 0 : def.html) this._drawHtmlSelectionBorder(node);
|
|
1134
|
+
}
|
|
1135
|
+
if (activeNodes.size > 0) {
|
|
1136
|
+
for (const nodeId of activeNodes) {
|
|
1137
|
+
const node = graph.nodes.get(nodeId);
|
|
1138
|
+
if (node) this._drawActiveNodeBorder(node, time);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
if (tempEdge) {
|
|
1142
|
+
const a = this.screenToWorld(tempEdge.x1, tempEdge.y1);
|
|
1143
|
+
const b = this.screenToWorld(tempEdge.x2, tempEdge.y2);
|
|
1144
|
+
const prevDash = this.ctx.getLineDash();
|
|
1145
|
+
this.ctx.setLineDash([5 / this.scale, 5 / this.scale]);
|
|
1146
|
+
this.ctx.strokeStyle = this._rgba(this.theme.accentBright, 0.7);
|
|
1147
|
+
this.ctx.lineWidth = 1.5 / this.scale;
|
|
1148
|
+
let ptsForArrow = null;
|
|
1149
|
+
if (this.edgeStyle === "line") {
|
|
1150
|
+
this._drawLine(a.x, a.y, b.x, b.y);
|
|
1151
|
+
ptsForArrow = [{ x: a.x, y: a.y }, { x: b.x, y: b.y }];
|
|
1152
|
+
} else if (this.edgeStyle === "orthogonal") {
|
|
1153
|
+
ptsForArrow = this._drawOrthogonal(a.x, a.y, b.x, b.y);
|
|
1154
|
+
} else {
|
|
1155
|
+
this._drawCurve(a.x, a.y, b.x, b.y);
|
|
1156
|
+
ptsForArrow = [{ x: a.x, y: a.y }, { x: b.x, y: b.y }];
|
|
1157
|
+
}
|
|
1158
|
+
this.ctx.setLineDash(prevDash);
|
|
1159
|
+
if (ptsForArrow && ptsForArrow.length >= 2) {
|
|
1160
|
+
const p1 = ptsForArrow[ptsForArrow.length - 2];
|
|
1161
|
+
const p2 = ptsForArrow[ptsForArrow.length - 1];
|
|
1162
|
+
this.ctx.fillStyle = this.theme.accentBright;
|
|
1163
|
+
this._drawArrowhead(p1.x, p1.y, p2.x, p2.y, 10);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
this._resetTransform();
|
|
1167
|
+
}
|
|
1006
1168
|
};
|
|
1007
|
-
__publicField(_CanvasRenderer, "FONT_SIZE",
|
|
1169
|
+
__publicField(_CanvasRenderer, "FONT_SIZE", 11);
|
|
1008
1170
|
__publicField(_CanvasRenderer, "SELECTED_NODE_COLOR", "#6cf");
|
|
1009
1171
|
let CanvasRenderer = _CanvasRenderer;
|
|
1010
1172
|
function roundRect(ctx, x, y, w, h, r = 6) {
|
|
@@ -1021,6 +1183,38 @@ function roundRect(ctx, x, y, w, h, r = 6) {
|
|
|
1021
1183
|
ctx.quadraticCurveTo(x, y, x + r.tl, y);
|
|
1022
1184
|
ctx.closePath();
|
|
1023
1185
|
}
|
|
1186
|
+
function cubicBezierPoint(x0, y0, x1, y1, x2, y2, x3, y3, t) {
|
|
1187
|
+
const mt = 1 - t;
|
|
1188
|
+
return {
|
|
1189
|
+
x: mt * mt * mt * x0 + 3 * mt * mt * t * x1 + 3 * mt * t * t * x2 + t * t * t * x3,
|
|
1190
|
+
y: mt * mt * mt * y0 + 3 * mt * mt * t * y1 + 3 * mt * t * t * y2 + t * t * t * y3
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
function polylinePoint(pts, t) {
|
|
1194
|
+
let totalLen = 0;
|
|
1195
|
+
const lens = [];
|
|
1196
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
1197
|
+
const dx = pts[i + 1].x - pts[i].x;
|
|
1198
|
+
const dy = pts[i + 1].y - pts[i].y;
|
|
1199
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
1200
|
+
lens.push(len);
|
|
1201
|
+
totalLen += len;
|
|
1202
|
+
}
|
|
1203
|
+
if (totalLen === 0) return pts[0];
|
|
1204
|
+
let target = t * totalLen;
|
|
1205
|
+
let accum = 0;
|
|
1206
|
+
for (let i = 0; i < lens.length; i++) {
|
|
1207
|
+
if (accum + lens[i] >= target) {
|
|
1208
|
+
const segT = lens[i] > 0 ? (target - accum) / lens[i] : 0;
|
|
1209
|
+
return {
|
|
1210
|
+
x: pts[i].x + (pts[i + 1].x - pts[i].x) * segT,
|
|
1211
|
+
y: pts[i].y + (pts[i + 1].y - pts[i].y) * segT
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
accum += lens[i];
|
|
1215
|
+
}
|
|
1216
|
+
return pts[pts.length - 1];
|
|
1217
|
+
}
|
|
1024
1218
|
function findEdgeId(graph, a, b, c, d) {
|
|
1025
1219
|
for (const [id, e] of graph.edges) {
|
|
1026
1220
|
if (e.fromNode === a && e.fromPort === b && e.toNode === c && e.toPort === d)
|
|
@@ -1126,12 +1320,13 @@ class CommandStack {
|
|
|
1126
1320
|
}
|
|
1127
1321
|
}
|
|
1128
1322
|
const _Controller = class _Controller {
|
|
1129
|
-
constructor({ graph, renderer, hooks, htmlOverlay, contextMenu, portRenderer }) {
|
|
1323
|
+
constructor({ graph, renderer, hooks, htmlOverlay, contextMenu, edgeRenderer, portRenderer }) {
|
|
1130
1324
|
this.graph = graph;
|
|
1131
1325
|
this.renderer = renderer;
|
|
1132
1326
|
this.hooks = hooks;
|
|
1133
1327
|
this.htmlOverlay = htmlOverlay;
|
|
1134
1328
|
this.contextMenu = contextMenu;
|
|
1329
|
+
this.edgeRenderer = edgeRenderer;
|
|
1135
1330
|
this.portRenderer = portRenderer;
|
|
1136
1331
|
this.stack = new CommandStack();
|
|
1137
1332
|
this.selection = /* @__PURE__ */ new Set();
|
|
@@ -1142,6 +1337,9 @@ const _Controller = class _Controller {
|
|
|
1142
1337
|
this.gDragging = null;
|
|
1143
1338
|
this.gResizing = null;
|
|
1144
1339
|
this.boxSelecting = null;
|
|
1340
|
+
this.activeEdges = /* @__PURE__ */ new Set();
|
|
1341
|
+
this.activeEdgeTimes = /* @__PURE__ */ new Map();
|
|
1342
|
+
this.activeNodes = /* @__PURE__ */ new Set();
|
|
1145
1343
|
this.snapToGrid = true;
|
|
1146
1344
|
this.gridSize = 20;
|
|
1147
1345
|
this._cursor = "default";
|
|
@@ -1282,13 +1480,11 @@ const _Controller = class _Controller {
|
|
|
1282
1480
|
for (const n of this.graph.nodes.values()) {
|
|
1283
1481
|
for (let i = 0; i < n.inputs.length; i++) {
|
|
1284
1482
|
const r = portRect(n, n.inputs[i], i, "in");
|
|
1285
|
-
if (rectHas(r, x, y))
|
|
1286
|
-
return { node: n, port: n.inputs[i], dir: "in", idx: i };
|
|
1483
|
+
if (rectHas(r, x, y)) return { node: n, port: n.inputs[i], dir: "in", idx: i };
|
|
1287
1484
|
}
|
|
1288
1485
|
for (let i = 0; i < n.outputs.length; i++) {
|
|
1289
1486
|
const r = portRect(n, n.outputs[i], i, "out");
|
|
1290
|
-
if (rectHas(r, x, y))
|
|
1291
|
-
return { node: n, port: n.outputs[i], dir: "out", idx: i };
|
|
1487
|
+
if (rectHas(r, x, y)) return { node: n, port: n.outputs[i], dir: "out", idx: i };
|
|
1292
1488
|
}
|
|
1293
1489
|
}
|
|
1294
1490
|
return null;
|
|
@@ -1537,13 +1733,7 @@ const _Controller = class _Controller {
|
|
|
1537
1733
|
const portIn = this._findPortAtWorld(w.x, w.y);
|
|
1538
1734
|
if (portIn && portIn.dir === "in") {
|
|
1539
1735
|
this.stack.exec(
|
|
1540
|
-
AddEdgeCmd(
|
|
1541
|
-
this.graph,
|
|
1542
|
-
from.fromNode,
|
|
1543
|
-
from.fromPort,
|
|
1544
|
-
portIn.node.id,
|
|
1545
|
-
portIn.port.id
|
|
1546
|
-
)
|
|
1736
|
+
AddEdgeCmd(this.graph, from.fromNode, from.fromPort, portIn.node.id, portIn.port.id)
|
|
1547
1737
|
);
|
|
1548
1738
|
}
|
|
1549
1739
|
this.connecting = null;
|
|
@@ -1711,16 +1901,32 @@ const _Controller = class _Controller {
|
|
|
1711
1901
|
this.render();
|
|
1712
1902
|
}
|
|
1713
1903
|
render() {
|
|
1714
|
-
var _a
|
|
1904
|
+
var _a;
|
|
1715
1905
|
const tEdge = this.renderTempEdge();
|
|
1716
1906
|
this.renderer.draw(this.graph, {
|
|
1717
1907
|
selection: this.selection,
|
|
1718
|
-
tempEdge:
|
|
1908
|
+
tempEdge: null,
|
|
1909
|
+
// Don't draw temp edge on background
|
|
1719
1910
|
boxSelecting: this.boxSelecting,
|
|
1720
|
-
activeEdges: this.activeEdges || /* @__PURE__ */ new Set()
|
|
1721
|
-
|
|
1911
|
+
activeEdges: this.activeEdges || /* @__PURE__ */ new Set(),
|
|
1912
|
+
drawEdges: !this.edgeRenderer
|
|
1913
|
+
// Only draw edges here if no separate edge renderer
|
|
1722
1914
|
});
|
|
1723
1915
|
(_a = this.htmlOverlay) == null ? void 0 : _a.draw(this.graph, this.selection);
|
|
1916
|
+
if (this.edgeRenderer) {
|
|
1917
|
+
const edgeCtx = this.edgeRenderer.ctx;
|
|
1918
|
+
edgeCtx.clearRect(0, 0, this.edgeRenderer.canvas.width, this.edgeRenderer.canvas.height);
|
|
1919
|
+
this.edgeRenderer._applyTransform();
|
|
1920
|
+
this.edgeRenderer.drawEdgesOnly(this.graph, {
|
|
1921
|
+
activeEdges: this.activeEdges,
|
|
1922
|
+
activeEdgeTimes: this.activeEdgeTimes,
|
|
1923
|
+
activeNodes: this.activeNodes,
|
|
1924
|
+
selection: this.selection,
|
|
1925
|
+
time: performance.now(),
|
|
1926
|
+
tempEdge: tEdge
|
|
1927
|
+
});
|
|
1928
|
+
this.edgeRenderer._resetTransform();
|
|
1929
|
+
}
|
|
1724
1930
|
if (this.boxSelecting) {
|
|
1725
1931
|
const { startX, startY, currentX, currentY } = this.boxSelecting;
|
|
1726
1932
|
const minX = Math.min(startX, currentX);
|
|
@@ -1729,14 +1935,28 @@ const _Controller = class _Controller {
|
|
|
1729
1935
|
const height = Math.abs(currentY - startY);
|
|
1730
1936
|
const screenStart = this.renderer.worldToScreen(minX, minY);
|
|
1731
1937
|
const screenEnd = this.renderer.worldToScreen(minX + width, minY + height);
|
|
1732
|
-
const ctx = this.renderer.ctx;
|
|
1938
|
+
const ctx = this.edgeRenderer ? this.edgeRenderer.ctx : this.renderer.ctx;
|
|
1733
1939
|
ctx.save();
|
|
1734
|
-
this.
|
|
1940
|
+
if (this.edgeRenderer) {
|
|
1941
|
+
this.edgeRenderer._resetTransform();
|
|
1942
|
+
} else {
|
|
1943
|
+
this.renderer._resetTransform();
|
|
1944
|
+
}
|
|
1735
1945
|
ctx.strokeStyle = "#6cf";
|
|
1736
1946
|
ctx.fillStyle = "rgba(102, 204, 255, 0.1)";
|
|
1737
1947
|
ctx.lineWidth = 2;
|
|
1738
|
-
ctx.strokeRect(
|
|
1739
|
-
|
|
1948
|
+
ctx.strokeRect(
|
|
1949
|
+
screenStart.x,
|
|
1950
|
+
screenStart.y,
|
|
1951
|
+
screenEnd.x - screenStart.x,
|
|
1952
|
+
screenEnd.y - screenStart.y
|
|
1953
|
+
);
|
|
1954
|
+
ctx.fillRect(
|
|
1955
|
+
screenStart.x,
|
|
1956
|
+
screenStart.y,
|
|
1957
|
+
screenEnd.x - screenStart.x,
|
|
1958
|
+
screenEnd.y - screenStart.y
|
|
1959
|
+
);
|
|
1740
1960
|
ctx.restore();
|
|
1741
1961
|
}
|
|
1742
1962
|
if (this.portRenderer) {
|
|
@@ -1748,11 +1968,7 @@ const _Controller = class _Controller {
|
|
|
1748
1968
|
this.portRenderer._applyTransform();
|
|
1749
1969
|
for (const n of this.graph.nodes.values()) {
|
|
1750
1970
|
if (n.type !== "core/Group") {
|
|
1751
|
-
|
|
1752
|
-
const hasHtmlOverlay = !!(def == null ? void 0 : def.html);
|
|
1753
|
-
if (hasHtmlOverlay) {
|
|
1754
|
-
this.portRenderer._drawPorts(n);
|
|
1755
|
-
}
|
|
1971
|
+
this.portRenderer._drawPorts(n);
|
|
1756
1972
|
}
|
|
1757
1973
|
}
|
|
1758
1974
|
this.portRenderer._resetTransform();
|
|
@@ -1760,10 +1976,7 @@ const _Controller = class _Controller {
|
|
|
1760
1976
|
}
|
|
1761
1977
|
renderTempEdge() {
|
|
1762
1978
|
if (!this.connecting) return null;
|
|
1763
|
-
const a = this._portAnchorScreen(
|
|
1764
|
-
this.connecting.fromNode,
|
|
1765
|
-
this.connecting.fromPort
|
|
1766
|
-
);
|
|
1979
|
+
const a = this._portAnchorScreen(this.connecting.fromNode, this.connecting.fromPort);
|
|
1767
1980
|
return {
|
|
1768
1981
|
x1: a.x,
|
|
1769
1982
|
y1: a.y,
|
|
@@ -1775,7 +1988,7 @@ const _Controller = class _Controller {
|
|
|
1775
1988
|
const n = this.graph.nodes.get(nodeId);
|
|
1776
1989
|
const iOut = n.outputs.findIndex((p) => p.id === portId);
|
|
1777
1990
|
const r = portRect(n, null, iOut, "out");
|
|
1778
|
-
return this.renderer.worldToScreen(r.x, r.y +
|
|
1991
|
+
return this.renderer.worldToScreen(r.x + r.w / 2, r.y + r.h / 2);
|
|
1779
1992
|
}
|
|
1780
1993
|
};
|
|
1781
1994
|
__publicField(_Controller, "MIN_NODE_WIDTH", 80);
|
|
@@ -1964,7 +2177,8 @@ class ContextMenu {
|
|
|
1964
2177
|
itemEl._hideTimeout = null;
|
|
1965
2178
|
}
|
|
1966
2179
|
if (item.submenu) {
|
|
1967
|
-
|
|
2180
|
+
const submenuItems = typeof item.submenu === "function" ? item.submenu() : item.submenu;
|
|
2181
|
+
this._showSubmenu(submenuItems, itemEl);
|
|
1968
2182
|
}
|
|
1969
2183
|
});
|
|
1970
2184
|
itemEl.addEventListener("mouseleave", (e) => {
|
|
@@ -2104,11 +2318,9 @@ class Runner {
|
|
|
2104
2318
|
this._last = 0;
|
|
2105
2319
|
this.cyclesPerFrame = Math.max(1, cyclesPerFrame | 0);
|
|
2106
2320
|
}
|
|
2107
|
-
// 외부에서 실행 중인지 확인
|
|
2108
2321
|
isRunning() {
|
|
2109
2322
|
return this.running;
|
|
2110
2323
|
}
|
|
2111
|
-
// 실행 도중에도 CPS 변경 가능
|
|
2112
2324
|
setCyclesPerFrame(n) {
|
|
2113
2325
|
this.cyclesPerFrame = Math.max(1, n | 0);
|
|
2114
2326
|
}
|
|
@@ -2141,24 +2353,22 @@ class Runner {
|
|
|
2141
2353
|
}
|
|
2142
2354
|
}
|
|
2143
2355
|
/**
|
|
2144
|
-
* Execute connected nodes once from a starting node
|
|
2145
|
-
*
|
|
2146
|
-
* @param {number} dt - Delta time
|
|
2356
|
+
* Execute connected nodes once from a starting node.
|
|
2357
|
+
* Returns execEdgeOrder: exec edges in the order they were traversed.
|
|
2147
2358
|
*/
|
|
2148
2359
|
runOnce(startNodeId, dt = 0) {
|
|
2149
|
-
console.log("[Runner.runOnce] Starting exec flow from node:", startNodeId);
|
|
2150
|
-
const executedNodes = [];
|
|
2151
2360
|
const allConnectedNodes = /* @__PURE__ */ new Set();
|
|
2152
|
-
|
|
2153
|
-
|
|
2361
|
+
const execEdgeOrder = [];
|
|
2362
|
+
const queue = [{ nodeId: startNodeId, fromEdgeId: null }];
|
|
2363
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2364
|
+
while (queue.length > 0) {
|
|
2365
|
+
const { nodeId: currentNodeId, fromEdgeId } = queue.shift();
|
|
2366
|
+
if (visited.has(currentNodeId)) continue;
|
|
2367
|
+
visited.add(currentNodeId);
|
|
2368
|
+
if (fromEdgeId) execEdgeOrder.push(fromEdgeId);
|
|
2154
2369
|
const node = this.graph.nodes.get(currentNodeId);
|
|
2155
|
-
if (!node)
|
|
2156
|
-
console.warn(`[Runner.runOnce] Node not found: ${currentNodeId}`);
|
|
2157
|
-
break;
|
|
2158
|
-
}
|
|
2159
|
-
executedNodes.push(currentNodeId);
|
|
2370
|
+
if (!node) continue;
|
|
2160
2371
|
allConnectedNodes.add(currentNodeId);
|
|
2161
|
-
console.log(`[Runner.runOnce] Executing: ${node.title} (${node.type})`);
|
|
2162
2372
|
for (const input of node.inputs) {
|
|
2163
2373
|
if (input.portType === "data") {
|
|
2164
2374
|
for (const edge of this.graph.edges.values()) {
|
|
@@ -2173,40 +2383,38 @@ class Runner {
|
|
|
2173
2383
|
}
|
|
2174
2384
|
}
|
|
2175
2385
|
this.executeNode(currentNodeId, dt);
|
|
2176
|
-
|
|
2386
|
+
const execOutputs = node.outputs.filter((p) => p.portType === "exec");
|
|
2387
|
+
for (const execOutput of execOutputs) {
|
|
2388
|
+
for (const edge of this.graph.edges.values()) {
|
|
2389
|
+
if (edge.fromNode === currentNodeId && edge.fromPort === execOutput.id) {
|
|
2390
|
+
queue.push({ nodeId: edge.toNode, fromEdgeId: edge.id });
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2177
2394
|
}
|
|
2178
|
-
console.log("[Runner.runOnce] Executed nodes:", executedNodes.length);
|
|
2179
2395
|
const connectedEdges = /* @__PURE__ */ new Set();
|
|
2180
2396
|
for (const edge of this.graph.edges.values()) {
|
|
2181
2397
|
if (allConnectedNodes.has(edge.fromNode) && allConnectedNodes.has(edge.toNode)) {
|
|
2182
2398
|
connectedEdges.add(edge.id);
|
|
2183
2399
|
}
|
|
2184
2400
|
}
|
|
2185
|
-
|
|
2186
|
-
return { connectedNodes: allConnectedNodes, connectedEdges };
|
|
2401
|
+
return { connectedNodes: allConnectedNodes, connectedEdges, execEdgeOrder };
|
|
2187
2402
|
}
|
|
2188
|
-
|
|
2189
|
-
* Find the next node to execute by following exec output
|
|
2190
|
-
* @param {string} nodeId - Current node ID
|
|
2191
|
-
* @returns {string|null} Next node ID or null
|
|
2192
|
-
*/
|
|
2193
|
-
findNextExecNode(nodeId) {
|
|
2403
|
+
findAllNextExecNodes(nodeId) {
|
|
2194
2404
|
const node = this.graph.nodes.get(nodeId);
|
|
2195
|
-
if (!node) return
|
|
2196
|
-
const
|
|
2197
|
-
if (
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2405
|
+
if (!node) return [];
|
|
2406
|
+
const execOutputs = node.outputs.filter((p) => p.portType === "exec");
|
|
2407
|
+
if (execOutputs.length === 0) return [];
|
|
2408
|
+
const nextNodes = [];
|
|
2409
|
+
for (const execOutput of execOutputs) {
|
|
2410
|
+
for (const edge of this.graph.edges.values()) {
|
|
2411
|
+
if (edge.fromNode === nodeId && edge.fromPort === execOutput.id) {
|
|
2412
|
+
nextNodes.push(edge.toNode);
|
|
2413
|
+
}
|
|
2201
2414
|
}
|
|
2202
2415
|
}
|
|
2203
|
-
return
|
|
2416
|
+
return nextNodes;
|
|
2204
2417
|
}
|
|
2205
|
-
/**
|
|
2206
|
-
* Execute a single node
|
|
2207
|
-
* @param {string} nodeId - Node ID to execute
|
|
2208
|
-
* @param {number} dt - Delta time
|
|
2209
|
-
*/
|
|
2210
2418
|
executeNode(nodeId, dt) {
|
|
2211
2419
|
var _a, _b;
|
|
2212
2420
|
const node = this.graph.nodes.get(nodeId);
|
|
@@ -2304,7 +2512,7 @@ class HtmlOverlay {
|
|
|
2304
2512
|
const header = document.createElement("div");
|
|
2305
2513
|
header.className = "node-header";
|
|
2306
2514
|
Object.assign(header.style, {
|
|
2307
|
-
height: "
|
|
2515
|
+
height: "26px",
|
|
2308
2516
|
flexShrink: "0",
|
|
2309
2517
|
display: "flex",
|
|
2310
2518
|
alignItems: "center",
|
|
@@ -2331,7 +2539,7 @@ class HtmlOverlay {
|
|
|
2331
2539
|
return container;
|
|
2332
2540
|
}
|
|
2333
2541
|
/** 노드용 엘리먼트 생성(한 번만) */
|
|
2334
|
-
_ensureNodeElement(node, def) {
|
|
2542
|
+
_ensureNodeElement(node, def, graph) {
|
|
2335
2543
|
var _a;
|
|
2336
2544
|
let el = this.nodes.get(node.id);
|
|
2337
2545
|
if (!el) {
|
|
@@ -2340,7 +2548,7 @@ class HtmlOverlay {
|
|
|
2340
2548
|
} else if (def.html) {
|
|
2341
2549
|
el = this._createDefaultNodeLayout(node);
|
|
2342
2550
|
if (def.html.init) {
|
|
2343
|
-
def.html.init(node, el, el._domParts);
|
|
2551
|
+
def.html.init(node, el, { ...el._domParts, graph });
|
|
2344
2552
|
}
|
|
2345
2553
|
} else {
|
|
2346
2554
|
return null;
|
|
@@ -2363,7 +2571,7 @@ class HtmlOverlay {
|
|
|
2363
2571
|
const def = this.registry.types.get(node.type);
|
|
2364
2572
|
const hasHtml = !!(def == null ? void 0 : def.html);
|
|
2365
2573
|
if (!hasHtml) continue;
|
|
2366
|
-
const el = this._ensureNodeElement(node, def);
|
|
2574
|
+
const el = this._ensureNodeElement(node, def, graph);
|
|
2367
2575
|
if (!el) continue;
|
|
2368
2576
|
el.style.left = `${node.computed.x}px`;
|
|
2369
2577
|
el.style.top = `${node.computed.y}px`;
|
|
@@ -2386,6 +2594,15 @@ class HtmlOverlay {
|
|
|
2386
2594
|
}
|
|
2387
2595
|
}
|
|
2388
2596
|
}
|
|
2597
|
+
/**
|
|
2598
|
+
* Sync container transform with renderer state (lightweight update)
|
|
2599
|
+
* Called when zoom/pan occurs without needing full redraw
|
|
2600
|
+
*/
|
|
2601
|
+
syncTransform() {
|
|
2602
|
+
const { scale, offsetX, offsetY } = this.renderer;
|
|
2603
|
+
this.container.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
|
|
2604
|
+
this.container.style.transformOrigin = "0 0";
|
|
2605
|
+
}
|
|
2389
2606
|
clear() {
|
|
2390
2607
|
for (const [, el] of this.nodes) {
|
|
2391
2608
|
el.remove();
|
|
@@ -2511,10 +2728,44 @@ class PropertyPanel {
|
|
|
2511
2728
|
this.hooks = hooks;
|
|
2512
2729
|
this.registry = registry;
|
|
2513
2730
|
this.render = render;
|
|
2731
|
+
this._def = null;
|
|
2514
2732
|
this.panel = null;
|
|
2515
2733
|
this.currentNode = null;
|
|
2516
2734
|
this.isVisible = false;
|
|
2735
|
+
this._selfUpdating = false;
|
|
2517
2736
|
this._createPanel();
|
|
2737
|
+
this._bindHooks();
|
|
2738
|
+
}
|
|
2739
|
+
_bindHooks() {
|
|
2740
|
+
var _a, _b, _c, _d, _e, _f;
|
|
2741
|
+
(_a = this.hooks) == null ? void 0 : _a.on("edge:create", () => {
|
|
2742
|
+
if (this._canRefresh()) this._renderContent();
|
|
2743
|
+
});
|
|
2744
|
+
(_b = this.hooks) == null ? void 0 : _b.on("edge:delete", () => {
|
|
2745
|
+
if (this._canRefresh()) this._renderContent();
|
|
2746
|
+
});
|
|
2747
|
+
(_c = this.hooks) == null ? void 0 : _c.on("node:updated", (node) => {
|
|
2748
|
+
var _a2;
|
|
2749
|
+
if (this._canRefresh() && ((_a2 = this.currentNode) == null ? void 0 : _a2.id) === (node == null ? void 0 : node.id) && !this._selfUpdating) {
|
|
2750
|
+
this._renderContent();
|
|
2751
|
+
}
|
|
2752
|
+
});
|
|
2753
|
+
(_d = this.hooks) == null ? void 0 : _d.on("node:move", (node) => {
|
|
2754
|
+
var _a2;
|
|
2755
|
+
if (this._canRefresh() && ((_a2 = this.currentNode) == null ? void 0 : _a2.id) === (node == null ? void 0 : node.id)) {
|
|
2756
|
+
this._updatePositionFields();
|
|
2757
|
+
}
|
|
2758
|
+
});
|
|
2759
|
+
(_e = this.hooks) == null ? void 0 : _e.on("runner:tick", () => {
|
|
2760
|
+
if (this._canRefresh()) this._updateLiveValues();
|
|
2761
|
+
});
|
|
2762
|
+
(_f = this.hooks) == null ? void 0 : _f.on("runner:stop", () => {
|
|
2763
|
+
if (this._canRefresh()) this._updateLiveValues();
|
|
2764
|
+
});
|
|
2765
|
+
}
|
|
2766
|
+
_canRefresh() {
|
|
2767
|
+
if (!this.isVisible || !this.currentNode) return false;
|
|
2768
|
+
return !this.panel.querySelector("[data-field]:focus");
|
|
2518
2769
|
}
|
|
2519
2770
|
_createPanel() {
|
|
2520
2771
|
this.panel = document.createElement("div");
|
|
@@ -2544,8 +2795,10 @@ class PropertyPanel {
|
|
|
2544
2795
|
});
|
|
2545
2796
|
}
|
|
2546
2797
|
open(node) {
|
|
2798
|
+
var _a, _b;
|
|
2547
2799
|
if (!node) return;
|
|
2548
2800
|
this.currentNode = node;
|
|
2801
|
+
this._def = ((_b = (_a = this.registry) == null ? void 0 : _a.types) == null ? void 0 : _b.get(node.type)) || null;
|
|
2549
2802
|
this.isVisible = true;
|
|
2550
2803
|
this._renderContent();
|
|
2551
2804
|
this.panel.style.display = "block";
|
|
@@ -2581,9 +2834,9 @@ class PropertyPanel {
|
|
|
2581
2834
|
</div>
|
|
2582
2835
|
</div>
|
|
2583
2836
|
</div>
|
|
2584
|
-
|
|
2837
|
+
|
|
2585
2838
|
<div class="section">
|
|
2586
|
-
<div class="section-title">Position & Size</div>
|
|
2839
|
+
<div class="section-title">Position & Size</div>
|
|
2587
2840
|
<div class="section-body">
|
|
2588
2841
|
<div class="field-row">
|
|
2589
2842
|
<div class="field">
|
|
@@ -2607,16 +2860,92 @@ class PropertyPanel {
|
|
|
2607
2860
|
</div>
|
|
2608
2861
|
</div>
|
|
2609
2862
|
</div>
|
|
2610
|
-
|
|
2863
|
+
|
|
2864
|
+
${this._renderConnections(node)}
|
|
2611
2865
|
${this._renderPorts(node)}
|
|
2866
|
+
${this._renderLiveValues(node)}
|
|
2612
2867
|
${this._renderState(node)}
|
|
2613
|
-
|
|
2868
|
+
|
|
2614
2869
|
<div class="panel-actions">
|
|
2615
2870
|
<button class="btn-secondary panel-close-btn">Close</button>
|
|
2616
2871
|
</div>
|
|
2617
2872
|
`;
|
|
2618
2873
|
this._attachInputListeners();
|
|
2619
2874
|
}
|
|
2875
|
+
_renderConnections(node) {
|
|
2876
|
+
const edges = [...this.graph.edges.values()];
|
|
2877
|
+
const incoming = edges.filter((e) => e.toNode === node.id);
|
|
2878
|
+
const outgoing = edges.filter((e) => e.fromNode === node.id);
|
|
2879
|
+
if (!incoming.length && !outgoing.length) return "";
|
|
2880
|
+
const edgeLabel = (e, dir) => {
|
|
2881
|
+
const otherId = dir === "in" ? e.fromNode : e.toNode;
|
|
2882
|
+
const other = this.graph.nodes.get(otherId);
|
|
2883
|
+
return `<div class="port-item">
|
|
2884
|
+
<span class="port-icon data"></span>
|
|
2885
|
+
<span class="port-name" style="font-size:10px;color:#5a5a78;">${(other == null ? void 0 : other.title) ?? otherId}</span>
|
|
2886
|
+
</div>`;
|
|
2887
|
+
};
|
|
2888
|
+
return `
|
|
2889
|
+
<div class="section">
|
|
2890
|
+
<div class="section-title">Connections</div>
|
|
2891
|
+
<div class="section-body">
|
|
2892
|
+
${incoming.length ? `
|
|
2893
|
+
<div class="port-group">
|
|
2894
|
+
<div class="port-group-title">Incoming (${incoming.length})</div>
|
|
2895
|
+
${incoming.map((e) => edgeLabel(e, "in")).join("")}
|
|
2896
|
+
</div>` : ""}
|
|
2897
|
+
${outgoing.length ? `
|
|
2898
|
+
<div class="port-group">
|
|
2899
|
+
<div class="port-group-title">Outgoing (${outgoing.length})</div>
|
|
2900
|
+
${outgoing.map((e) => edgeLabel(e, "out")).join("")}
|
|
2901
|
+
</div>` : ""}
|
|
2902
|
+
</div>
|
|
2903
|
+
</div>
|
|
2904
|
+
`;
|
|
2905
|
+
}
|
|
2906
|
+
_renderLiveValues(node) {
|
|
2907
|
+
var _a, _b;
|
|
2908
|
+
const cur = (_b = (_a = this.graph) == null ? void 0 : _a._curBuf) == null ? void 0 : _b.call(_a);
|
|
2909
|
+
if (!cur) return "";
|
|
2910
|
+
const lines = [];
|
|
2911
|
+
for (const input of node.inputs) {
|
|
2912
|
+
`${node.id}:${input.id}`;
|
|
2913
|
+
for (const edge of this.graph.edges.values()) {
|
|
2914
|
+
if (edge.toNode === node.id && edge.toPort === input.id) {
|
|
2915
|
+
const upKey = `${edge.fromNode}:${edge.fromPort}`;
|
|
2916
|
+
const val = cur.get(upKey);
|
|
2917
|
+
if (val !== void 0) {
|
|
2918
|
+
lines.push(`<div class="port-item">
|
|
2919
|
+
<span class="port-icon data"></span>
|
|
2920
|
+
<span class="port-name">↳ ${input.name}</span>
|
|
2921
|
+
<span class="port-type" style="color:var(--color-primary);background:rgba(99,102,241,0.1);">${JSON.stringify(val)}</span>
|
|
2922
|
+
</div>`);
|
|
2923
|
+
}
|
|
2924
|
+
break;
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
for (const output of node.outputs) {
|
|
2929
|
+
const key = `${node.id}:${output.id}`;
|
|
2930
|
+
const val = cur.get(key);
|
|
2931
|
+
if (val !== void 0) {
|
|
2932
|
+
lines.push(`<div class="port-item">
|
|
2933
|
+
<span class="port-icon exec" style="background:#10b981;"></span>
|
|
2934
|
+
<span class="port-name">↳ ${output.name}</span>
|
|
2935
|
+
<span class="port-type" style="color:#10b981;background:rgba(16,185,129,0.1);">${JSON.stringify(val)}</span>
|
|
2936
|
+
</div>`);
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
if (!lines.length) return "";
|
|
2940
|
+
return `
|
|
2941
|
+
<div class="section">
|
|
2942
|
+
<div class="section-title">Live Values</div>
|
|
2943
|
+
<div class="section-body">
|
|
2944
|
+
${lines.join("")}
|
|
2945
|
+
</div>
|
|
2946
|
+
</div>
|
|
2947
|
+
`;
|
|
2948
|
+
}
|
|
2620
2949
|
_renderPorts(node) {
|
|
2621
2950
|
if (!node.inputs.length && !node.outputs.length) return "";
|
|
2622
2951
|
return `
|
|
@@ -2635,7 +2964,6 @@ class PropertyPanel {
|
|
|
2635
2964
|
`).join("")}
|
|
2636
2965
|
</div>
|
|
2637
2966
|
` : ""}
|
|
2638
|
-
|
|
2639
2967
|
${node.outputs.length ? `
|
|
2640
2968
|
<div class="port-group">
|
|
2641
2969
|
<div class="port-group-title">Outputs (${node.outputs.length})</div>
|
|
@@ -2653,38 +2981,56 @@ class PropertyPanel {
|
|
|
2653
2981
|
`;
|
|
2654
2982
|
}
|
|
2655
2983
|
_renderState(node) {
|
|
2656
|
-
if (!node.state
|
|
2984
|
+
if (!node.state) return "";
|
|
2985
|
+
const entries = Object.entries(node.state).filter(([key, value]) => {
|
|
2986
|
+
if (key.startsWith("_")) return false;
|
|
2987
|
+
const t = typeof value;
|
|
2988
|
+
return t === "string" || t === "number" || t === "boolean";
|
|
2989
|
+
});
|
|
2990
|
+
if (!entries.length) return "";
|
|
2991
|
+
const fieldHtml = ([key, value]) => {
|
|
2992
|
+
if (typeof value === "boolean") {
|
|
2993
|
+
return `
|
|
2994
|
+
<div class="field">
|
|
2995
|
+
<label>${key}</label>
|
|
2996
|
+
<select data-field="state.${key}">
|
|
2997
|
+
<option value="true"${value ? " selected" : ""}>true</option>
|
|
2998
|
+
<option value="false"${!value ? " selected" : ""}>false</option>
|
|
2999
|
+
</select>
|
|
3000
|
+
</div>`;
|
|
3001
|
+
}
|
|
3002
|
+
return `
|
|
3003
|
+
<div class="field">
|
|
3004
|
+
<label>${key}</label>
|
|
3005
|
+
<input type="${typeof value === "number" ? "number" : "text"}"
|
|
3006
|
+
data-field="state.${key}"
|
|
3007
|
+
value="${value}" />
|
|
3008
|
+
</div>`;
|
|
3009
|
+
};
|
|
2657
3010
|
return `
|
|
2658
3011
|
<div class="section">
|
|
2659
3012
|
<div class="section-title">State</div>
|
|
2660
3013
|
<div class="section-body">
|
|
2661
|
-
${
|
|
2662
|
-
<div class="field">
|
|
2663
|
-
<label>${key}</label>
|
|
2664
|
-
<input
|
|
2665
|
-
type="${typeof value === "number" ? "number" : "text"}"
|
|
2666
|
-
data-field="state.${key}"
|
|
2667
|
-
value="${value}"
|
|
2668
|
-
/>
|
|
2669
|
-
</div>
|
|
2670
|
-
`).join("")}
|
|
3014
|
+
${entries.map(fieldHtml).join("")}
|
|
2671
3015
|
</div>
|
|
2672
3016
|
</div>
|
|
2673
3017
|
`;
|
|
2674
3018
|
}
|
|
2675
3019
|
_attachInputListeners() {
|
|
2676
|
-
|
|
2677
|
-
|
|
3020
|
+
var _a;
|
|
3021
|
+
this.panel.querySelectorAll("[data-field]").forEach((input) => {
|
|
2678
3022
|
input.addEventListener("change", () => {
|
|
3023
|
+
this._selfUpdating = true;
|
|
2679
3024
|
this._handleFieldChange(input.dataset.field, input.value);
|
|
3025
|
+
this._selfUpdating = false;
|
|
2680
3026
|
});
|
|
2681
3027
|
});
|
|
2682
|
-
this.panel.querySelector(".panel-close-btn").addEventListener("click", () => {
|
|
3028
|
+
(_a = this.panel.querySelector(".panel-close-btn")) == null ? void 0 : _a.addEventListener("click", () => {
|
|
2683
3029
|
this.close();
|
|
2684
3030
|
});
|
|
2685
3031
|
}
|
|
2686
3032
|
_handleFieldChange(field, value) {
|
|
2687
|
-
var _a;
|
|
3033
|
+
var _a, _b;
|
|
2688
3034
|
const node = this.currentNode;
|
|
2689
3035
|
if (!node) return;
|
|
2690
3036
|
switch (field) {
|
|
@@ -2708,31 +3054,277 @@ class PropertyPanel {
|
|
|
2708
3054
|
default:
|
|
2709
3055
|
if (field.startsWith("state.")) {
|
|
2710
3056
|
const key = field.substring(6);
|
|
2711
|
-
if (node.state) {
|
|
2712
|
-
const
|
|
2713
|
-
|
|
3057
|
+
if (node.state && key in node.state) {
|
|
3058
|
+
const orig = node.state[key];
|
|
3059
|
+
if (typeof orig === "boolean") {
|
|
3060
|
+
node.state[key] = value === "true";
|
|
3061
|
+
} else if (typeof orig === "number") {
|
|
3062
|
+
node.state[key] = parseFloat(value);
|
|
3063
|
+
} else {
|
|
3064
|
+
node.state[key] = value;
|
|
3065
|
+
}
|
|
2714
3066
|
}
|
|
2715
3067
|
}
|
|
2716
3068
|
}
|
|
2717
3069
|
(_a = this.hooks) == null ? void 0 : _a.emit("node:updated", node);
|
|
2718
|
-
|
|
2719
|
-
|
|
3070
|
+
(_b = this.render) == null ? void 0 : _b.call(this);
|
|
3071
|
+
}
|
|
3072
|
+
/** Lightweight update of position fields only (no full re-render) */
|
|
3073
|
+
_updatePositionFields() {
|
|
3074
|
+
const node = this.currentNode;
|
|
3075
|
+
if (!node) return;
|
|
3076
|
+
const xEl = this.panel.querySelector('[data-field="x"]');
|
|
3077
|
+
const yEl = this.panel.querySelector('[data-field="y"]');
|
|
3078
|
+
if (xEl) xEl.value = Math.round(node.computed.x);
|
|
3079
|
+
if (yEl) yEl.value = Math.round(node.computed.y);
|
|
3080
|
+
}
|
|
3081
|
+
/** Lightweight in-place update of the Live Values section */
|
|
3082
|
+
_updateLiveValues() {
|
|
3083
|
+
var _a, _b;
|
|
3084
|
+
const node = this.currentNode;
|
|
3085
|
+
if (!node) return;
|
|
3086
|
+
const cur = (_b = (_a = this.graph) == null ? void 0 : _a._curBuf) == null ? void 0 : _b.call(_a);
|
|
3087
|
+
if (!cur) return;
|
|
3088
|
+
let section = this.panel.querySelector(".live-values-section");
|
|
3089
|
+
const newHtml = this._renderLiveValues(node);
|
|
3090
|
+
if (!newHtml) {
|
|
3091
|
+
if (section) section.remove();
|
|
3092
|
+
return;
|
|
3093
|
+
}
|
|
3094
|
+
const wrapper = document.createElement("div");
|
|
3095
|
+
wrapper.innerHTML = newHtml;
|
|
3096
|
+
const newSection = wrapper.firstElementChild;
|
|
3097
|
+
newSection.classList.add("live-values-section");
|
|
3098
|
+
if (section) {
|
|
3099
|
+
section.replaceWith(newSection);
|
|
3100
|
+
} else {
|
|
3101
|
+
this.panel.querySelectorAll(".section");
|
|
3102
|
+
const actions = this.panel.querySelector(".panel-actions");
|
|
3103
|
+
if (actions) {
|
|
3104
|
+
actions.before(newSection);
|
|
3105
|
+
} else {
|
|
3106
|
+
this.panel.querySelector(".panel-content").appendChild(newSection);
|
|
3107
|
+
}
|
|
2720
3108
|
}
|
|
2721
3109
|
}
|
|
2722
3110
|
destroy() {
|
|
2723
|
-
|
|
2724
|
-
|
|
3111
|
+
var _a;
|
|
3112
|
+
(_a = this.panel) == null ? void 0 : _a.remove();
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
class HelpOverlay {
|
|
3116
|
+
constructor(container, options = {}) {
|
|
3117
|
+
this.container = container;
|
|
3118
|
+
this.options = {
|
|
3119
|
+
shortcuts: options.shortcuts || this._getDefaultShortcuts(),
|
|
3120
|
+
onToggle: options.onToggle || null
|
|
3121
|
+
};
|
|
3122
|
+
this.isVisible = false;
|
|
3123
|
+
this.overlay = null;
|
|
3124
|
+
this.toggleBtn = null;
|
|
3125
|
+
this._createElements();
|
|
3126
|
+
this._bindEvents();
|
|
3127
|
+
}
|
|
3128
|
+
_getDefaultShortcuts() {
|
|
3129
|
+
return [
|
|
3130
|
+
{
|
|
3131
|
+
group: "Selection",
|
|
3132
|
+
items: [
|
|
3133
|
+
{ label: "Select node", key: "Click" },
|
|
3134
|
+
{ label: "Multi-select", key: "Shift+Click" },
|
|
3135
|
+
{ label: "Box select", key: "Ctrl+Drag" }
|
|
3136
|
+
]
|
|
3137
|
+
},
|
|
3138
|
+
{
|
|
3139
|
+
group: "Edit",
|
|
3140
|
+
items: [
|
|
3141
|
+
{ label: "Delete", key: "Del" },
|
|
3142
|
+
{ label: "Undo", key: "Ctrl+Z" },
|
|
3143
|
+
{ label: "Redo", key: "Ctrl+Y" }
|
|
3144
|
+
]
|
|
3145
|
+
},
|
|
3146
|
+
{
|
|
3147
|
+
group: "Group & Align",
|
|
3148
|
+
items: [
|
|
3149
|
+
{ label: "Create group", key: "Ctrl+G" },
|
|
3150
|
+
{ label: "Align horizontal", key: "A" },
|
|
3151
|
+
{ label: "Align vertical", key: "Shift+A" }
|
|
3152
|
+
]
|
|
3153
|
+
},
|
|
3154
|
+
{
|
|
3155
|
+
group: "View",
|
|
3156
|
+
items: [
|
|
3157
|
+
{ label: "Toggle snap", key: "G" },
|
|
3158
|
+
{ label: "Pan", key: "Mid+Drag" },
|
|
3159
|
+
{ label: "Zoom", key: "Scroll" },
|
|
3160
|
+
{ label: "Context menu", key: "RClick" }
|
|
3161
|
+
]
|
|
3162
|
+
}
|
|
3163
|
+
];
|
|
3164
|
+
}
|
|
3165
|
+
_createElements() {
|
|
3166
|
+
this.toggleBtn = document.createElement("div");
|
|
3167
|
+
this.toggleBtn.id = "helpToggle";
|
|
3168
|
+
this.toggleBtn.title = "Keyboard shortcuts (?)";
|
|
3169
|
+
this.toggleBtn.textContent = "?";
|
|
3170
|
+
this.container.appendChild(this.toggleBtn);
|
|
3171
|
+
this.overlay = document.createElement("div");
|
|
3172
|
+
this.overlay.id = "helpOverlay";
|
|
3173
|
+
const sectionsHtml = this.options.shortcuts.map(
|
|
3174
|
+
(group) => `
|
|
3175
|
+
<h4>${group.group}</h4>
|
|
3176
|
+
${group.items.map(
|
|
3177
|
+
(item) => `
|
|
3178
|
+
<div class="shortcut-item">
|
|
3179
|
+
<span>${item.label}</span>
|
|
3180
|
+
<span class="shortcut-key">${item.key}</span>
|
|
3181
|
+
</div>
|
|
3182
|
+
`
|
|
3183
|
+
).join("")}
|
|
3184
|
+
`
|
|
3185
|
+
).join("");
|
|
3186
|
+
this.overlay.innerHTML = `
|
|
3187
|
+
<h3>
|
|
3188
|
+
<span>Keyboard Shortcuts</span>
|
|
3189
|
+
<button class="close-btn" id="helpClose" title="Close">×</button>
|
|
3190
|
+
</h3>
|
|
3191
|
+
${sectionsHtml}
|
|
3192
|
+
`;
|
|
3193
|
+
this.container.appendChild(this.overlay);
|
|
3194
|
+
}
|
|
3195
|
+
_bindEvents() {
|
|
3196
|
+
this.toggleBtn.addEventListener("click", () => this.toggle());
|
|
3197
|
+
const closeBtn = this.overlay.querySelector("#helpClose");
|
|
3198
|
+
if (closeBtn) {
|
|
3199
|
+
closeBtn.addEventListener("click", (e) => {
|
|
3200
|
+
e.stopPropagation();
|
|
3201
|
+
this.close();
|
|
3202
|
+
});
|
|
2725
3203
|
}
|
|
3204
|
+
document.addEventListener("mousedown", (e) => {
|
|
3205
|
+
if (this.isVisible) {
|
|
3206
|
+
if (!this.overlay.contains(e.target) && !this.toggleBtn.contains(e.target)) {
|
|
3207
|
+
this.close();
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
});
|
|
3211
|
+
window.addEventListener("keydown", (e) => {
|
|
3212
|
+
if (e.key === "?" || e.shiftKey && e.key === "/") {
|
|
3213
|
+
if (!["INPUT", "TEXTAREA", "SELECT"].includes(document.activeElement.tagName)) {
|
|
3214
|
+
e.preventDefault();
|
|
3215
|
+
this.toggle();
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
if (e.key === "Escape" && this.isVisible) {
|
|
3219
|
+
this.close();
|
|
3220
|
+
}
|
|
3221
|
+
});
|
|
3222
|
+
}
|
|
3223
|
+
toggle() {
|
|
3224
|
+
if (this.isVisible) this.close();
|
|
3225
|
+
else this.open();
|
|
3226
|
+
}
|
|
3227
|
+
open() {
|
|
3228
|
+
this.isVisible = true;
|
|
3229
|
+
this.overlay.classList.add("visible");
|
|
3230
|
+
this.toggleBtn.classList.add("active");
|
|
3231
|
+
if (this.options.onToggle) this.options.onToggle(true);
|
|
3232
|
+
}
|
|
3233
|
+
close() {
|
|
3234
|
+
this.isVisible = false;
|
|
3235
|
+
this.overlay.classList.remove("visible");
|
|
3236
|
+
this.toggleBtn.classList.remove("active");
|
|
3237
|
+
if (this.options.onToggle) this.options.onToggle(false);
|
|
3238
|
+
}
|
|
3239
|
+
destroy() {
|
|
3240
|
+
var _a, _b;
|
|
3241
|
+
(_a = this.toggleBtn) == null ? void 0 : _a.remove();
|
|
3242
|
+
(_b = this.overlay) == null ? void 0 : _b.remove();
|
|
2726
3243
|
}
|
|
2727
3244
|
}
|
|
3245
|
+
function setupDefaultContextMenu(contextMenu, { controller, graph, hooks }) {
|
|
3246
|
+
const getNodeTypes = () => {
|
|
3247
|
+
const nodeTypes = [];
|
|
3248
|
+
for (const [key, typeDef] of graph.registry.types.entries()) {
|
|
3249
|
+
nodeTypes.push({
|
|
3250
|
+
id: `add-${key}`,
|
|
3251
|
+
label: typeDef.title || key,
|
|
3252
|
+
action: () => {
|
|
3253
|
+
const worldPos = contextMenu.worldPosition || { x: 100, y: 100 };
|
|
3254
|
+
const node = graph.addNode(key, {
|
|
3255
|
+
x: worldPos.x,
|
|
3256
|
+
y: worldPos.y
|
|
3257
|
+
});
|
|
3258
|
+
hooks == null ? void 0 : hooks.emit("node:updated", node);
|
|
3259
|
+
controller.render();
|
|
3260
|
+
}
|
|
3261
|
+
});
|
|
3262
|
+
}
|
|
3263
|
+
return nodeTypes;
|
|
3264
|
+
};
|
|
3265
|
+
contextMenu.addItem("add-node", "Add Node", {
|
|
3266
|
+
condition: (target) => !target,
|
|
3267
|
+
submenu: getNodeTypes,
|
|
3268
|
+
// Pass function instead of array
|
|
3269
|
+
order: 5
|
|
3270
|
+
});
|
|
3271
|
+
contextMenu.addItem("delete-node", "Delete Node", {
|
|
3272
|
+
condition: (target) => target && target.type !== "core/Group",
|
|
3273
|
+
action: (target) => {
|
|
3274
|
+
const cmd = RemoveNodeCmd(graph, target);
|
|
3275
|
+
controller.stack.exec(cmd);
|
|
3276
|
+
hooks == null ? void 0 : hooks.emit("node:updated", target);
|
|
3277
|
+
},
|
|
3278
|
+
order: 10
|
|
3279
|
+
});
|
|
3280
|
+
const colors = [
|
|
3281
|
+
{ name: "Default", color: "#39424e" },
|
|
3282
|
+
{ name: "Slate", color: "#4a5568" },
|
|
3283
|
+
{ name: "Gray", color: "#2d3748" },
|
|
3284
|
+
{ name: "Blue", color: "#1a365d" },
|
|
3285
|
+
{ name: "Green", color: "#22543d" },
|
|
3286
|
+
{ name: "Red", color: "#742a2a" },
|
|
3287
|
+
{ name: "Purple", color: "#44337a" }
|
|
3288
|
+
];
|
|
3289
|
+
contextMenu.addItem("change-group-color", "Change Color", {
|
|
3290
|
+
condition: (target) => target && target.type === "core/Group",
|
|
3291
|
+
submenu: colors.map((colorInfo) => ({
|
|
3292
|
+
id: `color-${colorInfo.color}`,
|
|
3293
|
+
label: colorInfo.name,
|
|
3294
|
+
color: colorInfo.color,
|
|
3295
|
+
action: (target) => {
|
|
3296
|
+
const currentColor = target.state.color || "#39424e";
|
|
3297
|
+
const cmd = ChangeGroupColorCmd(target, currentColor, colorInfo.color);
|
|
3298
|
+
controller.stack.exec(cmd);
|
|
3299
|
+
hooks == null ? void 0 : hooks.emit("node:updated", target);
|
|
3300
|
+
}
|
|
3301
|
+
})),
|
|
3302
|
+
order: 20
|
|
3303
|
+
});
|
|
3304
|
+
contextMenu.addItem("delete-group", "Delete Group", {
|
|
3305
|
+
condition: (target) => target && target.type === "core/Group",
|
|
3306
|
+
action: (target) => {
|
|
3307
|
+
const cmd = RemoveNodeCmd(graph, target);
|
|
3308
|
+
controller.stack.exec(cmd);
|
|
3309
|
+
hooks == null ? void 0 : hooks.emit("node:updated", target);
|
|
3310
|
+
},
|
|
3311
|
+
order: 20
|
|
3312
|
+
});
|
|
3313
|
+
}
|
|
2728
3314
|
function createGraphEditor(target, {
|
|
2729
3315
|
theme,
|
|
2730
3316
|
hooks: customHooks,
|
|
2731
3317
|
autorun = true,
|
|
2732
3318
|
showMinimap = true,
|
|
2733
3319
|
enablePropertyPanel = true,
|
|
2734
|
-
propertyPanelContainer = null
|
|
3320
|
+
propertyPanelContainer = null,
|
|
3321
|
+
enableHelp = true,
|
|
3322
|
+
helpShortcuts = null,
|
|
3323
|
+
setupDefaultContextMenu: setupDefaultContextMenu$1 = true,
|
|
3324
|
+
setupContextMenu = null,
|
|
3325
|
+
plugins = []
|
|
2735
3326
|
} = {}) {
|
|
3327
|
+
var _a;
|
|
2736
3328
|
let canvas;
|
|
2737
3329
|
let container;
|
|
2738
3330
|
if (typeof target === "string") {
|
|
@@ -2780,6 +3372,46 @@ function createGraphEditor(target, {
|
|
|
2780
3372
|
const graph = new Graph({ hooks, registry });
|
|
2781
3373
|
const renderer = new CanvasRenderer(canvas, { theme, registry });
|
|
2782
3374
|
const htmlOverlay = new HtmlOverlay(canvas.parentElement, renderer, registry);
|
|
3375
|
+
renderer.setTransformChangeCallback(() => {
|
|
3376
|
+
htmlOverlay.syncTransform();
|
|
3377
|
+
});
|
|
3378
|
+
const edgeCanvas = document.createElement("canvas");
|
|
3379
|
+
edgeCanvas.id = "edge-canvas";
|
|
3380
|
+
Object.assign(edgeCanvas.style, {
|
|
3381
|
+
position: "absolute",
|
|
3382
|
+
top: "0",
|
|
3383
|
+
left: "0",
|
|
3384
|
+
pointerEvents: "none",
|
|
3385
|
+
// Pass through clicks
|
|
3386
|
+
zIndex: "15"
|
|
3387
|
+
// Above HTML overlay (10), below port canvas (20)
|
|
3388
|
+
});
|
|
3389
|
+
canvas.parentElement.appendChild(edgeCanvas);
|
|
3390
|
+
const edgeRenderer = new CanvasRenderer(edgeCanvas, { theme, registry });
|
|
3391
|
+
Object.defineProperty(edgeRenderer, "scale", {
|
|
3392
|
+
get() {
|
|
3393
|
+
return renderer.scale;
|
|
3394
|
+
},
|
|
3395
|
+
set(v) {
|
|
3396
|
+
renderer.scale = v;
|
|
3397
|
+
}
|
|
3398
|
+
});
|
|
3399
|
+
Object.defineProperty(edgeRenderer, "offsetX", {
|
|
3400
|
+
get() {
|
|
3401
|
+
return renderer.offsetX;
|
|
3402
|
+
},
|
|
3403
|
+
set(v) {
|
|
3404
|
+
renderer.offsetX = v;
|
|
3405
|
+
}
|
|
3406
|
+
});
|
|
3407
|
+
Object.defineProperty(edgeRenderer, "offsetY", {
|
|
3408
|
+
get() {
|
|
3409
|
+
return renderer.offsetY;
|
|
3410
|
+
},
|
|
3411
|
+
set(v) {
|
|
3412
|
+
renderer.offsetY = v;
|
|
3413
|
+
}
|
|
3414
|
+
});
|
|
2783
3415
|
const portCanvas = document.createElement("canvas");
|
|
2784
3416
|
portCanvas.id = "port-canvas";
|
|
2785
3417
|
Object.assign(portCanvas.style, {
|
|
@@ -2789,7 +3421,7 @@ function createGraphEditor(target, {
|
|
|
2789
3421
|
pointerEvents: "none",
|
|
2790
3422
|
// Pass through clicks
|
|
2791
3423
|
zIndex: "20"
|
|
2792
|
-
// Above
|
|
3424
|
+
// Above edge canvas (15)
|
|
2793
3425
|
});
|
|
2794
3426
|
canvas.parentElement.appendChild(portCanvas);
|
|
2795
3427
|
const portRenderer = new CanvasRenderer(portCanvas, { theme, registry });
|
|
@@ -2797,7 +3429,7 @@ function createGraphEditor(target, {
|
|
|
2797
3429
|
portRenderer.scale = renderer.scale;
|
|
2798
3430
|
portRenderer.offsetX = renderer.offsetX;
|
|
2799
3431
|
portRenderer.offsetY = renderer.offsetY;
|
|
2800
|
-
const controller = new Controller({ graph, renderer, hooks, htmlOverlay, portRenderer });
|
|
3432
|
+
const controller = new Controller({ graph, renderer, hooks, htmlOverlay, edgeRenderer, portRenderer });
|
|
2801
3433
|
const contextMenu = new ContextMenu({
|
|
2802
3434
|
graph,
|
|
2803
3435
|
hooks,
|
|
@@ -2821,7 +3453,15 @@ function createGraphEditor(target, {
|
|
|
2821
3453
|
propertyPanel.open(node);
|
|
2822
3454
|
});
|
|
2823
3455
|
}
|
|
3456
|
+
let helpOverlay = null;
|
|
3457
|
+
if (enableHelp) {
|
|
3458
|
+
helpOverlay = new HelpOverlay(container, {
|
|
3459
|
+
shortcuts: helpShortcuts
|
|
3460
|
+
});
|
|
3461
|
+
}
|
|
2824
3462
|
const runner = new Runner({ graph, registry, hooks });
|
|
3463
|
+
graph.runner = runner;
|
|
3464
|
+
graph.controller = controller;
|
|
2825
3465
|
hooks.on("runner:tick", ({ time, dt }) => {
|
|
2826
3466
|
renderer.draw(graph, {
|
|
2827
3467
|
selection: controller.selection,
|
|
@@ -2856,695 +3496,37 @@ function createGraphEditor(target, {
|
|
|
2856
3496
|
hooks.on("node:updated", () => {
|
|
2857
3497
|
controller.render();
|
|
2858
3498
|
});
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
inputs: [{ name: "in", datatype: "any" }],
|
|
2863
|
-
outputs: [{ name: "out", datatype: "any" }],
|
|
2864
|
-
onCreate(node) {
|
|
2865
|
-
node.state.text = "hello";
|
|
2866
|
-
},
|
|
2867
|
-
onExecute(node, { dt, getInput, setOutput }) {
|
|
2868
|
-
const incoming = getInput("in");
|
|
2869
|
-
const out = (incoming ?? node.state.text ?? "").toString().toUpperCase();
|
|
2870
|
-
setOutput(
|
|
2871
|
-
"out",
|
|
2872
|
-
out + ` · ${Math.floor(performance.now() / 1e3 % 100)}`
|
|
2873
|
-
);
|
|
2874
|
-
},
|
|
2875
|
-
onDraw(node, { ctx, theme: theme2 }) {
|
|
2876
|
-
const { x, y } = node.pos;
|
|
2877
|
-
const { width: w } = node.size;
|
|
2878
|
-
}
|
|
3499
|
+
hooks.on("graph:deserialize", () => {
|
|
3500
|
+
renderer.setTransform({ scale: 1, offsetX: 0, offsetY: 0 });
|
|
3501
|
+
controller.render();
|
|
2879
3502
|
});
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
header.style.borderBottom = "1px solid #444";
|
|
2895
|
-
header.style.color = "#eee";
|
|
2896
|
-
header.style.fontSize = "12px";
|
|
2897
|
-
header.style.fontWeight = "bold";
|
|
2898
|
-
header.textContent = "My HTML Node";
|
|
2899
|
-
body.style.padding = "8px";
|
|
2900
|
-
body.style.color = "#ccc";
|
|
2901
|
-
body.style.fontSize = "12px";
|
|
2902
|
-
const contentDiv = document.createElement("div");
|
|
2903
|
-
contentDiv.textContent = "Event Name";
|
|
2904
|
-
body.appendChild(contentDiv);
|
|
2905
|
-
const input = document.createElement("input");
|
|
2906
|
-
Object.assign(input.style, {
|
|
2907
|
-
marginTop: "4px",
|
|
2908
|
-
padding: "4px",
|
|
2909
|
-
background: "#111",
|
|
2910
|
-
border: "1px solid #555",
|
|
2911
|
-
color: "#fff",
|
|
2912
|
-
borderRadius: "4px",
|
|
2913
|
-
pointerEvents: "auto"
|
|
2914
|
-
});
|
|
2915
|
-
input.placeholder = "Type here...";
|
|
2916
|
-
input.addEventListener("input", (e) => {
|
|
2917
|
-
node.state.text = e.target.value;
|
|
2918
|
-
});
|
|
2919
|
-
input.addEventListener("mousedown", (e) => e.stopPropagation());
|
|
2920
|
-
body.appendChild(input);
|
|
2921
|
-
el._input = input;
|
|
2922
|
-
},
|
|
2923
|
-
// 매 프레임(또는 필요시) 업데이트
|
|
2924
|
-
update(node, el, { header, _body, selected }) {
|
|
2925
|
-
el.style.borderColor = selected ? "#6cf" : "#444";
|
|
2926
|
-
header.style.backgroundColor = selected ? "#3a4a5a" : "#333";
|
|
2927
|
-
if (el._input.value !== (node.state.text || "")) {
|
|
2928
|
-
el._input.value = node.state.text || "";
|
|
3503
|
+
if (setupDefaultContextMenu$1) {
|
|
3504
|
+
setupDefaultContextMenu(contextMenu, { controller, graph, hooks });
|
|
3505
|
+
}
|
|
3506
|
+
if (setupContextMenu) {
|
|
3507
|
+
setupContextMenu(contextMenu, { controller, graph, hooks });
|
|
3508
|
+
}
|
|
3509
|
+
if (plugins && plugins.length > 0) {
|
|
3510
|
+
for (const plugin of plugins) {
|
|
3511
|
+
if (typeof plugin.install === "function") {
|
|
3512
|
+
try {
|
|
3513
|
+
plugin.install({ graph, registry, hooks, runner, controller, contextMenu }, plugin.options || {});
|
|
3514
|
+
} catch (err) {
|
|
3515
|
+
console.error(`[createGraphEditor] Failed to install plugin "${plugin.name || "unknown"}":`, err);
|
|
3516
|
+
(_a = hooks == null ? void 0 : hooks.emit) == null ? void 0 : _a.call(hooks, "error", err);
|
|
2929
3517
|
}
|
|
3518
|
+
} else {
|
|
3519
|
+
console.warn(`[createGraphEditor] Plugin "${plugin.name || "unknown"}" does not have an install() method`);
|
|
2930
3520
|
}
|
|
2931
|
-
},
|
|
2932
|
-
onCreate(node) {
|
|
2933
|
-
node.state.text = "";
|
|
2934
|
-
},
|
|
2935
|
-
onExecute(node, { getInput, setOutput }) {
|
|
2936
|
-
const incoming = getInput("in");
|
|
2937
|
-
setOutput("out", incoming);
|
|
2938
|
-
}
|
|
2939
|
-
// onDraw는 생략 가능 (HTML이 덮으니까)
|
|
2940
|
-
// 하지만 포트 등은 그려야 할 수도 있음.
|
|
2941
|
-
// 현재 구조상 CanvasRenderer가 기본 노드를 그리므로,
|
|
2942
|
-
// 투명하게 하거나 겹쳐서 그릴 수 있음.
|
|
2943
|
-
});
|
|
2944
|
-
registry.register("core/TodoNode", {
|
|
2945
|
-
title: "Todo List",
|
|
2946
|
-
size: { w: 240, h: 300 },
|
|
2947
|
-
inputs: [{ name: "in", datatype: "any" }],
|
|
2948
|
-
outputs: [{ name: "out", datatype: "any" }],
|
|
2949
|
-
html: {
|
|
2950
|
-
init(node, el, { header, body }) {
|
|
2951
|
-
el.style.backgroundColor = "#1e1e24";
|
|
2952
|
-
el.style.borderRadius = "8px";
|
|
2953
|
-
el.style.boxShadow = "0 4px 12px rgba(0,0,0,0.5)";
|
|
2954
|
-
el.style.border = "1px solid #333";
|
|
2955
|
-
header.style.backgroundColor = "#2a2a31";
|
|
2956
|
-
header.style.padding = "8px";
|
|
2957
|
-
header.style.fontWeight = "bold";
|
|
2958
|
-
header.style.color = "#e9e9ef";
|
|
2959
|
-
header.textContent = node.title;
|
|
2960
|
-
body.style.display = "flex";
|
|
2961
|
-
body.style.flexDirection = "column";
|
|
2962
|
-
body.style.padding = "8px";
|
|
2963
|
-
body.style.color = "#e9e9ef";
|
|
2964
|
-
const inputRow = document.createElement("div");
|
|
2965
|
-
Object.assign(inputRow.style, { display: "flex", gap: "4px", marginBottom: "8px" });
|
|
2966
|
-
const input = document.createElement("input");
|
|
2967
|
-
Object.assign(input.style, {
|
|
2968
|
-
flex: "1",
|
|
2969
|
-
padding: "6px",
|
|
2970
|
-
borderRadius: "4px",
|
|
2971
|
-
border: "1px solid #444",
|
|
2972
|
-
background: "#141417",
|
|
2973
|
-
color: "#fff",
|
|
2974
|
-
pointerEvents: "auto"
|
|
2975
|
-
});
|
|
2976
|
-
input.placeholder = "Add task...";
|
|
2977
|
-
const addBtn = document.createElement("button");
|
|
2978
|
-
addBtn.textContent = "+";
|
|
2979
|
-
Object.assign(addBtn.style, {
|
|
2980
|
-
padding: "0 12px",
|
|
2981
|
-
cursor: "pointer",
|
|
2982
|
-
background: "#4f5b66",
|
|
2983
|
-
color: "#fff",
|
|
2984
|
-
border: "none",
|
|
2985
|
-
borderRadius: "4px",
|
|
2986
|
-
pointerEvents: "auto"
|
|
2987
|
-
});
|
|
2988
|
-
inputRow.append(input, addBtn);
|
|
2989
|
-
const list = document.createElement("ul");
|
|
2990
|
-
Object.assign(list.style, {
|
|
2991
|
-
listStyle: "none",
|
|
2992
|
-
padding: "0",
|
|
2993
|
-
margin: "0",
|
|
2994
|
-
overflow: "hidden",
|
|
2995
|
-
flex: "1"
|
|
2996
|
-
});
|
|
2997
|
-
body.append(inputRow, list);
|
|
2998
|
-
const addTodo = () => {
|
|
2999
|
-
const text = input.value.trim();
|
|
3000
|
-
if (!text) return;
|
|
3001
|
-
const todos = node.state.todos || [];
|
|
3002
|
-
node.state.todos = [...todos, { id: Date.now(), text, done: false }];
|
|
3003
|
-
input.value = "";
|
|
3004
|
-
hooks.emit("node:updated", node);
|
|
3005
|
-
};
|
|
3006
|
-
addBtn.onclick = addTodo;
|
|
3007
|
-
input.onkeydown = (e) => {
|
|
3008
|
-
if (e.key === "Enter") addTodo();
|
|
3009
|
-
e.stopPropagation();
|
|
3010
|
-
};
|
|
3011
|
-
input.onmousedown = (e) => e.stopPropagation();
|
|
3012
|
-
el._refs = { list };
|
|
3013
|
-
},
|
|
3014
|
-
update(node, el, { selected }) {
|
|
3015
|
-
el.style.borderColor = selected ? "#6cf" : "#333";
|
|
3016
|
-
const { list } = el._refs;
|
|
3017
|
-
const todos = node.state.todos || [];
|
|
3018
|
-
list.innerHTML = "";
|
|
3019
|
-
todos.forEach((todo) => {
|
|
3020
|
-
const li = document.createElement("li");
|
|
3021
|
-
Object.assign(li.style, {
|
|
3022
|
-
display: "flex",
|
|
3023
|
-
alignItems: "center",
|
|
3024
|
-
padding: "6px 0",
|
|
3025
|
-
borderBottom: "1px solid #2a2a31"
|
|
3026
|
-
});
|
|
3027
|
-
const chk = document.createElement("input");
|
|
3028
|
-
chk.type = "checkbox";
|
|
3029
|
-
chk.checked = todo.done;
|
|
3030
|
-
chk.style.marginRight = "8px";
|
|
3031
|
-
chk.style.pointerEvents = "auto";
|
|
3032
|
-
chk.onchange = () => {
|
|
3033
|
-
todo.done = chk.checked;
|
|
3034
|
-
hooks.emit("node:updated", node);
|
|
3035
|
-
};
|
|
3036
|
-
chk.onmousedown = (e) => e.stopPropagation();
|
|
3037
|
-
const span = document.createElement("span");
|
|
3038
|
-
span.textContent = todo.text;
|
|
3039
|
-
span.style.flex = "1";
|
|
3040
|
-
span.style.textDecoration = todo.done ? "line-through" : "none";
|
|
3041
|
-
span.style.color = todo.done ? "#777" : "#eee";
|
|
3042
|
-
const del = document.createElement("button");
|
|
3043
|
-
del.textContent = "×";
|
|
3044
|
-
Object.assign(del.style, {
|
|
3045
|
-
background: "none",
|
|
3046
|
-
border: "none",
|
|
3047
|
-
color: "#f44",
|
|
3048
|
-
cursor: "pointer",
|
|
3049
|
-
fontSize: "16px",
|
|
3050
|
-
pointerEvents: "auto"
|
|
3051
|
-
});
|
|
3052
|
-
del.onclick = () => {
|
|
3053
|
-
node.state.todos = node.state.todos.filter((t) => t.id !== todo.id);
|
|
3054
|
-
hooks.emit("node:updated", node);
|
|
3055
|
-
};
|
|
3056
|
-
del.onmousedown = (e) => e.stopPropagation();
|
|
3057
|
-
li.append(chk, span, del);
|
|
3058
|
-
list.appendChild(li);
|
|
3059
|
-
});
|
|
3060
|
-
}
|
|
3061
|
-
},
|
|
3062
|
-
onCreate(node) {
|
|
3063
|
-
node.state.todos = [
|
|
3064
|
-
{ id: 1, text: "Welcome to Free Node", done: false },
|
|
3065
|
-
{ id: 2, text: "Try adding a task", done: true }
|
|
3066
|
-
];
|
|
3067
|
-
}
|
|
3068
|
-
});
|
|
3069
|
-
registry.register("math/Add", {
|
|
3070
|
-
title: "Add",
|
|
3071
|
-
size: { w: 140, h: 100 },
|
|
3072
|
-
inputs: [
|
|
3073
|
-
{ name: "exec", portType: "exec" },
|
|
3074
|
-
{ name: "a", portType: "data", datatype: "number" },
|
|
3075
|
-
{ name: "b", portType: "data", datatype: "number" }
|
|
3076
|
-
],
|
|
3077
|
-
outputs: [
|
|
3078
|
-
{ name: "exec", portType: "exec" },
|
|
3079
|
-
{ name: "result", portType: "data", datatype: "number" }
|
|
3080
|
-
],
|
|
3081
|
-
onCreate(node) {
|
|
3082
|
-
node.state.a = 0;
|
|
3083
|
-
node.state.b = 0;
|
|
3084
|
-
},
|
|
3085
|
-
onExecute(node, { getInput, setOutput }) {
|
|
3086
|
-
const a = getInput("a") ?? 0;
|
|
3087
|
-
const b = getInput("b") ?? 0;
|
|
3088
|
-
const result = a + b;
|
|
3089
|
-
console.log("[Add] a:", a, "b:", b, "result:", result);
|
|
3090
|
-
setOutput("result", result);
|
|
3091
|
-
}
|
|
3092
|
-
});
|
|
3093
|
-
registry.register("math/Subtract", {
|
|
3094
|
-
title: "Subtract",
|
|
3095
|
-
size: { w: 140, h: 80 },
|
|
3096
|
-
inputs: [
|
|
3097
|
-
{ name: "a", datatype: "number" },
|
|
3098
|
-
{ name: "b", datatype: "number" }
|
|
3099
|
-
],
|
|
3100
|
-
outputs: [{ name: "result", datatype: "number" }],
|
|
3101
|
-
onExecute(node, { getInput, setOutput }) {
|
|
3102
|
-
const a = getInput("a") ?? 0;
|
|
3103
|
-
const b = getInput("b") ?? 0;
|
|
3104
|
-
setOutput("result", a - b);
|
|
3105
|
-
}
|
|
3106
|
-
});
|
|
3107
|
-
registry.register("math/Multiply", {
|
|
3108
|
-
title: "Multiply",
|
|
3109
|
-
size: { w: 140, h: 100 },
|
|
3110
|
-
inputs: [
|
|
3111
|
-
{ name: "exec", portType: "exec" },
|
|
3112
|
-
{ name: "a", portType: "data", datatype: "number" },
|
|
3113
|
-
{ name: "b", portType: "data", datatype: "number" }
|
|
3114
|
-
],
|
|
3115
|
-
outputs: [
|
|
3116
|
-
{ name: "exec", portType: "exec" },
|
|
3117
|
-
{ name: "result", portType: "data", datatype: "number" }
|
|
3118
|
-
],
|
|
3119
|
-
onExecute(node, { getInput, setOutput }) {
|
|
3120
|
-
const a = getInput("a") ?? 0;
|
|
3121
|
-
const b = getInput("b") ?? 0;
|
|
3122
|
-
const result = a * b;
|
|
3123
|
-
console.log("[Multiply] a:", a, "b:", b, "result:", result);
|
|
3124
|
-
setOutput("result", result);
|
|
3125
|
-
}
|
|
3126
|
-
});
|
|
3127
|
-
registry.register("math/Divide", {
|
|
3128
|
-
title: "Divide",
|
|
3129
|
-
size: { w: 140, h: 80 },
|
|
3130
|
-
inputs: [
|
|
3131
|
-
{ name: "a", datatype: "number" },
|
|
3132
|
-
{ name: "b", datatype: "number" }
|
|
3133
|
-
],
|
|
3134
|
-
outputs: [{ name: "result", datatype: "number" }],
|
|
3135
|
-
onExecute(node, { getInput, setOutput }) {
|
|
3136
|
-
const a = getInput("a") ?? 0;
|
|
3137
|
-
const b = getInput("b") ?? 1;
|
|
3138
|
-
setOutput("result", b !== 0 ? a / b : 0);
|
|
3139
|
-
}
|
|
3140
|
-
});
|
|
3141
|
-
registry.register("logic/AND", {
|
|
3142
|
-
title: "AND",
|
|
3143
|
-
size: { w: 120, h: 100 },
|
|
3144
|
-
inputs: [
|
|
3145
|
-
{ name: "exec", portType: "exec" },
|
|
3146
|
-
{ name: "a", portType: "data", datatype: "boolean" },
|
|
3147
|
-
{ name: "b", portType: "data", datatype: "boolean" }
|
|
3148
|
-
],
|
|
3149
|
-
outputs: [
|
|
3150
|
-
{ name: "exec", portType: "exec" },
|
|
3151
|
-
{ name: "result", portType: "data", datatype: "boolean" }
|
|
3152
|
-
],
|
|
3153
|
-
onExecute(node, { getInput, setOutput }) {
|
|
3154
|
-
const a = getInput("a") ?? false;
|
|
3155
|
-
const b = getInput("b") ?? false;
|
|
3156
|
-
console.log("[AND] Inputs - a:", a, "b:", b);
|
|
3157
|
-
const result = a && b;
|
|
3158
|
-
console.log("[AND] Result:", result);
|
|
3159
|
-
setOutput("result", result);
|
|
3160
|
-
}
|
|
3161
|
-
});
|
|
3162
|
-
registry.register("logic/OR", {
|
|
3163
|
-
title: "OR",
|
|
3164
|
-
size: { w: 120, h: 80 },
|
|
3165
|
-
inputs: [
|
|
3166
|
-
{ name: "a", datatype: "boolean" },
|
|
3167
|
-
{ name: "b", datatype: "boolean" }
|
|
3168
|
-
],
|
|
3169
|
-
outputs: [{ name: "result", datatype: "boolean" }],
|
|
3170
|
-
onExecute(node, { getInput, setOutput }) {
|
|
3171
|
-
const a = getInput("a") ?? false;
|
|
3172
|
-
const b = getInput("b") ?? false;
|
|
3173
|
-
setOutput("result", a || b);
|
|
3174
|
-
}
|
|
3175
|
-
});
|
|
3176
|
-
registry.register("logic/NOT", {
|
|
3177
|
-
title: "NOT",
|
|
3178
|
-
size: { w: 120, h: 70 },
|
|
3179
|
-
inputs: [{ name: "in", datatype: "boolean" }],
|
|
3180
|
-
outputs: [{ name: "out", datatype: "boolean" }],
|
|
3181
|
-
onExecute(node, { getInput, setOutput }) {
|
|
3182
|
-
const val = getInput("in") ?? false;
|
|
3183
|
-
setOutput("out", !val);
|
|
3184
|
-
}
|
|
3185
|
-
});
|
|
3186
|
-
registry.register("value/Number", {
|
|
3187
|
-
title: "Number",
|
|
3188
|
-
size: { w: 140, h: 60 },
|
|
3189
|
-
outputs: [{ name: "value", portType: "data", datatype: "number" }],
|
|
3190
|
-
onCreate(node) {
|
|
3191
|
-
node.state.value = 0;
|
|
3192
|
-
},
|
|
3193
|
-
onExecute(node, { setOutput }) {
|
|
3194
|
-
console.log("[Number] Outputting value:", node.state.value ?? 0);
|
|
3195
|
-
setOutput("value", node.state.value ?? 0);
|
|
3196
|
-
},
|
|
3197
|
-
html: {
|
|
3198
|
-
init(node, el, { header, body }) {
|
|
3199
|
-
el.style.backgroundColor = "#1e1e24";
|
|
3200
|
-
el.style.border = "1px solid #444";
|
|
3201
|
-
el.style.borderRadius = "8px";
|
|
3202
|
-
header.style.backgroundColor = "#2a2a31";
|
|
3203
|
-
header.style.borderBottom = "1px solid #444";
|
|
3204
|
-
header.style.color = "#eee";
|
|
3205
|
-
header.style.fontSize = "12px";
|
|
3206
|
-
header.textContent = "Number";
|
|
3207
|
-
body.style.padding = "12px";
|
|
3208
|
-
body.style.display = "flex";
|
|
3209
|
-
body.style.alignItems = "center";
|
|
3210
|
-
body.style.justifyContent = "center";
|
|
3211
|
-
const input = document.createElement("input");
|
|
3212
|
-
input.type = "number";
|
|
3213
|
-
input.value = node.state.value ?? 0;
|
|
3214
|
-
Object.assign(input.style, {
|
|
3215
|
-
width: "100%",
|
|
3216
|
-
padding: "6px",
|
|
3217
|
-
background: "#141417",
|
|
3218
|
-
border: "1px solid #444",
|
|
3219
|
-
borderRadius: "4px",
|
|
3220
|
-
color: "#fff",
|
|
3221
|
-
fontSize: "14px",
|
|
3222
|
-
textAlign: "center",
|
|
3223
|
-
pointerEvents: "auto"
|
|
3224
|
-
});
|
|
3225
|
-
input.addEventListener("change", (e) => {
|
|
3226
|
-
node.state.value = parseFloat(e.target.value) || 0;
|
|
3227
|
-
});
|
|
3228
|
-
input.addEventListener("mousedown", (e) => e.stopPropagation());
|
|
3229
|
-
input.addEventListener("keydown", (e) => e.stopPropagation());
|
|
3230
|
-
body.appendChild(input);
|
|
3231
|
-
},
|
|
3232
|
-
update(node, el, { header, body, selected }) {
|
|
3233
|
-
el.style.borderColor = selected ? "#6cf" : "#444";
|
|
3234
|
-
header.style.backgroundColor = selected ? "#3a4a5a" : "#2a2a31";
|
|
3235
|
-
}
|
|
3236
|
-
},
|
|
3237
|
-
onDraw(node, { ctx, theme: theme2 }) {
|
|
3238
|
-
const { x, y } = node.computed;
|
|
3239
|
-
ctx.fillStyle = "#8f8";
|
|
3240
|
-
ctx.font = "14px sans-serif";
|
|
3241
|
-
ctx.textAlign = "center";
|
|
3242
|
-
ctx.fillText(String(node.state.value ?? 0), x + 70, y + 42);
|
|
3243
|
-
}
|
|
3244
|
-
});
|
|
3245
|
-
registry.register("value/String", {
|
|
3246
|
-
title: "String",
|
|
3247
|
-
size: { w: 160, h: 60 },
|
|
3248
|
-
outputs: [{ name: "value", datatype: "string" }],
|
|
3249
|
-
onCreate(node) {
|
|
3250
|
-
node.state.value = "Hello";
|
|
3251
|
-
},
|
|
3252
|
-
onExecute(node, { setOutput }) {
|
|
3253
|
-
setOutput("value", node.state.value ?? "");
|
|
3254
|
-
},
|
|
3255
|
-
onDraw(node, { ctx, theme: theme2 }) {
|
|
3256
|
-
const { x, y } = node.computed;
|
|
3257
|
-
ctx.fillStyle = "#8f8";
|
|
3258
|
-
ctx.font = "12px sans-serif";
|
|
3259
|
-
ctx.textAlign = "center";
|
|
3260
|
-
const text = String(node.state.value ?? "");
|
|
3261
|
-
const displayText = text.length > 15 ? text.substring(0, 15) + "..." : text;
|
|
3262
|
-
ctx.fillText(displayText, x + 80, y + 42);
|
|
3263
|
-
}
|
|
3264
|
-
});
|
|
3265
|
-
registry.register("value/Boolean", {
|
|
3266
|
-
title: "Boolean",
|
|
3267
|
-
size: { w: 140, h: 60 },
|
|
3268
|
-
outputs: [{ name: "value", portType: "data", datatype: "boolean" }],
|
|
3269
|
-
onCreate(node) {
|
|
3270
|
-
node.state.value = true;
|
|
3271
|
-
},
|
|
3272
|
-
onExecute(node, { setOutput }) {
|
|
3273
|
-
console.log("[Boolean] Outputting value:", node.state.value ?? false);
|
|
3274
|
-
setOutput("value", node.state.value ?? false);
|
|
3275
|
-
},
|
|
3276
|
-
onDraw(node, { ctx, theme: theme2 }) {
|
|
3277
|
-
const { x, y } = node.computed;
|
|
3278
|
-
ctx.fillStyle = node.state.value ? "#8f8" : "#f88";
|
|
3279
|
-
ctx.font = "14px sans-serif";
|
|
3280
|
-
ctx.textAlign = "center";
|
|
3281
|
-
ctx.fillText(String(node.state.value), x + 70, y + 42);
|
|
3282
|
-
}
|
|
3283
|
-
});
|
|
3284
|
-
registry.register("util/Print", {
|
|
3285
|
-
title: "Print",
|
|
3286
|
-
size: { w: 140, h: 80 },
|
|
3287
|
-
inputs: [
|
|
3288
|
-
{ name: "exec", portType: "exec" },
|
|
3289
|
-
{ name: "value", portType: "data", datatype: "any" }
|
|
3290
|
-
],
|
|
3291
|
-
onCreate(node) {
|
|
3292
|
-
node.state.lastValue = null;
|
|
3293
|
-
},
|
|
3294
|
-
onExecute(node, { getInput }) {
|
|
3295
|
-
const val = getInput("value");
|
|
3296
|
-
if (val !== node.state.lastValue) {
|
|
3297
|
-
console.log("[Print]", val);
|
|
3298
|
-
node.state.lastValue = val;
|
|
3299
|
-
}
|
|
3300
|
-
}
|
|
3301
|
-
});
|
|
3302
|
-
registry.register("util/Watch", {
|
|
3303
|
-
title: "Watch",
|
|
3304
|
-
size: { w: 180, h: 110 },
|
|
3305
|
-
inputs: [
|
|
3306
|
-
{ name: "exec", portType: "exec" },
|
|
3307
|
-
{ name: "value", portType: "data", datatype: "any" }
|
|
3308
|
-
],
|
|
3309
|
-
outputs: [
|
|
3310
|
-
{ name: "exec", portType: "exec" },
|
|
3311
|
-
{ name: "value", portType: "data", datatype: "any" }
|
|
3312
|
-
],
|
|
3313
|
-
onCreate(node) {
|
|
3314
|
-
node.state.displayValue = "---";
|
|
3315
|
-
},
|
|
3316
|
-
onExecute(node, { getInput, setOutput }) {
|
|
3317
|
-
const val = getInput("value");
|
|
3318
|
-
console.log("[Watch] onExecute called, value:", val);
|
|
3319
|
-
node.state.displayValue = String(val ?? "---");
|
|
3320
|
-
setOutput("value", val);
|
|
3321
|
-
},
|
|
3322
|
-
onDraw(node, { ctx, theme: theme2 }) {
|
|
3323
|
-
const { x, y } = node.computed;
|
|
3324
|
-
ctx.fillStyle = "#fa3";
|
|
3325
|
-
ctx.font = "11px monospace";
|
|
3326
|
-
ctx.textAlign = "left";
|
|
3327
|
-
const text = String(node.state.displayValue ?? "---");
|
|
3328
|
-
const displayText = text.length > 20 ? text.substring(0, 20) + "..." : text;
|
|
3329
|
-
ctx.fillText(displayText, x + 8, y + 50);
|
|
3330
|
-
}
|
|
3331
|
-
});
|
|
3332
|
-
registry.register("util/Timer", {
|
|
3333
|
-
title: "Timer",
|
|
3334
|
-
size: { w: 140, h: 60 },
|
|
3335
|
-
outputs: [{ name: "time", datatype: "number" }],
|
|
3336
|
-
onCreate(node) {
|
|
3337
|
-
node.state.startTime = performance.now();
|
|
3338
|
-
},
|
|
3339
|
-
onExecute(node, { setOutput }) {
|
|
3340
|
-
const elapsed = (performance.now() - (node.state.startTime ?? 0)) / 1e3;
|
|
3341
|
-
setOutput("time", elapsed.toFixed(2));
|
|
3342
|
-
}
|
|
3343
|
-
});
|
|
3344
|
-
registry.register("util/Trigger", {
|
|
3345
|
-
title: "Trigger",
|
|
3346
|
-
size: { w: 140, h: 80 },
|
|
3347
|
-
outputs: [{ name: "exec", portType: "exec" }],
|
|
3348
|
-
// Changed to exec port
|
|
3349
|
-
html: {
|
|
3350
|
-
init(node, el, { header, body }) {
|
|
3351
|
-
el.style.backgroundColor = "#1e1e24";
|
|
3352
|
-
el.style.border = "1px solid #444";
|
|
3353
|
-
el.style.borderRadius = "8px";
|
|
3354
|
-
header.style.backgroundColor = "#2a2a31";
|
|
3355
|
-
header.style.borderBottom = "1px solid #444";
|
|
3356
|
-
header.style.color = "#eee";
|
|
3357
|
-
header.style.fontSize = "12px";
|
|
3358
|
-
header.textContent = "Trigger";
|
|
3359
|
-
body.style.padding = "12px";
|
|
3360
|
-
body.style.display = "flex";
|
|
3361
|
-
body.style.alignItems = "center";
|
|
3362
|
-
body.style.justifyContent = "center";
|
|
3363
|
-
const button = document.createElement("button");
|
|
3364
|
-
button.textContent = "Fire!";
|
|
3365
|
-
Object.assign(button.style, {
|
|
3366
|
-
padding: "8px 16px",
|
|
3367
|
-
background: "#4a9eff",
|
|
3368
|
-
border: "none",
|
|
3369
|
-
borderRadius: "4px",
|
|
3370
|
-
color: "#fff",
|
|
3371
|
-
fontWeight: "bold",
|
|
3372
|
-
cursor: "pointer",
|
|
3373
|
-
pointerEvents: "auto",
|
|
3374
|
-
transition: "background 0.2s"
|
|
3375
|
-
});
|
|
3376
|
-
button.addEventListener("mousedown", (e) => {
|
|
3377
|
-
e.stopPropagation();
|
|
3378
|
-
button.style.background = "#2a7ede";
|
|
3379
|
-
});
|
|
3380
|
-
button.addEventListener("mouseup", () => {
|
|
3381
|
-
button.style.background = "#4a9eff";
|
|
3382
|
-
});
|
|
3383
|
-
button.addEventListener("click", (e) => {
|
|
3384
|
-
e.stopPropagation();
|
|
3385
|
-
node.state.triggered = true;
|
|
3386
|
-
console.log("[Trigger] Button clicked!");
|
|
3387
|
-
if (node.__runnerRef && node.__controllerRef) {
|
|
3388
|
-
console.log("[Trigger] Runner and controller found");
|
|
3389
|
-
const runner2 = node.__runnerRef;
|
|
3390
|
-
const controller2 = node.__controllerRef;
|
|
3391
|
-
const graph2 = controller2.graph;
|
|
3392
|
-
console.log("[Trigger] Calling runner.runOnce with node.id:", node.id);
|
|
3393
|
-
const result = runner2.runOnce(node.id, 0);
|
|
3394
|
-
const connectedEdges = result.connectedEdges;
|
|
3395
|
-
const startTime = performance.now();
|
|
3396
|
-
const animationDuration = 500;
|
|
3397
|
-
const animate = () => {
|
|
3398
|
-
var _a;
|
|
3399
|
-
const elapsed = performance.now() - startTime;
|
|
3400
|
-
if (elapsed < animationDuration) {
|
|
3401
|
-
controller2.renderer.draw(graph2, {
|
|
3402
|
-
selection: controller2.selection,
|
|
3403
|
-
tempEdge: null,
|
|
3404
|
-
running: true,
|
|
3405
|
-
time: performance.now(),
|
|
3406
|
-
dt: 0,
|
|
3407
|
-
activeEdges: connectedEdges
|
|
3408
|
-
// Only animate connected edges
|
|
3409
|
-
});
|
|
3410
|
-
(_a = controller2.htmlOverlay) == null ? void 0 : _a.draw(graph2, controller2.selection);
|
|
3411
|
-
requestAnimationFrame(animate);
|
|
3412
|
-
} else {
|
|
3413
|
-
controller2.render();
|
|
3414
|
-
node.state.triggered = false;
|
|
3415
|
-
}
|
|
3416
|
-
};
|
|
3417
|
-
animate();
|
|
3418
|
-
}
|
|
3419
|
-
});
|
|
3420
|
-
body.appendChild(button);
|
|
3421
|
-
},
|
|
3422
|
-
update(node, el, { header, body, selected }) {
|
|
3423
|
-
el.style.borderColor = selected ? "#6cf" : "#444";
|
|
3424
|
-
header.style.backgroundColor = selected ? "#3a4a5a" : "#2a2a31";
|
|
3425
|
-
}
|
|
3426
|
-
},
|
|
3427
|
-
onCreate(node) {
|
|
3428
|
-
node.state.triggered = false;
|
|
3429
|
-
},
|
|
3430
|
-
onExecute(node, { setOutput }) {
|
|
3431
|
-
console.log("[Trigger] Outputting triggered:", node.state.triggered);
|
|
3432
|
-
setOutput("triggered", node.state.triggered);
|
|
3433
3521
|
}
|
|
3434
|
-
});
|
|
3435
|
-
registry.register("core/Group", {
|
|
3436
|
-
title: "Group",
|
|
3437
|
-
size: { w: 240, h: 160 },
|
|
3438
|
-
onDraw(node, { ctx, theme: theme2 }) {
|
|
3439
|
-
const { x, y, w, h } = node.computed;
|
|
3440
|
-
const headerH = 24;
|
|
3441
|
-
const color = node.state.color || "#39424e";
|
|
3442
|
-
const bgAlpha = 0.5;
|
|
3443
|
-
const textColor = theme2.text || "#e9e9ef";
|
|
3444
|
-
const rgba = (hex, a) => {
|
|
3445
|
-
const c = hex.replace("#", "");
|
|
3446
|
-
const n = parseInt(
|
|
3447
|
-
c.length === 3 ? c.split("").map((x2) => x2 + x2).join("") : c,
|
|
3448
|
-
16
|
|
3449
|
-
);
|
|
3450
|
-
const r = n >> 16 & 255, g = n >> 8 & 255, b = n & 255;
|
|
3451
|
-
return `rgba(${r},${g},${b},${a})`;
|
|
3452
|
-
};
|
|
3453
|
-
const roundRect2 = (ctx2, x2, y2, w2, h2, r) => {
|
|
3454
|
-
if (w2 < 2 * r) r = w2 / 2;
|
|
3455
|
-
if (h2 < 2 * r) r = h2 / 2;
|
|
3456
|
-
ctx2.beginPath();
|
|
3457
|
-
ctx2.moveTo(x2 + r, y2);
|
|
3458
|
-
ctx2.arcTo(x2 + w2, y2, x2 + w2, y2 + h2, r);
|
|
3459
|
-
ctx2.arcTo(x2 + w2, y2 + h2, x2, y2 + h2, r);
|
|
3460
|
-
ctx2.arcTo(x2, y2 + h2, x2, y2, r);
|
|
3461
|
-
ctx2.arcTo(x2, y2, x2 + w2, y2, r);
|
|
3462
|
-
ctx2.closePath();
|
|
3463
|
-
};
|
|
3464
|
-
ctx.fillStyle = rgba(color, bgAlpha);
|
|
3465
|
-
roundRect2(ctx, x, y, w, h, 10);
|
|
3466
|
-
ctx.fill();
|
|
3467
|
-
ctx.fillStyle = rgba(color, 0.3);
|
|
3468
|
-
ctx.beginPath();
|
|
3469
|
-
ctx.roundRect(x, y, w, headerH, [10, 10, 0, 0]);
|
|
3470
|
-
ctx.fill();
|
|
3471
|
-
ctx.fillStyle = textColor;
|
|
3472
|
-
ctx.font = "600 13px system-ui";
|
|
3473
|
-
ctx.textBaseline = "top";
|
|
3474
|
-
ctx.fillText(node.title, x + 12, y + 6);
|
|
3475
|
-
}
|
|
3476
|
-
});
|
|
3477
|
-
function setupDefaultContextMenu(contextMenu2, { controller: controller2, graph: graph2, hooks: hooks2 }) {
|
|
3478
|
-
const nodeTypes = [];
|
|
3479
|
-
for (const [key, typeDef] of graph2.registry.types.entries()) {
|
|
3480
|
-
nodeTypes.push({
|
|
3481
|
-
id: `add-${key}`,
|
|
3482
|
-
label: typeDef.title || key,
|
|
3483
|
-
action: () => {
|
|
3484
|
-
const worldPos = contextMenu2.worldPosition || { x: 100, y: 100 };
|
|
3485
|
-
const node = graph2.addNode(key, {
|
|
3486
|
-
x: worldPos.x,
|
|
3487
|
-
y: worldPos.y
|
|
3488
|
-
});
|
|
3489
|
-
hooks2 == null ? void 0 : hooks2.emit("node:updated", node);
|
|
3490
|
-
controller2.render();
|
|
3491
|
-
}
|
|
3492
|
-
});
|
|
3493
|
-
}
|
|
3494
|
-
contextMenu2.addItem("add-node", "Add Node", {
|
|
3495
|
-
condition: (target2) => !target2,
|
|
3496
|
-
submenu: nodeTypes,
|
|
3497
|
-
order: 5
|
|
3498
|
-
});
|
|
3499
|
-
contextMenu2.addItem("delete-node", "Delete Node", {
|
|
3500
|
-
condition: (target2) => target2 && target2.type !== "core/Group",
|
|
3501
|
-
action: (target2) => {
|
|
3502
|
-
const cmd = RemoveNodeCmd(graph2, target2);
|
|
3503
|
-
controller2.stack.exec(cmd);
|
|
3504
|
-
hooks2 == null ? void 0 : hooks2.emit("node:updated", target2);
|
|
3505
|
-
},
|
|
3506
|
-
order: 10
|
|
3507
|
-
});
|
|
3508
|
-
const colors = [
|
|
3509
|
-
{ name: "Default", color: "#39424e" },
|
|
3510
|
-
{ name: "Slate", color: "#4a5568" },
|
|
3511
|
-
{ name: "Gray", color: "#2d3748" },
|
|
3512
|
-
{ name: "Blue", color: "#1a365d" },
|
|
3513
|
-
{ name: "Green", color: "#22543d" },
|
|
3514
|
-
{ name: "Red", color: "#742a2a" },
|
|
3515
|
-
{ name: "Purple", color: "#44337a" }
|
|
3516
|
-
];
|
|
3517
|
-
contextMenu2.addItem("change-group-color", "Change Color", {
|
|
3518
|
-
condition: (target2) => target2 && target2.type === "core/Group",
|
|
3519
|
-
submenu: colors.map((colorInfo) => ({
|
|
3520
|
-
id: `color-${colorInfo.color}`,
|
|
3521
|
-
label: colorInfo.name,
|
|
3522
|
-
color: colorInfo.color,
|
|
3523
|
-
action: (target2) => {
|
|
3524
|
-
const currentColor = target2.state.color || "#39424e";
|
|
3525
|
-
const cmd = ChangeGroupColorCmd(target2, currentColor, colorInfo.color);
|
|
3526
|
-
controller2.stack.exec(cmd);
|
|
3527
|
-
hooks2 == null ? void 0 : hooks2.emit("node:updated", target2);
|
|
3528
|
-
}
|
|
3529
|
-
})),
|
|
3530
|
-
order: 20
|
|
3531
|
-
});
|
|
3532
|
-
contextMenu2.addItem("delete-group", "Delete Group", {
|
|
3533
|
-
condition: (target2) => target2 && target2.type === "core/Group",
|
|
3534
|
-
action: (target2) => {
|
|
3535
|
-
const cmd = RemoveNodeCmd(graph2, target2);
|
|
3536
|
-
controller2.stack.exec(cmd);
|
|
3537
|
-
hooks2 == null ? void 0 : hooks2.emit("node:updated", target2);
|
|
3538
|
-
},
|
|
3539
|
-
order: 20
|
|
3540
|
-
});
|
|
3541
3522
|
}
|
|
3542
|
-
setupDefaultContextMenu(contextMenu, { controller, graph, hooks });
|
|
3543
3523
|
renderer.resize(canvas.clientWidth, canvas.clientHeight);
|
|
3524
|
+
edgeRenderer.resize(canvas.clientWidth, canvas.clientHeight);
|
|
3544
3525
|
portRenderer.resize(canvas.clientWidth, canvas.clientHeight);
|
|
3545
3526
|
controller.render();
|
|
3546
3527
|
const ro = new ResizeObserver(() => {
|
|
3547
3528
|
renderer.resize(canvas.clientWidth, canvas.clientHeight);
|
|
3529
|
+
edgeRenderer.resize(canvas.clientWidth, canvas.clientHeight);
|
|
3548
3530
|
portRenderer.resize(canvas.clientWidth, canvas.clientHeight);
|
|
3549
3531
|
controller.render();
|
|
3550
3532
|
});
|
|
@@ -3589,6 +3571,7 @@ function createGraphEditor(target, {
|
|
|
3589
3571
|
contextMenu.destroy();
|
|
3590
3572
|
if (propertyPanel) propertyPanel.destroy();
|
|
3591
3573
|
if (minimap) minimap.destroy();
|
|
3574
|
+
if (helpOverlay) helpOverlay.destroy();
|
|
3592
3575
|
}
|
|
3593
3576
|
};
|
|
3594
3577
|
if (autorun) runner.start();
|