+1
-1
@@ -1,5 +1,5 @@
|
||||
[tool.bumpversion]
|
||||
current_version = "1.1.2"
|
||||
current_version = "1.2.0"
|
||||
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
||||
serialize = ["{major}.{minor}.{patch}"]
|
||||
search = "{current_version}"
|
||||
|
||||
+1
-1
@@ -102,4 +102,4 @@ cookiecutter-pypackage-env/
|
||||
|
||||
node.zip
|
||||
|
||||
.claude/
|
||||
.claude/
|
||||
|
||||
@@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [1.2.0] - 2026-06-23
|
||||
|
||||
### Added
|
||||
|
||||
- Added **Trace** link style with node-aware routing, 90°/45° path constraints, rounded corners, and no center marker.
|
||||
- Added a node bbox spatial index to reduce routing intersection checks on large graphs.
|
||||
|
||||
### Changed
|
||||
|
||||
- Implemented Trace in `web/js/Trace.js` with bbox indexing in `web/js/NodeBBoxIndex.js`.
|
||||
- Improved custom link rendering for backward links and collapsed-node layouts.
|
||||
- Added attribution for routing logic adapted from `quick-connections`.
|
||||
|
||||
---
|
||||
|
||||
## [1.1.2] - 2026-01-19
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -17,7 +17,7 @@ A one-click collection of alternate connection styles for ComfyUI.
|
||||
|
||||
------------------------------------------------------------------
|
||||
|
||||
Different types of **link connections** and **geometry awareness**.
|
||||
Different types of **link connections**, **geometry awareness**, and routed **Trace** links.
|
||||
|
||||
<div align="center">
|
||||
<img width=250 src="imgs/curved.png">
|
||||
@@ -69,13 +69,17 @@ Restart ComfyUI.
|
||||
Open **Settings** → **Extra Links** and configure:
|
||||
|
||||
- **Enable**: toggle on/off
|
||||
- **Shape**: `Curved` | `Manhattan` | `Subway`
|
||||
- **Shape**: `Curved` | `Manhattan` | `Subway` | `Trace`
|
||||
- **Radius**: corner smoothing
|
||||
- **Offset**: horizontal separation for multi-outputs
|
||||
- **Curvature**: strength of turn-in/out
|
||||
|
||||
Changes apply immediately to the canvas.
|
||||
|
||||
### Trace
|
||||
|
||||
`Trace` routes links with mostly 90° and 45° segments, rounded corners, and node-aware obstacle routing. It avoids the center marker used by the other link styles.
|
||||
|
||||
------------------------------------------------------------------
|
||||
|
||||
## Compatibility
|
||||
@@ -111,6 +115,7 @@ If issues persist, please open an issue with:
|
||||
|
||||
## Versioning & Releases
|
||||
|
||||
- **1.2.0** - Added Trace mode with node-aware 90°/45° routing.
|
||||
- **1.1.2** - Fixed links inside subgraphs.
|
||||
- **1.1.1** - Fixed collapsed state node links.
|
||||
- **1.1.0** - Added subway style and geometry awareness.
|
||||
@@ -129,8 +134,10 @@ Tags:
|
||||
|
||||
## Development
|
||||
|
||||
- Main code lives in `extralinks/web/ExtraLinks.js`
|
||||
- Settings/registration live in `extralinks/web/extralinks_register.js`
|
||||
- Main code lives in `extralinks/web/js/ExtraLinks.js`
|
||||
- Trace routing lives in `extralinks/web/js/Trace.js`
|
||||
- Node bbox indexing lives in `extralinks/web/js/NodeBBoxIndex.js`
|
||||
- Settings/registration live in `extralinks/web/js/extralinks_register.js`
|
||||
- Python exposes `WEB_DIRECTORY` via `extralinks/__init__.py`
|
||||
|
||||
Typical flow:
|
||||
@@ -161,4 +168,5 @@ MIT (see [`LICENSE`](LICENSE))
|
||||
## Credits
|
||||
|
||||
- Built by [@edoardocarmignani](https://github.com/edoardocarmignani)
|
||||
- Trace routing is adapted from [quick-connections](https://github.com/niknah/quick-connections) by [@niknah](https://github.com/niknah)
|
||||
- Thanks to the ComfyUI community for the incredible work
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
|
||||
__author__ = 'Edoardo Carmignani'
|
||||
__email__ = 'edoardo.carmignani@gmail.com'
|
||||
__version__ = '1.1.2'
|
||||
__version__ = '1.2.0'
|
||||
|
||||
WEB_DIRECTORY = './web'
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "extralinks"
|
||||
version = "1.1.2"
|
||||
version = "1.2.0"
|
||||
description = "More connection links."
|
||||
authors = [
|
||||
{name = "Edoardo Carmignani", email = "edoardo.carmignani@gmail.com"}
|
||||
|
||||
+62
-31
@@ -1,4 +1,15 @@
|
||||
import { app } from "../../../scripts/app.js"
|
||||
import { traceRenderer } from "./Trace.js"
|
||||
|
||||
function _getGraphLink(graph, linkId) {
|
||||
return graph?.getLink?.(linkId)
|
||||
?? graph?.getLink?.(Number(linkId))
|
||||
?? graph?.links?.[linkId]
|
||||
?? graph?.links?.[String(linkId)]
|
||||
?? graph?.links?.get?.(linkId)
|
||||
?? graph?.links?.get?.(Number(linkId))
|
||||
?? null;
|
||||
}
|
||||
|
||||
const LinkRenderers = {
|
||||
curved: {
|
||||
@@ -21,7 +32,7 @@ const LinkRenderers = {
|
||||
const oX = Math.min(offsetX, (dx * 0.5))
|
||||
const x1 = x0 + oX;
|
||||
|
||||
const r = Math.min(radius, Math.abs(dx * 0.5), Math.abs(dy * 0.5), Math.abs(x0 - x1));
|
||||
const r = dx > 0 ? Math.min(radius, Math.abs(dx * 0.5), Math.abs(dy * 0.5), Math.abs(x0 - x1)) : radius;
|
||||
|
||||
path.moveTo(x0, y1);
|
||||
|
||||
@@ -51,11 +62,11 @@ const LinkRenderers = {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeHeight = start_node.collapsed ? start_node.height / 2 : start_node.size[1];
|
||||
const sourceBottom = start_node.collapsed ? start_node.pos[1] - (nodeHeight - offset) / 2 : nodeHeight + start_node.pos[1];
|
||||
const sourceLeft = (start_node.pos[1] + start_node.size[1] < y2) ? start_node.pos[0] + start_node.size[0] : start_node.pos[0] + 10;
|
||||
const sourceBottom = start_node.collapsed ? y1 + offset / 2 : start_node.pos[1] + start_node.size[1];
|
||||
const targetBottom = end_node ? (end_node.collapsed ? y2 + offset / 2 : end_node.pos[1] + end_node.size[1]) : sourceBottom;
|
||||
const sourceLeft = (sourceBottom < y2) ? start_node.pos[0] + start_node.size[0] : start_node.pos[0] + 10;
|
||||
|
||||
const trayY = sourceBottom + ((slot_id + 1) * offset / 2);
|
||||
const trayY = Math.max(y1, Math.max(sourceBottom, targetBottom) + ((slot_id + 1) * offset / 2));
|
||||
|
||||
const turnX1 = x0 + offset;
|
||||
let turnX2 = x2 - offset;
|
||||
@@ -64,10 +75,13 @@ const LinkRenderers = {
|
||||
turnX2 = sourceLeft - offset;
|
||||
}
|
||||
|
||||
path.arcTo(turnX1, y1, turnX1, trayY, r);
|
||||
path.arcTo(turnX1, trayY, turnX2, trayY, r);
|
||||
path.arcTo(turnX2, trayY, turnX2, y2, r);
|
||||
path.arcTo(turnX2, y2, x2, y2, r);
|
||||
const r12 = Math.min(r, (trayY - y1) / 2);
|
||||
const r34 = Math.min(r, Math.abs(y2 - trayY) / 2);
|
||||
|
||||
path.arcTo(turnX1, y1, turnX1, trayY, r12);
|
||||
path.arcTo(turnX1, trayY, turnX2, trayY, r12);
|
||||
path.arcTo(turnX2, trayY, turnX2, y2, r34);
|
||||
path.arcTo(turnX2, y2, x2, y2, r34);
|
||||
path.lineTo(x2, y2);
|
||||
|
||||
pos[0] = (x2 > sourceLeft) ? (turnX2 + turnX1) / 2 : (x0 + x2) / 2;
|
||||
@@ -91,7 +105,7 @@ const LinkRenderers = {
|
||||
const dirX = Math.sign(dx);
|
||||
const limitedOffset = offset * (outputs > 1 ? Math.log(outputs) : 1);
|
||||
const fanOffset = Math.max((limitedOffset - slot_id * curvature) * dirX, 0);
|
||||
const r = Math.min(radius, Math.abs(dx) * 0.5, Math.abs(dy) * 0.5);
|
||||
const r = dx > 0 ? Math.min(radius, Math.abs(dx) * 0.5, Math.abs(dy) * 0.5) : radius;
|
||||
|
||||
path.moveTo(x0, y1);
|
||||
|
||||
@@ -116,11 +130,11 @@ const LinkRenderers = {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeHeight = start_node.collapsed ? start_node.height / 2 : start_node.size[1];
|
||||
const sourceBottom = start_node.collapsed ? start_node.pos[1] - (nodeHeight - offset) / 2 : nodeHeight + start_node.pos[1];
|
||||
const sourceLeft = (start_node.pos[1] + start_node.size[1] < y2) ? start_node.pos[0] + start_node.size[0] : start_node.pos[0] + 10;
|
||||
const sourceBottom = start_node.collapsed ? y1 + offset / 2 : start_node.pos[1] + start_node.size[1];
|
||||
const targetBottom = end_node ? (end_node.collapsed ? y2 + offset / 2 : end_node.pos[1] + end_node.size[1]) : sourceBottom;
|
||||
const sourceLeft = (sourceBottom < y2) ? start_node.pos[0] + start_node.size[0] : start_node.pos[0] + 10;
|
||||
|
||||
const trayY = sourceBottom + ((slot_id + 1) * offset / 2);
|
||||
const trayY = Math.max(y1, Math.max(sourceBottom, targetBottom) + ((slot_id + 1) * offset / 2));
|
||||
|
||||
const turnX1 = x0 + offset;
|
||||
let turnX2 = x2 - offset;
|
||||
@@ -129,10 +143,13 @@ const LinkRenderers = {
|
||||
turnX2 = sourceLeft - offset;
|
||||
}
|
||||
|
||||
path.arcTo(turnX1, y1, turnX1, trayY, r);
|
||||
path.arcTo(turnX1, trayY, turnX2, trayY, r);
|
||||
path.arcTo(turnX2, trayY, turnX2, y2, r);
|
||||
path.arcTo(turnX2, y2, x2, y2, r);
|
||||
const r12 = Math.min(r, (trayY - y1) / 2);
|
||||
const r34 = Math.min(r, Math.abs(y2 - trayY) / 2);
|
||||
|
||||
path.arcTo(turnX1, y1, turnX1, trayY, r12);
|
||||
path.arcTo(turnX1, trayY, turnX2, trayY, r12);
|
||||
path.arcTo(turnX2, trayY, turnX2, y2, r34);
|
||||
path.arcTo(turnX2, y2, x2, y2, r34);
|
||||
path.lineTo(x2, y2);
|
||||
|
||||
pos[0] = (x2 > sourceLeft) ? (turnX2 + turnX1) / 2 : (x0 + x2) / 2;
|
||||
@@ -152,7 +169,7 @@ const LinkRenderers = {
|
||||
const dx = x2 - x0;
|
||||
const dy = y2 - y0;
|
||||
|
||||
const r = Math.min(radius, offset, Math.abs(dx), Math.abs(dy));
|
||||
const r = dx > 0 ? Math.min(radius, offset, Math.abs(dx), Math.abs(dy)) : radius;
|
||||
|
||||
const midX = (x0 + x2) * 0.5;
|
||||
const hspace = (dx * 0.5);
|
||||
@@ -189,21 +206,24 @@ const LinkRenderers = {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeHeight = start_node.collapsed ? start_node.height / 2 : start_node.size[1];
|
||||
const sourceBottom = start_node.collapsed ? start_node.pos[1] - (nodeHeight - offset) / 2 : nodeHeight + start_node.pos[1];
|
||||
const sourceLeft = (start_node.pos[1] + nodeHeight < y2) ? start_node.pos[0] + start_node.size[0] : start_node.pos[0] + 10;
|
||||
const sourceBottom = start_node.collapsed ? y0 + offset / 2 : start_node.pos[1] + start_node.size[1];
|
||||
const targetBottom = end_node ? (end_node.collapsed ? y2 + offset / 2 : end_node.pos[1] + end_node.size[1]) : sourceBottom;
|
||||
const sourceLeft = (sourceBottom < y2) ? start_node.pos[0] + start_node.size[0] : start_node.pos[0] + 10;
|
||||
|
||||
const trayY = sourceBottom + ((slot_id + 1) * offset / 2);
|
||||
const trayY = Math.max(y0, Math.max(sourceBottom, targetBottom) + ((slot_id + 1) * offset / 2));
|
||||
let turnX2 = x2 - offset;
|
||||
|
||||
if (Math.floor(x2) > Math.floor(sourceLeft)) {
|
||||
turnX2 = sourceLeft - offset;
|
||||
}
|
||||
|
||||
path.arcTo(turnX1, y0, turnX1, trayY, r);
|
||||
path.arcTo(turnX1, trayY, turnX2, trayY, r);
|
||||
path.arcTo(turnX2, trayY, turnX2, y2, r);
|
||||
path.arcTo(turnX2, y2, x2, y2, r);
|
||||
const r12 = Math.min(r, (trayY - y0) / 2);
|
||||
const r34 = Math.min(r, Math.abs(y2 - trayY) / 2);
|
||||
|
||||
path.arcTo(turnX1, y0, turnX1, trayY, r12);
|
||||
path.arcTo(turnX1, trayY, turnX2, trayY, r12);
|
||||
path.arcTo(turnX2, trayY, turnX2, y2, r34);
|
||||
path.arcTo(turnX2, y2, x2, y2, r34);
|
||||
path.lineTo(x2, y2);
|
||||
|
||||
pos[0] = (x2 > sourceLeft) ? (turnX2 + x0 + offset) / 2 : (x0 + x2) / 2;
|
||||
@@ -212,6 +232,7 @@ const LinkRenderers = {
|
||||
}
|
||||
}
|
||||
},
|
||||
trace: traceRenderer,
|
||||
};
|
||||
|
||||
export class ExtraLinks {
|
||||
@@ -245,7 +266,7 @@ export class ExtraLinks {
|
||||
if (!app.extensionManager.setting.get("Extra Links.General.Enable")) {
|
||||
pathRendererConstructor.prototype.drawLinkPath = _originalDrawLinkPath;
|
||||
pathRendererConstructor.prototype.calculateCenterPoint = _originalCalculateCenterPoint;
|
||||
return;
|
||||
return _originalDrawLinkPath.call(this, ctx, path, link2, context, lineWidth, color2);
|
||||
}
|
||||
|
||||
const _graph = app.canvas.graph;
|
||||
@@ -255,13 +276,13 @@ export class ExtraLinks {
|
||||
const CURVATURE = app.extensionManager.setting.get("Extra Links.Shapes.Curvature") ?? 5;
|
||||
const SHAPE = app.extensionManager.setting.get("Extra Links.General.Shape") ?? "curved";
|
||||
|
||||
const full_link_object = _graph.links[link2?.id];
|
||||
const full_link_object = _getGraphLink(_graph, link2?.id);
|
||||
const outputId = full_link_object?.origin_slot ?? 0;
|
||||
|
||||
const start_node = _graph.getNodeById(full_link_object?.origin_id);
|
||||
const end_node = _graph.getNodeById(full_link_object?.target_id);
|
||||
|
||||
const is_dragging = (link2?.id == 'temp') || !_graph.links[link2.id];
|
||||
const is_dragging = (link2?.id == 'temp') || !full_link_object;
|
||||
|
||||
const startPos = link2.startPoint;
|
||||
const endPos = link2.endPoint;
|
||||
@@ -274,6 +295,10 @@ export class ExtraLinks {
|
||||
|
||||
const renderer = LinkRenderers[SHAPE] || LinkRenderers.curved;
|
||||
|
||||
const _linkId = link2?.id ?? null;
|
||||
const _isStale = _linkId != null && !full_link_object && !is_dragging;
|
||||
traceRenderer.prepare(_linkId, _graph, _isStale);
|
||||
|
||||
renderer.draw(
|
||||
path,
|
||||
startPos,
|
||||
@@ -291,6 +316,12 @@ export class ExtraLinks {
|
||||
ctx.stroke(path);
|
||||
|
||||
pathRendererConstructor.prototype.calculateCenterPoint = (...args) => {
|
||||
if (SHAPE === "trace") {
|
||||
delete link2.centerPos;
|
||||
delete link2.centerAngle;
|
||||
return null;
|
||||
}
|
||||
|
||||
link2.centerPos = { x: pos[0], y: pos[1] }
|
||||
if (context.style.centerMarkerShape === 'arrow') {
|
||||
link2.centerAngle = pos[2];
|
||||
@@ -300,4 +331,4 @@ export class ExtraLinks {
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* Trace logic adapted from quick-connections <https://github.com/niknah/quick-connections>
|
||||
*/
|
||||
|
||||
const EPSILON = 1e-6;
|
||||
const INSIDE = 1;
|
||||
const DEFAULT_CELL_SIZE = 256;
|
||||
|
||||
function clipT(num, denom, c) {
|
||||
const tE = c[0], tL = c[1];
|
||||
if (Math.abs(denom) < EPSILON) {
|
||||
return num < 0;
|
||||
}
|
||||
const t = num / denom;
|
||||
if (denom > 0) {
|
||||
if (t > tL) {
|
||||
return 0;
|
||||
}
|
||||
if (t > tE) {
|
||||
c[0] = t;
|
||||
}
|
||||
} else {
|
||||
if (t < tE) {
|
||||
return 0;
|
||||
}
|
||||
if (t < tL) {
|
||||
c[1] = t;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
function liangBarsky(a, b, box, da, db) {
|
||||
const x1 = a[0], y1 = a[1];
|
||||
const x2 = b[0], y2 = b[1];
|
||||
const dx = x2 - x1;
|
||||
const dy = y2 - y1;
|
||||
if (da === undefined || db === undefined) {
|
||||
da = a;
|
||||
db = b;
|
||||
} else {
|
||||
da[0] = a[0];
|
||||
da[1] = a[1];
|
||||
db[0] = b[0];
|
||||
db[1] = b[1];
|
||||
}
|
||||
if (Math.abs(dx) < EPSILON
|
||||
&& Math.abs(dy) < EPSILON
|
||||
&& x1 >= box[0]
|
||||
&& x1 <= box[2]
|
||||
&& y1 >= box[1]
|
||||
&& y1 <= box[3]) {
|
||||
return INSIDE;
|
||||
}
|
||||
const c = [0, 1];
|
||||
if (clipT(box[0] - x1, dx, c)
|
||||
&& clipT(x1 - box[2], -dx, c)
|
||||
&& clipT(box[1] - y1, dy, c)
|
||||
&& clipT(y1 - box[3], -dy, c)) {
|
||||
const tE = c[0], tL = c[1];
|
||||
if (tL < 1) {
|
||||
db[0] = x1 + tL * dx;
|
||||
db[1] = y1 + tL * dy;
|
||||
}
|
||||
if (tE > 0) {
|
||||
da[0] += tE * dx;
|
||||
da[1] += tE * dy;
|
||||
}
|
||||
return INSIDE;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getAreaFromNode(node) {
|
||||
if (!node) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
if (node.getBounding) {
|
||||
const area = new Float32Array(4);
|
||||
node.getBounding(area);
|
||||
return [
|
||||
area[0],
|
||||
area[1],
|
||||
area[0] + area[2],
|
||||
area[1] + area[3],
|
||||
];
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
if (node.pos && node.size) {
|
||||
return [
|
||||
node.pos[0],
|
||||
node.pos[1],
|
||||
node.pos[0] + node.size[0],
|
||||
node.pos[1] + node.size[1],
|
||||
];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeBox(item, index) {
|
||||
const node = item?.node || item;
|
||||
const area = item?.area || getAreaFromNode(node);
|
||||
if (!area) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: node?.id ?? item?.id ?? index,
|
||||
node,
|
||||
area: Array.from(area),
|
||||
linesArea: Array.from(item?.linesArea || area),
|
||||
source: item,
|
||||
};
|
||||
}
|
||||
|
||||
function cellRange(area, cellSize) {
|
||||
return {
|
||||
x1: Math.floor(area[0] / cellSize),
|
||||
y1: Math.floor(area[1] / cellSize),
|
||||
x2: Math.floor(area[2] / cellSize),
|
||||
y2: Math.floor(area[3] / cellSize),
|
||||
};
|
||||
}
|
||||
|
||||
function cellKey(x, y) {
|
||||
return `${x},${y}`;
|
||||
}
|
||||
|
||||
function expandedArea(area, padding) {
|
||||
return [
|
||||
area[0] - padding,
|
||||
area[1] - padding,
|
||||
area[2] + padding,
|
||||
area[3] + padding,
|
||||
];
|
||||
}
|
||||
|
||||
function segmentArea(from, to, padding) {
|
||||
return [
|
||||
Math.min(from[0], to[0]) - padding,
|
||||
Math.min(from[1], to[1]) - padding,
|
||||
Math.max(from[0], to[0]) + padding,
|
||||
Math.max(from[1], to[1]) + padding,
|
||||
];
|
||||
}
|
||||
|
||||
export function buildNodeBBoxIndex(items, options = {}) {
|
||||
const cellSize = options.cellSize || DEFAULT_CELL_SIZE;
|
||||
const padding = options.padding || 0;
|
||||
const boxes = [];
|
||||
const grid = new Map();
|
||||
|
||||
(items || []).forEach((item, index) => {
|
||||
const box = normalizeBox(item, index);
|
||||
if (!box) {
|
||||
return;
|
||||
}
|
||||
|
||||
box.area = expandedArea(box.area, padding);
|
||||
box.linesArea = expandedArea(box.linesArea, padding);
|
||||
const boxIndex = boxes.push(box) - 1;
|
||||
const range = cellRange(box.area, cellSize);
|
||||
|
||||
for (let x = range.x1; x <= range.x2; x++) {
|
||||
for (let y = range.y1; y <= range.y2; y++) {
|
||||
const key = cellKey(x, y);
|
||||
let bucket = grid.get(key);
|
||||
if (!bucket) {
|
||||
bucket = new Set();
|
||||
grid.set(key, bucket);
|
||||
}
|
||||
bucket.add(boxIndex);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { cellSize, boxes, grid };
|
||||
}
|
||||
|
||||
export function querySegmentCandidates(index, from, to, padding = 0) {
|
||||
if (!index) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const seen = new Set();
|
||||
const candidates = [];
|
||||
const range = cellRange(segmentArea(from, to, padding), index.cellSize);
|
||||
|
||||
for (let x = range.x1; x <= range.x2; x++) {
|
||||
for (let y = range.y1; y <= range.y2; y++) {
|
||||
const bucket = index.grid.get(cellKey(x, y));
|
||||
if (!bucket) {
|
||||
continue;
|
||||
}
|
||||
for (const boxIndex of bucket) {
|
||||
if (seen.has(boxIndex)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(boxIndex);
|
||||
candidates.push(index.boxes[boxIndex]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
export function findSegmentIntersections(index, from, to, options = {}) {
|
||||
const padding = options.padding || 0;
|
||||
const excludeNodeIds = options.excludeNodeIds || null;
|
||||
const candidates = querySegmentCandidates(index, from, to, padding);
|
||||
const hits = [];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (excludeNodeIds?.has?.(candidate.id) || excludeNodeIds?.has?.(String(candidate.id))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const clipA = [-1, -1];
|
||||
const clipB = [-1, -1];
|
||||
const clipped = liangBarsky(from, to, candidate.area, clipA, clipB);
|
||||
if (clipped !== INSIDE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const centerX = candidate.area[0] + ((candidate.area[2] - candidate.area[0]) / 2);
|
||||
const centerY = candidate.area[1] + ((candidate.area[3] - candidate.area[1]) / 2);
|
||||
const dx = centerX - from[0];
|
||||
const dy = centerY - from[1];
|
||||
hits.push({
|
||||
start: clipA,
|
||||
end: clipB,
|
||||
node: candidate.source || candidate,
|
||||
distance: (dx * dx) + (dy * dy),
|
||||
});
|
||||
}
|
||||
|
||||
hits.sort((a, b) => a.distance - b.distance);
|
||||
return hits;
|
||||
}
|
||||
+721
@@ -0,0 +1,721 @@
|
||||
/**
|
||||
* Trace logic adapted from quick-connections <https://github.com/niknah/quick-connections>
|
||||
*/
|
||||
|
||||
import { buildNodeBBoxIndex, findSegmentIntersections } from "./NodeBBoxIndex.js"
|
||||
|
||||
class MapLinks {
|
||||
constructor(canvas, config) {
|
||||
this.canvas = canvas;
|
||||
this.nodesByRight = [];
|
||||
this.nodesById = [];
|
||||
this.pathsByLinkId = new Map();
|
||||
this.nodeBBoxIndex = null;
|
||||
this.config = config;
|
||||
this.maxDirectLineDistance = Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
findClippedNode(outputXY, inputXY) {
|
||||
const intersections = findSegmentIntersections(this.nodeBBoxIndex, outputXY, inputXY);
|
||||
if (intersections.length) {
|
||||
return intersections[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
testPath(path) {
|
||||
const len1 = (path.length - 1);
|
||||
for (let p = 0; p < len1; ++p) {
|
||||
const clipped = this.findClippedNode(path[p], path[p + 1]);
|
||||
if (clipped) {
|
||||
return clipped;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
mapFinalLink(outputXY, inputXY) {
|
||||
const clipped = this.findClippedNode(outputXY, inputXY);
|
||||
if (!clipped) {
|
||||
const dist = Math.sqrt(((outputXY[0] - inputXY[0]) ** 2) + ((outputXY[1] - inputXY[1]) ** 2));
|
||||
if (dist < this.maxDirectLineDistance) {
|
||||
return { path: _path90Or45(outputXY, inputXY) };
|
||||
}
|
||||
}
|
||||
|
||||
const horzDistance = inputXY[0] - outputXY[0];
|
||||
const vertDistance = inputXY[1] - outputXY[1];
|
||||
const horzDistanceAbs = Math.abs(horzDistance);
|
||||
const vertDistanceAbs = Math.abs(vertDistance);
|
||||
|
||||
if (horzDistanceAbs > vertDistanceAbs) {
|
||||
const goingLeft = inputXY[0] < outputXY[0];
|
||||
const pathStraight45 = [
|
||||
[outputXY[0], outputXY[1]],
|
||||
[inputXY[0] - (goingLeft ? -vertDistanceAbs : vertDistanceAbs), outputXY[1]],
|
||||
[inputXY[0], inputXY[1]],
|
||||
];
|
||||
if (!this.testPath(pathStraight45)) {
|
||||
return { path: pathStraight45 };
|
||||
}
|
||||
|
||||
const path45Straight = [
|
||||
[outputXY[0], outputXY[1]],
|
||||
[outputXY[0] + (goingLeft ? -vertDistanceAbs : vertDistanceAbs), inputXY[1]],
|
||||
[inputXY[0], inputXY[1]],
|
||||
];
|
||||
if (!this.testPath(path45Straight)) {
|
||||
return { path: path45Straight };
|
||||
}
|
||||
} else {
|
||||
const goingUp = inputXY[1] < outputXY[1];
|
||||
const pathStraight45 = [
|
||||
[outputXY[0], outputXY[1]],
|
||||
[outputXY[0], inputXY[1] + (goingUp ? horzDistanceAbs : -horzDistanceAbs)],
|
||||
[inputXY[0], inputXY[1]],
|
||||
];
|
||||
if (!this.testPath(pathStraight45)) {
|
||||
return { path: pathStraight45 };
|
||||
}
|
||||
|
||||
const path45Straight = [
|
||||
[outputXY[0], outputXY[1]],
|
||||
[inputXY[0], outputXY[1] - (goingUp ? horzDistanceAbs : -horzDistanceAbs)],
|
||||
[inputXY[0], inputXY[1]],
|
||||
];
|
||||
if (!this.testPath(path45Straight)) {
|
||||
return { path: path45Straight };
|
||||
}
|
||||
}
|
||||
|
||||
const path90Straight = [
|
||||
[outputXY[0], outputXY[1]],
|
||||
[outputXY[0], inputXY[1]],
|
||||
[inputXY[0], inputXY[1]],
|
||||
];
|
||||
const clippedVert = this.testPath(path90Straight);
|
||||
if (!clippedVert) {
|
||||
return { path: path90Straight };
|
||||
}
|
||||
|
||||
const pathStraight90 = [
|
||||
[outputXY[0], outputXY[1]],
|
||||
[inputXY[0], outputXY[1]],
|
||||
[inputXY[0], inputXY[1]],
|
||||
];
|
||||
const clippedHorz = this.testPath(pathStraight90);
|
||||
if (!clippedHorz) {
|
||||
return { path: pathStraight90 };
|
||||
}
|
||||
return {
|
||||
clippedHorz,
|
||||
clippedVert,
|
||||
};
|
||||
}
|
||||
|
||||
mapLink(outputXY, inputXY, isBlocked) {
|
||||
const { clippedHorz, clippedVert, path } = this.mapFinalLink(outputXY, inputXY);
|
||||
if (path) {
|
||||
return path;
|
||||
}
|
||||
|
||||
const horzDistance = inputXY[0] - outputXY[0];
|
||||
const vertDistance = inputXY[1] - outputXY[1];
|
||||
const horzDistanceAbs = Math.abs(horzDistance);
|
||||
const vertDistanceAbs = Math.abs(vertDistance);
|
||||
|
||||
let blockedNodeId;
|
||||
let pathAroundNode;
|
||||
let lastPathLocation;
|
||||
let linesArea;
|
||||
|
||||
if (horzDistanceAbs > vertDistanceAbs) {
|
||||
blockedNodeId = clippedHorz.node.node.id;
|
||||
linesArea = clippedHorz.node.linesArea;
|
||||
const horzEdge = horzDistance <= 0
|
||||
? (linesArea[2])
|
||||
: (linesArea[0] - 1);
|
||||
pathAroundNode = [
|
||||
[outputXY[0], outputXY[1]],
|
||||
[horzEdge, outputXY[1]],
|
||||
];
|
||||
|
||||
if (horzDistance <= 0) {
|
||||
linesArea[2] += this.config.lineSpace;
|
||||
} else {
|
||||
linesArea[0] -= this.config.lineSpace;
|
||||
}
|
||||
|
||||
const vertDistanceViaBlockTop =
|
||||
Math.abs(inputXY[1] - linesArea[1]) +
|
||||
Math.abs(linesArea[1] - outputXY[1]);
|
||||
const vertDistanceViaBlockBottom =
|
||||
Math.abs(inputXY[1] - linesArea[3]) +
|
||||
Math.abs(linesArea[3] - outputXY[1]);
|
||||
|
||||
lastPathLocation = [
|
||||
horzEdge,
|
||||
vertDistanceViaBlockTop <= vertDistanceViaBlockBottom ?
|
||||
(linesArea[1])
|
||||
: (linesArea[3]),
|
||||
];
|
||||
const unblockNotPossible1 = this.testPath([...pathAroundNode, lastPathLocation]);
|
||||
if (unblockNotPossible1) {
|
||||
lastPathLocation = [
|
||||
horzEdge,
|
||||
vertDistanceViaBlockTop > vertDistanceViaBlockBottom ?
|
||||
(linesArea[1])
|
||||
: (linesArea[3]),
|
||||
];
|
||||
}
|
||||
if (lastPathLocation[1] < outputXY[1]) {
|
||||
linesArea[1] -= this.config.lineSpace;
|
||||
lastPathLocation[1] -= 1;
|
||||
} else {
|
||||
linesArea[3] += this.config.lineSpace;
|
||||
lastPathLocation[1] += 1;
|
||||
}
|
||||
} else {
|
||||
blockedNodeId = clippedVert.node.node.id;
|
||||
linesArea = clippedVert.node.linesArea;
|
||||
const vertEdge =
|
||||
vertDistance <= 0
|
||||
? (linesArea[3] + 1)
|
||||
: (linesArea[1] - 1);
|
||||
pathAroundNode = [
|
||||
[outputXY[0], outputXY[1]],
|
||||
[outputXY[0], vertEdge],
|
||||
];
|
||||
if (vertDistance <= 0) {
|
||||
linesArea[3] += this.config.lineSpace;
|
||||
} else {
|
||||
linesArea[1] -= this.config.lineSpace;
|
||||
}
|
||||
|
||||
const horzDistanceViaBlockLeft =
|
||||
Math.abs(inputXY[0] - linesArea[0]) +
|
||||
Math.abs(linesArea[0] - outputXY[0]);
|
||||
const horzDistanceViaBlockRight =
|
||||
Math.abs(inputXY[0] - linesArea[2]) +
|
||||
Math.abs(linesArea[2] - outputXY[0]);
|
||||
|
||||
lastPathLocation = [
|
||||
horzDistanceViaBlockLeft <= horzDistanceViaBlockRight ?
|
||||
(linesArea[0] - 1)
|
||||
: (linesArea[2]),
|
||||
vertEdge,
|
||||
];
|
||||
const unblockNotPossible1 = this.testPath([...pathAroundNode, lastPathLocation]);
|
||||
if (unblockNotPossible1) {
|
||||
lastPathLocation = [
|
||||
horzDistanceViaBlockLeft > horzDistanceViaBlockRight ?
|
||||
(linesArea[0])
|
||||
: (linesArea[2]),
|
||||
vertEdge,
|
||||
];
|
||||
}
|
||||
if (lastPathLocation[0] < outputXY[0]) {
|
||||
linesArea[0] -= this.config.lineSpace;
|
||||
} else {
|
||||
linesArea[2] += this.config.lineSpace;
|
||||
}
|
||||
}
|
||||
|
||||
if (isBlocked[blockedNodeId] > 3) {
|
||||
isBlocked.blocked = true;
|
||||
return _path90Or45(outputXY, inputXY);
|
||||
}
|
||||
if (isBlocked[blockedNodeId])
|
||||
++isBlocked[blockedNodeId];
|
||||
else
|
||||
isBlocked[blockedNodeId] = 1;
|
||||
const nextPath = this.mapLink(
|
||||
lastPathLocation,
|
||||
inputXY,
|
||||
isBlocked,
|
||||
);
|
||||
return [...pathAroundNode, lastPathLocation, ...nextPath.slice(1)];
|
||||
}
|
||||
|
||||
expandSourceNodeLinesArea(sourceNodeInfo, path) {
|
||||
if (path.length < 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const linesArea = sourceNodeInfo.linesArea;
|
||||
if (path[1][0] === path[2][0]) {
|
||||
linesArea[2] += this.config.lineSpace;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
expandTargetNodeLinesArea(targetNodeInfo, path) {
|
||||
if (path.length < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const linesArea = targetNodeInfo.linesArea;
|
||||
const path2Len = path.length - 2;
|
||||
if (path[path2Len - 1][0] === path[path2Len][0]) {
|
||||
linesArea[0] -= this.config.lineSpace;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
getNodeOnPos(xy) {
|
||||
for (let i = 0; i < this.nodesByRight.length; ++i) {
|
||||
const nodeI = this.nodesByRight[i];
|
||||
const { linesArea } = nodeI;
|
||||
if (xy[0] >= linesArea[0]
|
||||
&& xy[1] >= linesArea[1]
|
||||
&& xy[0] < linesArea[2]
|
||||
&& xy[1] < linesArea[3]
|
||||
) {
|
||||
return nodeI;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
mapLinks(nodesByExecution) {
|
||||
const graphLinks = this.canvas.graph.links;
|
||||
if (!graphLinks) {
|
||||
console.error('Missing graph.links', this.canvas.graph);
|
||||
return;
|
||||
}
|
||||
|
||||
this.nodesByRight = [];
|
||||
this.nodesById = {};
|
||||
this.pathsByLinkId.clear();
|
||||
this.nodesByRight = nodesByExecution.map((node) => {
|
||||
const area = _getNodeArea(node);
|
||||
if (!area) {
|
||||
return null;
|
||||
}
|
||||
const linesArea = Array.from(area);
|
||||
linesArea[0] += this.config.nodeSpace[0];
|
||||
linesArea[1] += this.config.nodeSpace[1];
|
||||
linesArea[2] += this.config.nodeSpace[2];
|
||||
linesArea[3] += this.config.nodeSpace[3];
|
||||
const obj = {
|
||||
node,
|
||||
area,
|
||||
linesArea,
|
||||
};
|
||||
this.nodesById[node.id] = obj;
|
||||
return obj;
|
||||
}).filter(Boolean);
|
||||
this.nodeBBoxIndex = buildNodeBBoxIndex(this.nodesByRight);
|
||||
const nodesByRightId = this.nodesByRight.reduce(
|
||||
(a, x) => {
|
||||
a[x.node.id] = x.node;
|
||||
return a;
|
||||
},
|
||||
{},
|
||||
);
|
||||
this.nodesByRight.filter((nodeI) => {
|
||||
const { node } = nodeI;
|
||||
const outputs = node.outputs;
|
||||
if (!outputs) {
|
||||
return false;
|
||||
}
|
||||
outputs.filter((output, slot) => {
|
||||
const links = output.links;
|
||||
if (!links) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const outputXYConnection = _getSlotPosition(node, slot, false);
|
||||
if (!outputXYConnection) {
|
||||
return false;
|
||||
}
|
||||
const outputNodeInfo = this.nodesById[node.id];
|
||||
let outputXY = Array.from(outputXYConnection);
|
||||
links.filter((linkId) => {
|
||||
outputXY[0] = outputNodeInfo.linesArea[2];
|
||||
const link = _getGraphLink(graphLinks, linkId);
|
||||
if (!link) {
|
||||
return false;
|
||||
}
|
||||
let targetNode = this.canvas.graph.getNodeById(link.target_id);
|
||||
if (!targetNode) {
|
||||
targetNode = nodesByRightId[link.target_id];
|
||||
}
|
||||
if (!targetNode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const inputXYConnection = _getSlotPosition(targetNode, link.target_slot, true);
|
||||
if (!inputXYConnection) {
|
||||
return false;
|
||||
}
|
||||
const inputXY = Array.from(inputXYConnection);
|
||||
const nodeInfo = this.nodesById[targetNode.id];
|
||||
if (!nodeInfo) {
|
||||
return false;
|
||||
}
|
||||
inputXY[0] = nodeInfo.linesArea[0] - 1;
|
||||
|
||||
const inputBlockedByNode =
|
||||
this.getNodeOnPos(inputXY);
|
||||
const outputBlockedByNode =
|
||||
this.getNodeOnPos(outputXY);
|
||||
|
||||
let path = null;
|
||||
if (!inputBlockedByNode && !outputBlockedByNode) {
|
||||
const isBlocked = {};
|
||||
const pathFound = this.mapLink(outputXY, inputXY, isBlocked);
|
||||
|
||||
if (pathFound) {
|
||||
while (pathFound.length >= 2) {
|
||||
const lastPathPoint1 = pathFound[pathFound.length - 1];
|
||||
const lastPathPoint2 = pathFound[pathFound.length - 2];
|
||||
if (lastPathPoint1[0] === lastPathPoint2[0]
|
||||
&& lastPathPoint1[1] === lastPathPoint2[1]
|
||||
) {
|
||||
pathFound.pop();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isBlocked.blocked && pathFound && pathFound.length > 2) {
|
||||
path = [outputXYConnection, ...pathFound, inputXYConnection];
|
||||
this.expandTargetNodeLinesArea(nodeInfo, path);
|
||||
}
|
||||
}
|
||||
if (!path) {
|
||||
path = [outputXYConnection, outputXY, inputXY, inputXYConnection];
|
||||
}
|
||||
this.expandSourceNodeLinesArea(nodeI, path);
|
||||
this.pathsByLinkId.set(linkId, path);
|
||||
this.pathsByLinkId.set(String(linkId), path);
|
||||
outputXY = [
|
||||
outputXY[0] + this.config.lineSpace,
|
||||
outputXY[1],
|
||||
];
|
||||
return false;
|
||||
});
|
||||
return false;
|
||||
});
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class TraceRoutes {
|
||||
constructor() {
|
||||
this.mapLinks = null;
|
||||
this.graph = null;
|
||||
this.maxDirectLineDistance = Number.MAX_SAFE_INTEGER;
|
||||
this.config = {
|
||||
lineSpace: Math.floor((_getLiteGraph()?.NODE_SLOT_HEIGHT ?? 20) / 2),
|
||||
nodeSpace: [-8, -4, 12, 4],
|
||||
};
|
||||
}
|
||||
|
||||
recalcMapLinks() {
|
||||
const canvas = { graph: this.graph };
|
||||
this.mapLinks = new MapLinks(canvas, this.config);
|
||||
this.mapLinks.maxDirectLineDistance = this.maxDirectLineDistance;
|
||||
const nodesByExecution = _getRoutingNodes(this.graph);
|
||||
try {
|
||||
this.mapLinks.mapLinks(nodesByExecution);
|
||||
} catch (e) {
|
||||
console.error('mapLinks error', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _getLiteGraph() {
|
||||
return globalThis.LiteGraph || globalThis.window?.LiteGraph;
|
||||
}
|
||||
|
||||
function _getGraphLink(graphLinks, linkId) {
|
||||
return graphLinks?.[linkId] ?? graphLinks?.[String(linkId)] ?? graphLinks?.get?.(linkId) ?? graphLinks?.get?.(String(linkId));
|
||||
}
|
||||
|
||||
function _appendUniqueNode(nodes, seen, node) {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = node.id ?? `${node.type ?? "node"}:${node.pos?.[0]},${node.pos?.[1]}`;
|
||||
if (seen.has(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
seen.add(id);
|
||||
nodes.push(node);
|
||||
}
|
||||
|
||||
function _getRoutingNodes(graph) {
|
||||
const nodes = [];
|
||||
const seen = new Set();
|
||||
|
||||
let executionNodes = [];
|
||||
try {
|
||||
executionNodes = graph?.computeExecutionOrder?.() || [];
|
||||
} catch (e) {
|
||||
executionNodes = [];
|
||||
}
|
||||
|
||||
executionNodes.forEach((node) => _appendUniqueNode(nodes, seen, node));
|
||||
(graph?._nodes || []).forEach((node) => _appendUniqueNode(nodes, seen, node));
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
function _getNodeArea(node) {
|
||||
if (!node) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
if (node.getBounding) {
|
||||
const barea = new Float32Array(4);
|
||||
node.getBounding(barea);
|
||||
return [
|
||||
barea[0],
|
||||
barea[1],
|
||||
barea[0] + barea[2],
|
||||
barea[1] + barea[3],
|
||||
];
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
if (node.pos && node.size) {
|
||||
return [
|
||||
node.pos[0],
|
||||
node.pos[1],
|
||||
node.pos[0] + node.size[0],
|
||||
node.pos[1] + node.size[1],
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function _getSlotPosition(node, slot, isInput) {
|
||||
try {
|
||||
if (node.getSlotPosition) {
|
||||
return node.getSlotPosition(slot, isInput);
|
||||
}
|
||||
if (isInput && node.getInputPos) {
|
||||
return node.getInputPos(slot);
|
||||
}
|
||||
if (!isInput && node.getOutputPos) {
|
||||
return node.getOutputPos(slot);
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
if (!node.pos || !node.size) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const slotHeight = _getLiteGraph()?.NODE_SLOT_HEIGHT ?? 20;
|
||||
const y = node.pos[1] + ((slot ?? 0) + 0.5) * slotHeight;
|
||||
return isInput ? [node.pos[0], y] : [node.pos[0] + node.size[0], y];
|
||||
}
|
||||
|
||||
function _drawTracePath(path2d, route, cornerRadius) {
|
||||
if (route.length <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
let isPrevDotRound = false;
|
||||
for (let p = 0; p < route.length; ++p) {
|
||||
const pos = route[p];
|
||||
|
||||
if (p === 0) {
|
||||
path2d.moveTo(pos[0], pos[1]);
|
||||
}
|
||||
const prevPos = pos;
|
||||
const cornerPos = route[p + 1];
|
||||
const nextPos = route[p + 2];
|
||||
|
||||
let drawn = false;
|
||||
if (nextPos) {
|
||||
const xDiffBefore = cornerPos[0] - prevPos[0];
|
||||
const yDiffBefore = cornerPos[1] - prevPos[1];
|
||||
const xDiffAfter = nextPos[0] - cornerPos[0];
|
||||
const yDiffAfter = nextPos[1] - cornerPos[1];
|
||||
const lenBefore = Math.sqrt((xDiffBefore ** 2) + (yDiffBefore ** 2));
|
||||
const lenAfter = Math.sqrt((xDiffAfter ** 2) + (yDiffAfter ** 2));
|
||||
const cross = (xDiffBefore * yDiffAfter) - (yDiffBefore * xDiffAfter);
|
||||
|
||||
if (lenBefore > 0.001 && lenAfter > 0.001 && Math.abs(cross) > 0.001) {
|
||||
const radius = Math.min(cornerRadius, lenBefore * 0.5, lenAfter * 0.5);
|
||||
const beforePos = [
|
||||
cornerPos[0] - ((xDiffBefore / lenBefore) * radius),
|
||||
cornerPos[1] - ((yDiffBefore / lenBefore) * radius),
|
||||
];
|
||||
const afterPos = [
|
||||
cornerPos[0] + ((xDiffAfter / lenAfter) * radius),
|
||||
cornerPos[1] + ((yDiffAfter / lenAfter) * radius),
|
||||
];
|
||||
|
||||
if (isPrevDotRound
|
||||
&& Math.abs(isPrevDotRound[0] - beforePos[0]) <= cornerRadius
|
||||
&& Math.abs(isPrevDotRound[1] - beforePos[1]) <= cornerRadius) {
|
||||
} else {
|
||||
path2d.lineTo(beforePos[0], beforePos[1]);
|
||||
path2d.quadraticCurveTo(cornerPos[0], cornerPos[1], afterPos[0], afterPos[1]);
|
||||
}
|
||||
isPrevDotRound = beforePos;
|
||||
drawn = true;
|
||||
}
|
||||
}
|
||||
if (p > 0 && !drawn) {
|
||||
if (!isPrevDotRound) {
|
||||
path2d.lineTo(pos[0], pos[1]);
|
||||
}
|
||||
isPrevDotRound = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _centerOf(route, pos) {
|
||||
let best = -1;
|
||||
for (let i = 0; i < route.length - 1; i++) {
|
||||
const length = ((route[i + 1][0] - route[i][0]) ** 2) + ((route[i + 1][1] - route[i][1]) ** 2);
|
||||
if (length > best) {
|
||||
best = length;
|
||||
pos[0] = (route[i][0] + route[i + 1][0]) * 0.5;
|
||||
pos[1] = (route[i][1] + route[i + 1][1]) * 0.5;
|
||||
pos[2] = Math.atan2(route[i + 1][1] - route[i][1], route[i + 1][0] - route[i][0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _drawFallback90Or45(path, start, end, pos) {
|
||||
const route = _path90Or45([start.x, start.y], [end.x, end.y]);
|
||||
_centerOf(route, pos);
|
||||
_drawTracePath(path, route, _traceRoutes.config.lineSpace);
|
||||
}
|
||||
|
||||
function _samePoint(a, b) {
|
||||
return a && b && Math.abs(a[0] - b[0]) < 0.001 && Math.abs(a[1] - b[1]) < 0.001;
|
||||
}
|
||||
|
||||
function _appendUniquePoint(route, point) {
|
||||
if (!_samePoint(route[route.length - 1], point)) {
|
||||
route.push(point);
|
||||
}
|
||||
}
|
||||
|
||||
function _path90Or45(from, to) {
|
||||
if (_samePoint(from, to)) {
|
||||
return [from];
|
||||
}
|
||||
|
||||
const dx = to[0] - from[0];
|
||||
const dy = to[1] - from[1];
|
||||
const absDx = Math.abs(dx);
|
||||
const absDy = Math.abs(dy);
|
||||
|
||||
if (dx === 0 || dy === 0 || Math.abs(absDx - absDy) < 0.001) {
|
||||
return [from, to];
|
||||
}
|
||||
|
||||
let mid;
|
||||
if (absDx > absDy) {
|
||||
mid = [to[0] - (Math.sign(dx) * absDy), from[1]];
|
||||
} else {
|
||||
mid = [from[0], to[1] - (Math.sign(dy) * absDx)];
|
||||
}
|
||||
|
||||
return [from, mid, to];
|
||||
}
|
||||
|
||||
function _normalizeRoute90Or45(points) {
|
||||
if (points.length <= 1) {
|
||||
return points;
|
||||
}
|
||||
|
||||
const route = [points[0]];
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
const segment = _path90Or45(points[i], points[i + 1]);
|
||||
for (const point of segment.slice(1)) {
|
||||
_appendUniquePoint(route, point);
|
||||
}
|
||||
}
|
||||
return route;
|
||||
}
|
||||
|
||||
const _traceRoutes = new TraceRoutes();
|
||||
|
||||
let _lastGraph = null;
|
||||
let _lastHash = null;
|
||||
|
||||
function _graphHash(graph) {
|
||||
if (!graph) {
|
||||
return "";
|
||||
}
|
||||
|
||||
let hash = "";
|
||||
const nodes = graph._nodes || [];
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
hash += `${node.id}:${node.pos?.[0]},${node.pos?.[1]},${node.size?.[0]},${node.size?.[1]};`;
|
||||
}
|
||||
const links = graph.links || {};
|
||||
const linkIds = Object.keys(links).sort();
|
||||
for (let i = 0; i < linkIds.length; i++) {
|
||||
const link = links[linkIds[i]];
|
||||
hash += `link:${linkIds[i]},${link?.origin_id},${link?.origin_slot},${link?.target_id},${link?.target_slot};`;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
function _ensureTraceRoutes(graph) {
|
||||
if (!graph) {
|
||||
return;
|
||||
}
|
||||
|
||||
_traceRoutes.graph = graph;
|
||||
const hash = _graphHash(graph);
|
||||
if (_lastGraph === graph && _lastHash === hash && _traceRoutes.mapLinks) {
|
||||
return;
|
||||
}
|
||||
|
||||
_lastGraph = graph;
|
||||
_lastHash = hash;
|
||||
_traceRoutes.recalcMapLinks();
|
||||
}
|
||||
|
||||
export const traceRenderer = {
|
||||
_linkId: null,
|
||||
|
||||
prepare(linkId, graph, isStale) {
|
||||
this._linkId = linkId;
|
||||
if (isStale) {
|
||||
_lastHash = null;
|
||||
}
|
||||
_ensureTraceRoutes(graph);
|
||||
},
|
||||
|
||||
draw(path, start, end, slot_id, start_node, end_node, radius, offset, curvature, pos, is_dragging) {
|
||||
if (is_dragging || !start_node || !end_node) {
|
||||
_drawFallback90Or45(path, start, end, pos);
|
||||
return;
|
||||
}
|
||||
|
||||
const route = this._linkId != null
|
||||
? _traceRoutes.mapLinks?.pathsByLinkId.get(this._linkId)
|
||||
: null;
|
||||
|
||||
if (!route) {
|
||||
_drawFallback90Or45(path, start, end, pos);
|
||||
return;
|
||||
}
|
||||
|
||||
const fullRoute = _normalizeRoute90Or45([[start.x, start.y], ...route.slice(1, -1), [end.x, end.y]]);
|
||||
_centerOf(fullRoute, pos);
|
||||
_drawTracePath(path, fullRoute, _traceRoutes.config.lineSpace);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -47,7 +47,7 @@ app.registerExtension({
|
||||
},
|
||||
{
|
||||
id: "Extra Links.ExtraLinks",
|
||||
name: "Version 1.1.2",
|
||||
name: "Version 1.2.0",
|
||||
type: () => {
|
||||
const container = document.createElement("span");
|
||||
const btn = document.createElement("button");
|
||||
@@ -80,7 +80,8 @@ app.registerExtension({
|
||||
type: "combo",
|
||||
options: [{text: "Curved", value: "curved"},
|
||||
{text: "Manhattan", value: "manhattan"},
|
||||
{text: "Subway", value: "subway"}],
|
||||
{text: "Subway", value: "subway"},
|
||||
{text: "Trace", value: "trace"}],
|
||||
defaultValue: "curved",
|
||||
category: ["Extra Links", "General", "Shape"],
|
||||
onChange: (val) => {
|
||||
|
||||
Reference in New Issue
Block a user