Merge branch 'master' into fix-range

This commit is contained in:
mTvare
2025-07-06 14:34:34 +05:30
committed by GitHub
101 changed files with 3704 additions and 2818 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
The Graphite frontend is a web app that provides the presentation for the editor. It displays the GUI based on state from the backend and provides users with interactive widgets that send updates to the backend, which is the source of truth for state information. The frontend is built out of reactive components using the [Svelte](https://svelte.dev/) framework. The backend is written in Rust and compiled to WebAssembly (WASM) to be run in the browser alongside the JS code.
For lack of other options, the frontend is currently written as a web app. Maintaining web compatibility will always be a requirement, but the long-term plan is to port this code to a Rust-based native GUI framework, either written by the Rust community or created by our project if necessary. As a medium-term compromise, we may wrap the web-based frontend in a desktop webview windowing solution like Electron (probably not) or [Tauri](https://tauri.studio/) (probably).
For lack of other options, the frontend is currently written as a web app. Maintaining web compatibility will always be a requirement, but the long-term plan is to port this code to a Rust-based native GUI framework, either written by the Rust community or created by our project if necessary. As a medium-term compromise, we may wrap the web-based frontend in a desktop webview windowing solution like Electron (probably not) or [Tauri](https://tauri.app/) (probably).
## Bundled assets: `assets/`
+2
View File
@@ -104,6 +104,8 @@
--color-f-white-rgb: 255, 255, 255;
--color-error-red: #d6536e;
--color-error-red-rgb: 214, 83, 110;
--color-warning-yellow: #d5aa43;
--color-warning-yellow-rgb: 213, 170, 67;
--color-data-general: #cfcfcf;
--color-data-general-dim: #8a8a8a;
@@ -760,6 +760,8 @@
.text-input {
word-break: break-all;
unicode-bidi: plaintext;
text-align: left;
}
.text-input div {
@@ -773,6 +775,8 @@
overflow-wrap: anywhere;
white-space: pre-wrap;
word-break: normal;
unicode-bidi: plaintext;
text-align: left;
display: inline-block;
// Workaround to force Chrome to display the flashing text entry cursor when text is empty
padding-left: 1px;
+46 -451
View File
@@ -1,11 +1,11 @@
<script lang="ts">
import { getContext, onMount, tick } from "svelte";
import { getContext } from "svelte";
import { cubicInOut } from "svelte/easing";
import { fade } from "svelte/transition";
import type { Editor } from "@graphite/editor";
import type { Node } from "@graphite/messages";
import type { FrontendNodeWire, FrontendNode, FrontendGraphInput, FrontendGraphOutput, FrontendGraphDataType, WirePath } from "@graphite/messages";
import type { FrontendNode, FrontendGraphInput, FrontendGraphOutput } from "@graphite/messages";
import type { NodeGraphState } from "@graphite/state-providers/node-graph";
import type { IconName } from "@graphite/utility-functions/icons";
@@ -27,26 +27,13 @@
const nodeGraph = getContext<NodeGraphState>("nodeGraph");
let graph: HTMLDivElement | undefined;
let nodesContainer: HTMLDivElement | undefined;
// TODO: Using this not-complete code, or another better approach, make it so the dragged in-progress connector correctly handles showing/hiding the SVG shape of the connector caps
// let wireInProgressFromLayerTop: bigint | undefined = undefined;
// let wireInProgressFromLayerBottom: bigint | undefined = undefined;
let nodeWirePaths: WirePath[] = [];
// TODO: Convert these arrays-of-arrays to a Map?
let inputs: SVGSVGElement[][] = [];
let outputs: SVGSVGElement[][] = [];
let nodeElements: HTMLDivElement[] = [];
$: watchNodes($nodeGraph.nodes);
// Key value is node id + input/output index
// Imports/Export are stored at a key value of 0
$: gridSpacing = calculateGridSpacing($nodeGraph.transform.scale);
$: dotRadius = 1 + Math.floor($nodeGraph.transform.scale - 0.5 + 0.001) / 2;
$: wirePaths = createWirePaths($nodeGraph.wirePathInProgress, nodeWirePaths);
let inputElement: HTMLInputElement;
let hoveringImportIndex: number | undefined = undefined;
let hoveringExportIndex: number | undefined = undefined;
@@ -121,80 +108,6 @@
return sparse;
}
function createWirePaths(wirePathInProgress: WirePath | undefined, nodeWirePaths: WirePath[]): WirePath[] {
const maybeWirePathInProgress = wirePathInProgress ? [wirePathInProgress] : [];
return [...maybeWirePathInProgress, ...nodeWirePaths];
}
async function watchNodes(nodes: Map<bigint, FrontendNode>) {
Array.from(nodes.keys()).forEach((_, index) => {
if (!inputs[index + 1]) inputs[index + 1] = [];
if (!outputs[index + 1]) outputs[index + 1] = [];
});
if (!inputs[0]) inputs[0] = [];
if (!outputs[0]) outputs[0] = [];
await refreshWires();
}
function resolveWire(wire: FrontendNodeWire): { nodeOutput: SVGSVGElement; nodeInput: SVGSVGElement } | undefined {
// TODO: Avoid the linear search
const wireStartNodeIdIndex = Array.from($nodeGraph.nodes.keys()).findIndex((nodeId) => nodeId === (wire.wireStart as Node).nodeId);
let nodeOutputConnectors = outputs[wireStartNodeIdIndex + 1];
if (nodeOutputConnectors === undefined && (wire.wireStart as Node).nodeId === undefined) {
nodeOutputConnectors = outputs[0];
}
const indexOutput = Number(wire.wireStart.index);
const nodeOutput = nodeOutputConnectors?.[indexOutput] as SVGSVGElement | undefined;
if (nodeOutput === undefined) return undefined;
// TODO: Avoid the linear search
const wireEndNodeIdIndex = Array.from($nodeGraph.nodes.keys()).findIndex((nodeId) => nodeId === (wire.wireEnd as Node).nodeId);
let nodeInputConnectors = inputs[wireEndNodeIdIndex + 1] || undefined;
if (nodeInputConnectors === undefined && (wire.wireEnd as Node).nodeId === undefined) {
nodeInputConnectors = inputs[0];
}
const indexInput = Number(wire.wireEnd.index);
const nodeInput = nodeInputConnectors?.[indexInput] as SVGSVGElement | undefined;
if (nodeInput === undefined) return undefined;
return { nodeOutput, nodeInput };
}
function createWirePath(outputPort: SVGSVGElement, inputPort: SVGSVGElement, verticalOut: boolean, verticalIn: boolean, dashed: boolean, directNotGridAligned: boolean): WirePath {
const inputPortRect = inputPort.getBoundingClientRect();
const outputPortRect = outputPort.getBoundingClientRect();
const pathString = directNotGridAligned
? buildCurvedWirePathString(outputPortRect, inputPortRect, verticalOut, verticalIn)
: buildStraightWirePathString(outputPortRect, inputPortRect, verticalOut, verticalIn);
const dataType = (outputPort.getAttribute("data-datatype") as FrontendGraphDataType) || "General";
const thick = verticalIn && verticalOut;
return { pathString, dataType, thick, dashed };
}
async function refreshWires() {
await tick();
nodeWirePaths = $nodeGraph.wires.flatMap((wire) => {
// TODO: This call contains linear searches, which combined with the loop we're in, causes O(n^2) complexity as the graph grows
const resolvedWires = resolveWire(wire);
if (!resolvedWires) return [];
const { nodeOutput, nodeInput } = resolvedWires;
const wireStartNode = wire.wireStart.nodeId !== undefined ? $nodeGraph.nodes.get(wire.wireStart.nodeId) : undefined;
const wireStart = wireStartNode?.isLayer || false;
const wireEndNode = wire.wireEnd.nodeId !== undefined ? $nodeGraph.nodes.get(wire.wireEnd.nodeId) : undefined;
const wireEnd = (wireEndNode?.isLayer && Number(wire.wireEnd.index) === 0) || false;
return [createWirePath(nodeOutput, nodeInput, wireStart, wireEnd, wire.dashed, $nodeGraph.wiresDirectNotGridAligned)];
});
}
onMount(refreshWires);
function nodeIcon(icon?: string): IconName {
if (!icon) return "NodeNodes";
const iconMap: Record<string, IconName> = {
@@ -203,325 +116,6 @@
return iconMap[icon] || "NodeNodes";
}
function buildStraightWirePathLocations(outputBounds: DOMRect, inputBounds: DOMRect, verticalOut: boolean, verticalIn: boolean): { x: number; y: number }[] {
if (!nodesContainer) return [];
const VERTICAL_WIRE_OVERLAP_ON_SHAPED_CAP = 1;
const LINE_WIDTH = 2;
// Calculate coordinates for input and output connectors
const inX = verticalIn ? inputBounds.x + inputBounds.width / 2 : inputBounds.x;
const inY = verticalIn ? inputBounds.y + inputBounds.height - VERTICAL_WIRE_OVERLAP_ON_SHAPED_CAP : inputBounds.y + inputBounds.height / 2;
const outX = verticalOut ? outputBounds.x + outputBounds.width / 2 : outputBounds.x + outputBounds.width - 1;
const outY = verticalOut ? outputBounds.y + VERTICAL_WIRE_OVERLAP_ON_SHAPED_CAP : outputBounds.y + outputBounds.height / 2;
// Adjust for scale
const containerBounds = nodesContainer.getBoundingClientRect();
const scale = $nodeGraph.transform.scale;
const inConnectorX = Math.round((inX - containerBounds.x) / scale);
const inConnectorY = Math.round((inY - containerBounds.y) / scale);
const outConnectorX = Math.round((outX - containerBounds.x) / scale);
const outConnectorY = Math.round((outY - containerBounds.y) / scale);
// Helper functions for calculating coordinates
const calculateMidX = () => (inConnectorX + outConnectorX) / 2 + (((inConnectorX + outConnectorX) / 2) % gridSpacing);
const calculateMidY = () => (inConnectorY + outConnectorY) / 2 + (((inConnectorY + outConnectorY) / 2) % gridSpacing);
const calculateMidYAlternate = () => (inConnectorY + outConnectorY) / 2 - (((inConnectorY + outConnectorY) / 2) % gridSpacing);
// Define X coordinate calculations
const x1 = () => outConnectorX;
const x2 = () => outConnectorX + gridSpacing;
const x3 = () => inConnectorX - 2 * gridSpacing;
const x4 = () => inConnectorX;
const x5 = () => inConnectorX - 2 * gridSpacing + LINE_WIDTH;
const x6 = () => outConnectorX + gridSpacing + LINE_WIDTH;
const x7 = () => outConnectorX + 2 * gridSpacing + LINE_WIDTH;
const x8 = () => inConnectorX + LINE_WIDTH;
const x9 = () => outConnectorX + 2 * gridSpacing;
const x10 = () => calculateMidX() + LINE_WIDTH;
const x11 = () => outConnectorX - gridSpacing;
const x12 = () => outConnectorX - 4 * gridSpacing;
const x13 = () => calculateMidX();
const x14 = () => inConnectorX + gridSpacing;
const x15 = () => inConnectorX - 4 * gridSpacing;
const x16 = () => inConnectorX + 8 * gridSpacing;
const x17 = () => calculateMidX() - 2 * LINE_WIDTH;
const x18 = () => outConnectorX + gridSpacing - 2 * LINE_WIDTH;
const x19 = () => outConnectorX - 2 * LINE_WIDTH;
const x20 = () => calculateMidX() - LINE_WIDTH;
// Define Y coordinate calculations
const y1 = () => outConnectorY;
const y2 = () => outConnectorY - gridSpacing;
const y3 = () => inConnectorY;
const y4 = () => outConnectorY - gridSpacing + 5.5 * LINE_WIDTH;
const y5 = () => inConnectorY - 2 * gridSpacing;
const y6 = () => outConnectorY + 4 * LINE_WIDTH;
const y7 = () => outConnectorY + 5 * LINE_WIDTH;
const y8 = () => outConnectorY - 2 * gridSpacing + 5.5 * LINE_WIDTH;
const y9 = () => outConnectorY + 6 * LINE_WIDTH;
const y10 = () => inConnectorY + 2 * gridSpacing;
const y111 = () => inConnectorY + gridSpacing + 6.5 * LINE_WIDTH;
const y12 = () => inConnectorY + gridSpacing - 5.5 * LINE_WIDTH;
const y13 = () => inConnectorY - gridSpacing;
const y14 = () => inConnectorY + gridSpacing;
const y15 = () => calculateMidY();
const y16 = () => calculateMidYAlternate();
// Helper function for constructing coordinate pairs
const construct = (...coords: [() => number, () => number][]) => coords.map(([x, y]) => ({ x: x(), y: y() }));
// Define wire path shapes that get used more than once
const wire1 = () => construct([x1, y1], [x1, y4], [x5, y4], [x5, y3], [x4, y3]);
const wire2 = () => construct([x1, y1], [x1, y16], [x3, y16], [x3, y3], [x4, y3]);
const wire3 = () => construct([x1, y1], [x1, y4], [x12, y4], [x12, y10], [x3, y10], [x3, y3], [x4, y3]);
const wire4 = () => construct([x1, y1], [x1, y4], [x13, y4], [x13, y10], [x3, y10], [x3, y3], [x4, y3]);
// `outConnector` point and `inConnector` point lying on the same horizontal grid line and `outConnector` point lies to the right of `inConnector` point
if (outConnectorY === inConnectorY && outConnectorX > inConnectorX && (verticalOut || !verticalIn)) return construct([x1, y1], [x2, y1], [x2, y2], [x3, y2], [x3, y3], [x4, y3]);
// Handle straight lines
if (outConnectorY === inConnectorY || (outConnectorX === inConnectorX && verticalOut)) return construct([x1, y1], [x4, y3]);
// Handle standard right-angle paths
// Start vertical, then horizontal
// `outConnector` point lies to the left of `inConnector` point
if (verticalOut && inConnectorX > outConnectorX) {
// `outConnector` point lies above `inConnector` point
if (outConnectorY < inConnectorY) {
// `outConnector` point lies on the vertical grid line 4 units to the left of `inConnector` point point
if (-4 * gridSpacing <= outConnectorX - inConnectorX && outConnectorX - inConnectorX < -3 * gridSpacing) return wire1();
// `outConnector` point lying on vertical grid lines 3 and 2 units to the left of `inConnector` point
if (-3 * gridSpacing <= outConnectorX - inConnectorX && outConnectorX - inConnectorX <= -1 * gridSpacing) {
if (-2 * gridSpacing <= outConnectorY - inConnectorY && outConnectorY - inConnectorY <= -1 * gridSpacing) return construct([x1, y1], [x1, y2], [x2, y2], [x2, y3], [x4, y3]);
if (-1 * gridSpacing <= outConnectorY - inConnectorY && outConnectorY - inConnectorY <= 0 * gridSpacing) return construct([x1, y1], [x1, y4], [x6, y4], [x6, y3], [x4, y3]);
return construct([x1, y1], [x1, y4], [x7, y4], [x7, y5], [x3, y5], [x3, y3], [x4, y3]);
}
// `outConnector` point lying on vertical grid line 1 units to the left of `inConnector` point
if (-1 * gridSpacing < outConnectorX - inConnectorX && outConnectorX - inConnectorX <= 0 * gridSpacing) {
// `outConnector` point lying on horizontal grid line 1 unit above `inConnector` point
if (-2 * gridSpacing <= outConnectorY - inConnectorY && outConnectorY - inConnectorY <= -1 * gridSpacing) return construct([x1, y6], [x2, y6], [x8, y3]);
// `outConnector` point lying on the same horizontal grid line as `inConnector` point
if (-1 * gridSpacing <= outConnectorY - inConnectorY && outConnectorY - inConnectorY <= 0 * gridSpacing) return construct([x1, y7], [x4, y3]);
return construct([x1, y1], [x1, y2], [x9, y2], [x9, y5], [x3, y5], [x3, y3], [x4, y3]);
}
return construct([x1, y1], [x1, y4], [x10, y4], [x10, y3], [x4, y3]);
}
// `outConnector` point lies below `inConnector` point
// `outConnector` point lying on vertical grid line 1 unit to the left of `inConnector` point
if (-1 * gridSpacing <= outConnectorX - inConnectorX && outConnectorX - inConnectorX <= 0 * gridSpacing) {
// `outConnector` point lying on the horizontal grid lines 1 and 2 units below the `inConnector` point
if (0 * gridSpacing <= outConnectorY - inConnectorY && outConnectorY - inConnectorY <= 2 * gridSpacing) construct([x1, y6], [x11, y6], [x11, y3], [x4, y3]);
return wire2();
}
return construct([x1, y1], [x1, y3], [x4, y3]);
}
// `outConnector` point lies to the right of `inConnector` point
if (verticalOut && inConnectorX <= outConnectorX) {
// `outConnector` point lying on any horizontal grid line above `inConnector` point
if (outConnectorY < inConnectorY) {
// `outConnector` point lying on horizontal grid line 1 unit above `inConnector` point
if (-2 * gridSpacing < outConnectorY - inConnectorY && outConnectorY - inConnectorY <= -1 * gridSpacing) return wire1();
// `outConnector` point lying on the same horizontal grid line as `inConnector` point
if (-1 * gridSpacing < outConnectorY - inConnectorY && outConnectorY - inConnectorY <= 0 * gridSpacing) return construct([x1, y1], [x1, y8], [x5, y8], [x5, y3], [x4, y3]);
// `outConnector` point lying on vertical grid lines 1 and 2 units to the right of `inConnector` point
if (gridSpacing <= outConnectorX - inConnectorX && outConnectorX - inConnectorX <= 3 * gridSpacing) {
return construct([x1, y1], [x1, y4], [x9, y4], [x9, y5], [x3, y5], [x3, y3], [x4, y3]);
}
return construct([x1, y1], [x1, y4], [x10, y4], [x10, y5], [x5, y5], [x5, y3], [x4, y3]);
}
// `outConnector` point lies below `inConnector` point
if (outConnectorY - inConnectorY <= gridSpacing) {
// `outConnector` point lies on the horizontal grid line 1 unit below the `inConnector` Point
if (0 <= outConnectorX - inConnectorX && outConnectorX - inConnectorX <= 13 * gridSpacing) return construct([x1, y9], [x3, y9], [x3, y3], [x4, y3]);
if (13 < outConnectorX - inConnectorX && outConnectorX - inConnectorX <= 18 * gridSpacing) return wire3();
return wire4();
}
// `outConnector` point lies on the horizontal grid line 2 units below `outConnector` point
if (gridSpacing <= outConnectorY - inConnectorY && outConnectorY - inConnectorY <= 2 * gridSpacing) {
if (0 <= outConnectorX - inConnectorX && outConnectorX - inConnectorX <= 13 * gridSpacing) return construct([x1, y7], [x5, y7], [x5, y3], [x4, y3]);
if (13 < outConnectorX - inConnectorX && outConnectorX - inConnectorX <= 18 * gridSpacing) return wire3();
return wire4();
}
// 0 to 4 units below the `outConnector` Point
if (outConnectorY - inConnectorY <= 4 * gridSpacing) return wire1();
return wire2();
}
// Start horizontal, then vertical
if (verticalIn) {
// when `outConnector` lies below `inConnector`
if (outConnectorY > inConnectorY) {
// `outConnectorX` lies to the left of `inConnectorX`
if (outConnectorX < inConnectorX) return construct([x1, y1], [x4, y1], [x4, y3]);
// `outConnectorX` lies to the right of `inConnectorX`
if (outConnectorY - inConnectorY <= gridSpacing) {
// `outConnector` point directly below `inConnector` point
if (0 <= outConnectorX - inConnectorX && outConnectorX - inConnectorX <= gridSpacing) return construct([x1, y1], [x14, y1], [x14, y2], [x4, y2], [x4, y3]);
// `outConnector` point lies below `inConnector` point and strictly to the right of `inConnector` point
return construct([x1, y1], [x2, y1], [x2, y111], [x4, y111], [x4, y3]);
}
return construct([x1, y1], [x2, y1], [x2, y2], [x4, y2], [x4, y3]);
}
// `outConnectorY` lies on or above the `inConnectorY` point
if (-6 * gridSpacing < inConnectorX - outConnectorX && inConnectorX - outConnectorX < 4 * gridSpacing) {
// edge case: `outConnector` point lying on vertical grid lines ranging from 4 units to left to 5 units to right of `inConnector` point
if (-1 * gridSpacing < inConnectorX - outConnectorX && inConnectorX - outConnectorX < 4 * gridSpacing) {
return construct([x1, y1], [x2, y1], [x2, y2], [x15, y2], [x15, y12], [x4, y12], [x4, y3]);
}
return construct([x1, y1], [x16, y1], [x16, y12], [x4, y12], [x4, y3]);
}
// left of edge case: `outConnector` point lying on vertical grid lines more than 4 units to left of `inConnector` point
if (4 * gridSpacing < inConnectorX - outConnectorX) return construct([x1, y1], [x17, y1], [x17, y12], [x4, y12], [x4, y3]);
// right of edge case: `outConnector` point lying on the vertical grid lines more than 5 units to right of `inConnector` point
if (6 * gridSpacing > inConnectorX - outConnectorX) return construct([x1, y1], [x18, y1], [x18, y12], [x4, y12], [x4, y3]);
}
// Both horizontal - use horizontal middle point
// When `inConnector` point is one of the two closest diagonally opposite points
if (0 <= inConnectorX - outConnectorX && inConnectorX - outConnectorX <= gridSpacing && inConnectorY - outConnectorY >= -1 * gridSpacing && inConnectorY - outConnectorY <= gridSpacing) {
return construct([x19, y1], [x19, y3], [x4, y3]);
}
// When `inConnector` point lies on the horizontal line 1 unit above and below the `outConnector` point
if (-1 * gridSpacing <= outConnectorY - inConnectorY && outConnectorY - inConnectorY <= gridSpacing && outConnectorX > inConnectorX) {
// Horizontal line above `outConnectorY`
if (inConnectorY < outConnectorY) return construct([x1, y1], [x2, y1], [x2, y13], [x3, y13], [x3, y3], [x4, y3]);
// Horizontal line below `outConnectorY`
return construct([x1, y1], [x2, y1], [x2, y14], [x3, y14], [x3, y3], [x4, y3]);
}
// `outConnector` point to the right of `inConnector` point
if (outConnectorX > inConnectorX - gridSpacing) return construct([x1, y1], [x18, y1], [x18, y15], [x5, y15], [x5, y3], [x4, y3]);
// When `inConnector` point lies on the vertical grid line two units to the right of `outConnector` point
if (gridSpacing <= inConnectorX - outConnectorX && inConnectorX - outConnectorX <= 2 * gridSpacing) return construct([x1, y1], [x18, y1], [x18, y3], [x4, y3]);
return construct([x1, y1], [x20, y1], [x20, y3], [x4, y3]);
}
function buildStraightWirePathString(outputBounds: DOMRect, inputBounds: DOMRect, verticalOut: boolean, verticalIn: boolean): string {
const locations = buildStraightWirePathLocations(outputBounds, inputBounds, verticalOut, verticalIn);
if (locations.length === 0) return "[error]";
if (locations.length === 2) return `M${locations[0].x},${locations[0].y} L${locations[1].x},${locations[1].y}`;
const CORNER_RADIUS = 10;
// Create path with rounded corners
let path = `M${locations[0].x},${locations[0].y}`;
for (let i = 1; i < locations.length - 1; i++) {
const prev = locations[i - 1];
const curr = locations[i];
const next = locations[i + 1];
// Calculate corner points
const isVertical = curr.x === prev.x;
const cornerStart = {
x: curr.x + (isVertical ? 0 : prev.x < curr.x ? -CORNER_RADIUS : CORNER_RADIUS),
y: curr.y + (isVertical ? (prev.y < curr.y ? -CORNER_RADIUS : CORNER_RADIUS) : 0),
};
const cornerEnd = {
x: curr.x + (isVertical ? (next.x < curr.x ? -CORNER_RADIUS : CORNER_RADIUS) : 0),
y: curr.y + (isVertical ? 0 : next.y < curr.y ? -CORNER_RADIUS : CORNER_RADIUS),
};
// Add line to corner start, quadratic curve for corner, then continue to next point
path += ` L${cornerStart.x},${cornerStart.y}`;
path += ` Q${curr.x},${curr.y} ${cornerEnd.x},${cornerEnd.y}`;
}
path += ` L${locations[locations.length - 1].x},${locations[locations.length - 1].y}`;
return path;
}
function buildCurvedWirePathLocations(outputBounds: DOMRect, inputBounds: DOMRect, verticalOut: boolean, verticalIn: boolean): { x: number; y: number }[] {
if (!nodesContainer) return [];
const VERTICAL_WIRE_OVERLAP_ON_SHAPED_CAP = 1;
const containerBounds = nodesContainer.getBoundingClientRect();
const outX = verticalOut ? outputBounds.x + outputBounds.width / 2 : outputBounds.x + outputBounds.width - 1;
const outY = verticalOut ? outputBounds.y + VERTICAL_WIRE_OVERLAP_ON_SHAPED_CAP : outputBounds.y + outputBounds.height / 2;
const outConnectorX = (outX - containerBounds.x) / $nodeGraph.transform.scale;
const outConnectorY = (outY - containerBounds.y) / $nodeGraph.transform.scale;
const inX = verticalIn ? inputBounds.x + inputBounds.width / 2 : inputBounds.x;
const inY = verticalIn ? inputBounds.y + inputBounds.height - VERTICAL_WIRE_OVERLAP_ON_SHAPED_CAP : inputBounds.y + inputBounds.height / 2;
const inConnectorX = (inX - containerBounds.x) / $nodeGraph.transform.scale;
const inConnectorY = (inY - containerBounds.y) / $nodeGraph.transform.scale;
const horizontalGap = Math.abs(outConnectorX - inConnectorX);
const verticalGap = Math.abs(outConnectorY - inConnectorY);
const curveLength = 24;
const curveFalloffRate = curveLength * Math.PI * 2;
const horizontalCurveAmount = -(2 ** ((-10 * horizontalGap) / curveFalloffRate)) + 1;
const verticalCurveAmount = -(2 ** ((-10 * verticalGap) / curveFalloffRate)) + 1;
const horizontalCurve = horizontalCurveAmount * curveLength;
const verticalCurve = verticalCurveAmount * curveLength;
return [
{ x: outConnectorX, y: outConnectorY },
{ x: verticalOut ? outConnectorX : outConnectorX + horizontalCurve, y: verticalOut ? outConnectorY - verticalCurve : outConnectorY },
{ x: verticalIn ? inConnectorX : inConnectorX - horizontalCurve, y: verticalIn ? inConnectorY + verticalCurve : inConnectorY },
{ x: inConnectorX, y: inConnectorY },
];
}
function buildCurvedWirePathString(outputBounds: DOMRect, inputBounds: DOMRect, verticalOut: boolean, verticalIn: boolean): string {
const locations = buildCurvedWirePathLocations(outputBounds, inputBounds, verticalOut, verticalIn);
if (locations.length === 0) return "[error]";
const SMOOTHING = 0.5;
const delta01 = { x: (locations[1].x - locations[0].x) * SMOOTHING, y: (locations[1].y - locations[0].y) * SMOOTHING };
const delta23 = { x: (locations[3].x - locations[2].x) * SMOOTHING, y: (locations[3].y - locations[2].y) * SMOOTHING };
return `
M${locations[0].x},${locations[0].y}
L${locations[1].x},${locations[1].y}
C${locations[1].x + delta01.x},${locations[1].y + delta01.y}
${locations[2].x - delta23.x},${locations[2].y - delta23.y}
${locations[2].x},${locations[2].y}
L${locations[3].x},${locations[3].y}
`
.split("\n")
.map((line) => line.trim())
.join(" ");
}
function toggleLayerDisplay(displayAsLayer: boolean, toggleId: bigint) {
let node = $nodeGraph.nodes.get(toggleId);
if (node) editor.handle.setToNodeOrLayer(node.id, displayAsLayer);
@@ -719,19 +313,21 @@
</div>
{/if}
<!-- Node connection wires -->
<!-- Thick vertical layer connection wires -->
<div class="wires" style:transform-origin={`0 0`} style:transform={`translate(${$nodeGraph.transform.x}px, ${$nodeGraph.transform.y}px) scale(${$nodeGraph.transform.scale})`}>
<svg>
{#each wirePaths as { pathString, dataType, thick, dashed }}
{#if thick}
<path
d={pathString}
style:--data-line-width={`${thick ? 8 : 2}px`}
style:--data-color={`var(--color-data-${dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${dataType.toLowerCase()}-dim)`}
style:--data-dasharray={`3,${dashed ? 2 : 0}`}
/>
{/if}
{#each $nodeGraph.wires.values() as map}
{#each map.values() as { pathString, dataType, thick, dashed }}
{#if thick}
<path
d={pathString}
style:--data-line-width={"8px"}
style:--data-color={`var(--color-data-${dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${dataType.toLowerCase()}-dim)`}
style:--data-dasharray={`3,${dashed ? 2 : 0}`}
/>
{/if}
{/each}
{/each}
</svg>
</div>
@@ -749,7 +345,6 @@
style:--data-color-dim={`var(--color-data-${outputMetadata.dataType.toLowerCase()}-dim)`}
style:--offset-left={position.x / 24}
style:--offset-top={position.y / 24}
bind:this={outputs[0][index]}
>
<title>{`${dataTypeTooltip(outputMetadata)}\n\n${outputConnectedToText(outputMetadata)}`}</title>
{#if outputMetadata.connectedTo !== undefined}
@@ -823,7 +418,6 @@
style:--data-color-dim={`var(--color-data-${inputMetadata.dataType.toLowerCase()}-dim)`}
style:--offset-left={position.x / 24}
style:--offset-top={position.y / 24}
bind:this={inputs[0][index]}
>
<title>{`${dataTypeTooltip(inputMetadata)}\n\n${inputConnectedToText(inputMetadata)}`}</title>
{#if inputMetadata.connectedTo !== undefined}
@@ -887,14 +481,11 @@
</div>
<!-- Layers and nodes -->
<div
class="layers-and-nodes"
style:transform-origin={`0 0`}
style:transform={`translate(${$nodeGraph.transform.x}px, ${$nodeGraph.transform.y}px) scale(${$nodeGraph.transform.scale})`}
bind:this={nodesContainer}
>
<div class="layers-and-nodes" style:transform-origin={`0 0`} style:transform={`translate(${$nodeGraph.transform.x}px, ${$nodeGraph.transform.y}px) scale(${$nodeGraph.transform.scale})`}>
<!-- Layers -->
{#each Array.from($nodeGraph.nodes.values()).flatMap((node, nodeIndex) => (node.isLayer ? [{ node, nodeIndex }] : [])) as { node, nodeIndex } (nodeIndex)}
{#each Array.from($nodeGraph.nodes)
.filter(([nodeId, node]) => node.isLayer && $nodeGraph.visibleNodes.has(nodeId))
.map(([_, node], nodeIndex) => ({ node, nodeIndex })) as { node, nodeIndex } (nodeIndex)}
{@const clipPathId = String(Math.random()).substring(2)}
{@const stackDataInput = node.exposedInputs[0]}
{@const layerAreaWidth = $nodeGraph.layerWidths.get(node.id) || 8}
@@ -916,7 +507,6 @@
style:--node-chain-area-left-extension={layerChainWidth !== 0 ? layerChainWidth + 0.5 : 0}
title={`${node.displayName}\n\n${description || ""}`.trim() + (editor.handle.inDevelopmentMode() ? `\n\nNode ID: ${node.id}` : "")}
data-node={node.id}
bind:this={nodeElements[nodeIndex]}
>
{#if node.errors}
<span class="node-error faded" transition:fade={FADE_TRANSITION} title="" data-node-error>{node.errors}</span>
@@ -936,7 +526,6 @@
data-datatype={node.primaryOutput.dataType}
style:--data-color={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()}-dim)`}
bind:this={outputs[nodeIndex + 1][0]}
>
<title>{`${dataTypeTooltip(node.primaryOutput)}\n\n${outputConnectedToText(node.primaryOutput)}`}</title>
{#if node.primaryOutput.connectedTo.length > 0}
@@ -958,7 +547,6 @@
data-datatype={node.primaryInput?.dataType}
style:--data-color={`var(--color-data-${(node.primaryInput?.dataType || "General").toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${(node.primaryInput?.dataType || "General").toLowerCase()}-dim)`}
bind:this={inputs[nodeIndex + 1][0]}
>
{#if node.primaryInput}
<title>{`${dataTypeTooltip(node.primaryInput)}\n\n${validTypesText(node.primaryInput)}\n\n${inputConnectedToText(node.primaryInput)}`}</title>
@@ -984,7 +572,6 @@
data-datatype={stackDataInput.dataType}
style:--data-color={`var(--color-data-${stackDataInput.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${stackDataInput.dataType.toLowerCase()}-dim)`}
bind:this={inputs[nodeIndex + 1][1]}
>
<title>{`${dataTypeTooltip(stackDataInput)}\n\n${validTypesText(stackDataInput)}\n\n${inputConnectedToText(stackDataInput)}`}</title>
{#if stackDataInput.connectedTo !== undefined}
@@ -1025,22 +612,35 @@
<!-- Node connection wires -->
<div class="wires">
<svg>
{#each wirePaths as { pathString, dataType, thick, dashed }}\
{#if !thick}
<path
d={pathString}
style:--data-line-width={`${thick ? 8 : 2}px`}
style:--data-color={`var(--color-data-${dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${dataType.toLowerCase()}-dim)`}
style:--data-dasharray={dashed ? "4" : undefined}
/>
{/if}
{#each $nodeGraph.wires.values() as map}
{#each map.values() as { pathString, dataType, thick, dashed }}
{#if !thick}
<path
d={pathString}
style:--data-line-width={"2px"}
style:--data-color={`var(--color-data-${dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${dataType.toLowerCase()}-dim)`}
style:--data-dasharray={`3,${dashed ? 2 : 0}`}
/>
{/if}
{/each}
{/each}
{#if $nodeGraph.wirePathInProgress}
<path
d={$nodeGraph.wirePathInProgress?.pathString}
style:--data-line-width={`${$nodeGraph.wirePathInProgress.thick ? 8 : 2}px`}
style:--data-color={`var(--color-data-${$nodeGraph.wirePathInProgress.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${$nodeGraph.wirePathInProgress.dataType.toLowerCase()}-dim)`}
style:--data-dasharray={`3,${$nodeGraph.wirePathInProgress.dashed ? 2 : 0}`}
/>
{/if}
</svg>
</div>
<!-- Nodes -->
{#each Array.from($nodeGraph.nodes.values()).flatMap((node, nodeIndex) => (node.isLayer ? [] : [{ node, nodeIndex }])) as { node, nodeIndex } (nodeIndex)}
{#each Array.from($nodeGraph.nodes)
.filter(([nodeId, node]) => !node.isLayer && $nodeGraph.visibleNodes.has(nodeId))
.map(([_, node], nodeIndex) => ({ node, nodeIndex })) as { node, nodeIndex } (nodeIndex)}
{@const exposedInputsOutputs = zipWithUndefined(node.exposedInputs, node.exposedOutputs)}
{@const clipPathId = String(Math.random()).substring(2)}
{@const description = (node.reference && $nodeGraph.nodeDescriptions.get(node.reference)) || undefined}
@@ -1056,7 +656,6 @@
style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`}
title={`${node.displayName}\n\n${description || ""}`.trim() + (editor.handle.inDevelopmentMode() ? `\n\nNode ID: ${node.id}` : "")}
data-node={node.id}
bind:this={nodeElements[nodeIndex]}
>
{#if node.errors}
<span class="node-error faded" transition:fade={FADE_TRANSITION} title="" data-node-error>{node.errors}</span>
@@ -1091,7 +690,6 @@
data-datatype={node.primaryInput?.dataType}
style:--data-color={`var(--color-data-${node.primaryInput.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${node.primaryInput.dataType.toLowerCase()}-dim)`}
bind:this={inputs[nodeIndex + 1][0]}
>
<title>{`${dataTypeTooltip(node.primaryInput)}\n\n${validTypesText(node.primaryInput)}\n\n${inputConnectedToText(node.primaryInput)}`}</title>
{#if node.primaryInput.connectedTo !== undefined}
@@ -1111,7 +709,6 @@
data-datatype={secondary.dataType}
style:--data-color={`var(--color-data-${secondary.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${secondary.dataType.toLowerCase()}-dim)`}
bind:this={inputs[nodeIndex + 1][index + (node.primaryInput ? 1 : 0)]}
>
<title>{`${dataTypeTooltip(secondary)}\n\n${validTypesText(secondary)}\n\n${inputConnectedToText(secondary)}`}</title>
{#if secondary.connectedTo !== undefined}
@@ -1134,7 +731,6 @@
data-datatype={node.primaryOutput.dataType}
style:--data-color={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()}-dim)`}
bind:this={outputs[nodeIndex + 1][0]}
>
<title>{`${dataTypeTooltip(node.primaryOutput)}\n\n${outputConnectedToText(node.primaryOutput)}`}</title>
{#if node.primaryOutput.connectedTo !== undefined}
@@ -1144,7 +740,7 @@
{/if}
</svg>
{/if}
{#each node.exposedOutputs as secondary, outputIndex}
{#each node.exposedOutputs as secondary}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 8 8"
@@ -1153,7 +749,6 @@
data-datatype={secondary.dataType}
style:--data-color={`var(--color-data-${secondary.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${secondary.dataType.toLowerCase()}-dim)`}
bind:this={outputs[nodeIndex + 1][outputIndex + (node.primaryOutput ? 1 : 0)]}
>
<title>{`${dataTypeTooltip(secondary)}\n\n${outputConnectedToText(secondary)}`}</title>
{#if secondary.connectedTo !== undefined}
@@ -158,6 +158,7 @@
background: none;
color: var(--color-e-nearwhite);
caret-color: var(--color-e-nearwhite);
unicode-bidi: plaintext;
&::selection {
background-color: var(--color-4-dimgray);
+24 -48
View File
@@ -96,16 +96,21 @@ export class UpdateLayerWidths extends JsMessage {
readonly hasLeftInputWire!: Map<bigint, boolean>;
}
export class UpdateNodeGraph extends JsMessage {
export class UpdateNodeGraphNodes extends JsMessage {
@Type(() => FrontendNode)
readonly nodes!: FrontendNode[];
@Type(() => FrontendNodeWire)
readonly wires!: FrontendNodeWire[];
readonly wiresDirectNotGridAligned!: boolean;
}
export class UpdateVisibleNodes extends JsMessage {
readonly nodes!: bigint[];
}
export class UpdateNodeGraphWires extends JsMessage {
readonly wires!: WireUpdate[];
}
export class ClearAllNodeGraphWires extends JsMessage {}
export class UpdateNodeGraphTransform extends JsMessage {
readonly transform!: NodeGraphTransform;
}
@@ -219,7 +224,7 @@ export class FrontendGraphInput {
readonly description!: string;
readonly resolvedType!: string | undefined;
readonly resolvedType!: string;
readonly validTypes!: string[];
@@ -252,7 +257,7 @@ export class FrontendGraphOutput {
readonly description!: string;
readonly resolvedType!: string | undefined;
readonly resolvedType!: string;
@CreateInputConnectorArray
connectedTo!: Node[];
@@ -297,44 +302,6 @@ export class FrontendNode {
readonly uiOnly!: boolean;
}
const CreateOutputConnector = Transform(({ obj }) => {
if (obj.wireStart.export !== undefined) {
return { index: obj.wireStart.export };
} else if (obj.wireStart.import !== undefined) {
return { index: obj.wireStart.import };
} else {
if (obj.wireStart.node.inputIndex !== undefined) {
return { nodeId: obj.wireStart.node.nodeId, index: obj.wireStart.node.inputIndex };
} else {
return { nodeId: obj.wireStart.node.nodeId, index: obj.wireStart.node.outputIndex };
}
}
});
const CreateInputConnector = Transform(({ obj }) => {
if (obj.wireEnd.export !== undefined) {
return { index: obj.wireEnd.export };
} else if (obj.wireEnd.import !== undefined) {
return { index: obj.wireEnd.import };
} else {
if (obj.wireEnd.node.inputIndex !== undefined) {
return { nodeId: obj.wireEnd.node.nodeId, index: obj.wireEnd.node.inputIndex };
} else {
return { nodeId: obj.wireEnd.node.nodeId, index: obj.wireEnd.node.outputIndex };
}
}
});
export class FrontendNodeWire {
@CreateOutputConnector
readonly wireStart!: Node;
@CreateInputConnector
readonly wireEnd!: Node;
readonly dashed!: boolean;
}
export class FrontendNodeType {
readonly name!: string;
@@ -356,6 +323,12 @@ export class WirePath {
readonly dashed!: boolean;
}
export class WireUpdate {
readonly id!: bigint;
readonly inputIndex!: number;
readonly wirePathUpdate!: WirePath | undefined;
}
export class IndexedDbDocumentDetails extends DocumentDetails {
@Transform(({ value }: { value: bigint }) => value.toString())
id!: string;
@@ -1645,6 +1618,7 @@ type JSMessageFactory = (data: any, wasm: WebAssembly.Memory, handle: EditorHand
type MessageMaker = typeof JsMessage | JSMessageFactory;
export const messageMakers: Record<string, MessageMaker> = {
ClearAllNodeGraphWires,
DisplayDialog,
DisplayDialogDismiss,
DisplayDialogPanic,
@@ -1700,10 +1674,12 @@ export const messageMakers: Record<string, MessageMaker> = {
UpdateLayerWidths,
UpdateMenuBarLayout,
UpdateMouseCursor,
UpdateNodeGraph,
UpdateNodeGraphNodes,
UpdateVisibleNodes,
UpdateNodeGraphWires,
UpdateNodeGraphTransform,
UpdateNodeGraphControlBarLayout,
UpdateNodeGraphSelection,
UpdateNodeGraphTransform,
UpdateNodeThumbnail,
UpdateOpenDocumentsList,
UpdatePropertyPanelSectionsLayout,
+39 -9
View File
@@ -7,9 +7,9 @@ import {
type FrontendClickTargets,
type ContextMenuInformation,
type FrontendNode,
type FrontendNodeWire as FrontendNodeWire,
type FrontendNodeType,
type WirePath,
ClearAllNodeGraphWires,
SendUIMetadata,
UpdateBox,
UpdateClickTargets,
@@ -19,7 +19,9 @@ import {
UpdateExportReorderIndex,
UpdateImportsExports,
UpdateLayerWidths,
UpdateNodeGraph,
UpdateNodeGraphNodes,
UpdateVisibleNodes,
UpdateNodeGraphWires,
UpdateNodeGraphSelection,
UpdateNodeGraphTransform,
UpdateNodeThumbnail,
@@ -40,8 +42,9 @@ export function createNodeGraphState(editor: Editor) {
addImport: undefined as { x: number; y: number } | undefined,
addExport: undefined as { x: number; y: number } | undefined,
nodes: new Map<bigint, FrontendNode>(),
wires: [] as FrontendNodeWire[],
wiresDirectNotGridAligned: false,
visibleNodes: new Set<bigint>(),
/// The index is the exposed input index. The exports have a first key value of u32::MAX.
wires: new Map<bigint, Map<number, WirePath>>(),
wirePathInProgress: undefined as WirePath | undefined,
nodeDescriptions: new Map<string, string>(),
nodeTypes: [] as FrontendNodeType[],
@@ -114,15 +117,42 @@ export function createNodeGraphState(editor: Editor) {
return state;
});
});
// TODO: Add a way to only update the nodes that have changed
editor.subscriptions.subscribeJsMessage(UpdateNodeGraph, (updateNodeGraph) => {
editor.subscriptions.subscribeJsMessage(UpdateNodeGraphNodes, (updateNodeGraphNodes) => {
update((state) => {
state.nodes.clear();
updateNodeGraph.nodes.forEach((node) => {
updateNodeGraphNodes.nodes.forEach((node) => {
state.nodes.set(node.id, node);
});
state.wires = updateNodeGraph.wires;
state.wiresDirectNotGridAligned = updateNodeGraph.wiresDirectNotGridAligned;
return state;
});
});
editor.subscriptions.subscribeJsMessage(UpdateVisibleNodes, (updateVisibleNodes) => {
update((state) => {
state.visibleNodes = new Set<bigint>(updateVisibleNodes.nodes);
return state;
});
});
editor.subscriptions.subscribeJsMessage(UpdateNodeGraphWires, (updateNodeWires) => {
update((state) => {
updateNodeWires.wires.forEach((wireUpdate) => {
let inputMap = state.wires.get(wireUpdate.id);
// If it doesn't exist, create it and set it in the outer map
if (!inputMap) {
inputMap = new Map();
state.wires.set(wireUpdate.id, inputMap);
}
if (wireUpdate.wirePathUpdate !== undefined) {
inputMap.set(wireUpdate.inputIndex, wireUpdate.wirePathUpdate);
} else {
inputMap.delete(wireUpdate.inputIndex);
}
});
return state;
});
});
editor.subscriptions.subscribeJsMessage(ClearAllNodeGraphWires, (_) => {
update((state) => {
state.wires.clear();
return state;
});
});
+2
View File
@@ -21,6 +21,7 @@ const ALLOWED_LICENSES = [
"BSD-3-Clause",
"BSL-1.0",
"CC0-1.0",
"CDLA-Permissive-2.0",
"ISC",
"MIT-0",
"MIT",
@@ -29,6 +30,7 @@ const ALLOWED_LICENSES = [
"Unicode-3.0",
"Unicode-DFS-2016",
"Zlib",
"NCSA",
];
// https://vitejs.dev/config/
+5 -2
View File
@@ -467,8 +467,9 @@ impl EditorHandle {
return Err(Error::new("Invalid color").into());
};
let message = ToolMessage::SelectPrimaryColor {
let message = ToolMessage::SelectWorkingColor {
color: primary_color.to_linear_srgb(),
primary: true,
};
self.dispatch(message);
@@ -482,8 +483,9 @@ impl EditorHandle {
return Err(Error::new("Invalid color").into());
};
let message = ToolMessage::SelectSecondaryColor {
let message = ToolMessage::SelectWorkingColor {
color: secondary_color.to_linear_srgb(),
primary: false,
};
self.dispatch(message);
@@ -602,6 +604,7 @@ impl EditorHandle {
node_id: Some(id),
node_type,
xy: Some((x / 24, y / 24)),
add_transaction: true,
};
self.dispatch(message);
}