Layer-based nodes redesign, just the basics so far (#1362)

* Redesign the nodes

* Basic vertical stacking syntax sugar

* Fix node connections

* Primary output and line thickness

---------

Co-authored-by: 0hypercube <0hypercube@gmail.com>
This commit is contained in:
Keavon Chambers 2023-08-04 14:56:00 -07:00 committed by GitHub
parent 94a0f8282e
commit 3450d638a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 491 additions and 225 deletions

View File

@ -28,6 +28,10 @@ resolver = "2"
exclude = ["node-graph/gpu-compiler"]
[workspace.dependencies]
# We are using this fork because:
# - They specify glam=0.22 whereas we use glam=0.24 so the encoding doesn't work.
# - Their current release doesn't allow doc comments and produces a compile error.
# See: https://github.com/GraphiteEditor/Graphite/pull/1346/files/a2206401b5b4cf669e71df57f6c95c67336802c8#r1280201659
specta = { git = "https://github.com/0HyperCube/specta.git", rev = "c47a22b4c0863d27bc47529f300de3969480c66d", features = [
"glam",
] }

View File

@ -307,9 +307,9 @@ impl<'a> ModifyInputsContext<'a> {
let [mut old_bounds_min, mut old_bounds_max] = [DVec2::ZERO, DVec2::ONE];
let [mut new_bounds_min, mut new_bounds_max] = [DVec2::ZERO, DVec2::ONE];
self.modify_inputs("Path Generator", false, |inputs| {
self.modify_inputs("Shape", false, |inputs| {
let [subpaths, mirror_angle_groups] = inputs.as_mut_slice() else {
panic!("Path generator does not have subpath and mirror angle inputs");
panic!("Shape does not have subpath and mirror angle inputs");
};
let NodeInput::Value {

View File

@ -80,7 +80,10 @@ pub struct FrontendNode {
pub primary_input: Option<FrontendGraphDataType>,
#[serde(rename = "exposedInputs")]
pub exposed_inputs: Vec<NodeGraphInput>,
pub outputs: Vec<NodeGraphOutput>, // TODO: Break this apart into `primary_output` and `exposed_outputs`
#[serde(rename = "primaryOutput")]
pub primary_output: Option<NodeGraphOutput>,
#[serde(rename = "exposedOutputs")]
pub exposed_outputs: Vec<NodeGraphOutput>,
pub position: (i32, i32),
pub disabled: bool,
pub previewed: bool,
@ -336,14 +339,11 @@ impl NodeGraphMessageHandler {
})
.collect();
let outputs = node_type
.outputs
.iter()
.map(|output_type| NodeGraphOutput {
data_type: output_type.data_type,
name: output_type.name.to_string(),
})
.collect();
let mut outputs = node_type.outputs.iter().map(|output_type| NodeGraphOutput {
data_type: output_type.data_type,
name: output_type.name.to_string(),
});
let primary_output = outputs.next();
let graph_identifier = GraphIdentifier::new(layer_id);
let thumbnail_svg = executor.thumbnails.get(&graph_identifier).and_then(|thumbnails| thumbnails.get(id)).map(|svg| svg.to_string());
@ -353,7 +353,8 @@ impl NodeGraphMessageHandler {
display_name: node.name.clone(),
primary_input,
exposed_inputs,
outputs,
primary_output,
exposed_outputs: outputs.collect::<Vec<_>>(),
position: node.metadata.position.into(),
previewed: network.outputs_contain(*id),
disabled: network.disabled.contains(id),

View File

@ -1662,7 +1662,7 @@ fn static_nodes() -> Vec<DocumentNodeType> {
..Default::default()
},
DocumentNodeType {
name: "Path Generator",
name: "Shape",
category: "Vector",
identifier: NodeImplementation::proto("graphene_core::vector::generator_nodes::PathGenerator<_>"),
inputs: vec![
@ -1998,7 +1998,7 @@ pub fn new_image_network(output_offset: i32, output_node_id: NodeId) -> NodeNetw
}
pub fn new_vector_network(subpaths: Vec<bezier_rs::Subpath<uuid::ManipulatorGroupId>>) -> NodeNetwork {
let path_generator = resolve_document_node_type("Path Generator").expect("Path Generator node does not exist");
let path_generator = resolve_document_node_type("Shape").expect("Shape node does not exist");
let transform = resolve_document_node_type("Transform").expect("Transform node does not exist");
let fill = resolve_document_node_type("Fill").expect("Fill node does not exist");
let stroke = resolve_document_node_type("Stroke").expect("Stroke node does not exist");

View File

@ -790,7 +790,7 @@ fn get_subpaths<'a>(layer_path: &[LayerId], document: &'a DocumentMessageHandler
let layer = document.document_legacy.layer(layer_path).ok().and_then(|layer| layer.as_layer().ok())?;
let network = &layer.network;
for (node, _node_id) in network.primary_flow() {
if node.name == "Path Generator" {
if node.name == "Shape" {
let subpaths_input = node.inputs.get(0)?;
let NodeInput::Value {
tagged_value: TaggedValue::Subpaths(subpaths),

View File

@ -140,6 +140,7 @@
--icon-expand-collapse-arrow: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8"><polygon fill="%23eee" points="3,0 1,0 5,4 1,8 3,8 7,4" /></svg>');
--icon-expand-collapse-arrow-hover: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8"><polygon fill="%23fff" points="3,0 1,0 5,4 1,8 3,8 7,4" /></svg>');
--icon-expand-collapse-arrow-disabled: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8"><polygon fill="%23888" points="3,0 1,0 5,4 1,8 3,8 7,4" /></svg>');
}
html,

View File

@ -285,12 +285,12 @@
</LayoutCol>
<LayoutCol class="hue-picker" on:pointerdown={onPointerDown} data-hue-picker>
{#if !isNone}
<div class="selection-pincers" style:top={`${(1 - hue) * 100}%`} />
<div class="selection-needle" style:top={`${(1 - hue) * 100}%`} />
{/if}
</LayoutCol>
<LayoutCol class="alpha-picker" on:pointerdown={onPointerDown} data-alpha-picker>
{#if !isNone}
<div class="selection-pincers" style:top={`${(1 - alpha) * 100}%`} />
<div class="selection-needle" style:top={`${(1 - alpha) * 100}%`} />
{/if}
</LayoutCol>
<LayoutCol class="details">
@ -434,11 +434,12 @@
background-blend-mode: screen;
background: linear-gradient(to top, #ff0000ff 16.666%, #ff000000 33.333%, #ff000000 66.666%, #ff0000ff 83.333%),
linear-gradient(to top, #00ff0000 0%, #00ff00ff 16.666%, #00ff00ff 50%, #00ff0000 66.666%), linear-gradient(to top, #0000ff00 33.333%, #0000ffff 50%, #0000ffff 83.333%, #0000ff00 100%);
--selection-pincers-color: var(--hue-color-contrasting);
--selection-needle-color: var(--hue-color-contrasting);
}
.alpha-picker {
background: linear-gradient(to bottom, var(--opaque-color), transparent);
--selection-needle-color: var(--new-color-contrasting);
&::before {
content: "";
@ -450,7 +451,6 @@
background-size: var(--color-transparent-checkered-background-size);
background-position: var(--color-transparent-checkered-background-position);
}
--selection-pincers-color: var(--new-color-contrasting);
}
.selection-circle {
@ -475,7 +475,7 @@
}
}
.selection-pincers {
.selection-needle {
position: absolute;
top: 0%;
width: 100%;
@ -489,7 +489,7 @@
left: 0;
border-style: solid;
border-width: 4px 0 4px 4px;
border-color: transparent transparent transparent var(--selection-pincers-color);
border-color: transparent transparent transparent var(--selection-needle-color);
}
&::after {
@ -499,7 +499,7 @@
right: 0;
border-style: solid;
border-width: 4px 4px 4px 0;
border-color: transparent var(--selection-pincers-color) transparent transparent;
border-color: transparent var(--selection-needle-color) transparent transparent;
}
}

View File

@ -2,9 +2,8 @@
import { getContext, onMount, tick } from "svelte";
import type { IconName } from "@graphite/utility-functions/icons";
import { UpdateNodeGraphSelection, type FrontendNodeLink, type FrontendNodeType, type FrontendNode } from "@graphite/wasm-communication/messages";
import { UpdateNodeGraphSelection } from "@graphite/wasm-communication/messages";
import type { FrontendNodeLink, FrontendNodeType, FrontendNode } from "@graphite/wasm-communication/messages";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
@ -24,6 +23,8 @@
const editor = getContext<Editor>("editor");
const nodeGraph = getContext<NodeGraphState>("nodeGraph");
type LinkPath = { pathString: string; dataType: string; thick: boolean };
let graph: LayoutRow | undefined;
let nodesContainer: HTMLDivElement | undefined;
let nodeSearchInput: TextInput | undefined;
@ -33,10 +34,10 @@
let selected: bigint[] = [];
let draggingNodes: { startX: number; startY: number; roundX: number; roundY: number } | undefined = undefined;
let selectIfNotDragged: undefined | bigint = undefined;
let linkInProgressFromConnector: HTMLDivElement | undefined = undefined;
let linkInProgressToConnector: HTMLDivElement | DOMRect | undefined = undefined;
let linkInProgressFromConnector: SVGSVGElement | undefined = undefined;
let linkInProgressToConnector: SVGSVGElement | DOMRect | undefined = undefined;
let disconnecting: { nodeId: bigint; inputIndex: number; linkIndex: number } | undefined = undefined;
let nodeLinkPaths: [string, string][] = [];
let nodeLinkPaths: LinkPath[] = [];
let searchTerm = "";
let nodeListLocation: { x: number; y: number } | undefined = undefined;
@ -109,14 +110,19 @@
return Array.from(categories);
}
function createLinkPathInProgress(linkInProgressFromConnector?: HTMLDivElement, linkInProgressToConnector?: HTMLDivElement | DOMRect): [string, string] | undefined {
function createLinkPathInProgress(linkInProgressFromConnector?: SVGSVGElement, linkInProgressToConnector?: SVGSVGElement | DOMRect): LinkPath | undefined {
if (linkInProgressFromConnector && linkInProgressToConnector && nodesContainer) {
return createWirePath(linkInProgressFromConnector, linkInProgressToConnector, false, false);
const from = connectorToNodeIndex(linkInProgressFromConnector);
const to = linkInProgressToConnector instanceof SVGSVGElement ? connectorToNodeIndex(linkInProgressToConnector) : undefined;
const linkStart = $nodeGraph.nodes.find((node) => node.id === from?.nodeId)?.displayName === "Layer";
const linkEnd = $nodeGraph.nodes.find((node) => node.id === to?.nodeId)?.displayName === "Layer" && to?.index !== 0;
return createWirePath(linkInProgressFromConnector, linkInProgressToConnector, linkStart, linkEnd);
}
return undefined;
}
function createLinkPaths(linkPathInProgress: [string, string] | undefined, nodeLinkPaths: [string, string][]): [string, string][] {
function createLinkPaths(linkPathInProgress: LinkPath | undefined, nodeLinkPaths: LinkPath[]): LinkPath[] {
const optionalTuple = linkPathInProgress ? [linkPathInProgress] : [];
return [...optionalTuple, ...nodeLinkPaths];
}
@ -126,7 +132,7 @@
await refreshLinks();
}
function resolveLink(link: FrontendNodeLink, containerBounds: HTMLDivElement): { nodePrimaryOutput: HTMLDivElement | undefined; nodePrimaryInput: HTMLDivElement | undefined } {
function resolveLink(link: FrontendNodeLink, containerBounds: HTMLDivElement): { nodeOutput: SVGSVGElement | undefined; nodeInput: SVGSVGElement | undefined } {
const outputIndex = Number(link.linkStartOutputIndex);
const inputIndex = Number(link.linkEndInputIndex);
@ -134,9 +140,9 @@
const nodeInputConnectors = containerBounds.querySelectorAll(`[data-node="${String(link.linkEnd)}"] [data-port="input"]`) || undefined;
const nodePrimaryOutput = nodeOutputConnectors?.[outputIndex] as HTMLDivElement | undefined;
const nodePrimaryInput = nodeInputConnectors?.[inputIndex] as HTMLDivElement | undefined;
return { nodePrimaryOutput, nodePrimaryInput };
const nodeOutput = nodeOutputConnectors?.[outputIndex] as SVGSVGElement | undefined;
const nodeInput = nodeInputConnectors?.[inputIndex] as SVGSVGElement | undefined;
return { nodeOutput, nodeInput };
}
async function refreshLinks(): Promise<void> {
@ -147,11 +153,13 @@
const links = $nodeGraph.links;
nodeLinkPaths = links.flatMap((link, index) => {
const { nodePrimaryInput, nodePrimaryOutput } = resolveLink(link, theNodesContainer);
if (!nodePrimaryInput || !nodePrimaryOutput) return [];
const { nodeInput, nodeOutput } = resolveLink(link, theNodesContainer);
if (!nodeInput || !nodeOutput) return [];
if (disconnecting?.linkIndex === index) return [];
const linkStart = $nodeGraph.nodes.find((node) => node.id === link.linkStart)?.displayName === "Layer";
const linkEnd = $nodeGraph.nodes.find((node) => node.id === link.linkEnd)?.displayName === "Layer" && link.linkEndInputIndex !== 0n;
return [createWirePath(nodePrimaryOutput, nodePrimaryInput.getBoundingClientRect(), false, false)];
return [createWirePath(nodeOutput, nodeInput.getBoundingClientRect(), linkStart, linkEnd)];
});
}
@ -205,13 +213,13 @@
return `M${locations[0].x},${locations[0].y} C${locations[1].x},${locations[1].y} ${locations[2].x},${locations[2].y} ${locations[3].x},${locations[3].y}`;
}
function createWirePath(outputPort: HTMLDivElement, inputPort: HTMLDivElement | DOMRect, verticalOut: boolean, verticalIn: boolean): [string, string] {
const inputPortRect = inputPort instanceof HTMLDivElement ? inputPort.getBoundingClientRect() : inputPort;
function createWirePath(outputPort: SVGSVGElement, inputPort: SVGSVGElement | DOMRect, verticalOut: boolean, verticalIn: boolean): LinkPath {
const inputPortRect = inputPort instanceof DOMRect ? inputPort : inputPort.getBoundingClientRect();
const pathString = buildWirePathString(outputPort.getBoundingClientRect(), inputPortRect, verticalOut, verticalIn);
const dataType = outputPort.getAttribute("data-datatype") || "general";
return [pathString, dataType];
return { pathString, dataType, thick: verticalIn && verticalOut };
}
function scroll(e: WheelEvent) {
@ -273,7 +281,7 @@
function pointerDown(e: PointerEvent) {
const [lmb, rmb] = [e.button === 0, e.button === 2];
const port = (e.target as HTMLDivElement).closest("[data-port]") as HTMLDivElement;
const port = (e.target as SVGSVGElement).closest("[data-port]") as SVGSVGElement;
const node = (e.target as HTMLElement).closest("[data-node]") as HTMLElement | undefined;
const nodeId = node?.getAttribute("data-node") || undefined;
const nodeList = (e.target as HTMLElement).closest("[data-node-list]") as HTMLElement | undefined;
@ -315,17 +323,19 @@
const inputNodeConnectionIndexSearch = inputNodeInPorts.indexOf(port);
const inputIndex = inputNodeConnectionIndexSearch > -1 ? inputNodeConnectionIndexSearch : undefined;
// Set the link to draw from the input that a previous link was on
if (inputIndex !== undefined && nodeId) {
if (inputIndex !== undefined && nodeId !== undefined) {
const nodeIdInt = BigInt(nodeId);
const inputIndexInt = BigInt(inputIndex);
const links = $nodeGraph.links;
const linkIndex = links.findIndex((value) => value.linkEnd === nodeIdInt && value.linkEndInputIndex === inputIndexInt);
const nodeOutputConnectors = nodesContainer?.querySelectorAll(`[data-node="${String(links[linkIndex].linkStart)}"] [data-port="output"]`) || undefined;
linkInProgressFromConnector = nodeOutputConnectors?.[Number(links[linkIndex].linkStartOutputIndex)] as HTMLDivElement | undefined;
const nodeInputConnectors = nodesContainer?.querySelectorAll(`[data-node="${String(links[linkIndex].linkEnd)}"] [data-port="input"]`) || undefined;
linkInProgressToConnector = nodeInputConnectors?.[Number(links[linkIndex].linkEndInputIndex)] as HTMLDivElement | undefined;
disconnecting = { nodeId: nodeIdInt, inputIndex, linkIndex };
refreshLinks();
if (linkIndex !== -1) {
const nodeOutputConnectors = nodesContainer?.querySelectorAll(`[data-node="${String(links[linkIndex].linkStart)}"] [data-port="output"]`) || undefined;
linkInProgressFromConnector = nodeOutputConnectors?.[Number(links[linkIndex].linkStartOutputIndex)] as SVGSVGElement | undefined;
const nodeInputConnectors = nodesContainer?.querySelectorAll(`[data-node="${String(links[linkIndex].linkEnd)}"] [data-port="input"]`) || undefined;
linkInProgressToConnector = nodeInputConnectors?.[Number(links[linkIndex].linkEndInputIndex)] as SVGSVGElement | undefined;
disconnecting = { nodeId: nodeIdInt, inputIndex, linkIndex };
refreshLinks();
}
}
}
@ -354,7 +364,7 @@
draggingNodes = { startX: e.x, startY: e.y, roundX: 0, roundY: 0 };
}
if (modifiedSelected) editor.instance.selectNodes(selected.length > 0 ? new BigUint64Array(selected): null);
if (modifiedSelected) editor.instance.selectNodes(selected.length > 0 ? new BigUint64Array(selected) : null);
return;
}
@ -384,7 +394,7 @@
transform.y += e.movementY / transform.scale;
} else if (linkInProgressFromConnector) {
const target = e.target as Element | undefined;
const dot = (target?.closest(`[data-port="input"]`) || undefined) as HTMLDivElement | undefined;
const dot = (target?.closest(`[data-port="input"]`) || undefined) as SVGSVGElement | undefined;
if (dot) {
linkInProgressToConnector = dot;
} else {
@ -401,6 +411,23 @@
}
}
function connectorToNodeIndex(svg: SVGSVGElement): { nodeId: bigint; index: number } | undefined {
const node = svg.closest("[data-node]");
if (!node) return undefined;
const nodeIdAttribute = node.getAttribute("data-node");
if (!nodeIdAttribute) return undefined;
const nodeId = BigInt(nodeIdAttribute);
const inputPortElements = Array.from(node.querySelectorAll(`[data-port="input"]`));
const outputPortElements = Array.from(node.querySelectorAll(`[data-port="output"]`));
const inputNodeConnectionIndexSearch = inputPortElements.includes(svg) ? inputPortElements.indexOf(svg) : outputPortElements.indexOf(svg);
const index = inputNodeConnectionIndexSearch > -1 ? inputNodeConnectionIndexSearch : undefined;
if (nodeId !== undefined && index !== undefined) return { nodeId, index };
else return undefined;
}
function pointerUp(e: PointerEvent) {
panning = false;
@ -409,26 +436,14 @@
}
disconnecting = undefined;
if (linkInProgressToConnector instanceof HTMLDivElement && linkInProgressFromConnector) {
const outputNode = linkInProgressFromConnector.closest("[data-node]");
const inputNode = linkInProgressToConnector.closest("[data-node]");
if (linkInProgressToConnector instanceof SVGSVGElement && linkInProgressFromConnector) {
const from = connectorToNodeIndex(linkInProgressFromConnector);
const to = connectorToNodeIndex(linkInProgressToConnector);
const outputConnectedNodeID = outputNode?.getAttribute("data-node") ?? undefined;
const inputConnectedNodeID = inputNode?.getAttribute("data-node") ?? undefined;
if (outputNode && inputNode && outputConnectedNodeID && inputConnectedNodeID) {
const inputNodeInPorts = Array.from(inputNode.querySelectorAll(`[data-port="input"]`));
const outputNodeInPorts = Array.from(outputNode.querySelectorAll(`[data-port="output"]`));
const inputNodeConnectionIndexSearch = inputNodeInPorts.indexOf(linkInProgressToConnector);
const outputNodeConnectionIndexSearch = outputNodeInPorts.indexOf(linkInProgressFromConnector);
const inputNodeConnectionIndex = inputNodeConnectionIndexSearch > -1 ? inputNodeConnectionIndexSearch : undefined;
const outputNodeConnectionIndex = outputNodeConnectionIndexSearch > -1 ? outputNodeConnectionIndexSearch : undefined;
if (inputNodeConnectionIndex !== undefined && outputNodeConnectionIndex !== undefined) {
editor.instance.connectNodesByLink(BigInt(outputConnectedNodeID), outputNodeConnectionIndex, BigInt(inputConnectedNodeID), inputNodeConnectionIndex);
}
if (from !== undefined && to !== undefined) {
const { nodeId: outputConnectedNodeID, index: outputNodeConnectionIndex } = from;
const { nodeId: inputConnectedNodeID, index: inputNodeConnectionIndex } = to;
editor.instance.connectNodesByLink(outputConnectedNodeID, outputNodeConnectionIndex, inputConnectedNodeID, inputNodeConnectionIndex);
}
} else if (draggingNodes) {
if (draggingNodes.startX === e.x || draggingNodes.startY === e.y) {
@ -456,10 +471,10 @@
// Find the link that the node has been dragged on top of
const link = $nodeGraph.links.find((link): boolean => {
const { nodePrimaryInput, nodePrimaryOutput } = resolveLink(link, theNodesContainer);
if (!nodePrimaryInput || !nodePrimaryOutput) return false;
const { nodeInput, nodeOutput } = resolveLink(link, theNodesContainer);
if (!nodeInput || !nodeOutput) return false;
const wireCurveLocations = buildWirePathLocations(nodePrimaryOutput.getBoundingClientRect(), nodePrimaryInput.getBoundingClientRect(), false, false);
const wireCurveLocations = buildWirePathLocations(nodeOutput.getBoundingClientRect(), nodeInput.getBoundingClientRect(), false, false);
const selectedNodeBounds = selectedNode.getBoundingClientRect();
const containerBoundsBounds = theNodesContainer.getBoundingClientRect();
@ -498,15 +513,20 @@
nodeListLocation = undefined;
}
function buildBorderMask(nodeWidth: number, primaryInputExists: boolean, parameters: number, primaryOutputExists: boolean, exposedOutputs: number): string {
const nodeHeight = Math.max(1 + parameters, 1 + exposedOutputs) * 24;
const boxes: { x: number; y: number; width: number; height: number }[] = [];
if (primaryInputExists) boxes.push({ x: -8, y: 4, width: 16, height: 16 });
for (let i = 0; i < parameters; i++) boxes.push({ x: -8, y: 4 + (i + 1) * 24, width: 16, height: 16 });
if (primaryOutputExists) boxes.push({ x: nodeWidth - 8, y: 4, width: 16, height: 16 });
for (let i = 0; i < exposedOutputs; i++) boxes.push({ x: nodeWidth - 8, y: 4 + (i + 1) * 24, width: 16, height: 16 });
const rectangles = boxes.map((box) => `M${box.x},${box.y} L${box.x + box.width},${box.y} L${box.x + box.width},${box.y + box.height} L${box.x},${box.y + box.height}z`);
return `M-2,-2 L${nodeWidth + 2},-2 L${nodeWidth + 2},${nodeHeight + 2} L-2,${nodeHeight + 2}z ${rectangles.join(" ")}`;
}
onMount(() => {
const outputPort1 = document.querySelectorAll(`[data-port="output"]`)[4] as HTMLDivElement | undefined;
const inputPort1 = document.querySelectorAll(`[data-port="input"]`)[1] as HTMLDivElement | undefined;
if (outputPort1 && inputPort1) createWirePath(outputPort1, inputPort1.getBoundingClientRect(), true, true);
const outputPort2 = document.querySelectorAll(`[data-port="output"]`)[6] as HTMLDivElement | undefined;
const inputPort2 = document.querySelectorAll(`[data-port="input"]`)[3] as HTMLDivElement | undefined;
if (outputPort2 && inputPort2) createWirePath(outputPort2, inputPort2.getBoundingClientRect(), true, false);
editor.subscriptions.subscribeJsMessage(UpdateNodeGraphSelection, (updateNodeGraphSelection) => {
selected = updateNodeGraphSelection.selected;
});
@ -530,6 +550,9 @@
"--dot-radius": `${dotRadius}px`,
}}
>
<img src="https://files.keavon.com/-/MountainousDroopyBlueshark/flyover.jpg" />
<div class="fade-artwork" />
<!-- Right click menu for adding nodes -->
{#if nodeListLocation}
<LayoutCol
class="node-list"
@ -559,92 +582,195 @@
</div>
</LayoutCol>
{/if}
<div class="nodes" style:transform={`scale(${transform.scale}) translate(${transform.x}px, ${transform.y}px)`} style:transform-origin={`0 0`} bind:this={nodesContainer}>
{#each $nodeGraph.nodes as node (String(node.id))}
{@const exposedInputsOutputs = [...node.exposedInputs, ...node.outputs.slice(1)]}
<!-- Node connection links -->
<div class="wires" style:transform={`scale(${transform.scale}) translate(${transform.x}px, ${transform.y}px)`} style:transform-origin={`0 0`}>
<svg>
{#each linkPaths as { pathString, dataType, thick }}
<path
d={pathString}
style:--data-line-width={`${thick ? 5 : 2}px`}
style:--data-color={`var(--color-data-${dataType})`}
style:--data-color-dim={`var(--color-data-${dataType}-dim)`}
/>
{/each}
</svg>
</div>
<!-- Layers and nodes -->
<div class="layers-and-nodes" style:transform={`scale(${transform.scale}) translate(${transform.x}px, ${transform.y}px)`} style:transform-origin={`0 0`} bind:this={nodesContainer}>
<!-- Layers -->
{#each $nodeGraph.nodes.filter((node) => node.displayName === "Layer") as node (String(node.id))}
{@const exposedInputsOutputs = [...node.exposedInputs, ...node.exposedOutputs]}
{@const clipPathId = `${Math.random()}`.substring(2)}
{@const stackDatainput = node.exposedInputs[0]}
<div
class="node"
class="layer"
class:selected={selected.includes(node.id)}
class:previewed={node.previewed}
class:disabled={node.disabled}
style:--offset-left={(node.position?.x || 0) + (selected.includes(node.id) ? draggingNodes?.roundX || 0 : 0)}
style:--offset-top={(node.position?.y || 0) + (selected.includes(node.id) ? draggingNodes?.roundY || 0 : 0)}
style:--clip-path-id={`url(#${clipPathId})`}
data-node={node.id}
>
<div class="primary">
<div class="ports">
{#if node.primaryInput}
<div
class="input port"
data-port="input"
data-datatype={node.primaryInput}
style:--data-color={`var(--color-data-${node.primaryInput})`}
style:--data-color-dim={`var(--color-data-${node.primaryInput}-dim)`}
>
<div />
</div>
{/if}
{#if node.outputs.length > 0}
<div
class="output port"
data-port="output"
data-datatype={node.outputs[0].dataType}
style:--data-color={`var(--color-data-${node.outputs[0].dataType})`}
style:--data-color-dim={`var(--color-data-${node.outputs[0].dataType}-dim)`}
>
<div />
</div>
{/if}
</div>
{#if node.thumbnailSvg}
{@html node.thumbnailSvg}
{:else}
<IconLabel icon={nodeIcon(node.displayName)} />
<div class="node-chain" />
<!-- Layer input port (from left) -->
<div class="input ports">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 8 8"
class="port"
data-port="input"
data-datatype={node.primaryInput}
style:--data-color={`var(--color-data-${node.primaryInput})`}
style:--data-color-dim={`var(--color-data-${node.primaryInput}-dim)`}
>
<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" />
</svg>
</div>
<div class="thumbnail">
{@html node.thumbnailSvg}
{#if node.primaryOutput}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 8 8"
class="port top"
data-port="output"
data-datatype={node.primaryOutput.dataType}
style:--data-color={`var(--color-data-${node.primaryOutput.dataType})`}
style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType}-dim)`}
>
<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" />
</svg>
{/if}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 8 8"
class="port bottom"
data-port="input"
data-datatype={stackDatainput.dataType}
style:--data-color={`var(--color-data-${stackDatainput.dataType})`}
style:--data-color-dim={`var(--color-data-${stackDatainput.dataType}-dim)`}
>
<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" />
</svg>
</div>
<div class="details">
<TextLabel>{node.displayName}</TextLabel>
</div>
<svg class="border-mask" width="0" height="0">
<defs>
<clipPath id={clipPathId}>
<path clip-rule="evenodd" d={buildBorderMask(120, node.primaryInput !== undefined, node.exposedInputs.length, node.primaryOutput !== undefined, node.exposedOutputs)} />
</clipPath>
</defs>
</svg>
</div>
{/each}
<!-- Nodes -->
{#each $nodeGraph.nodes.filter((node) => node.displayName !== "Layer") as node (String(node.id))}
{@const exposedInputsOutputs = [...node.exposedInputs, ...node.exposedOutputs]}
{@const clipPathId = `${Math.random()}`.substring(2)}
<div
class="node"
class:selected={selected.includes(node.id)}
class:previewed={node.previewed}
class:disabled={node.disabled}
class:is-layer={node.thumbnailSvg !== undefined}
style:--offset-left={(node.position?.x || 0) + (selected.includes(node.id) ? draggingNodes?.roundX || 0 : 0)}
style:--offset-top={(node.position?.y || 0) + (selected.includes(node.id) ? draggingNodes?.roundY || 0 : 0)}
style:--clip-path-id={`url(#${clipPathId})`}
data-node={node.id}
>
<!-- Primary row -->
<div class="primary" class:no-parameter-section={exposedInputsOutputs.length === 0}>
<IconLabel icon={nodeIcon(node.displayName)} />
<TextLabel>{node.displayName}</TextLabel>
</div>
<!-- Parameter rows -->
{#if exposedInputsOutputs.length > 0}
<div class="parameters">
{#each exposedInputsOutputs as parameter, index (index)}
<div class="parameter">
<div class="ports">
{#if index < node.exposedInputs.length}
<div
class="input port"
data-port="input"
data-datatype={parameter.dataType}
style:--data-color={`var(--color-data-${parameter.dataType})`}
style:--data-color-dim={`var(--color-data-${parameter.dataType}-dim)`}
>
<div />
</div>
{:else}
<div
class="output port"
data-port="output"
data-datatype={parameter.dataType}
style:--data-color={`var(--color-data-${parameter.dataType})`}
style:--data-color-dim={`var(--color-data-${parameter.dataType}-dim)`}
>
<div />
</div>
{/if}
</div>
{#each exposedInputsOutputs as parameter, index}
<div class="parameter expanded">
<div class="expand-arrow" />
<TextLabel class={index < node.exposedInputs.length ? "name" : "output"}>{parameter.name}</TextLabel>
</div>
{/each}
</div>
{/if}
<!-- Input ports -->
<div class="input ports">
{#if node.primaryInput}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 8 8"
class="port"
data-port="input"
data-datatype={node.primaryInput}
style:--data-color={`var(--color-data-${node.primaryInput})`}
style:--data-color-dim={`var(--color-data-${node.primaryInput}-dim)`}
>
<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" />
</svg>
{/if}
{#each node.exposedInputs as parameter, index}
{#if index < node.exposedInputs.length}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 8 8"
class="port"
data-port="input"
data-datatype={parameter.dataType}
style:--data-color={`var(--color-data-${parameter.dataType})`}
style:--data-color-dim={`var(--color-data-${parameter.dataType}-dim)`}
>
<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" />
</svg>
{/if}
{/each}
</div>
<!-- Output ports -->
<div class="output ports">
{#if node.primaryOutput}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 8 8"
class="port"
data-port="output"
data-datatype={node.primaryOutput.dataType}
style:--data-color={`var(--color-data-${node.primaryOutput.dataType})`}
style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType}-dim)`}
>
<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" />
</svg>
{/if}
{#each node.exposedOutputs as parameter}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 8 8"
class="port"
data-port="output"
data-datatype={parameter.dataType}
style:--data-color={`var(--color-data-${parameter.dataType})`}
style:--data-color-dim={`var(--color-data-${parameter.dataType}-dim)`}
>
<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" />
</svg>
{/each}
</div>
<svg class="border-mask" width="0" height="0">
<defs>
<clipPath id={clipPathId}>
<path
clip-rule="evenodd"
d={buildBorderMask(120, node.primaryInput !== undefined, node.exposedInputs.length, node.primaryOutput !== undefined, node.exposedOutputs.length)}
/>
</clipPath>
</defs>
</svg>
</div>
{/each}
</div>
<div class="wires" style:transform={`scale(${transform.scale}) translate(${transform.x}px, ${transform.y}px)`} style:transform-origin={`0 0`}>
<svg>
{#each linkPaths as [pathString, dataType], index (index)}
<path d={pathString} style:--data-color={`var(--color-data-${dataType})`} style:--data-color-dim={`var(--color-data-${dataType}-dim)`} />
{/each}
</svg>
</div>
</LayoutRow>
</LayoutCol>
@ -712,6 +838,13 @@
}
}
.fade-artwork {
background: var(--color-2-mildblack);
opacity: 0.8;
width: 100%;
height: 100%;
}
.graph {
position: relative;
background: var(--color-2-mildblack);
@ -721,6 +854,11 @@
border-radius: 2px;
overflow: hidden;
> img {
position: absolute;
bottom: 0;
}
// We're displaying the dotted grid in a pseudo-element because `image-rendering` is an inherited property and we don't want it to apply to child elements
&::before {
content: "";
@ -735,7 +873,7 @@
}
}
.nodes,
.layers-and-nodes,
.wires {
position: absolute;
width: 100%;
@ -753,27 +891,46 @@
path {
fill: none;
// stroke: var(--color-data-raster-dim);
stroke: var(--data-color-dim);
stroke-width: 2px;
stroke-width: var(--data-line-width);
}
}
}
&.nodes {
&.layers-and-nodes {
.layer,
.node {
position: absolute;
display: flex;
flex-direction: column;
min-width: 120px;
border-radius: 4px;
background: var(--color-4-dimgray);
left: calc((var(--offset-left) + 0.5) * 24px);
top: calc((var(--offset-top) - 0.5) * 24px);
backdrop-filter: blur(8px) brightness(100% - 33%);
left: calc(var(--offset-left) * 24px);
top: calc(var(--offset-top) * 24px);
&::after {
content: "";
position: absolute;
box-sizing: border-box;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
clip-path: var(--clip-path-id);
}
.border-mask {
position: absolute;
top: 0;
}
&.selected {
border: 1px solid var(--color-e-nearwhite);
margin: -1px;
.primary {
background: rgba(255, 255, 255, 0.15);
}
.parameters {
background: rgba(255, 255, 255, 0.1);
}
}
&.disabled {
@ -783,24 +940,170 @@
.icon-label {
fill: var(--color-a-softgray);
}
.expand-arrow::after {
background: var(--icon-expand-collapse-arrow-disabled);
}
}
&.previewed {
outline: 3px solid var(--color-data-vector);
&.previewed::after {
border: 1px dashed var(--color-data-vector);
}
.ports {
position: absolute;
&.input {
left: -3px;
}
&.output {
right: -5px;
}
}
.port {
fill: var(--data-color);
// Double the intended value because of margin collapsing, but for the first and last we divide it by two as intended
margin: calc(24px - 8px) 0;
width: 8px;
height: 8px;
&:first-of-type {
margin-top: calc((24px - 8px) / 2);
}
&:last-of-type {
margin-bottom: calc((24px - 8px) / 2);
}
}
.expand-arrow {
width: 16px;
height: 16px;
margin: 0;
padding: 0;
position: relative;
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: center;
&::after {
content: "";
position: absolute;
width: 8px;
height: 8px;
background: var(--icon-expand-collapse-arrow);
}
&:hover::after {
background: var(--icon-expand-collapse-arrow-hover);
}
}
.expanded .expand-arrow::after {
transform: rotate(90deg);
}
}
.layer {
border-radius: 8px;
min-width: 216px;
&::after {
border: 1px solid var(--color-5-dullgray);
border-radius: 8px;
}
.node-chain {
width: 36px;
}
.thumbnail {
background: var(--color-2-mildblack);
border: 1px solid var(--color-data-vector-dim);
border-radius: 2px;
position: relative;
box-sizing: border-box;
width: 72px;
height: 48px;
&::before {
content: "";
background: var(--color-transparent-checkered-background);
background-size: var(--color-transparent-checkered-background-size);
background-position: var(--color-transparent-checkered-background-position);
}
&::before,
svg:not(.port) {
position: absolute;
margin: auto;
inset: 1px;
pointer-events: none;
}
.port {
position: absolute;
margin: 0 auto;
left: 0;
right: 0;
&.top {
top: -12px;
}
&.bottom {
bottom: -12px;
}
}
}
.details {
margin-left: 12px;
.text-label {
line-height: 48px;
}
}
.input.ports,
.input.ports .port {
position: absolute;
margin: auto 0;
top: 0;
bottom: 0;
}
}
.node {
flex-direction: column;
border-radius: 2px;
min-width: 120px;
top: calc((var(--offset-top) + 0.5) * 24px);
&::after {
border: 1px solid var(--color-data-vector-dim);
border-radius: 2px;
}
.primary {
display: flex;
align-items: center;
position: relative;
gap: 4px;
width: 100%;
height: 24px;
background: var(--color-5-dullgray);
border-radius: 4px;
border-radius: 2px 2px 0 0;
font-style: italic;
background: rgba(255, 255, 255, 0.05);
&.no-parameter-section {
border-radius: 2px;
}
.icon-label {
margin-left: 4px;
margin: 0 8px;
}
.text-label {
@ -818,10 +1121,16 @@
position: relative;
display: flex;
align-items: center;
width: 100%;
height: 24px;
width: calc(100% - 24px * 2);
margin-left: 24px;
margin-right: 24px;
&:last-of-type {
border-radius: 0 0 2px 2px;
}
.expand-arrow {
margin-left: 4px;
}
.text-label {
width: 100%;
@ -832,17 +1141,6 @@
}
}
// Squares to cover up the rounded corners of the primary area and make them have a straight edge
&::before,
&::after {
content: "";
position: absolute;
background: var(--color-5-dullgray);
width: 4px;
height: 4px;
top: -4px;
}
&::before {
left: 0;
}
@ -851,46 +1149,6 @@
right: 0;
}
}
.ports {
position: absolute;
width: 100%;
height: 100%;
.port {
position: absolute;
margin: auto 0;
top: 0;
bottom: 0;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--data-color-dim);
// background: var(--color-data-raster-dim);
div {
background: var(--data-color);
// background: var(--color-data-raster);
width: 8px;
height: 8px;
border-radius: 50%;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
}
&.input {
left: calc(-12px - 6px);
}
&.output {
right: calc(-12px - 6px);
}
}
}
}
}
}

View File

@ -49,7 +49,7 @@
margin-bottom: 4px;
border: 0;
border-radius: 4px;
background: var(--color-5-dullgray);
background: var(--color-2-mildblack);
.expand-arrow {
width: 8px;
@ -87,7 +87,7 @@
}
&:hover {
background: var(--color-6-lowergray);
background: var(--color-4-dimgray);
.expand-arrow::after {
background: var(--icon-expand-collapse-arrow-hover);
@ -98,7 +98,7 @@
}
+ .body {
border: 1px solid var(--color-6-lowergray);
border: 1px solid var(--color-4-dimgray);
}
}
}
@ -108,7 +108,7 @@
padding-top: 1px;
margin-top: -1px;
margin-bottom: 4px;
border: 1px solid var(--color-5-dullgray);
border: 1px solid var(--color-2-mildblack);
border-radius: 0 0 4px 4px;
overflow: hidden;

View File

@ -14,8 +14,8 @@
const PANEL_SIZES = {
/**/ root: 100,
/* ├── */ content: 80,
/* │ ├── */ document: 80,
/* │ └── */ graph: 20,
/* │ ├── */ document: 50,
/* │ └── */ graph: 50,
/* └── */ details: 20,
/* ├── */ properties: 45,
/* └── */ layers: 55,

View File

@ -96,7 +96,9 @@ export class FrontendNode {
readonly exposedInputs!: NodeGraphInput[];
readonly outputs!: NodeGraphOutput[];
readonly primaryOutput!: NodeGraphOutput | undefined;
readonly exposedOutputs!: NodeGraphOutput[];
@TupleToVec2
readonly position!: XY | undefined;