mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-18 18:38:05 +08:00
Add layer node chains, import/export edge connectors, and refactor graph editing to go thru a NodeNetworkInterface (#1794)
* WIP: NodeNetworkInterface * Organize ModifyInputsContext to use network interface * Improve ClickTarget and Position state * Rework ClickTarget state * Continue fixing NodeGraphMessageHandler * Restructure network_metadata * Final(?) NodeNetworkInterface struct * Final(??) NodeNetworkInterface * Final(???) NodeNetworkInterface. Separated persistent and transient data * Final NodeNetworkInterface data structure. Implemented all basic getters * Continue migrating functionality to network interface * Migrate all NodeGraphMessage's to use network interface * Fix all helper functions in NodeGraphMessageHandler * Move document metadata to network interface, remove various cached fields * Move all editor only NodeNetwork implementations to NodeNetworkInterface * Fix all DocumentNodeDefinitions * Rework and migrate GraphOperationMessages to network interface * Continue migration to NodeNetworkInterface * Save point before merging master * Fix all errors in network_interface * 850 -> 160 errors * Fix all errors :D * Render default document * Visualize click targets * merge conflicts * Cache transient metadata separately, store entire interface in document history * Start migration to storing selected nodes for each network * Remove selected nodes from document message handler * Move outward wires and all nodes bounding box to transient metadata * Fix connecting/disconnecting nodes * Layer stack organization for disconnecting/connecting nodes * Basic chain locking * Improve chain positioning * Add copy/pasting * Move upstream nodes on shift+drag * merge conflict fixes * Improve Graph.svelte code quality * Final improvements to Graph.svelte * Fix layer panel * Performance optimizations * Bug fixes and derived PTZ * Chain organization improvement and bug fixes * Bug fixes, remove all warnings * Automatic file upgrade * Final code review * Fix editor tests * Fix compile errors * Remove select tool intersection check when panning * WIP: Import/Exports * Fix JS issues * Finish simplified import/export UI * Import/Export viewport edge UI * Remove minimum y bound on import/export ports * Improve performance while panning graph * cargo fmt * Fix CI code build * Format the demo artwork graph with chains * Code review --------- Co-authored-by: Keavon Chambers <keavon@keavon.com> Co-authored-by: dennis@kobert.dev <dennis@kobert.dev>
This commit is contained in:
@@ -371,11 +371,12 @@
|
||||
</LayoutRow>
|
||||
<LayoutRow class="list-area" scrollableY={true}>
|
||||
<LayoutCol class="list" bind:this={list} on:click={() => deselectAllLayers()} on:dragover={(e) => draggable && updateInsertLine(e)} on:dragend={() => draggable && drop()}>
|
||||
{#each layers as listing, index (String(listing.entry.id))}
|
||||
{#each layers as listing, index}
|
||||
<LayoutRow
|
||||
class="layer"
|
||||
classes={{
|
||||
selected: fakeHighlight !== undefined ? fakeHighlight === listing.entry.id : $nodeGraph.selected.includes(listing.entry.id),
|
||||
selected: fakeHighlight !== undefined ? fakeHighlight === listing.entry.id : listing.entry.selected,
|
||||
"in-selected-network": listing.entry.inSelectedNetwork,
|
||||
"insert-folder": (draggingData?.highlightFolder || false) && draggingData?.insertParentId === listing.entry.id,
|
||||
}}
|
||||
styles={{ "--layer-indent-levels": `${listing.entry.depth - 1}` }}
|
||||
@@ -497,8 +498,13 @@
|
||||
margin: 0 4px;
|
||||
padding-left: calc(var(--layer-indent-levels) * 16px);
|
||||
|
||||
// Dimming
|
||||
&.selected {
|
||||
background: var(--color-4-dimgray);
|
||||
background: #404040;
|
||||
|
||||
&.in-selected-network {
|
||||
background: var(--color-4-dimgray);
|
||||
}
|
||||
}
|
||||
|
||||
&.insert-folder {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import type { NodeGraphState } from "@graphite/state-providers/node-graph";
|
||||
import type { IconName } from "@graphite/utility-functions/icons";
|
||||
import type { Editor } from "@graphite/wasm-communication/editor";
|
||||
import type { Node } from "@graphite/wasm-communication/messages";
|
||||
import type { FrontendNodeWire, FrontendNodeType, FrontendNode, FrontendGraphInput, FrontendGraphOutput, FrontendGraphDataType, WirePath } from "@graphite/wasm-communication/messages";
|
||||
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
@@ -35,6 +36,7 @@
|
||||
let nodeWirePaths: WirePath[] = [];
|
||||
let searchTerm = "";
|
||||
|
||||
// TODO: Convert these arrays-of-arrays to a Map?
|
||||
let inputs: SVGSVGElement[][] = [];
|
||||
let outputs: SVGSVGElement[][] = [];
|
||||
let nodeElements: HTMLDivElement[] = [];
|
||||
@@ -46,7 +48,7 @@
|
||||
$: nodeCategories = buildNodeCategories($nodeGraph.nodeTypes, searchTerm);
|
||||
|
||||
$: (() => {
|
||||
if ($nodeGraph.contextMenuInformation?.contextMenuData == "CreateNode") {
|
||||
if ($nodeGraph.contextMenuInformation?.contextMenuData === "CreateNode") {
|
||||
setTimeout(() => nodeSearchInput?.focus(), 0);
|
||||
}
|
||||
})();
|
||||
@@ -107,49 +109,76 @@
|
||||
return [...maybeWirePathInProgress, ...nodeWirePaths];
|
||||
}
|
||||
|
||||
async function watchNodes(nodes: FrontendNode[]) {
|
||||
nodes.forEach((_, index) => {
|
||||
if (!inputs[index]) inputs[index] = [];
|
||||
if (!outputs[index]) outputs[index] = [];
|
||||
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 | undefined; nodeInput: SVGSVGElement | undefined } {
|
||||
const outputIndex = Number(wire.wireStartOutputIndex);
|
||||
const inputIndex = Number(wire.wireEndInputIndex);
|
||||
// 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;
|
||||
|
||||
const nodeOutputConnectors = outputs[$nodeGraph.nodes.findIndex((n) => n.id === wire.wireStart)];
|
||||
const nodeInputConnectors = inputs[$nodeGraph.nodes.findIndex((n) => n.id === wire.wireEnd)] || 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;
|
||||
|
||||
const nodeOutput = nodeOutputConnectors?.[outputIndex] as SVGSVGElement | undefined;
|
||||
const nodeInput = nodeInputConnectors?.[inputIndex] as SVGSVGElement | undefined;
|
||||
return { nodeOutput, nodeInput };
|
||||
}
|
||||
|
||||
function createWirePath(outputPort: SVGSVGElement, inputPort: SVGSVGElement, verticalOut: boolean, verticalIn: boolean, dashed: boolean): WirePath {
|
||||
const inputPortRect = inputPort.getBoundingClientRect();
|
||||
const outputPortRect = outputPort.getBoundingClientRect();
|
||||
|
||||
const pathString = buildWirePathString(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();
|
||||
|
||||
const wires = $nodeGraph.wires;
|
||||
nodeWirePaths = wires.flatMap((wire) => {
|
||||
const { nodeInput, nodeOutput } = resolveWire(wire);
|
||||
if (!nodeInput || !nodeOutput) return [];
|
||||
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 { nodeOutput, nodeInput } = resolveWire(wire);
|
||||
if (!nodeOutput || !nodeInput) return [];
|
||||
|
||||
const wireStart = $nodeGraph.nodes.find((n) => n.id === wire.wireStart)?.isLayer || false;
|
||||
const wireEnd = ($nodeGraph.nodes.find((n) => n.id === wire.wireEnd)?.isLayer && Number(wire.wireEndInputIndex) == 0) || false;
|
||||
const wireStartNode = $nodeGraph.nodes.get((wire.wireStart as Node).nodeId);
|
||||
const wireStart = wireStartNode?.isLayer || false;
|
||||
|
||||
return [createWirePath(nodeOutput, nodeInput.getBoundingClientRect(), wireStart, wireEnd, wire.dashed)];
|
||||
const wireEndNode = $nodeGraph.nodes.get((wire.wireEnd as Node).nodeId);
|
||||
const wireEnd = (wireEndNode?.isLayer && Number(wire.wireEnd.index) === 0) || false;
|
||||
|
||||
return [createWirePath(nodeOutput, nodeInput, wireStart, wireEnd, wire.dashed)];
|
||||
});
|
||||
}
|
||||
|
||||
onMount(refreshWires);
|
||||
|
||||
function nodeIcon(nodeName: string): IconName {
|
||||
function nodeIcon(icon?: string): IconName {
|
||||
if (!icon) return "NodeNodes";
|
||||
const iconMap: Record<string, IconName> = {
|
||||
Output: "NodeOutput",
|
||||
};
|
||||
return iconMap[nodeName] || "NodeNodes";
|
||||
return iconMap[icon] || "NodeNodes";
|
||||
}
|
||||
|
||||
function buildWirePathLocations(outputBounds: DOMRect, inputBounds: DOMRect, verticalOut: boolean, verticalIn: boolean): { x: number; y: number }[] {
|
||||
@@ -219,25 +248,13 @@
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function createWirePath(outputPort: SVGSVGElement, inputPort: SVGSVGElement | DOMRect, verticalOut: boolean, verticalIn: boolean, dashed: boolean): WirePath {
|
||||
const inputPortRect = inputPort instanceof DOMRect ? inputPort : inputPort.getBoundingClientRect();
|
||||
const outputPortRect = outputPort.getBoundingClientRect();
|
||||
|
||||
const pathString = buildWirePathString(outputPortRect, inputPortRect, verticalOut, verticalIn);
|
||||
const dataType = (outputPort.getAttribute("data-datatype") as FrontendGraphDataType) || "General";
|
||||
|
||||
return { pathString, dataType, thick: verticalIn && verticalOut, dashed };
|
||||
}
|
||||
|
||||
function toggleLayerDisplay(displayAsLayer: boolean, toggleId: bigint) {
|
||||
let node = $nodeGraph.nodes.find((node) => node.id === toggleId);
|
||||
if (node !== undefined) {
|
||||
editor.handle.setToNodeOrLayer(node.id, displayAsLayer);
|
||||
}
|
||||
let node = $nodeGraph.nodes.get(toggleId);
|
||||
if (node) editor.handle.setToNodeOrLayer(node.id, displayAsLayer);
|
||||
}
|
||||
|
||||
function canBeToggledBetweenNodeAndLayer(toggleDisplayAsLayerNodeId: bigint) {
|
||||
return $nodeGraph.nodes.find((node) => node.id === toggleDisplayAsLayerNodeId)?.canBeLayer || false;
|
||||
return $nodeGraph.nodes.get(toggleDisplayAsLayerNodeId)?.canBeLayer || false;
|
||||
}
|
||||
|
||||
function createNode(nodeType: string) {
|
||||
@@ -296,13 +313,52 @@
|
||||
return value.resolvedType ? `Resolved Data: ${value.resolvedType}` : `Unresolved Data: ${value.dataType}`;
|
||||
}
|
||||
|
||||
function connectedToText(output: FrontendGraphOutput): string {
|
||||
if (output.connected.length === 0) {
|
||||
function outputConnectedToText(output: FrontendGraphOutput): string {
|
||||
if (output.connectedTo.length === 0) {
|
||||
return "Connected to nothing";
|
||||
} else {
|
||||
return output.connected.map((nodeId, index) => `Connected to ${nodeId}, port index ${output.connectedIndex[index]}`).join("\n");
|
||||
return output.connectedTo
|
||||
.map((inputConnector) => {
|
||||
if ((inputConnector as Node).nodeId === undefined) {
|
||||
return `Connected to export index ${inputConnector.index}`;
|
||||
} else {
|
||||
return `Connected to ${(inputConnector as Node).nodeId}, port index ${inputConnector.index}`;
|
||||
}
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
}
|
||||
|
||||
function inputConnectedToText(input: FrontendGraphInput): string {
|
||||
if (input.connectedTo === undefined) {
|
||||
return "Connected to nothing";
|
||||
} else {
|
||||
if ((input.connectedTo as Node).nodeId === undefined) {
|
||||
return `Connected to import index ${input.connectedTo.index}`;
|
||||
} else {
|
||||
return `Connected to ${(input.connectedTo as Node).nodeId}, port index ${input.connectedTo.index}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function primaryOutputConnectedToLayer(node: FrontendNode): boolean {
|
||||
let firstConnectedNode = Array.from($nodeGraph.nodes.values()).find((n) =>
|
||||
node.primaryOutput?.connectedTo.some((connector) => {
|
||||
if ((connector as Node).nodeId === undefined) return false;
|
||||
if (connector.index !== 0n) return false;
|
||||
return n.id === (connector as Node).nodeId || false;
|
||||
}),
|
||||
);
|
||||
return firstConnectedNode?.isLayer || false;
|
||||
}
|
||||
|
||||
function primaryInputConnectedToLayer(node: FrontendNode): boolean {
|
||||
const connectedNode = Array.from($nodeGraph.nodes.values()).find((n) => {
|
||||
if ((node.primaryInput?.connectedTo as Node) === undefined) return false;
|
||||
return n.id === (node.primaryInput?.connectedTo as Node).nodeId;
|
||||
});
|
||||
return connectedNode?.isLayer || false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -375,20 +431,93 @@
|
||||
{/if}
|
||||
</LayoutCol>
|
||||
{/if}
|
||||
|
||||
<!-- Click target debug visualizations -->
|
||||
{#if $nodeGraph.clickTargets}
|
||||
<div class="click-targets" style:transform-origin={`0 0`} style:transform={`translate(${$nodeGraph.transform.x}px, ${$nodeGraph.transform.y}px) scale(${$nodeGraph.transform.scale})`}>
|
||||
<svg>
|
||||
{#each $nodeGraph.clickTargets.nodeClickTargets as pathString}
|
||||
<path class="node" d={pathString} />
|
||||
{/each}
|
||||
{#each $nodeGraph.clickTargets.layerClickTargets as pathString}
|
||||
<path class="layer" d={pathString} />
|
||||
{/each}
|
||||
{#each $nodeGraph.clickTargets.portClickTargets as pathString}
|
||||
<path class="port" d={pathString} />
|
||||
{/each}
|
||||
{#each $nodeGraph.clickTargets.visibilityClickTargets as pathString}
|
||||
<path class="visibility" d={pathString} />
|
||||
{/each}
|
||||
<path class="all-nodes-bounding-box" d={$nodeGraph.clickTargets.allNodesBoundingBox} />
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Node 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 }}
|
||||
<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 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}
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Import and Export ports -->
|
||||
<div class="imports-and-exports" style:transform-origin={`0 0`} style:transform={`translate(${$nodeGraph.transform.x}px, ${$nodeGraph.transform.y}px) scale(${$nodeGraph.transform.scale})`}>
|
||||
{#each $nodeGraph.imports as { outputMetadata, position }, index}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 8 8"
|
||||
class="port"
|
||||
data-port="output"
|
||||
data-datatype={outputMetadata.dataType}
|
||||
style:--data-color={`var(--color-data-${outputMetadata.dataType.toLowerCase()})`}
|
||||
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${outputConnectedToText(outputMetadata)}`}</title>
|
||||
{#if outputMetadata.connectedTo !== undefined}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
|
||||
{:else}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
|
||||
{/if}
|
||||
</svg>
|
||||
<p class="import-text" style:--offset-left={position.x / 24} style:--offset-top={position.y / 24}>{outputMetadata.name}</p>
|
||||
{/each}
|
||||
{#each $nodeGraph.exports as { inputMetadata, position }, index}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 8 8"
|
||||
class="port"
|
||||
data-port="input"
|
||||
data-datatype={inputMetadata.dataType}
|
||||
style:--data-color={`var(--color-data-${inputMetadata.dataType.toLowerCase()})`}
|
||||
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${inputConnectedToText(inputMetadata)}`}</title>
|
||||
{#if inputMetadata.connectedTo !== undefined}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
|
||||
{:else}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
|
||||
{/if}
|
||||
</svg>
|
||||
<p class="export-text" style:--offset-left={position.x / 24} style:--offset-top={position.y / 24}>{inputMetadata.name}</p>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Layers and nodes -->
|
||||
<div
|
||||
class="layers-and-nodes"
|
||||
@@ -397,22 +526,24 @@
|
||||
bind:this={nodesContainer}
|
||||
>
|
||||
<!-- Layers -->
|
||||
{#each $nodeGraph.nodes.flatMap((node, nodeIndex) => (node.isLayer ? [{ node, nodeIndex }] : [])) as { node, nodeIndex } (nodeIndex)}
|
||||
{#each Array.from($nodeGraph.nodes.values()).flatMap((node, nodeIndex) => (node.isLayer ? [{ 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}
|
||||
{@const layerChainWidth = $nodeGraph.chainWidths.get(node.id) || 0}
|
||||
<div
|
||||
class="layer"
|
||||
class:selected={$nodeGraph.selected.includes(node.id)}
|
||||
class:in-selected-network={$nodeGraph.inSelectedNetwork}
|
||||
class:previewed={node.previewed}
|
||||
class:disabled={!node.visible}
|
||||
style:--offset-left={(node.position?.x || 0) - 1}
|
||||
style:--offset-left={node.position?.x || 0}
|
||||
style:--offset-top={node.position?.y || 0}
|
||||
style:--clip-path-id={`url(#${clipPathId})`}
|
||||
style:--data-color={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()})`}
|
||||
style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`}
|
||||
style:--layer-area-width={layerAreaWidth}
|
||||
style:--node-chain-area-left-extension={node.exposedInputs.length === 0 ? 0 : 1.5}
|
||||
style:--node-chain-area-left-extension={layerChainWidth !== 0 ? layerChainWidth + 0.5 : 0}
|
||||
data-node={node.id}
|
||||
bind:this={nodeElements[nodeIndex]}
|
||||
>
|
||||
@@ -434,12 +565,12 @@
|
||||
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][0]}
|
||||
bind:this={outputs[nodeIndex + 1][0]}
|
||||
>
|
||||
<title>{`${dataTypeTooltip(node.primaryOutput)}\n${connectedToText(node.primaryOutput)}`}</title>
|
||||
{#if node.primaryOutput.connected.length > 0}
|
||||
<title>{`${dataTypeTooltip(node.primaryOutput)}\n${outputConnectedToText(node.primaryOutput)}`}</title>
|
||||
{#if node.primaryOutput.connectedTo.length > 0}
|
||||
<path d="M0,6.953l2.521,-1.694a2.649,2.649,0,0,1,2.959,0l2.52,1.694v5.047h-8z" fill="var(--data-color)" />
|
||||
{#if Number(node.primaryOutput?.connectedIndex) === 0 && $nodeGraph.nodes.find((n) => node.primaryOutput?.connected.includes(n.id))?.isLayer}
|
||||
{#if primaryOutputConnectedToLayer(node)}
|
||||
<path d="M0,-3.5h8v8l-2.521,-1.681a2.666,2.666,0,0,0,-2.959,0l-2.52,1.681z" fill="var(--data-color-dim)" />
|
||||
{/if}
|
||||
{:else}
|
||||
@@ -456,14 +587,14 @@
|
||||
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][0]}
|
||||
bind:this={inputs[nodeIndex + 1][0]}
|
||||
>
|
||||
{#if node.primaryInput}
|
||||
<title>{`${dataTypeTooltip(node.primaryInput)}\nConnected to ${node.primaryInput?.connected !== undefined ? node.primaryInput.connected : "nothing"}`}</title>
|
||||
<title>{`${dataTypeTooltip(node.primaryInput)}\n${inputConnectedToText(node.primaryInput)}`}</title>
|
||||
{/if}
|
||||
{#if node.primaryInput?.connected !== undefined}
|
||||
{#if node.primaryInput?.connectedTo !== undefined}
|
||||
<path d="M0,0H8V8L5.479,6.319a2.666,2.666,0,0,0-2.959,0L0,8Z" fill="var(--data-color)" />
|
||||
{#if $nodeGraph.nodes.find((n) => n.id === node.primaryInput?.connected)?.isLayer}
|
||||
{#if primaryInputConnectedToLayer(node)}
|
||||
<path d="M0,10.95l2.52,-1.69c0.89,-0.6,2.06,-0.6,2.96,0l2.52,1.69v5.05h-8v-5.05z" fill="var(--data-color-dim)" />
|
||||
{/if}
|
||||
{:else}
|
||||
@@ -482,10 +613,10 @@
|
||||
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]}
|
||||
bind:this={inputs[nodeIndex + 1][1]}
|
||||
>
|
||||
<title>{`${dataTypeTooltip(stackDataInput)}\nConnected to ${stackDataInput.connected !== undefined ? stackDataInput.connected : "nothing"}`}</title>
|
||||
{#if stackDataInput.connected !== undefined}
|
||||
<title>{`${dataTypeTooltip(stackDataInput)}\n${inputConnectedToText(stackDataInput)}`}</title>
|
||||
{#if stackDataInput.connectedTo !== undefined}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
|
||||
{:else}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
|
||||
@@ -496,7 +627,7 @@
|
||||
<div class="details">
|
||||
<!-- TODO: Allow the user to edit the name, just like in the Layers panel -->
|
||||
<span title={editor.handle.inDevelopmentMode() ? `Node ID: ${node.id}` : undefined}>
|
||||
{node.alias}
|
||||
{node.displayName}
|
||||
</span>
|
||||
</div>
|
||||
<IconButton
|
||||
@@ -514,14 +645,32 @@
|
||||
<defs>
|
||||
<clipPath id={clipPathId}>
|
||||
<!-- Keep this equation in sync with the equivalent one in the CSS rule for `.layer { width: ... }` below -->
|
||||
<path clip-rule="evenodd" d={layerBorderMask(24 * layerAreaWidth - 12, node.exposedInputs.length === 0 ? 0 : 36)} />
|
||||
<path clip-rule="evenodd" d={layerBorderMask(24 * layerAreaWidth - 12, layerChainWidth ? (0.5 + layerChainWidth) * 24 : 0)} />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<!-- 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}
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Nodes -->
|
||||
{#each $nodeGraph.nodes.flatMap((node, nodeIndex) => (node.isLayer ? [] : [{ node, nodeIndex }])) as { node, nodeIndex } (nodeIndex)}
|
||||
{#each Array.from($nodeGraph.nodes.values()).flatMap((node, nodeIndex) => (node.isLayer ? [] : [{ node, nodeIndex }])) as { node, nodeIndex } (nodeIndex)}
|
||||
{@const exposedInputsOutputs = [...node.exposedInputs, ...node.exposedOutputs]}
|
||||
{@const clipPathId = String(Math.random()).substring(2)}
|
||||
<div
|
||||
@@ -542,14 +691,14 @@
|
||||
<span class="node-error hover" transition:fade={FADE_TRANSITION} data-node-error>{node.errors}</span>
|
||||
{/if}
|
||||
<!-- Primary row -->
|
||||
<div class="primary" class:no-parameter-section={exposedInputsOutputs.length === 0}>
|
||||
<IconLabel icon={nodeIcon(node.name)} />
|
||||
<div class="primary" class:in-selected-network={$nodeGraph.inSelectedNetwork} class:no-parameter-section={exposedInputsOutputs.length === 0}>
|
||||
<IconLabel icon={nodeIcon(node.reference)} />
|
||||
<!-- TODO: Allow the user to edit the name, just like in the Layers panel -->
|
||||
<TextLabel tooltip={editor.handle.inDevelopmentMode() ? `Node ID: ${node.id}` : undefined}>{node.alias || node.name}</TextLabel>
|
||||
<TextLabel tooltip={editor.handle.inDevelopmentMode() ? `Node ID: ${node.id}` : undefined}>{node.displayName}</TextLabel>
|
||||
</div>
|
||||
<!-- Parameter rows -->
|
||||
{#if exposedInputsOutputs.length > 0}
|
||||
<div class="parameters">
|
||||
<div class="parameters" class:in-selected-network={$nodeGraph.inSelectedNetwork}>
|
||||
{#each exposedInputsOutputs as parameter, index}
|
||||
<div class={`parameter expanded ${index < node.exposedInputs.length ? "input" : "output"}`}>
|
||||
<TextLabel tooltip={parameter.name}>{parameter.name}</TextLabel>
|
||||
@@ -568,10 +717,10 @@
|
||||
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][0]}
|
||||
bind:this={inputs[nodeIndex + 1][0]}
|
||||
>
|
||||
<title>{`${dataTypeTooltip(node.primaryInput)}\nConnected to ${node.primaryInput.connected !== undefined ? node.primaryInput.connected : "nothing"}`}</title>
|
||||
{#if node.primaryInput.connected !== undefined}
|
||||
<title>{`${dataTypeTooltip(node.primaryInput)}\n${inputConnectedToText(node.primaryInput)}`}</title>
|
||||
{#if node.primaryInput.connectedTo !== undefined}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
|
||||
{:else}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
|
||||
@@ -588,10 +737,10 @@
|
||||
data-datatype={parameter.dataType}
|
||||
style:--data-color={`var(--color-data-${parameter.dataType.toLowerCase()})`}
|
||||
style:--data-color-dim={`var(--color-data-${parameter.dataType.toLowerCase()}-dim)`}
|
||||
bind:this={inputs[nodeIndex][index + (node.primaryInput ? 1 : 0)]}
|
||||
bind:this={inputs[nodeIndex + 1][index + (node.primaryInput ? 1 : 0)]}
|
||||
>
|
||||
<title>{`${dataTypeTooltip(parameter)}\nConnected to ${parameter.connected !== undefined ? parameter.connected : "nothing"}`}</title>
|
||||
{#if parameter.connected !== undefined}
|
||||
<title>{`${dataTypeTooltip(parameter)}\n${inputConnectedToText(parameter)}`}</title>
|
||||
{#if parameter.connectedTo !== undefined}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
|
||||
{:else}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
|
||||
@@ -611,10 +760,10 @@
|
||||
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][0]}
|
||||
bind:this={outputs[nodeIndex + 1][0]}
|
||||
>
|
||||
<title>{`${dataTypeTooltip(node.primaryOutput)}\n${connectedToText(node.primaryOutput)}`}</title>
|
||||
{#if node.primaryOutput.connected !== undefined}
|
||||
<title>{`${dataTypeTooltip(node.primaryOutput)}\n${outputConnectedToText(node.primaryOutput)}`}</title>
|
||||
{#if node.primaryOutput.connectedTo !== undefined}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
|
||||
{:else}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
|
||||
@@ -630,10 +779,10 @@
|
||||
data-datatype={parameter.dataType}
|
||||
style:--data-color={`var(--color-data-${parameter.dataType.toLowerCase()})`}
|
||||
style:--data-color-dim={`var(--color-data-${parameter.dataType.toLowerCase()}-dim)`}
|
||||
bind:this={outputs[nodeIndex][outputIndex + (node.primaryOutput ? 1 : 0)]}
|
||||
bind:this={outputs[nodeIndex + 1][outputIndex + (node.primaryOutput ? 1 : 0)]}
|
||||
>
|
||||
<title>{`${dataTypeTooltip(parameter)}\n${connectedToText(parameter)}`}</title>
|
||||
{#if parameter.connected !== undefined}
|
||||
<title>{`${dataTypeTooltip(parameter)}\n${outputConnectedToText(parameter)}`}</title>
|
||||
{#if parameter.connectedTo !== undefined}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
|
||||
{:else}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
|
||||
@@ -768,6 +917,42 @@
|
||||
}
|
||||
}
|
||||
|
||||
.click-targets {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 10;
|
||||
|
||||
svg {
|
||||
overflow: visible;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
stroke-width: 1;
|
||||
fill: none;
|
||||
|
||||
.layer {
|
||||
stroke: yellow;
|
||||
}
|
||||
|
||||
.node {
|
||||
stroke: blue;
|
||||
}
|
||||
|
||||
.port {
|
||||
stroke: green;
|
||||
}
|
||||
|
||||
.visibility {
|
||||
stroke: red;
|
||||
}
|
||||
|
||||
.all-nodes-bounding-box {
|
||||
stroke: purple;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wires {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
@@ -788,6 +973,40 @@
|
||||
}
|
||||
}
|
||||
|
||||
.imports-and-exports {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.port {
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-top: 4px;
|
||||
margin-left: 5px;
|
||||
top: calc(var(--offset-top) * 24px);
|
||||
left: calc(var(--offset-left) * 24px);
|
||||
}
|
||||
|
||||
.export-text {
|
||||
position: absolute;
|
||||
margin-top: 0;
|
||||
margin-left: 20px;
|
||||
top: calc(var(--offset-top) * 24px);
|
||||
left: calc(var(--offset-left) * 24px);
|
||||
}
|
||||
|
||||
.import-text {
|
||||
position: absolute;
|
||||
text-align: right;
|
||||
top: calc(var(--offset-top) * 24px);
|
||||
left: calc(var(--offset-left) * 24px);
|
||||
margin-top: 0;
|
||||
margin-left: calc(-100px - 2px);
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.layers-and-nodes {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
@@ -930,10 +1149,10 @@
|
||||
border-radius: 8px;
|
||||
--extra-width-to-reach-grid-multiple: 8px;
|
||||
--node-chain-area-left-extension: 0;
|
||||
// Keep this equation in sync with the equivalent one in the Svelte template `<clipPath><path d="layerBorderMask(...)" /></clipPath>` above
|
||||
width: calc(24px * var(--layer-area-width) - 12px);
|
||||
// Keep this equation in sync with the equivalent one in the Svelte template `<clipPath><path d="layerBorderMask(...)" /></clipPath>` above, as well as the `left` port offset CSS rule above in `.ports.input` above.
|
||||
width: calc((var(--layer-area-width) - 0.5) * 24px);
|
||||
padding-left: calc(var(--node-chain-area-left-extension) * 24px);
|
||||
margin-left: calc((1.5 - var(--node-chain-area-left-extension)) * 24px);
|
||||
margin-left: calc((0.5 - var(--node-chain-area-left-extension)) * 24px);
|
||||
|
||||
&::after {
|
||||
border: 1px solid var(--color-5-dullgray);
|
||||
@@ -943,6 +1162,10 @@
|
||||
&.selected {
|
||||
// This is the result of blending `rgba(255, 255, 255, 0.1)` over `rgba(0, 0, 0, 0.33)`
|
||||
background: rgba(66, 66, 66, 0.4);
|
||||
|
||||
&.in-selected-network {
|
||||
background: rgba(80, 80, 80, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
@@ -1004,6 +1227,10 @@
|
||||
right: -12px;
|
||||
}
|
||||
|
||||
.input.ports {
|
||||
left: calc(-3px + var(--node-chain-area-left-extension) * 24px - 36px);
|
||||
}
|
||||
|
||||
.visibility,
|
||||
.input.ports,
|
||||
.input.ports .port {
|
||||
@@ -1032,10 +1259,18 @@
|
||||
&.selected {
|
||||
.primary {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
|
||||
&.in-selected-network {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.parameters {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
|
||||
&.in-selected-network {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
Layers,
|
||||
Properties,
|
||||
};
|
||||
type PanelTypes = keyof typeof PANEL_COMPONENTS;
|
||||
type PanelType = keyof typeof PANEL_COMPONENTS;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -33,7 +33,7 @@
|
||||
export let tabCloseButtons = false;
|
||||
export let tabLabels: { name: string; tooltip?: string }[];
|
||||
export let tabActiveIndex: number;
|
||||
export let panelType: PanelTypes | undefined = undefined;
|
||||
export let panelType: PanelType | undefined = undefined;
|
||||
export let clickAction: ((index: number) => void) | undefined = undefined;
|
||||
export let closeAction: ((index: number) => void) | undefined = undefined;
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<LayoutCol class="panel">
|
||||
<LayoutCol class="panel" on:pointerdown={() => panelType && editor.handle.setActivePanel(panelType)}>
|
||||
<LayoutRow class="tab-bar" classes={{ "min-widths": tabMinWidths }}>
|
||||
<LayoutRow class="tab-group" scrollableX={true}>
|
||||
{#each tabLabels as tabLabel, tabIndex}
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
import { type Editor } from "@graphite/wasm-communication/editor";
|
||||
import type { FrontendGraphOutput, FrontendGraphInput } from "@graphite/wasm-communication/messages";
|
||||
import {
|
||||
type Box,
|
||||
type FrontendClickTargets,
|
||||
type ContextMenuInformation,
|
||||
type FrontendNode,
|
||||
type FrontendNodeWire as FrontendNodeWire,
|
||||
type FrontendNodeType,
|
||||
type WirePath,
|
||||
UpdateBox,
|
||||
UpdateClickTargets,
|
||||
UpdateContextMenuInformation,
|
||||
UpdateInSelectedNetwork,
|
||||
UpdateImportsExports,
|
||||
UpdateLayerWidths,
|
||||
UpdateNodeGraph,
|
||||
UpdateNodeGraphSelection,
|
||||
@@ -24,9 +29,13 @@ import {
|
||||
export function createNodeGraphState(editor: Editor) {
|
||||
const { subscribe, update } = writable({
|
||||
box: undefined as Box | undefined,
|
||||
clickTargets: undefined as FrontendClickTargets | undefined,
|
||||
contextMenuInformation: undefined as ContextMenuInformation | undefined,
|
||||
layerWidths: new Map<bigint, number>(),
|
||||
nodes: [] as FrontendNode[],
|
||||
chainWidths: new Map<bigint, number>(),
|
||||
imports: [] as { outputMetadata: FrontendGraphOutput; position: { x: number; y: number } }[],
|
||||
exports: [] as { inputMetadata: FrontendGraphInput; position: { x: number; y: number } }[],
|
||||
nodes: new Map<bigint, FrontendNode>(),
|
||||
wires: [] as FrontendNodeWire[],
|
||||
wirePathInProgress: undefined as WirePath | undefined,
|
||||
nodeTypes: [] as FrontendNodeType[],
|
||||
@@ -34,6 +43,7 @@ export function createNodeGraphState(editor: Editor) {
|
||||
thumbnails: new Map<bigint, string>(),
|
||||
selected: [] as bigint[],
|
||||
transform: { scale: 1, x: 0, y: 0 },
|
||||
inSelectedNetwork: true,
|
||||
});
|
||||
|
||||
// Set up message subscriptions on creation
|
||||
@@ -43,22 +53,45 @@ export function createNodeGraphState(editor: Editor) {
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateClickTargets, (UpdateClickTargets) => {
|
||||
update((state) => {
|
||||
state.clickTargets = UpdateClickTargets.clickTargets;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateContextMenuInformation, (updateContextMenuInformation) => {
|
||||
update((state) => {
|
||||
state.contextMenuInformation = updateContextMenuInformation.contextMenuInformation;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateImportsExports, (updateImportsExports) => {
|
||||
update((state) => {
|
||||
state.imports = updateImportsExports.imports;
|
||||
state.exports = updateImportsExports.exports;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateInSelectedNetwork, (updateInSelectedNetwork) => {
|
||||
update((state) => {
|
||||
state.inSelectedNetwork = updateInSelectedNetwork.inSelectedNetwork;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateLayerWidths, (updateLayerWidths) => {
|
||||
update((state) => {
|
||||
state.layerWidths = updateLayerWidths.layerWidths;
|
||||
state.chainWidths = updateLayerWidths.chainWidths;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
// TODO: Add a way to only update the nodes that have changed
|
||||
editor.subscriptions.subscribeJsMessage(UpdateNodeGraph, (updateNodeGraph) => {
|
||||
update((state) => {
|
||||
state.nodes = updateNodeGraph.nodes;
|
||||
state.nodes.clear();
|
||||
updateNodeGraph.nodes.forEach((node) => {
|
||||
state.nodes.set(node.id, node);
|
||||
});
|
||||
state.wires = updateNodeGraph.wires;
|
||||
return state;
|
||||
});
|
||||
|
||||
@@ -12,6 +12,29 @@ export class JsMessage {
|
||||
}
|
||||
|
||||
const TupleToVec2 = Transform(({ value }: { value: [number, number] | undefined }) => (value === undefined ? undefined : { x: value[0], y: value[1] }));
|
||||
const ImportsToVec2Array = Transform(({ obj }) => {
|
||||
const imports: { outputMetadata: FrontendGraphOutput; position: XY }[] = [];
|
||||
obj.imports.forEach(([outputMetadata, x, y]: [FrontendGraphOutput, number, number]) => {
|
||||
outputMetadata.connectedTo = outputMetadata.connectedTo.map((connector: any) => {
|
||||
if (connector.export !== undefined) return { index: connector.export.index };
|
||||
return { nodeId: connector.node.nodeId, index: connector.node.inputIndex };
|
||||
});
|
||||
imports.push({ outputMetadata, position: { x, y } });
|
||||
});
|
||||
return imports;
|
||||
});
|
||||
const ExportsToVec2Array = Transform(({ obj }) => {
|
||||
const exports: { inputMetadata: FrontendGraphInput; position: XY }[] = [];
|
||||
obj.exports.forEach(([inputMetadata, x, y]: [FrontendGraphInput, number, number]) => {
|
||||
inputMetadata.connectedTo = ((connectedTo: any) => {
|
||||
if (connectedTo?.import !== undefined) return { index: connectedTo?.import.index };
|
||||
return { nodeId: connectedTo?.node.nodeId, index: connectedTo?.node.outputIndex };
|
||||
})(inputMetadata.connectedTo);
|
||||
exports.push({ inputMetadata, position: { x, y } });
|
||||
});
|
||||
return exports;
|
||||
});
|
||||
|
||||
// const BigIntTupleToVec2 = Transform(({ value }: { value: [bigint, bigint] | undefined }) => (value === undefined ? undefined : { x: Number(value[0]), y: Number(value[1]) }));
|
||||
|
||||
export type XY = { x: number; y: number };
|
||||
@@ -29,6 +52,10 @@ export class UpdateBox extends JsMessage {
|
||||
readonly box!: Box | undefined;
|
||||
}
|
||||
|
||||
export class UpdateClickTargets extends JsMessage {
|
||||
readonly clickTargets!: FrontendClickTargets | undefined;
|
||||
}
|
||||
|
||||
const ContextTupleToVec2 = Transform((data) => {
|
||||
if (data.obj.contextMenuInformation === undefined) return undefined;
|
||||
const contextMenuCoordinates = { x: data.obj.contextMenuInformation.contextMenuCoordinates[0], y: data.obj.contextMenuInformation.contextMenuCoordinates[1] };
|
||||
@@ -43,11 +70,27 @@ export class UpdateContextMenuInformation extends JsMessage {
|
||||
@ContextTupleToVec2
|
||||
readonly contextMenuInformation!: ContextMenuInformation | undefined;
|
||||
}
|
||||
|
||||
export class UpdateImportsExports extends JsMessage {
|
||||
@ImportsToVec2Array
|
||||
readonly imports!: { outputMetadata: FrontendGraphOutput; position: XY }[];
|
||||
|
||||
@ExportsToVec2Array
|
||||
readonly exports!: { inputMetadata: FrontendGraphInput; position: XY }[];
|
||||
}
|
||||
|
||||
export class UpdateInSelectedNetwork extends JsMessage {
|
||||
readonly inSelectedNetwork!: boolean;
|
||||
}
|
||||
|
||||
const LayerWidths = Transform(({ obj }) => obj.layerWidths);
|
||||
const ChainWidths = Transform(({ obj }) => obj.chainWidths);
|
||||
|
||||
export class UpdateLayerWidths extends JsMessage {
|
||||
@LayerWidths
|
||||
readonly layerWidths!: Map<bigint, number>;
|
||||
@ChainWidths
|
||||
readonly chainWidths!: Map<bigint, number>;
|
||||
}
|
||||
|
||||
export class UpdateNodeGraph extends JsMessage {
|
||||
@@ -123,6 +166,14 @@ export class Box {
|
||||
readonly endY!: number;
|
||||
}
|
||||
|
||||
export type FrontendClickTargets = {
|
||||
readonly nodeClickTargets: string[];
|
||||
readonly layerClickTargets: string[];
|
||||
readonly portClickTargets: string[];
|
||||
readonly visibilityClickTargets: string[];
|
||||
readonly allNodesBoundingBox: string;
|
||||
};
|
||||
|
||||
export type ContextMenuInformation = {
|
||||
contextMenuCoordinates: XY;
|
||||
|
||||
@@ -131,6 +182,37 @@ export type ContextMenuInformation = {
|
||||
|
||||
export type FrontendGraphDataType = "General" | "Raster" | "VectorData" | "Number" | "Graphic" | "Artboard";
|
||||
|
||||
export class Node {
|
||||
readonly nodeId!: bigint;
|
||||
readonly index!: bigint;
|
||||
}
|
||||
|
||||
export class Export {
|
||||
readonly index!: bigint;
|
||||
}
|
||||
|
||||
export class Import {
|
||||
readonly index!: bigint;
|
||||
}
|
||||
|
||||
export type OutputConnector = Node | Import;
|
||||
|
||||
export type InputConnector = Node | Export;
|
||||
|
||||
const CreateOutputConnectorOptional = Transform(({ obj }) => {
|
||||
if (obj.connectedTo?.export !== undefined) {
|
||||
return { index: obj.connectedTo?.export };
|
||||
} else if (obj.connectedTo?.import !== undefined) {
|
||||
return { index: obj.connectedTo?.import };
|
||||
} else {
|
||||
if (obj.connectedTo?.node.inputIndex !== undefined) {
|
||||
return { nodeId: obj.connectedTo?.node.nodeId, index: obj.connectedTo?.node.inputIndex };
|
||||
} else {
|
||||
return { nodeId: obj.connectedTo?.node.nodeId, index: obj.connectedTo?.node.outputIndex };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export class FrontendGraphInput {
|
||||
readonly dataType!: FrontendGraphDataType;
|
||||
|
||||
@@ -138,9 +220,28 @@ export class FrontendGraphInput {
|
||||
|
||||
readonly resolvedType!: string | undefined;
|
||||
|
||||
readonly connected!: bigint | undefined;
|
||||
@CreateOutputConnectorOptional
|
||||
readonly connectedTo!: OutputConnector | undefined;
|
||||
}
|
||||
|
||||
const CreateInputConnectorArray = Transform(({ obj }) => {
|
||||
const newInputConnectors: InputConnector[] = [];
|
||||
obj.connectedTo.forEach((connector: any) => {
|
||||
if (connector.export !== undefined) {
|
||||
newInputConnectors.push({ index: connector.export });
|
||||
} else if (connector.import !== undefined) {
|
||||
newInputConnectors.push({ index: connector.import });
|
||||
} else {
|
||||
if (connector.node.inputIndex !== undefined) {
|
||||
newInputConnectors.push({ nodeId: connector.node.nodeId, index: connector.node.inputIndex });
|
||||
} else {
|
||||
newInputConnectors.push({ nodeId: connector.node.nodeId, index: connector.node.outputIndex });
|
||||
}
|
||||
}
|
||||
});
|
||||
return newInputConnectors;
|
||||
});
|
||||
|
||||
export class FrontendGraphOutput {
|
||||
readonly dataType!: FrontendGraphDataType;
|
||||
|
||||
@@ -148,9 +249,8 @@ export class FrontendGraphOutput {
|
||||
|
||||
readonly resolvedType!: string | undefined;
|
||||
|
||||
readonly connected!: bigint[];
|
||||
|
||||
readonly connectedIndex!: bigint[];
|
||||
@CreateInputConnectorArray
|
||||
readonly connectedTo!: InputConnector[];
|
||||
}
|
||||
|
||||
export class FrontendNode {
|
||||
@@ -160,22 +260,26 @@ export class FrontendNode {
|
||||
|
||||
readonly id!: bigint;
|
||||
|
||||
readonly alias!: string;
|
||||
readonly reference!: string | undefined;
|
||||
|
||||
readonly name!: string;
|
||||
readonly displayName!: string;
|
||||
|
||||
@Type(() => FrontendGraphInput)
|
||||
readonly primaryInput!: FrontendGraphInput | undefined;
|
||||
|
||||
@Type(() => FrontendGraphInput)
|
||||
readonly exposedInputs!: FrontendGraphInput[];
|
||||
|
||||
@Type(() => FrontendGraphOutput)
|
||||
readonly primaryOutput!: FrontendGraphOutput | undefined;
|
||||
|
||||
@Type(() => FrontendGraphOutput)
|
||||
readonly exposedOutputs!: FrontendGraphOutput[];
|
||||
|
||||
@TupleToVec2
|
||||
readonly position!: XY | undefined;
|
||||
|
||||
//TODO: Store field for the width of the left node chain
|
||||
// TODO: Store field for the width of the left node chain
|
||||
|
||||
readonly previewed!: boolean;
|
||||
|
||||
@@ -188,14 +292,40 @@ 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 {
|
||||
readonly wireStart!: bigint;
|
||||
@CreateOutputConnector
|
||||
readonly wireStart!: OutputConnector;
|
||||
|
||||
readonly wireStartOutputIndex!: bigint;
|
||||
|
||||
readonly wireEnd!: bigint;
|
||||
|
||||
readonly wireEndInputIndex!: bigint;
|
||||
@CreateInputConnector
|
||||
readonly wireEnd!: InputConnector;
|
||||
|
||||
readonly dashed!: boolean;
|
||||
}
|
||||
@@ -747,6 +877,10 @@ export class LayerPanelEntry {
|
||||
parentsUnlocked!: boolean;
|
||||
|
||||
parentId!: bigint | undefined;
|
||||
|
||||
selected!: boolean;
|
||||
|
||||
inSelectedNetwork!: boolean;
|
||||
}
|
||||
|
||||
export class DisplayDialogDismiss extends JsMessage {}
|
||||
@@ -1351,7 +1485,12 @@ function createLayoutGroup(layoutGroup: any): LayoutGroup {
|
||||
}
|
||||
|
||||
if (layoutGroup.section) {
|
||||
const result: WidgetSection = { name: layoutGroup.section.name, visible: layoutGroup.section.visible, id: layoutGroup.section.id, layout: layoutGroup.section.layout.map(createLayoutGroup) };
|
||||
const result: WidgetSection = {
|
||||
name: layoutGroup.section.name,
|
||||
visible: layoutGroup.section.visible,
|
||||
id: layoutGroup.section.id,
|
||||
layout: layoutGroup.section.layout.map(createLayoutGroup),
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1448,7 +1587,10 @@ export const messageMakers: Record<string, MessageMaker> = {
|
||||
TriggerVisitLink,
|
||||
UpdateActiveDocument,
|
||||
UpdateBox,
|
||||
UpdateClickTargets,
|
||||
UpdateContextMenuInformation,
|
||||
UpdateInSelectedNetwork,
|
||||
UpdateImportsExports,
|
||||
UpdateLayerWidths,
|
||||
UpdateDialogButtons,
|
||||
UpdateDialogColumn1,
|
||||
|
||||
Reference in New Issue
Block a user