mirror of
https://github.com/rio-labs/rio.git
synced 2026-01-15 09:39:37 -06:00
222 lines
6.6 KiB
TypeScript
222 lines
6.6 KiB
TypeScript
import { pixelsPerRem } from "../../app";
|
|
import { componentsByElement, componentsById } from "../../componentManagement";
|
|
import { ComponentBase } from "../componentBase";
|
|
import { NodeInputComponent } from "../nodeInput";
|
|
import { NodeOutputComponent } from "../nodeOutput";
|
|
import { GraphEditorComponent } from "./graphEditor";
|
|
import { AugmentedConnectionState } from "./graphStore";
|
|
|
|
/// Temporary function to get a component by its key
|
|
///
|
|
/// TODO / FIXME / REMOVEME
|
|
export function devel_getComponentByKey(key: string): ComponentBase {
|
|
for (let component of Object.values(componentsById)) {
|
|
if (component === undefined) {
|
|
continue;
|
|
}
|
|
|
|
// @ts-ignore
|
|
if (component.state._key_ === key) {
|
|
return component;
|
|
}
|
|
}
|
|
|
|
throw new Error(`Could not find component with key ${key}`);
|
|
}
|
|
|
|
/// Given the circle HTML element of a port, walk up the DOM to find the port
|
|
/// component that contains it.
|
|
export function getPortFromCircle(
|
|
circleElement: HTMLElement
|
|
): NodeInputComponent | NodeOutputComponent {
|
|
let portElement = circleElement.parentElement as HTMLElement;
|
|
console.assert(
|
|
portElement.classList.contains("rio-graph-editor-port"),
|
|
"Port element does not have the expected class"
|
|
);
|
|
|
|
let portComponent = componentsByElement.get(portElement) as ComponentBase;
|
|
console.assert(
|
|
portComponent !== undefined,
|
|
"Port element does not have a corresponding component"
|
|
);
|
|
|
|
console.assert(
|
|
portComponent instanceof NodeInputComponent ||
|
|
portComponent instanceof NodeOutputComponent,
|
|
"Port component is not of the expected type"
|
|
);
|
|
|
|
// @ts-ignore
|
|
return portComponent;
|
|
}
|
|
|
|
/// Given a port component, walk up the DOM to find the node component that
|
|
/// contains it.
|
|
export function getNodeFromPort(
|
|
port: NodeInputComponent | NodeOutputComponent
|
|
): ComponentBase {
|
|
// Walk up to find the node's body element
|
|
let bodyElement = port.element.closest(
|
|
".rio-graph-editor-node-body"
|
|
) as HTMLElement;
|
|
|
|
// Then walk back down to find the Rio component. Take care - there may ba
|
|
// alignments and margins inbetween
|
|
let nodeElement = bodyElement.querySelector(
|
|
".rio-component"
|
|
) as HTMLElement;
|
|
|
|
// Now use the Rio component to find the actual component
|
|
return componentsByElement.get(nodeElement) as ComponentBase;
|
|
}
|
|
|
|
/// Given the HTML element of a node, return the node's component by walking
|
|
/// the DOM.
|
|
export function getNodeComponentFromElement(
|
|
nodeElement: HTMLElement
|
|
): ComponentBase {
|
|
console.assert(
|
|
nodeElement.classList.contains("rio-graph-editor-node"),
|
|
"Node element does not have the expected class"
|
|
);
|
|
|
|
// The node body contains a Rio component. That component's ID is also the
|
|
// node's ID.
|
|
let componentElement = nodeElement.querySelector(
|
|
".rio-component"
|
|
) as HTMLElement;
|
|
|
|
return componentsByElement.get(componentElement) as ComponentBase;
|
|
}
|
|
|
|
/// Creates a SVG path element representing a connection. Does not add it to
|
|
/// the DOM.
|
|
export function makeConnectionElement(): SVGPathElement {
|
|
const svgPath = document.createElementNS(
|
|
"http://www.w3.org/2000/svg",
|
|
"path"
|
|
) as SVGPathElement;
|
|
|
|
svgPath.setAttribute("stroke", "var(--rio-local-text-color)");
|
|
svgPath.setAttribute("stroke-opacity", "0.5");
|
|
svgPath.setAttribute("stroke-width", "0.2rem");
|
|
svgPath.setAttribute("fill", "none");
|
|
|
|
return svgPath;
|
|
}
|
|
|
|
/// Given a port component, return the coordinates of the port's socket relative
|
|
/// to the viewport.
|
|
export function getPortViewportPosition(
|
|
portComponent: NodeInputComponent | NodeOutputComponent
|
|
): [number, number] {
|
|
// Find the circle's HTML element
|
|
let circleElement = portComponent.element.querySelector(
|
|
".rio-graph-editor-port-circle"
|
|
) as HTMLElement;
|
|
|
|
// Get the circle's bounding box
|
|
let circleBox = circleElement.getBoundingClientRect();
|
|
|
|
// Return the center of the circle
|
|
return [
|
|
circleBox.left + circleBox.width * 0.5,
|
|
circleBox.top + circleBox.height * 0.5,
|
|
];
|
|
}
|
|
|
|
/// Updates the SVG path of a connection based on the state of the
|
|
/// connection. This is a convenience function which determines the start
|
|
/// and end points and delegates to the more general function.
|
|
export function updateConnectionFromObject(
|
|
ge: GraphEditorComponent,
|
|
connectionState: AugmentedConnectionState
|
|
): void {
|
|
// From Port
|
|
let fromPortComponent = componentsById[
|
|
connectionState.fromPort
|
|
] as NodeOutputComponent;
|
|
|
|
let [x1, y1] = getPortViewportPosition(fromPortComponent);
|
|
|
|
// To Port
|
|
let toPortComponent = componentsById[
|
|
connectionState.toPort
|
|
] as NodeInputComponent;
|
|
|
|
let [x4, y4] = getPortViewportPosition(toPortComponent);
|
|
|
|
// Convert the coordinates to the editor's coordinate system
|
|
const editorRect = ge.element.getBoundingClientRect();
|
|
x1 = x1 - editorRect.left;
|
|
y1 = y1 - editorRect.top;
|
|
x4 = x4 - editorRect.left;
|
|
y4 = y4 - editorRect.top;
|
|
|
|
// Update the SVG path
|
|
updateConnectionFromCoordinates(connectionState.element, x1, y1, x4, y4);
|
|
}
|
|
|
|
/// Updates the SVG path of a connection based on the coordinates of the
|
|
/// start and end points.
|
|
///
|
|
/// All coordinates are relative to the editor.
|
|
export function updateConnectionFromCoordinates(
|
|
connectionElement: SVGPathElement,
|
|
x1: number,
|
|
y1: number,
|
|
x4: number,
|
|
y4: number
|
|
): void {
|
|
// Control the curve's bend
|
|
let signedDistance = Math.abs(x4 - x1);
|
|
|
|
let velocity = Math.pow(signedDistance * 10, 0.6);
|
|
velocity = Math.max(velocity, 3 * pixelsPerRem);
|
|
|
|
// Calculate the intermediate points
|
|
const x2 = x1 + velocity;
|
|
const y2 = y1;
|
|
|
|
const x3 = x4 - velocity;
|
|
const y3 = y4;
|
|
|
|
// Update the SVG path
|
|
connectionElement.setAttribute(
|
|
"d",
|
|
`M${x1} ${y1} C ${x2} ${y2}, ${x3} ${y3}, ${x4} ${y4}`
|
|
);
|
|
}
|
|
|
|
/// Returns `true` if the two lines intersect, `false` otherwise.
|
|
///
|
|
/// ChatGPT generated black magic.
|
|
export function linesIntersect(
|
|
l1x1: number,
|
|
l1y1: number,
|
|
l1x2: number,
|
|
l1y2: number,
|
|
l2x1: number,
|
|
l2y1: number,
|
|
l2x2: number,
|
|
l2y2: number
|
|
): boolean {
|
|
const denominator =
|
|
(l1x1 - l1x2) * (l2y1 - l2y2) - (l1y1 - l1y2) * (l2x1 - l2x2);
|
|
|
|
if (denominator === 0) {
|
|
return false;
|
|
}
|
|
|
|
const t =
|
|
((l1x1 - l2x1) * (l2y1 - l2y2) - (l1y1 - l2y1) * (l2x1 - l2x2)) /
|
|
denominator;
|
|
|
|
const u =
|
|
-((l1x1 - l1x2) * (l1y1 - l2y1) - (l1y1 - l1y2) * (l1x1 - l2x1)) /
|
|
denominator;
|
|
|
|
return t >= 0 && t <= 1 && u >= 0 && u <= 1;
|
|
}
|