modulearn 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,623 @@
1
+ /* ModuLearn — graph-engine.js
2
+ *
3
+ * A small, self-contained canvas node-graph engine that replaces the vendored
4
+ * LiteGraph.js. It exposes the exact three globals graph.js consumes —
5
+ * `LiteGraph`, `LGraph`, `LGraphCanvas` — with byte-compatible signatures, so
6
+ * graph.js runs on top of it unchanged.
7
+ *
8
+ * Coordinate model (must match graph.js/zoomToFit's assumptions):
9
+ * screen = (graph + ds.offset) * ds.scale
10
+ * so graph = screen / scale - offset. Node.pos is the top-left of the node
11
+ * BODY; the title bar is drawn in the TITLE_H band directly above it. All the
12
+ * graph.js render hooks (canvas.onDrawForeground, node.onDrawForeground) are
13
+ * invoked in graph space, node hooks with the origin translated to node.pos.
14
+ *
15
+ * Scope: reproduces LiteGraph's look/feel and the surface graph.js uses —
16
+ * node boxes, typed slots, bezier wires, four widget kinds (number/combo/
17
+ * toggle/text), node drag, background pan, wheel zoom, port-drag connect with
18
+ * exact type-match, selection + delete, and collapse. Nothing more.
19
+ */
20
+ (function () {
21
+ "use strict";
22
+
23
+ // ---- layout constants (LiteGraph-ish) --------------------------------------
24
+ const TITLE_H = 30; // title bar band above node.pos
25
+ const SLOT_H = 20; // per input/output row
26
+ const WIDGET_H = 20; // per widget row
27
+ const SLOT_TOP = 6; // gap from body top to first slot row
28
+ const PORT_R = 4; // slot dot radius
29
+ const HIT_R = 11; // port grab radius (graph units)
30
+
31
+ function roundRect(ctx, x, y, w, h, r) {
32
+ r = Math.min(r, w / 2, h / 2);
33
+ ctx.beginPath();
34
+ ctx.moveTo(x + r, y);
35
+ ctx.arcTo(x + w, y, x + w, y + h, r);
36
+ ctx.arcTo(x + w, y + h, x, y + h, r);
37
+ ctx.arcTo(x, y + h, x, y, r);
38
+ ctx.arcTo(x, y, x + w, y, r);
39
+ ctx.closePath();
40
+ }
41
+
42
+ // ---- LiteGraph namespace ---------------------------------------------------
43
+ const registered = {}; // type id -> node constructor
44
+
45
+ const LiteGraph = {
46
+ NODE_TITLE_HEIGHT: TITLE_H,
47
+ NODE_SLOT_HEIGHT: SLOT_H,
48
+ registerNodeType(id, Ctor) {
49
+ // Splice LGraphNode's methods under the constructor's own prototype
50
+ // WITHOUT clobbering props graph.js already set on it (onDrawForeground).
51
+ Object.setPrototypeOf(Ctor.prototype, LGraphNode.prototype);
52
+ Ctor.type = id;
53
+ registered[id] = Ctor;
54
+ },
55
+ createNode(type) {
56
+ const Ctor = registered[type];
57
+ if (!Ctor) return null;
58
+ const n = Object.create(Ctor.prototype);
59
+ initNode(n, type, Ctor.title || type);
60
+ Ctor.call(n); // runs the spec constructor: addInput/Output/Widget
61
+ return n;
62
+ },
63
+ };
64
+
65
+ // ---- node base -------------------------------------------------------------
66
+ function initNode(n, type, title) {
67
+ n.id = null;
68
+ n.type = type;
69
+ n.title = title;
70
+ n.graph = null;
71
+ n.pos = [0, 0];
72
+ n.size = [140, 60];
73
+ n.inputs = [];
74
+ n.outputs = [];
75
+ n.widgets = [];
76
+ n.properties = {};
77
+ n.flags = { collapsed: false };
78
+ }
79
+
80
+ function LGraphNode() {}
81
+ LGraphNode.prototype.addInput = function (name, type) {
82
+ this.inputs.push({ name, type, link: null });
83
+ };
84
+ LGraphNode.prototype.addOutput = function (name, type) {
85
+ this.outputs.push({ name, type, links: [] });
86
+ };
87
+ LGraphNode.prototype.addWidget = function (kind, name, value, callback, options) {
88
+ const w = { type: kind, name, value, callback, options: options || {} };
89
+ this.widgets.push(w);
90
+ return w;
91
+ };
92
+ LGraphNode.prototype.computeSize = function () {
93
+ const rows = Math.max(this.inputs.length, this.outputs.length);
94
+ let h = SLOT_TOP * 2 + rows * SLOT_H + this.widgets.length * WIDGET_H;
95
+ h = Math.max(h, 42);
96
+ return [140, h];
97
+ };
98
+ // Graph-space position of an input/output slot's connection dot.
99
+ LGraphNode.prototype.getConnectionPos = function (isInput, slot, out) {
100
+ out = out || [0, 0];
101
+ if (this.flags.collapsed) {
102
+ out[0] = this.pos[0] + (isInput ? 0 : this.size[0]);
103
+ out[1] = this.pos[1] - TITLE_H / 2;
104
+ return out;
105
+ }
106
+ out[0] = this.pos[0] + (isInput ? 0 : this.size[0]);
107
+ out[1] = this.pos[1] + SLOT_TOP + slot * SLOT_H + SLOT_H * 0.5;
108
+ return out;
109
+ };
110
+ // Wire this node's output `slot` to `target`'s input `targetSlot`. An input
111
+ // holds a single link, so any prior link into it is replaced.
112
+ LGraphNode.prototype.connect = function (slot, target, targetSlot) {
113
+ const out = this.outputs[slot], inp = target.inputs[targetSlot];
114
+ const g = this.graph;
115
+ if (!out || !inp || !g) return false;
116
+ for (const id in g.links) {
117
+ const l = g.links[id];
118
+ if (l && l.target_id === target.id && l.target_slot === targetSlot) g._removeLink(id);
119
+ }
120
+ const id = ++g.last_link_id;
121
+ g.links[id] = {
122
+ id, origin_id: this.id, origin_slot: slot,
123
+ target_id: target.id, target_slot: targetSlot, type: out.type,
124
+ };
125
+ out.links.push(id);
126
+ inp.link = id;
127
+ if (g.onNodeConnectionChange) g.onNodeConnectionChange();
128
+ return true;
129
+ };
130
+
131
+ // ---- graph model -----------------------------------------------------------
132
+ function LGraph() {
133
+ this._nodes = [];
134
+ this.links = {};
135
+ this.last_node_id = 0;
136
+ this.last_link_id = 0;
137
+ this.onNodeAdded = null;
138
+ this.onNodeRemoved = null;
139
+ this.onNodeConnectionChange = null;
140
+ }
141
+ LGraph.prototype.add = function (node) {
142
+ if (node.id == null) node.id = ++this.last_node_id;
143
+ else this.last_node_id = Math.max(this.last_node_id, node.id);
144
+ node.graph = this;
145
+ this._nodes.push(node);
146
+ if (this.onNodeAdded) this.onNodeAdded(node);
147
+ return node;
148
+ };
149
+ LGraph.prototype.getNodeById = function (id) {
150
+ return this._nodes.find((n) => n.id == id) || null;
151
+ };
152
+ LGraph.prototype._removeLink = function (id) {
153
+ const l = this.links[id];
154
+ if (!l) return;
155
+ const src = this.getNodeById(l.origin_id);
156
+ if (src && src.outputs[l.origin_slot]) {
157
+ const arr = src.outputs[l.origin_slot].links;
158
+ const i = arr.indexOf(id); if (i >= 0) arr.splice(i, 1);
159
+ }
160
+ const dst = this.getNodeById(l.target_id);
161
+ if (dst && dst.inputs[l.target_slot] && dst.inputs[l.target_slot].link === id) {
162
+ dst.inputs[l.target_slot].link = null;
163
+ }
164
+ delete this.links[id];
165
+ };
166
+ LGraph.prototype.remove = function (node) {
167
+ let touched = false;
168
+ for (const id in this.links) {
169
+ const l = this.links[id];
170
+ if (l && (l.origin_id === node.id || l.target_id === node.id)) {
171
+ this._removeLink(id); touched = true;
172
+ }
173
+ }
174
+ const i = this._nodes.indexOf(node);
175
+ if (i >= 0) this._nodes.splice(i, 1);
176
+ node.graph = null;
177
+ if (touched && this.onNodeConnectionChange) this.onNodeConnectionChange();
178
+ if (this.onNodeRemoved) this.onNodeRemoved(node);
179
+ };
180
+ LGraph.prototype.clear = function () {
181
+ this._nodes = [];
182
+ this.links = {};
183
+ this.last_node_id = 0;
184
+ this.last_link_id = 0;
185
+ };
186
+ LGraph.prototype.start = function () { /* graph.js drives its own rAF loop */ };
187
+
188
+ // ---- canvas ----------------------------------------------------------------
189
+ function LGraphCanvas(selector, graph) {
190
+ this.canvas = typeof selector === "string"
191
+ ? document.querySelector(selector) : selector;
192
+ this.graph = graph;
193
+ this.ctx = this.canvas.getContext("2d");
194
+ this.ds = { scale: 1, offset: [0, 0] };
195
+ this.selected = null; // single-selection
196
+ this.node_over = null; // hovered node (graph.js drawPortCues)
197
+ this.connecting_output = null; // output slot obj mid-drag (graph.js drawPortCues)
198
+ this.onDrawForeground = null; // graph.js ambient hook (graph space)
199
+ this._connecting = null; // {node, slot}
200
+ this._drag = null; // active drag state
201
+ this._mouseGraph = [0, 0]; // last pointer in graph space
202
+ this._bindEvents();
203
+ }
204
+
205
+ LGraphCanvas.link_type_colors = {};
206
+
207
+ LGraphCanvas.prototype.resize = function () { this.draw(true, true); };
208
+ LGraphCanvas.prototype.setDirty = function () { this.draw(true, true); };
209
+
210
+ LGraphCanvas.prototype.convertOffsetToCanvas = function (pos) {
211
+ // screen -> graph
212
+ return [pos[0] / this.ds.scale - this.ds.offset[0],
213
+ pos[1] / this.ds.scale - this.ds.offset[1]];
214
+ };
215
+ LGraphCanvas.prototype._toGraph = function (clientX, clientY) {
216
+ const r = this.canvas.getBoundingClientRect();
217
+ return [(clientX - r.left) / this.ds.scale - this.ds.offset[0],
218
+ (clientY - r.top) / this.ds.scale - this.ds.offset[1]];
219
+ };
220
+
221
+ LGraphCanvas.prototype.selectNode = function (n) { this.selected = n; };
222
+ LGraphCanvas.prototype.centerOnNode = function (n) {
223
+ const s = this.ds.scale;
224
+ this.ds.offset[0] = this.canvas.width / (2 * s) - (n.pos[0] + n.size[0] / 2);
225
+ this.ds.offset[1] = this.canvas.height / (2 * s) - (n.pos[1] + n.size[1] / 2);
226
+ };
227
+
228
+ // ---- hit-testing (all in graph space) --------------------------------------
229
+ LGraphCanvas.prototype._nodeBox = function (n) {
230
+ const top = n.pos[1] - TITLE_H;
231
+ const h = TITLE_H + (n.flags.collapsed ? 0 : n.size[1]);
232
+ return [n.pos[0], top, n.size[0], h];
233
+ };
234
+ LGraphCanvas.prototype._nodeAt = function (gx, gy) {
235
+ for (let i = this.graph._nodes.length - 1; i >= 0; i--) {
236
+ const n = this.graph._nodes[i], b = this._nodeBox(n);
237
+ if (gx >= b[0] && gx <= b[0] + b[2] && gy >= b[1] && gy <= b[1] + b[3]) return n;
238
+ }
239
+ return null;
240
+ };
241
+ LGraphCanvas.prototype._portAt = function (gx, gy) {
242
+ const p = [0, 0];
243
+ for (let i = this.graph._nodes.length - 1; i >= 0; i--) {
244
+ const n = this.graph._nodes[i];
245
+ if (n.flags.collapsed) continue;
246
+ for (let s = 0; s < n.outputs.length; s++) {
247
+ n.getConnectionPos(false, s, p);
248
+ if (Math.hypot(gx - p[0], gy - p[1]) <= HIT_R)
249
+ return { node: n, isInput: false, slot: s, port: n.outputs[s] };
250
+ }
251
+ for (let s = 0; s < n.inputs.length; s++) {
252
+ n.getConnectionPos(true, s, p);
253
+ if (Math.hypot(gx - p[0], gy - p[1]) <= HIT_R)
254
+ return { node: n, isInput: true, slot: s, port: n.inputs[s] };
255
+ }
256
+ }
257
+ return null;
258
+ };
259
+ // widget row under a point, given the node it's inside.
260
+ LGraphCanvas.prototype._widgetAt = function (n, gx, gy) {
261
+ if (n.flags.collapsed || !n.widgets.length) return -1;
262
+ const rows = Math.max(n.inputs.length, n.outputs.length);
263
+ const top = n.pos[1] + SLOT_TOP + rows * SLOT_H + 2;
264
+ if (gx < n.pos[0] + 4 || gx > n.pos[0] + n.size[0] - 4) return -1;
265
+ const idx = Math.floor((gy - top) / WIDGET_H);
266
+ return (idx >= 0 && idx < n.widgets.length) ? idx : -1;
267
+ };
268
+ LGraphCanvas.prototype._collapseHit = function (n, gx, gy) {
269
+ // small toggle circle at the title's left
270
+ return Math.hypot(gx - (n.pos[0] + 14), gy - (n.pos[1] - TITLE_H / 2)) <= 7;
271
+ };
272
+
273
+ // ---- widget value editing --------------------------------------------------
274
+ function widgetDisplay(w) {
275
+ if (w.type === "toggle") return `${w.name}: ${w.value ? "on" : "off"}`;
276
+ if (w.type === "number") {
277
+ const prec = w.options.precision;
278
+ const v = prec === 0 ? Math.round(w.value) : w.value;
279
+ return `${w.name}: ${v}`;
280
+ }
281
+ return `${w.name}: ${w.value}`;
282
+ }
283
+ function applyNumber(w, v) {
284
+ const o = w.options;
285
+ if (o.precision === 0) v = Math.round(v);
286
+ else v = Math.round(v / (o.step || 1)) * (o.step || 1);
287
+ if (o.min != null) v = Math.max(o.min, v);
288
+ if (o.max != null) v = Math.min(o.max, v);
289
+ if (o.precision != null && o.precision > 0) v = +v.toFixed(o.precision);
290
+ return v;
291
+ }
292
+ LGraphCanvas.prototype._commitWidget = function (w, v) {
293
+ w.value = v;
294
+ if (w.callback) w.callback(v, this, null);
295
+ this.setDirty();
296
+ };
297
+ // A plain click (no meaningful drag) on a widget.
298
+ LGraphCanvas.prototype._clickWidget = function (w) {
299
+ if (w.type === "toggle") { this._commitWidget(w, !w.value); return; }
300
+ if (w.type === "combo") {
301
+ const vals = w.options.values || [];
302
+ const i = vals.indexOf(w.value);
303
+ if (vals.length) this._commitWidget(w, vals[(i + 1) % vals.length]);
304
+ return;
305
+ }
306
+ if (w.type === "number") {
307
+ const raw = window.prompt(w.name, String(w.value));
308
+ if (raw != null && raw.trim() !== "" && !isNaN(+raw))
309
+ this._commitWidget(w, applyNumber(w, +raw));
310
+ return;
311
+ }
312
+ // text (incl. int_list stored as "128,64")
313
+ const raw = window.prompt(w.name, String(w.value));
314
+ if (raw != null) this._commitWidget(w, raw);
315
+ };
316
+
317
+ // ---- interaction -----------------------------------------------------------
318
+ LGraphCanvas.prototype._bindEvents = function () {
319
+ const cv = this.canvas;
320
+ cv.addEventListener("mousedown", (e) => this._onDown(e));
321
+ window.addEventListener("mousemove", (e) => this._onMove(e));
322
+ window.addEventListener("mouseup", (e) => this._onUp(e));
323
+ cv.addEventListener("wheel", (e) => this._onWheel(e), { passive: false });
324
+ cv.addEventListener("contextmenu", (e) => e.preventDefault());
325
+ window.addEventListener("keydown", (e) => this._onKey(e));
326
+ };
327
+
328
+ LGraphCanvas.prototype._onDown = function (e) {
329
+ if (e.button !== 0) return;
330
+ const [gx, gy] = this._toGraph(e.clientX, e.clientY);
331
+ this._mouseGraph = [gx, gy];
332
+
333
+ // 1) start/pick-up a wire from a port
334
+ const hit = this._portAt(gx, gy);
335
+ if (hit) {
336
+ if (hit.isInput) {
337
+ // grab the existing link (if any) off this input to reconnect it
338
+ const lid = hit.port.link;
339
+ if (lid != null && this.graph.links[lid]) {
340
+ const l = this.graph.links[lid];
341
+ const src = this.graph.getNodeById(l.origin_id);
342
+ this.graph._removeLink(lid);
343
+ if (this.graph.onNodeConnectionChange) this.graph.onNodeConnectionChange();
344
+ this._connecting = { node: src, slot: l.origin_slot };
345
+ this.connecting_output = src.outputs[l.origin_slot];
346
+ e.preventDefault(); return;
347
+ }
348
+ return; // bare input: nothing to drag
349
+ }
350
+ this._connecting = { node: hit.node, slot: hit.slot };
351
+ this.connecting_output = hit.port;
352
+ e.preventDefault(); return;
353
+ }
354
+
355
+ // 2) node?
356
+ const node = this._nodeAt(gx, gy);
357
+ if (node) {
358
+ this.selected = node;
359
+ if (this._collapseHit(node, gx, gy)) {
360
+ node.flags.collapsed = !node.flags.collapsed;
361
+ this.setDirty(); e.preventDefault(); return;
362
+ }
363
+ const wi = this._widgetAt(node, gx, gy);
364
+ if (wi >= 0) {
365
+ this._drag = { kind: "widget", node, widget: node.widgets[wi],
366
+ startX: gx, startVal: node.widgets[wi].value, moved: false };
367
+ e.preventDefault(); return;
368
+ }
369
+ this._drag = { kind: "node", node, ox: gx - node.pos[0], oy: gy - node.pos[1] };
370
+ e.preventDefault(); return;
371
+ }
372
+
373
+ // 3) empty space: pan (and clear selection)
374
+ this.selected = null;
375
+ this._drag = { kind: "pan", sx: e.clientX, sy: e.clientY,
376
+ ox: this.ds.offset[0], oy: this.ds.offset[1] };
377
+ this.setDirty();
378
+ };
379
+
380
+ LGraphCanvas.prototype._onMove = function (e) {
381
+ const [gx, gy] = this._toGraph(e.clientX, e.clientY);
382
+ this._mouseGraph = [gx, gy];
383
+ this.node_over = this._nodeAt(gx, gy);
384
+
385
+ const d = this._drag;
386
+ if (d) {
387
+ if (d.kind === "node") {
388
+ d.node.pos[0] = gx - d.ox; d.node.pos[1] = gy - d.oy;
389
+ } else if (d.kind === "pan") {
390
+ this.ds.offset[0] = d.ox + (e.clientX - d.sx) / this.ds.scale;
391
+ this.ds.offset[1] = d.oy + (e.clientY - d.sy) / this.ds.scale;
392
+ } else if (d.kind === "widget") {
393
+ const dx = gx - d.startX;
394
+ if (Math.abs(dx) > 3) d.moved = true;
395
+ const w = d.widget;
396
+ if (w.type === "number" && d.moved) {
397
+ const step = w.options.step || 0.1;
398
+ this._commitWidget(w, applyNumber(w, d.startVal + dx * step * 0.25));
399
+ }
400
+ }
401
+ this.setDirty();
402
+ return;
403
+ }
404
+ if (this._connecting) this.setDirty();
405
+ };
406
+
407
+ LGraphCanvas.prototype._onUp = function (e) {
408
+ if (this._connecting) {
409
+ const [gx, gy] = this._toGraph(e.clientX, e.clientY);
410
+ const hit = this._portAt(gx, gy);
411
+ const c = this._connecting;
412
+ if (hit && hit.isInput && hit.port.type === c.node.outputs[c.slot].type) {
413
+ c.node.connect(c.slot, hit.node, hit.slot);
414
+ }
415
+ this._connecting = null;
416
+ this.connecting_output = null;
417
+ this.setDirty();
418
+ }
419
+ const d = this._drag;
420
+ if (d && d.kind === "widget" && !d.moved) this._clickWidget(d.widget);
421
+ this._drag = null;
422
+ };
423
+
424
+ LGraphCanvas.prototype._onWheel = function (e) {
425
+ e.preventDefault();
426
+ const r = this.canvas.getBoundingClientRect();
427
+ const sx = e.clientX - r.left, sy = e.clientY - r.top;
428
+ const before = [sx / this.ds.scale - this.ds.offset[0],
429
+ sy / this.ds.scale - this.ds.offset[1]];
430
+ const factor = e.deltaY < 0 ? 1.1 : 1 / 1.1;
431
+ this.ds.scale = Math.max(0.15, Math.min(3, this.ds.scale * factor));
432
+ // keep the graph point under the cursor fixed
433
+ this.ds.offset[0] = sx / this.ds.scale - before[0];
434
+ this.ds.offset[1] = sy / this.ds.scale - before[1];
435
+ this.setDirty();
436
+ };
437
+
438
+ LGraphCanvas.prototype._onKey = function (e) {
439
+ const typing = /^(input|textarea|select)$/i.test(e.target.tagName || "");
440
+ if (typing) return;
441
+ if ((e.key === "Delete" || e.key === "Backspace") && this.selected) {
442
+ e.preventDefault();
443
+ const n = this.selected; this.selected = null;
444
+ this.graph.remove(n);
445
+ this.setDirty();
446
+ }
447
+ };
448
+
449
+ // ---- rendering -------------------------------------------------------------
450
+ const COL = {
451
+ titleBg: "#2a323f", titleText: "#e6edf3",
452
+ bodyBg: "#1b212b", border: "#39424f", sel: "#c8663c",
453
+ slotText: "#c3ccd8", widgetBg: "#232b37", widgetText: "#e6edf3",
454
+ muted: "#8b98a9",
455
+ };
456
+ function linkColor(type) {
457
+ return LGraphCanvas.link_type_colors[type]
458
+ || LGraphCanvas.link_type_colors[(type || "").split("/")[0]]
459
+ || COL.muted;
460
+ }
461
+
462
+ LGraphCanvas.prototype.draw = function () {
463
+ const ctx = this.ctx, cv = this.canvas;
464
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
465
+ ctx.clearRect(0, 0, cv.width, cv.height);
466
+ ctx.fillStyle = "#14181f";
467
+ ctx.fillRect(0, 0, cv.width, cv.height);
468
+
469
+ const s = this.ds.scale, o = this.ds.offset;
470
+ ctx.setTransform(s, 0, 0, s, o[0] * s, o[1] * s);
471
+
472
+ this._drawGrid(ctx, cv);
473
+ this._drawLinks(ctx);
474
+ if (this._connecting) this._drawConnecting(ctx);
475
+ for (const n of this.graph._nodes) this._drawNode(ctx, n);
476
+ if (this.onDrawForeground) this.onDrawForeground(ctx); // graph.js ambient
477
+ };
478
+
479
+ LGraphCanvas.prototype._drawGrid = function (ctx, cv) {
480
+ const s = this.ds.scale, o = this.ds.offset;
481
+ const step = 50;
482
+ const x0 = -o[0], y0 = -o[1];
483
+ const x1 = cv.width / s - o[0], y1 = cv.height / s - o[1];
484
+ ctx.strokeStyle = "rgba(255,255,255,.03)";
485
+ ctx.lineWidth = 1 / s;
486
+ ctx.beginPath();
487
+ for (let x = Math.floor(x0 / step) * step; x < x1; x += step) {
488
+ ctx.moveTo(x, y0); ctx.lineTo(x, y1);
489
+ }
490
+ for (let y = Math.floor(y0 / step) * step; y < y1; y += step) {
491
+ ctx.moveTo(x0, y); ctx.lineTo(x1, y);
492
+ }
493
+ ctx.stroke();
494
+ };
495
+
496
+ LGraphCanvas.prototype._bezier = function (ctx, a, b, color, width) {
497
+ const dist = Math.max(40, Math.abs(b[0] - a[0]) * 0.5);
498
+ ctx.beginPath();
499
+ ctx.moveTo(a[0], a[1]);
500
+ ctx.bezierCurveTo(a[0] + dist, a[1], b[0] - dist, b[1], b[0], b[1]);
501
+ ctx.strokeStyle = color; ctx.lineWidth = width; ctx.stroke();
502
+ };
503
+
504
+ LGraphCanvas.prototype._drawLinks = function (ctx) {
505
+ const a = [0, 0], b = [0, 0];
506
+ for (const id in this.graph.links) {
507
+ const l = this.graph.links[id]; if (!l) continue;
508
+ const src = this.graph.getNodeById(l.origin_id);
509
+ const dst = this.graph.getNodeById(l.target_id);
510
+ if (!src || !dst) continue;
511
+ src.getConnectionPos(false, l.origin_slot, a);
512
+ dst.getConnectionPos(true, l.target_slot, b);
513
+ this._bezier(ctx, [a[0], a[1]], [b[0], b[1]], linkColor(l.type), 2.5);
514
+ }
515
+ };
516
+
517
+ LGraphCanvas.prototype._drawConnecting = function (ctx) {
518
+ const c = this._connecting, a = [0, 0];
519
+ c.node.getConnectionPos(false, c.slot, a);
520
+ this._bezier(ctx, [a[0], a[1]], this._mouseGraph,
521
+ linkColor(c.node.outputs[c.slot].type), 2);
522
+ };
523
+
524
+ LGraphCanvas.prototype._drawNode = function (ctx, n) {
525
+ const x = n.pos[0], y = n.pos[1], w = n.size[0];
526
+ const collapsed = n.flags.collapsed;
527
+ const bodyH = collapsed ? 0 : n.size[1];
528
+
529
+ // body
530
+ if (!collapsed) {
531
+ roundRect(ctx, x, y, w, bodyH, 6);
532
+ ctx.fillStyle = COL.bodyBg; ctx.fill();
533
+ }
534
+ // title
535
+ roundRect(ctx, x, y - TITLE_H, w, TITLE_H + (collapsed ? 0 : 6), 6);
536
+ ctx.fillStyle = COL.titleBg; ctx.fill();
537
+ // collapse dot
538
+ ctx.beginPath();
539
+ ctx.arc(x + 14, y - TITLE_H / 2, 4, 0, Math.PI * 2);
540
+ ctx.fillStyle = collapsed ? COL.sel : COL.muted; ctx.fill();
541
+ // title text
542
+ ctx.fillStyle = COL.titleText;
543
+ ctx.font = "13px ui-sans-serif, system-ui, sans-serif";
544
+ ctx.textBaseline = "middle"; ctx.textAlign = "left";
545
+ ctx.fillText(n.title || n.type, x + 26, y - TITLE_H / 2 + 1);
546
+
547
+ // selection ring
548
+ if (this.selected === n) {
549
+ roundRect(ctx, x - 1.5, y - TITLE_H - 1.5, w + 3, TITLE_H + bodyH + 3, 7);
550
+ ctx.strokeStyle = COL.sel; ctx.lineWidth = 1.5; ctx.stroke();
551
+ } else {
552
+ roundRect(ctx, x, y - TITLE_H, w, TITLE_H + bodyH, 6);
553
+ ctx.strokeStyle = COL.border; ctx.lineWidth = 1; ctx.stroke();
554
+ }
555
+
556
+ if (collapsed) { this._drawSlotDots(ctx, n, true); return; }
557
+
558
+ this._drawSlotDots(ctx, n, false);
559
+ this._drawWidgets(ctx, n);
560
+
561
+ // graph.js node hook (descriptors + Train status), origin at node body
562
+ if (n.onDrawForeground) {
563
+ ctx.save(); ctx.translate(x, y);
564
+ ctx.textAlign = "left"; ctx.textBaseline = "alphabetic";
565
+ n.onDrawForeground(ctx);
566
+ ctx.restore();
567
+ }
568
+ };
569
+
570
+ LGraphCanvas.prototype._drawSlotDots = function (ctx, n, collapsed) {
571
+ const p = [0, 0];
572
+ ctx.font = "11px ui-sans-serif, system-ui, sans-serif";
573
+ ctx.textBaseline = "middle";
574
+ n.inputs.forEach((inp, i) => {
575
+ n.getConnectionPos(true, i, p);
576
+ ctx.beginPath(); ctx.arc(p[0], p[1], PORT_R, 0, Math.PI * 2);
577
+ ctx.fillStyle = linkColor(inp.type); ctx.fill();
578
+ if (!collapsed) {
579
+ ctx.fillStyle = COL.slotText; ctx.textAlign = "left";
580
+ ctx.fillText(inp.name, p[0] + 8, p[1]);
581
+ }
582
+ });
583
+ n.outputs.forEach((out, i) => {
584
+ n.getConnectionPos(false, i, p);
585
+ ctx.beginPath(); ctx.arc(p[0], p[1], PORT_R, 0, Math.PI * 2);
586
+ ctx.fillStyle = linkColor(out.type); ctx.fill();
587
+ if (!collapsed) {
588
+ ctx.fillStyle = COL.slotText; ctx.textAlign = "right";
589
+ ctx.fillText(out.name, p[0] - 8, p[1]);
590
+ }
591
+ });
592
+ };
593
+
594
+ LGraphCanvas.prototype._drawWidgets = function (ctx, n) {
595
+ const rows = Math.max(n.inputs.length, n.outputs.length);
596
+ let y = n.pos[1] + SLOT_TOP + rows * SLOT_H + 2;
597
+ const x = n.pos[0] + 6, w = n.size[0] - 12;
598
+ ctx.font = "11px ui-sans-serif, system-ui, sans-serif";
599
+ ctx.textBaseline = "middle";
600
+ for (const wd of n.widgets) {
601
+ roundRect(ctx, x, y + 2, w, WIDGET_H - 4, 4);
602
+ ctx.fillStyle = COL.widgetBg; ctx.fill();
603
+ const cy = y + WIDGET_H / 2;
604
+ // affordance markers
605
+ ctx.fillStyle = COL.muted; ctx.textAlign = "right";
606
+ if (wd.type === "number" || wd.type === "combo")
607
+ ctx.fillText("‹ ›", x + w - 6, cy);
608
+ else if (wd.type === "toggle") {
609
+ ctx.fillStyle = wd.value ? "#3fb950" : COL.muted;
610
+ ctx.fillText(wd.value ? "◉" : "◯", x + w - 6, cy);
611
+ } else ctx.fillText("✎", x + w - 6, cy);
612
+ // label + value
613
+ ctx.fillStyle = COL.widgetText; ctx.textAlign = "left";
614
+ ctx.fillText(widgetDisplay(wd), x + 8, cy);
615
+ y += WIDGET_H;
616
+ }
617
+ };
618
+
619
+ // ---- export globals --------------------------------------------------------
620
+ window.LiteGraph = LiteGraph;
621
+ window.LGraph = LGraph;
622
+ window.LGraphCanvas = LGraphCanvas;
623
+ })();