mirror of
https://github.com/litruv/picoGraph.git
synced 2026-07-24 02:36:04 +10:00
updated graph rendering to be more efficient. added smooth scrolling, added color type, mmb drag for cutting
This commit is contained in:
257
scripts/ui/workspace/WorkspaceWireCutManager.js
Normal file
257
scripts/ui/workspace/WorkspaceWireCutManager.js
Normal file
@@ -0,0 +1,257 @@
|
||||
/**
|
||||
* @typedef {import('../BlueprintWorkspace.js').BlueprintWorkspace} BlueprintWorkspace
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{ x: number, y: number }} Point
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* pointerId: number,
|
||||
* startClient: Point,
|
||||
* currentClient: Point,
|
||||
* cutLine: SVGLineElement,
|
||||
* hasMoved: boolean,
|
||||
* }} CutState
|
||||
*/
|
||||
|
||||
/**
|
||||
* Drives the middle-mouse wire-cut gesture.
|
||||
* Hold middle mouse and drag a line across wires to disconnect them.
|
||||
*/
|
||||
export class WorkspaceWireCutManager {
|
||||
/** @type {BlueprintWorkspace} */
|
||||
#workspace;
|
||||
/** @type {CutState | null} */
|
||||
#state;
|
||||
|
||||
/**
|
||||
* @param {BlueprintWorkspace} workspace Owning workspace instance.
|
||||
*/
|
||||
constructor(workspace) {
|
||||
this.#workspace = workspace;
|
||||
this.#state = null;
|
||||
|
||||
this.handleMove = this.handleMove.bind(this);
|
||||
this.handleUp = this.handleUp.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a cut gesture from a middle-mouse pointerdown event.
|
||||
*
|
||||
* @param {PointerEvent} event Middle-mouse pointerdown event.
|
||||
*/
|
||||
beginCut(event) {
|
||||
if (this.#state) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const cutLine = document.createElementNS("http://www.w3.org/2000/svg", "line");
|
||||
cutLine.classList.add("wire-cut-line");
|
||||
|
||||
const svgPos = this.#clientToSvg(event.clientX, event.clientY);
|
||||
cutLine.setAttribute("x1", String(svgPos.x));
|
||||
cutLine.setAttribute("y1", String(svgPos.y));
|
||||
cutLine.setAttribute("x2", String(svgPos.x));
|
||||
cutLine.setAttribute("y2", String(svgPos.y));
|
||||
this.#workspace.connectionLayer.appendChild(cutLine);
|
||||
|
||||
this.#state = {
|
||||
pointerId: event.pointerId,
|
||||
startClient: { x: event.clientX, y: event.clientY },
|
||||
currentClient: { x: event.clientX, y: event.clientY },
|
||||
cutLine,
|
||||
hasMoved: false,
|
||||
};
|
||||
|
||||
window.addEventListener("pointermove", this.handleMove);
|
||||
window.addEventListener("pointerup", this.handleUp);
|
||||
window.addEventListener("pointercancel", this.handleUp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a cut gesture is currently active.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isCutting() {
|
||||
return this.#state !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts viewport client coordinates to SVG-layer space.
|
||||
*
|
||||
* @param {number} clientX
|
||||
* @param {number} clientY
|
||||
* @returns {Point}
|
||||
*/
|
||||
#clientToSvg(clientX, clientY) {
|
||||
const zoom = this.#workspace.zoomLevel || 1;
|
||||
const rect = this.#workspace.workspaceElement.getBoundingClientRect();
|
||||
return {
|
||||
x: (clientX - rect.left) / zoom,
|
||||
y: (clientY - rect.top) / zoom,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PointerEvent} event
|
||||
*/
|
||||
handleMove(event) {
|
||||
if (!this.#state || event.pointerId !== this.#state.pointerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#state.currentClient = { x: event.clientX, y: event.clientY };
|
||||
|
||||
const dx = event.clientX - this.#state.startClient.x;
|
||||
const dy = event.clientY - this.#state.startClient.y;
|
||||
if (!this.#state.hasMoved && Math.hypot(dx, dy) >= 4) {
|
||||
this.#state.hasMoved = true;
|
||||
}
|
||||
|
||||
const start = this.#clientToSvg(this.#state.startClient.x, this.#state.startClient.y);
|
||||
const end = this.#clientToSvg(event.clientX, event.clientY);
|
||||
|
||||
this.#state.cutLine.setAttribute("x1", String(start.x));
|
||||
this.#state.cutLine.setAttribute("y1", String(start.y));
|
||||
this.#state.cutLine.setAttribute("x2", String(end.x));
|
||||
this.#state.cutLine.setAttribute("y2", String(end.y));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PointerEvent} event
|
||||
*/
|
||||
handleUp(event) {
|
||||
if (!this.#state || event.pointerId !== this.#state.pointerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.removeEventListener("pointermove", this.handleMove);
|
||||
window.removeEventListener("pointerup", this.handleUp);
|
||||
window.removeEventListener("pointercancel", this.handleUp);
|
||||
|
||||
const state = this.#state;
|
||||
this.#state = null;
|
||||
state.cutLine.remove();
|
||||
|
||||
if (!state.hasMoved) {
|
||||
return;
|
||||
}
|
||||
|
||||
const p1 = this.#clientToSvg(state.startClient.x, state.startClient.y);
|
||||
const p2 = this.#clientToSvg(state.currentClient.x, state.currentClient.y);
|
||||
this.#cutIntersecting(p1, p2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all connections whose bezier curves intersect the given line segment.
|
||||
*
|
||||
* @param {Point} p1 Start of cut segment in SVG space.
|
||||
* @param {Point} p2 End of cut segment in SVG space.
|
||||
*/
|
||||
#cutIntersecting(p1, p2) {
|
||||
const { connectionElements, graph, historyManager } = this.#workspace;
|
||||
/** @type {string[]} */
|
||||
const toRemove = [];
|
||||
|
||||
connectionElements.forEach((pathEl, connectionId) => {
|
||||
const d = pathEl.getAttribute("d");
|
||||
if (!d) {
|
||||
return;
|
||||
}
|
||||
if (this.#bezierIntersectsSegment(d, p1, p2)) {
|
||||
toRemove.push(connectionId);
|
||||
}
|
||||
});
|
||||
|
||||
if (!toRemove.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Batch all removals into a single undo step.
|
||||
historyManager?.withSuspended(() => {
|
||||
toRemove.forEach((id) => graph.removeConnection(id));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the cubic bezier described by a path `d` attribute crosses the segment p1→p2.
|
||||
*
|
||||
* @param {string} d SVG path `d` attribute value.
|
||||
* @param {Point} p1
|
||||
* @param {Point} p2
|
||||
* @returns {boolean}
|
||||
*/
|
||||
#bezierIntersectsSegment(d, p1, p2) {
|
||||
const m = d.match(
|
||||
/M\s*([\d.\-e]+)\s+([\d.\-e]+)\s+C\s*([\d.\-e]+)\s+([\d.\-e]+)\s+([\d.\-e]+)\s+([\d.\-e]+)\s+([\d.\-e]+)\s+([\d.\-e]+)/i
|
||||
);
|
||||
if (!m) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bp0 = { x: parseFloat(m[1]), y: parseFloat(m[2]) };
|
||||
const bc1 = { x: parseFloat(m[3]), y: parseFloat(m[4]) };
|
||||
const bc2 = { x: parseFloat(m[5]), y: parseFloat(m[6]) };
|
||||
const bp3 = { x: parseFloat(m[7]), y: parseFloat(m[8]) };
|
||||
|
||||
const SAMPLES = 32;
|
||||
let prev = bp0;
|
||||
for (let i = 1; i <= SAMPLES; i++) {
|
||||
const t = i / SAMPLES;
|
||||
const curr = this.#cubicBezierPoint(bp0, bc1, bc2, bp3, t);
|
||||
if (this.#segmentsIntersect(prev, curr, p1, p2)) {
|
||||
return true;
|
||||
}
|
||||
prev = curr;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a cubic bezier at parameter t.
|
||||
*
|
||||
* @param {Point} p0
|
||||
* @param {Point} c1
|
||||
* @param {Point} c2
|
||||
* @param {Point} p3
|
||||
* @param {number} t
|
||||
* @returns {Point}
|
||||
*/
|
||||
#cubicBezierPoint(p0, c1, c2, p3, t) {
|
||||
const mt = 1 - t;
|
||||
return {
|
||||
x: mt * mt * mt * p0.x + 3 * mt * mt * t * c1.x + 3 * mt * t * t * c2.x + t * t * t * p3.x,
|
||||
y: mt * mt * mt * p0.y + 3 * mt * mt * t * c1.y + 3 * mt * t * t * c2.y + t * t * t * p3.y,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if line segments a1→a2 and b1→b2 cross each other.
|
||||
*
|
||||
* @param {Point} a1
|
||||
* @param {Point} a2
|
||||
* @param {Point} b1
|
||||
* @param {Point} b2
|
||||
* @returns {boolean}
|
||||
*/
|
||||
#segmentsIntersect(a1, a2, b1, b2) {
|
||||
const dx1 = a2.x - a1.x;
|
||||
const dy1 = a2.y - a1.y;
|
||||
const dx2 = b2.x - b1.x;
|
||||
const dy2 = b2.y - b1.y;
|
||||
const denom = dx1 * dy2 - dy1 * dx2;
|
||||
if (Math.abs(denom) < 1e-10) {
|
||||
return false;
|
||||
}
|
||||
const dx3 = b1.x - a1.x;
|
||||
const dy3 = b1.y - a1.y;
|
||||
const t = (dx3 * dy2 - dy3 * dx2) / denom;
|
||||
const u = (dx3 * dy1 - dy3 * dx1) / denom;
|
||||
return t >= 0 && t <= 1 && u >= 0 && u <= 1;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user