Complete nodes and wires

This commit is contained in:
Adam
2025-09-08 19:37:51 -07:00
parent cb1fb756b1
commit a25a23e61e
3 changed files with 310 additions and 370 deletions
+209 -332
View File
@@ -223,338 +223,7 @@
}
</script>
{#if $nodeGraph.shouldRenderSvelteNodes}
<div
class="graph-background"
style:--grid-spacing={`${gridSpacing}px`}
style:--grid-offset-x={`${$nodeGraph.transform.x}px`}
style:--grid-offset-y={`${$nodeGraph.transform.y}px`}
style:--grid-dot-radius={`${gridDotRadius}px`}
style:--fade-artwork={`${$nodeGraph.opacity}%`}
/>
<div
class="graph-background"
style:--grid-spacing={`${gridSpacing}px`}
style:--grid-offset-x={`${$nodeGraph.transform.x}px`}
style:--grid-offset-y={`${$nodeGraph.transform.y}px`}
style:--grid-dot-radius={`${gridDotRadius}px`}
style:--fade-artwork={`${$nodeGraph.opacity}%`}
/>
<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})`}>
{#each Array.from($nodeGraph.nodesToRender) as [nodeId, nodeToRender]}
{#if nodeToRender.nodeOrLayer.layer !== undefined}
{@const nodeMetadata = nodeToRender.metadata}
{@const layer = nodeToRender.nodeOrLayer.layer}
{@const clipPathId = String(Math.random()).substring(2)}
{@const layerAreaWidth = $nodeGraph.layerWidths.get(nodeToRender.metadata.nodeId) || 8}
{@const layerChainWidth = layer.chainWidth !== 0 ? layer.chainWidth + 0.5 : 0}
{@const description = (nodeMetadata.reference && $nodeGraph.nodeDescriptions.get(nodeMetadata.reference)) || undefined}
<div
class="layer"
class:selected={nodeMetadata.selected}
class:in-selected-network={$nodeGraph.inSelectedNetwork}
class:previewed={$nodeGraph.previewedNode === nodeId}
class:disabled={!nodeMetadata.visible}
style:--offset-left={layer.position.x}
style:--offset-top={layer.position.y}
style:--clip-path-id={`url(#${clipPathId})`}
style:--data-color={`var(--color-data-${layer.output.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${layer.output.dataType.toLowerCase()}-dim)`}
style:--layer-area-width={layerAreaWidth}
style:--node-chain-area-left-extension={layerChainWidth}
title={`${nodeMetadata.displayName}\n\n${description || ""}`.trim() + (editor.handle.inDevelopmentMode() ? `\n\nNode ID: ${nodeId}` : "")}
>
{#if nodeMetadata.errors}
<span class="node-error faded" transition:fade={FADE_TRANSITION} title="" data-node-error>{layer.errors}</span>
<span class="node-error hover" transition:fade={FADE_TRANSITION} title="" data-node-error>{layer.errors}</span>
{/if}
<div class="thumbnail">
{#if $nodeGraph.thumbnails.has(nodeId)}
{@html $nodeGraph.thumbnails.get(nodeId)}
{/if}
<!-- Layer stacking top output -->
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 8 12"
class="connector top"
style:--data-color={`var(--color-data-${layer.output.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${layer.output.dataType.toLowerCase()}-dim)`}
>
<title>{outputTooltip(layer.output)}</title>
<path d="M0,6.953l2.521,-1.694a2.649,2.649,0,0,1,2.959,0l2.52,1.694v5.047h-8z" fill={layer.output.connectedTo.length > 0 ? "var(--data-color)" : "var(--data-color-dim)"} />
{#if layer.output.connectedTo.length > 0 && layer.primaryOutputConnectedToLayer}
<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}
</svg>
<!-- Layer stacking bottom input -->
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 8 12"
class="connector bottom"
style:--data-color={`var(--color-data-${layer.bottomInput.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${layer.bottomInput.dataType.toLowerCase()}-dim)`}
>
{#if layer.bottomInput}
<title>{inputTooltip(layer.bottomInput)}</title>
{/if}
{#if layer.bottomInput?.connectedToNode !== undefined}
<path d="M0,0H8V8L5.479,6.319a2.666,2.666,0,0,0-2.959,0L0,8Z" fill="var(--data-color)" />
{#if layer.primaryInputConnectedToLayer}
<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}
<path d="M0,0H8V8L5.479,6.319a2.666,2.666,0,0,0-2.959,0L0,8Z" fill="var(--data-color-dim)" />
{/if}
</svg>
</div>
<!-- Layer input connector (from left) -->
{#if layer.sideInput}
<div class="input connectors">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 8 8"
class="connector"
style:--data-color={`var(--color-data-${layer.sideInput.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${layer.sideInput.dataType.toLowerCase()}-dim)`}
>
<title>{inputTooltip(layer.sideInput)}</title>
<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={layer.sideInput.connectedToNode !== undefined ? "var(--data-color)" : "var(--data-color-dim)"}
/>
</svg>
</div>
{/if}
<div class="details">
<!-- TODO: Allow the user to edit the name, just like in the Layers panel -->
<TextLabel>{nodeMetadata.displayName}</TextLabel>
</div>
<div class="solo-drag-grip" title="Drag only this layer without pushing others outside the stack"></div>
<IconButton
class={"visibility"}
size={24}
icon={nodeMetadata.visible ? "EyeVisible" : "EyeHidden"}
action={() => {
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
}}
tooltip={nodeMetadata.visible ? "Visible" : "Hidden"}
/>
<svg class="border-mask" width="0" height="0">
<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, layerChainWidth * 24, layer.layerHasLeftBorderGap)} />
</clipPath>
</defs>
</svg>
</div>
{/if}
{/each}
{#each Array.from($nodeGraph.nodesToRender) as [_, nodeToRender]}
{#each nodeToRender.wires as [wire, thick, dataType]}
<svg class="wire">
<path d={wire} style:--data-line-width={`${thick ? 8 : 2}px`} style:--data-color-dim={`var(--color-data-${dataType.toLowerCase()}-dim)`} style:--data-dasharray={"3,0"} />
</svg>
{/each}
{/each}
{#each Array.from($nodeGraph.nodesToRender) as [nodeId, nodeToRender]}
{#if nodeToRender.nodeOrLayer.node !== undefined && $nodeGraph.visibleNodes.has(nodeId)}
{@const nodeMetadata = nodeToRender.metadata}
{@const node = nodeToRender.nodeOrLayer.node}
{@const exposedInputsOutputs = collectExposedInputsOutputs(node.inputs, node.outputs)}
{@const clipPathId = String(Math.random()).substring(2)}
{@const description = (nodeMetadata.reference && $nodeGraph.nodeDescriptions.get(nodeMetadata.reference)) || undefined}
<div
class="node"
class:selected={nodeMetadata.selected}
class:previewed={$nodeGraph.previewedNode == nodeId}
class:disabled={!nodeMetadata.visible}
style:--offset-left={node.position.x}
style:--offset-top={node.position.y}
style:--clip-path-id={`url(#${clipPathId})`}
style:--data-color={`var(--color-data-${(node.outputs[0]?.dataType || "General").toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${(node.outputs[0]?.dataType || "General").toLowerCase()}-dim)`}
title={`${nodeMetadata.displayName}\n\n${description || ""}`.trim() + (editor.handle.inDevelopmentMode() ? `\n\nNode ID: ${nodeId}` : "")}
>
{#if nodeMetadata.errors}
<span class="node-error faded" transition:fade={FADE_TRANSITION} title="" data-node-error>{node.errors}</span>
<span class="node-error hover" transition:fade={FADE_TRANSITION} title="" data-node-error>{node.errors}</span>
{/if}
<!-- Primary row -->
<div class="primary" class:in-selected-network={$nodeGraph.inSelectedNetwork} class:no-secondary-section={exposedInputsOutputs.length === 0}>
<IconLabel icon={nodeIcon(nodeMetadata.reference)} />
<!-- TODO: Allow the user to edit the name, just like in the Layers panel -->
<TextLabel>{nodeMetadata.displayName}</TextLabel>
</div>
<!-- Secondary rows -->
{#if exposedInputsOutputs.length > 0}
<div class="secondary" class:in-selected-network={$nodeGraph.inSelectedNetwork}>
{#each exposedInputsOutputs as [input, output]}
<div class={`secondary-row expanded ${input ? "input" : output ? "output" : ""}`}>
<TextLabel tooltip={(input ? `${input.name}\n\n${input.description}` : output ? `${output.name}\n\n${output.description}` : "").trim()}>
{input?.name ?? output?.name ?? ""}
</TextLabel>
</div>
{/each}
</div>
{/if}
<!-- Input connectors -->
<div class="input connectors">
{#each node.inputs as input}
{#if input !== undefined}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 8 8"
class="connector"
style:--data-color={`var(--color-data-${input.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${input.dataType.toLowerCase()}-dim)`}
>
<title>{inputTooltip(input)}</title>
<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${input.connectedToString === "nothing" ? "-dim" : ""})`}
/>
</svg>
{/if}
{/each}
</div>
<!-- Output connectors -->
<div class="output connectors">
{#each node.outputs as output}
{#if output !== undefined}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 8 8"
class="connector"
style:--data-color={`var(--color-data-${output.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${output.dataType.toLowerCase()}-dim)`}
>
<title>{outputTooltip(output)}</title>
<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={output.connectedTo !== undefined ? "var(--data-color)" : "var(--data-color-dim)"}
/>
</svg>
{/if}
{/each}
</div>
<svg class="border-mask" width="0" height="0">
<defs>
<clipPath id={clipPathId}>
<path clip-rule="evenodd" d={nodeBorderMask(node.inputs, node.outputs)} />
</clipPath>
</defs>
</svg>
</div>
{/if}
{/each}
</div>
{:else}
<div class="native-node-graph-ui">{@html $nodeGraph.nativeNodeGraphSVGString}</div>
{/if}
{#each Array.from($nodeGraph.nodesToRender) as [_, nodeToRender]}
{#each nodeToRender.wires as [wire, thick, dataType]}
<svg class="wire">
<path d={wire} style:--data-line-width={`${thick ? 8 : 2}px`} style:--data-color-dim={`var(--color-data-${dataType.toLowerCase()}-dim)`} style:--data-dasharray={"3,0"} />
</svg>
{/each}
{/each}
{#each Array.from($nodeGraph.nodesToRender) as [nodeId, nodeToRender]}
{#if nodeToRender.nodeOrLayer.node !== undefined && $nodeGraph.visibleNodes.has(nodeId)}
{@const nodeMetadata = nodeToRender.metadata}
{@const node = nodeToRender.nodeOrLayer.node}
{@const exposedInputsOutputs = collectExposedInputsOutputs(node.inputs, node.outputs)}
{@const clipPathId = String(Math.random()).substring(2)}
{@const description = (nodeMetadata.reference && $nodeGraph.nodeDescriptions.get(nodeMetadata.reference)) || undefined}
<div
class="node"
class:selected={nodeMetadata.selected}
class:previewed={$nodeGraph.previewedNode == nodeId}
class:disabled={!nodeMetadata.visible}
style:--offset-left={node.position.x}
style:--offset-top={node.position.y}
style:--clip-path-id={`url(#${clipPathId})`}
style:--data-color={`var(--color-data-${(node.outputs[0]?.dataType || "General").toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${(node.outputs[0]?.dataType || "General").toLowerCase()}-dim)`}
title={`${nodeMetadata.displayName}\n\n${description || ""}`.trim() + (editor.handle.inDevelopmentMode() ? `\n\nNode ID: ${nodeId}` : "")}
>
{#if nodeMetadata.errors}
<span class="node-error faded" transition:fade={FADE_TRANSITION} title="" data-node-error>{node.errors}</span>
<span class="node-error hover" transition:fade={FADE_TRANSITION} title="" data-node-error>{node.errors}</span>
{/if}
<!-- Primary row -->
<div class="primary" class:in-selected-network={$nodeGraph.inSelectedNetwork} class:no-secondary-section={exposedInputsOutputs.length === 0}>
<IconLabel icon={nodeIcon(nodeMetadata.reference)} />
<!-- TODO: Allow the user to edit the name, just like in the Layers panel -->
<TextLabel>{nodeMetadata.displayName}</TextLabel>
</div>
<!-- Secondary rows -->
{#if exposedInputsOutputs.length > 0}
<div class="secondary" class:in-selected-network={$nodeGraph.inSelectedNetwork}>
{#each exposedInputsOutputs as [input, output]}
<div class={`secondary-row expanded ${input ? "input" : output ? "output" : ""}`}>
<TextLabel tooltip={(input ? `${input.name}\n\n${input.description}` : output ? `${output.name}\n\n${output.description}` : "").trim()}>
{input?.name ?? output?.name ?? ""}
</TextLabel>
</div>
{/each}
</div>
{/if}
<!-- Input connectors -->
<div class="input connectors">
{#each node.inputs as input}
{#if input !== undefined}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 8 8"
class="connector"
style:--data-color={`var(--color-data-${input.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${input.dataType.toLowerCase()}-dim)`}
>
<title>{inputTooltip(input)}</title>
<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${input.connectedToString === "nothing" ? "-dim" : ""})`}
/>
</svg>
{/if}
{/each}
</div>
<!-- Output connectors -->
<div class="output connectors">
{#each node.outputs as output}
{#if output !== undefined}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 8 8"
class="connector"
style:--data-color={`var(--color-data-${output.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${output.dataType.toLowerCase()}-dim)`}
>
<title>{outputTooltip(output)}</title>
<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={output.connectedTo !== undefined ? "var(--data-color)" : "var(--data-color-dim)"}
/>
</svg>
{/if}
{/each}
</div>
<svg class="border-mask" width="0" height="0">
<defs>
<clipPath id={clipPathId}>
<path clip-rule="evenodd" d={nodeBorderMask(node.inputs, node.outputs)} />
</clipPath>
</defs>
</svg>
</div>
{/if}
{/each}
</div>
<div class="native-node-graph-ui">{@html $nodeGraph.nativeNodeGraphSVGString}</div>
<div class="graph" bind:this={graph}>
<!-- Right click menu for adding nodes -->
@@ -889,6 +558,193 @@
height: 100%;
}
.wire {
position: absolute;
overflow: visible;
top: 0;
left: 0;
path {
fill: none;
stroke: var(--data-color-dim);
stroke-width: var(--data-line-width);
stroke-dasharray: var(--data-dasharray);
}
}
.graph {
position: relative;
overflow: hidden;
display: flex;
flex-direction: row;
flex-grow: 1;
> img {
position: absolute;
bottom: 0;
}
.breadcrumb-trail-buttons {
margin-top: 8px;
margin-left: 8px;
}
.context-menu {
width: max-content;
position: absolute;
box-sizing: border-box;
padding: 5px;
z-index: 3;
background-color: var(--color-3-darkgray);
border-radius: 4px;
.toggle-layer-or-node .text-label {
line-height: 24px;
margin-right: 8px;
}
.merge-selected-nodes {
justify-content: center;
}
}
.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;
}
.connector {
stroke: green;
}
.visibility {
stroke: red;
}
.all-nodes-bounding-box {
stroke: purple;
}
.modify-import-export {
stroke: orange;
}
}
}
.imports-and-exports {
width: 100%;
height: 100%;
position: absolute;
// Keeps the connectors above the wires
z-index: 1;
.connector {
position: absolute;
width: 8px;
height: 8px;
margin-top: 4px;
margin-left: 5px;
top: calc(var(--offset-top) * 24px);
left: calc(var(--offset-left) * 24px);
}
.reorder-bar {
position: absolute;
top: calc(var(--offset-top) * 24px);
left: calc(var(--offset-left) * 24px);
width: 50px;
height: 2px;
background: white;
}
.plus {
position: absolute;
top: calc(var(--offset-top) * 24px);
left: calc(var(--offset-left) * 24px);
}
.edit-import-export {
position: absolute;
display: flex;
align-items: center;
top: calc(var(--offset-top) * 24px);
margin-top: -5px;
height: 24px;
&.separator-bottom::after,
&.separator-top::before {
content: "";
position: absolute;
background: var(--color-8-uppergray);
height: 1px;
left: -4px;
right: -4px;
}
&.separator-bottom::after {
bottom: -1px;
}
&.separator-top::before {
top: 0;
}
&.import {
right: calc(100% - var(--offset-left) * 24px);
}
&.export {
left: calc(var(--offset-left) * 24px + 17px);
}
.import-text {
text-align: right;
text-wrap: nowrap;
}
.export-text {
text-wrap: nowrap;
}
.import-text-input {
text-align: right;
}
.remove-button-import {
margin-left: 3px;
}
.remove-button-export {
margin-right: 3px;
}
.reorder-drag-grip {
width: 8px;
height: 24px;
background-position: 2px 8px;
border-radius: 2px;
margin: -6px 0;
background-image: var(--icon-drag-grip-hover);
}
}
}
}
.breadcrumb-trail-buttons {
margin-top: 8px;
margin-left: 8px;
@@ -1642,4 +1498,25 @@
background: rgba(0, 168, 255, 0.05);
border: 1px solid #00a8ff;
}
.connector-tooltip {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
/* position the bottom-center at a point */
// transform: translate(-50%, -100%);
.connector-tooltip-div {
padding: 5px;
white-space: pre;
position: absolute;
top: calc(var(--offset-top));
left: calc(var(--offset-left));
color: black;
font-size: 12px;
background-color: white;
border: 1px solid black;
}
}
</style>