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
co-authored by 0hypercube
parent 94a0f8282e
commit 3450d638a6
11 changed files with 491 additions and 225 deletions
+4
View File
@@ -28,6 +28,10 @@ resolver = "2"
exclude = ["node-graph/gpu-compiler"] exclude = ["node-graph/gpu-compiler"]
[workspace.dependencies] [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 = [ specta = { git = "https://github.com/0HyperCube/specta.git", rev = "c47a22b4c0863d27bc47529f300de3969480c66d", features = [
"glam", "glam",
] } ] }
@@ -307,9 +307,9 @@ impl<'a> ModifyInputsContext<'a> {
let [mut old_bounds_min, mut old_bounds_max] = [DVec2::ZERO, DVec2::ONE]; 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]; 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 { 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 { let NodeInput::Value {
@@ -80,7 +80,10 @@ pub struct FrontendNode {
pub primary_input: Option<FrontendGraphDataType>, pub primary_input: Option<FrontendGraphDataType>,
#[serde(rename = "exposedInputs")] #[serde(rename = "exposedInputs")]
pub exposed_inputs: Vec<NodeGraphInput>, 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 position: (i32, i32),
pub disabled: bool, pub disabled: bool,
pub previewed: bool, pub previewed: bool,
@@ -336,14 +339,11 @@ impl NodeGraphMessageHandler {
}) })
.collect(); .collect();
let outputs = node_type let mut outputs = node_type.outputs.iter().map(|output_type| NodeGraphOutput {
.outputs data_type: output_type.data_type,
.iter() name: output_type.name.to_string(),
.map(|output_type| NodeGraphOutput { });
data_type: output_type.data_type, let primary_output = outputs.next();
name: output_type.name.to_string(),
})
.collect();
let graph_identifier = GraphIdentifier::new(layer_id); 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()); 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(), display_name: node.name.clone(),
primary_input, primary_input,
exposed_inputs, exposed_inputs,
outputs, primary_output,
exposed_outputs: outputs.collect::<Vec<_>>(),
position: node.metadata.position.into(), position: node.metadata.position.into(),
previewed: network.outputs_contain(*id), previewed: network.outputs_contain(*id),
disabled: network.disabled.contains(id), disabled: network.disabled.contains(id),
@@ -1662,7 +1662,7 @@ fn static_nodes() -> Vec<DocumentNodeType> {
..Default::default() ..Default::default()
}, },
DocumentNodeType { DocumentNodeType {
name: "Path Generator", name: "Shape",
category: "Vector", category: "Vector",
identifier: NodeImplementation::proto("graphene_core::vector::generator_nodes::PathGenerator<_>"), identifier: NodeImplementation::proto("graphene_core::vector::generator_nodes::PathGenerator<_>"),
inputs: vec![ 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 { 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 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 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"); let stroke = resolve_document_node_type("Stroke").expect("Stroke node does not exist");
@@ -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 layer = document.document_legacy.layer(layer_path).ok().and_then(|layer| layer.as_layer().ok())?;
let network = &layer.network; let network = &layer.network;
for (node, _node_id) in network.primary_flow() { 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 subpaths_input = node.inputs.get(0)?;
let NodeInput::Value { let NodeInput::Value {
tagged_value: TaggedValue::Subpaths(subpaths), tagged_value: TaggedValue::Subpaths(subpaths),
+1
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: 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-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, html,
@@ -285,12 +285,12 @@
</LayoutCol> </LayoutCol>
<LayoutCol class="hue-picker" on:pointerdown={onPointerDown} data-hue-picker> <LayoutCol class="hue-picker" on:pointerdown={onPointerDown} data-hue-picker>
{#if !isNone} {#if !isNone}
<div class="selection-pincers" style:top={`${(1 - hue) * 100}%`} /> <div class="selection-needle" style:top={`${(1 - hue) * 100}%`} />
{/if} {/if}
</LayoutCol> </LayoutCol>
<LayoutCol class="alpha-picker" on:pointerdown={onPointerDown} data-alpha-picker> <LayoutCol class="alpha-picker" on:pointerdown={onPointerDown} data-alpha-picker>
{#if !isNone} {#if !isNone}
<div class="selection-pincers" style:top={`${(1 - alpha) * 100}%`} /> <div class="selection-needle" style:top={`${(1 - alpha) * 100}%`} />
{/if} {/if}
</LayoutCol> </LayoutCol>
<LayoutCol class="details"> <LayoutCol class="details">
@@ -434,11 +434,12 @@
background-blend-mode: screen; background-blend-mode: screen;
background: linear-gradient(to top, #ff0000ff 16.666%, #ff000000 33.333%, #ff000000 66.666%, #ff0000ff 83.333%), 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%); 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 { .alpha-picker {
background: linear-gradient(to bottom, var(--opaque-color), transparent); background: linear-gradient(to bottom, var(--opaque-color), transparent);
--selection-needle-color: var(--new-color-contrasting);
&::before { &::before {
content: ""; content: "";
@@ -450,7 +451,6 @@
background-size: var(--color-transparent-checkered-background-size); background-size: var(--color-transparent-checkered-background-size);
background-position: var(--color-transparent-checkered-background-position); background-position: var(--color-transparent-checkered-background-position);
} }
--selection-pincers-color: var(--new-color-contrasting);
} }
.selection-circle { .selection-circle {
@@ -475,7 +475,7 @@
} }
} }
.selection-pincers { .selection-needle {
position: absolute; position: absolute;
top: 0%; top: 0%;
width: 100%; width: 100%;
@@ -489,7 +489,7 @@
left: 0; left: 0;
border-style: solid; border-style: solid;
border-width: 4px 0 4px 4px; 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 { &::after {
@@ -499,7 +499,7 @@
right: 0; right: 0;
border-style: solid; border-style: solid;
border-width: 4px 4px 4px 0; border-width: 4px 4px 4px 0;
border-color: transparent var(--selection-pincers-color) transparent transparent; border-color: transparent var(--selection-needle-color) transparent transparent;
} }
} }
+454 -196
View File
@@ -2,9 +2,8 @@
import { getContext, onMount, tick } from "svelte"; import { getContext, onMount, tick } from "svelte";
import type { IconName } from "@graphite/utility-functions/icons"; import type { IconName } from "@graphite/utility-functions/icons";
import { UpdateNodeGraphSelection } from "@graphite/wasm-communication/messages";
import { UpdateNodeGraphSelection, type FrontendNodeLink, type FrontendNodeType, type FrontendNode } from "@graphite/wasm-communication/messages"; import type { FrontendNodeLink, FrontendNodeType, FrontendNode } from "@graphite/wasm-communication/messages";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte"; import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte"; import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte"; import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
@@ -24,6 +23,8 @@
const editor = getContext<Editor>("editor"); const editor = getContext<Editor>("editor");
const nodeGraph = getContext<NodeGraphState>("nodeGraph"); const nodeGraph = getContext<NodeGraphState>("nodeGraph");
type LinkPath = { pathString: string; dataType: string; thick: boolean };
let graph: LayoutRow | undefined; let graph: LayoutRow | undefined;
let nodesContainer: HTMLDivElement | undefined; let nodesContainer: HTMLDivElement | undefined;
let nodeSearchInput: TextInput | undefined; let nodeSearchInput: TextInput | undefined;
@@ -33,10 +34,10 @@
let selected: bigint[] = []; let selected: bigint[] = [];
let draggingNodes: { startX: number; startY: number; roundX: number; roundY: number } | undefined = undefined; let draggingNodes: { startX: number; startY: number; roundX: number; roundY: number } | undefined = undefined;
let selectIfNotDragged: undefined | bigint = undefined; let selectIfNotDragged: undefined | bigint = undefined;
let linkInProgressFromConnector: HTMLDivElement | undefined = undefined; let linkInProgressFromConnector: SVGSVGElement | undefined = undefined;
let linkInProgressToConnector: HTMLDivElement | DOMRect | undefined = undefined; let linkInProgressToConnector: SVGSVGElement | DOMRect | undefined = undefined;
let disconnecting: { nodeId: bigint; inputIndex: number; linkIndex: number } | undefined = undefined; let disconnecting: { nodeId: bigint; inputIndex: number; linkIndex: number } | undefined = undefined;
let nodeLinkPaths: [string, string][] = []; let nodeLinkPaths: LinkPath[] = [];
let searchTerm = ""; let searchTerm = "";
let nodeListLocation: { x: number; y: number } | undefined = undefined; let nodeListLocation: { x: number; y: number } | undefined = undefined;
@@ -109,14 +110,19 @@
return Array.from(categories); 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) { 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; 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] : []; const optionalTuple = linkPathInProgress ? [linkPathInProgress] : [];
return [...optionalTuple, ...nodeLinkPaths]; return [...optionalTuple, ...nodeLinkPaths];
} }
@@ -126,7 +132,7 @@
await refreshLinks(); 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 outputIndex = Number(link.linkStartOutputIndex);
const inputIndex = Number(link.linkEndInputIndex); const inputIndex = Number(link.linkEndInputIndex);
@@ -134,9 +140,9 @@
const nodeInputConnectors = containerBounds.querySelectorAll(`[data-node="${String(link.linkEnd)}"] [data-port="input"]`) || undefined; const nodeInputConnectors = containerBounds.querySelectorAll(`[data-node="${String(link.linkEnd)}"] [data-port="input"]`) || undefined;
const nodePrimaryOutput = nodeOutputConnectors?.[outputIndex] as HTMLDivElement | undefined; const nodeOutput = nodeOutputConnectors?.[outputIndex] as SVGSVGElement | undefined;
const nodePrimaryInput = nodeInputConnectors?.[inputIndex] as HTMLDivElement | undefined; const nodeInput = nodeInputConnectors?.[inputIndex] as SVGSVGElement | undefined;
return { nodePrimaryOutput, nodePrimaryInput }; return { nodeOutput, nodeInput };
} }
async function refreshLinks(): Promise<void> { async function refreshLinks(): Promise<void> {
@@ -147,11 +153,13 @@
const links = $nodeGraph.links; const links = $nodeGraph.links;
nodeLinkPaths = links.flatMap((link, index) => { nodeLinkPaths = links.flatMap((link, index) => {
const { nodePrimaryInput, nodePrimaryOutput } = resolveLink(link, theNodesContainer); const { nodeInput, nodeOutput } = resolveLink(link, theNodesContainer);
if (!nodePrimaryInput || !nodePrimaryOutput) return []; if (!nodeInput || !nodeOutput) return [];
if (disconnecting?.linkIndex === index) 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}`; 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] { function createWirePath(outputPort: SVGSVGElement, inputPort: SVGSVGElement | DOMRect, verticalOut: boolean, verticalIn: boolean): LinkPath {
const inputPortRect = inputPort instanceof HTMLDivElement ? inputPort.getBoundingClientRect() : inputPort; const inputPortRect = inputPort instanceof DOMRect ? inputPort : inputPort.getBoundingClientRect();
const pathString = buildWirePathString(outputPort.getBoundingClientRect(), inputPortRect, verticalOut, verticalIn); const pathString = buildWirePathString(outputPort.getBoundingClientRect(), inputPortRect, verticalOut, verticalIn);
const dataType = outputPort.getAttribute("data-datatype") || "general"; const dataType = outputPort.getAttribute("data-datatype") || "general";
return [pathString, dataType]; return { pathString, dataType, thick: verticalIn && verticalOut };
} }
function scroll(e: WheelEvent) { function scroll(e: WheelEvent) {
@@ -273,7 +281,7 @@
function pointerDown(e: PointerEvent) { function pointerDown(e: PointerEvent) {
const [lmb, rmb] = [e.button === 0, e.button === 2]; 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 node = (e.target as HTMLElement).closest("[data-node]") as HTMLElement | undefined;
const nodeId = node?.getAttribute("data-node") || undefined; const nodeId = node?.getAttribute("data-node") || undefined;
const nodeList = (e.target as HTMLElement).closest("[data-node-list]") as HTMLElement | undefined; const nodeList = (e.target as HTMLElement).closest("[data-node-list]") as HTMLElement | undefined;
@@ -315,17 +323,19 @@
const inputNodeConnectionIndexSearch = inputNodeInPorts.indexOf(port); const inputNodeConnectionIndexSearch = inputNodeInPorts.indexOf(port);
const inputIndex = inputNodeConnectionIndexSearch > -1 ? inputNodeConnectionIndexSearch : undefined; const inputIndex = inputNodeConnectionIndexSearch > -1 ? inputNodeConnectionIndexSearch : undefined;
// Set the link to draw from the input that a previous link was on // 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 nodeIdInt = BigInt(nodeId);
const inputIndexInt = BigInt(inputIndex); const inputIndexInt = BigInt(inputIndex);
const links = $nodeGraph.links; const links = $nodeGraph.links;
const linkIndex = links.findIndex((value) => value.linkEnd === nodeIdInt && value.linkEndInputIndex === inputIndexInt); 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; if (linkIndex !== -1) {
linkInProgressFromConnector = nodeOutputConnectors?.[Number(links[linkIndex].linkStartOutputIndex)] as HTMLDivElement | undefined; const nodeOutputConnectors = nodesContainer?.querySelectorAll(`[data-node="${String(links[linkIndex].linkStart)}"] [data-port="output"]`) || undefined;
const nodeInputConnectors = nodesContainer?.querySelectorAll(`[data-node="${String(links[linkIndex].linkEnd)}"] [data-port="input"]`) || undefined; linkInProgressFromConnector = nodeOutputConnectors?.[Number(links[linkIndex].linkStartOutputIndex)] as SVGSVGElement | undefined;
linkInProgressToConnector = nodeInputConnectors?.[Number(links[linkIndex].linkEndInputIndex)] as HTMLDivElement | undefined; const nodeInputConnectors = nodesContainer?.querySelectorAll(`[data-node="${String(links[linkIndex].linkEnd)}"] [data-port="input"]`) || undefined;
disconnecting = { nodeId: nodeIdInt, inputIndex, linkIndex }; linkInProgressToConnector = nodeInputConnectors?.[Number(links[linkIndex].linkEndInputIndex)] as SVGSVGElement | undefined;
refreshLinks(); disconnecting = { nodeId: nodeIdInt, inputIndex, linkIndex };
refreshLinks();
}
} }
} }
@@ -354,7 +364,7 @@
draggingNodes = { startX: e.x, startY: e.y, roundX: 0, roundY: 0 }; 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; return;
} }
@@ -384,7 +394,7 @@
transform.y += e.movementY / transform.scale; transform.y += e.movementY / transform.scale;
} else if (linkInProgressFromConnector) { } else if (linkInProgressFromConnector) {
const target = e.target as Element | undefined; 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) { if (dot) {
linkInProgressToConnector = dot; linkInProgressToConnector = dot;
} else { } 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) { function pointerUp(e: PointerEvent) {
panning = false; panning = false;
@@ -409,26 +436,14 @@
} }
disconnecting = undefined; disconnecting = undefined;
if (linkInProgressToConnector instanceof HTMLDivElement && linkInProgressFromConnector) { if (linkInProgressToConnector instanceof SVGSVGElement && linkInProgressFromConnector) {
const outputNode = linkInProgressFromConnector.closest("[data-node]"); const from = connectorToNodeIndex(linkInProgressFromConnector);
const inputNode = linkInProgressToConnector.closest("[data-node]"); const to = connectorToNodeIndex(linkInProgressToConnector);
const outputConnectedNodeID = outputNode?.getAttribute("data-node") ?? undefined; if (from !== undefined && to !== undefined) {
const inputConnectedNodeID = inputNode?.getAttribute("data-node") ?? undefined; const { nodeId: outputConnectedNodeID, index: outputNodeConnectionIndex } = from;
const { nodeId: inputConnectedNodeID, index: inputNodeConnectionIndex } = to;
if (outputNode && inputNode && outputConnectedNodeID && inputConnectedNodeID) { editor.instance.connectNodesByLink(outputConnectedNodeID, outputNodeConnectionIndex, inputConnectedNodeID, inputNodeConnectionIndex);
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);
}
} }
} else if (draggingNodes) { } else if (draggingNodes) {
if (draggingNodes.startX === e.x || draggingNodes.startY === e.y) { 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 // Find the link that the node has been dragged on top of
const link = $nodeGraph.links.find((link): boolean => { const link = $nodeGraph.links.find((link): boolean => {
const { nodePrimaryInput, nodePrimaryOutput } = resolveLink(link, theNodesContainer); const { nodeInput, nodeOutput } = resolveLink(link, theNodesContainer);
if (!nodePrimaryInput || !nodePrimaryOutput) return false; 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 selectedNodeBounds = selectedNode.getBoundingClientRect();
const containerBoundsBounds = theNodesContainer.getBoundingClientRect(); const containerBoundsBounds = theNodesContainer.getBoundingClientRect();
@@ -498,15 +513,20 @@
nodeListLocation = undefined; 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(() => { 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) => { editor.subscriptions.subscribeJsMessage(UpdateNodeGraphSelection, (updateNodeGraphSelection) => {
selected = updateNodeGraphSelection.selected; selected = updateNodeGraphSelection.selected;
}); });
@@ -530,6 +550,9 @@
"--dot-radius": `${dotRadius}px`, "--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} {#if nodeListLocation}
<LayoutCol <LayoutCol
class="node-list" class="node-list"
@@ -559,92 +582,195 @@
</div> </div>
</LayoutCol> </LayoutCol>
{/if} {/if}
<div class="nodes" style:transform={`scale(${transform.scale}) translate(${transform.x}px, ${transform.y}px)`} style:transform-origin={`0 0`} bind:this={nodesContainer}> <!-- Node connection links -->
{#each $nodeGraph.nodes as node (String(node.id))} <div class="wires" style:transform={`scale(${transform.scale}) translate(${transform.x}px, ${transform.y}px)`} style:transform-origin={`0 0`}>
{@const exposedInputsOutputs = [...node.exposedInputs, ...node.outputs.slice(1)]} <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 <div
class="node" class="layer"
class:selected={selected.includes(node.id)} class:selected={selected.includes(node.id)}
class:previewed={node.previewed} class:previewed={node.previewed}
class:disabled={node.disabled} class:disabled={node.disabled}
style:--offset-left={(node.position?.x || 0) + (selected.includes(node.id) ? draggingNodes?.roundX || 0 : 0)} 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:--offset-top={(node.position?.y || 0) + (selected.includes(node.id) ? draggingNodes?.roundY || 0 : 0)}
style:--clip-path-id={`url(#${clipPathId})`}
data-node={node.id} data-node={node.id}
> >
<div class="primary"> <div class="node-chain" />
<div class="ports"> <!-- Layer input port (from left) -->
{#if node.primaryInput} <div class="input ports">
<div <svg
class="input port" xmlns="http://www.w3.org/2000/svg"
data-port="input" viewBox="0 0 8 8"
data-datatype={node.primaryInput} class="port"
style:--data-color={`var(--color-data-${node.primaryInput})`} data-port="input"
style:--data-color-dim={`var(--color-data-${node.primaryInput}-dim)`} data-datatype={node.primaryInput}
> style:--data-color={`var(--color-data-${node.primaryInput})`}
<div /> style:--data-color-dim={`var(--color-data-${node.primaryInput}-dim)`}
</div> >
{/if} <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" />
{#if node.outputs.length > 0} </svg>
<div </div>
class="output port" <div class="thumbnail">
data-port="output" {@html node.thumbnailSvg}
data-datatype={node.outputs[0].dataType} {#if node.primaryOutput}
style:--data-color={`var(--color-data-${node.outputs[0].dataType})`} <svg
style:--data-color-dim={`var(--color-data-${node.outputs[0].dataType}-dim)`} xmlns="http://www.w3.org/2000/svg"
> viewBox="0 0 8 8"
<div /> class="port top"
</div> data-port="output"
{/if} data-datatype={node.primaryOutput.dataType}
</div> style:--data-color={`var(--color-data-${node.primaryOutput.dataType})`}
{#if node.thumbnailSvg} style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType}-dim)`}
{@html node.thumbnailSvg} >
{: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" />
<IconLabel icon={nodeIcon(node.displayName)} /> </svg>
{/if} {/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> <TextLabel>{node.displayName}</TextLabel>
</div> </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} {#if exposedInputsOutputs.length > 0}
<div class="parameters"> <div class="parameters">
{#each exposedInputsOutputs as parameter, index (index)} {#each exposedInputsOutputs as parameter, index}
<div class="parameter"> <div class="parameter expanded">
<div class="ports"> <div class="expand-arrow" />
{#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>
<TextLabel class={index < node.exposedInputs.length ? "name" : "output"}>{parameter.name}</TextLabel> <TextLabel class={index < node.exposedInputs.length ? "name" : "output"}>{parameter.name}</TextLabel>
</div> </div>
{/each} {/each}
</div> </div>
{/if} {/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> </div>
{/each} {/each}
</div> </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> </LayoutRow>
</LayoutCol> </LayoutCol>
@@ -712,6 +838,13 @@
} }
} }
.fade-artwork {
background: var(--color-2-mildblack);
opacity: 0.8;
width: 100%;
height: 100%;
}
.graph { .graph {
position: relative; position: relative;
background: var(--color-2-mildblack); background: var(--color-2-mildblack);
@@ -721,6 +854,11 @@
border-radius: 2px; border-radius: 2px;
overflow: hidden; 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 // 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 { &::before {
content: ""; content: "";
@@ -735,7 +873,7 @@
} }
} }
.nodes, .layers-and-nodes,
.wires { .wires {
position: absolute; position: absolute;
width: 100%; width: 100%;
@@ -753,27 +891,46 @@
path { path {
fill: none; fill: none;
// stroke: var(--color-data-raster-dim);
stroke: var(--data-color-dim); stroke: var(--data-color-dim);
stroke-width: 2px; stroke-width: var(--data-line-width);
} }
} }
} }
&.nodes { &.layers-and-nodes {
.layer,
.node { .node {
position: absolute; position: absolute;
display: flex; display: flex;
flex-direction: column; backdrop-filter: blur(8px) brightness(100% - 33%);
min-width: 120px; left: calc(var(--offset-left) * 24px);
border-radius: 4px; top: calc(var(--offset-top) * 24px);
background: var(--color-4-dimgray);
left: calc((var(--offset-left) + 0.5) * 24px); &::after {
top: calc((var(--offset-top) - 0.5) * 24px); 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 { &.selected {
border: 1px solid var(--color-e-nearwhite); .primary {
margin: -1px; background: rgba(255, 255, 255, 0.15);
}
.parameters {
background: rgba(255, 255, 255, 0.1);
}
} }
&.disabled { &.disabled {
@@ -783,24 +940,170 @@
.icon-label { .icon-label {
fill: var(--color-a-softgray); fill: var(--color-a-softgray);
} }
.expand-arrow::after {
background: var(--icon-expand-collapse-arrow-disabled);
}
} }
&.previewed { &.previewed::after {
outline: 3px solid var(--color-data-vector); 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 { .primary {
display: flex; display: flex;
align-items: center; align-items: center;
position: relative; position: relative;
gap: 4px;
width: 100%; width: 100%;
height: 24px; height: 24px;
background: var(--color-5-dullgray); border-radius: 2px 2px 0 0;
border-radius: 4px; font-style: italic;
background: rgba(255, 255, 255, 0.05);
&.no-parameter-section {
border-radius: 2px;
}
.icon-label { .icon-label {
margin-left: 4px; margin: 0 8px;
} }
.text-label { .text-label {
@@ -818,10 +1121,16 @@
position: relative; position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
width: 100%;
height: 24px; height: 24px;
width: calc(100% - 24px * 2);
margin-left: 24px; &:last-of-type {
margin-right: 24px; border-radius: 0 0 2px 2px;
}
.expand-arrow {
margin-left: 4px;
}
.text-label { .text-label {
width: 100%; 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 { &::before {
left: 0; left: 0;
} }
@@ -851,46 +1149,6 @@
right: 0; 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);
}
}
}
} }
} }
} }
@@ -49,7 +49,7 @@
margin-bottom: 4px; margin-bottom: 4px;
border: 0; border: 0;
border-radius: 4px; border-radius: 4px;
background: var(--color-5-dullgray); background: var(--color-2-mildblack);
.expand-arrow { .expand-arrow {
width: 8px; width: 8px;
@@ -87,7 +87,7 @@
} }
&:hover { &:hover {
background: var(--color-6-lowergray); background: var(--color-4-dimgray);
.expand-arrow::after { .expand-arrow::after {
background: var(--icon-expand-collapse-arrow-hover); background: var(--icon-expand-collapse-arrow-hover);
@@ -98,7 +98,7 @@
} }
+ .body { + .body {
border: 1px solid var(--color-6-lowergray); border: 1px solid var(--color-4-dimgray);
} }
} }
} }
@@ -108,7 +108,7 @@
padding-top: 1px; padding-top: 1px;
margin-top: -1px; margin-top: -1px;
margin-bottom: 4px; margin-bottom: 4px;
border: 1px solid var(--color-5-dullgray); border: 1px solid var(--color-2-mildblack);
border-radius: 0 0 4px 4px; border-radius: 0 0 4px 4px;
overflow: hidden; overflow: hidden;
@@ -14,8 +14,8 @@
const PANEL_SIZES = { const PANEL_SIZES = {
/**/ root: 100, /**/ root: 100,
/* ├── */ content: 80, /* ├── */ content: 80,
/* │ ├── */ document: 80, /* │ ├── */ document: 50,
/* │ └── */ graph: 20, /* │ └── */ graph: 50,
/* └── */ details: 20, /* └── */ details: 20,
/* ├── */ properties: 45, /* ├── */ properties: 45,
/* └── */ layers: 55, /* └── */ layers: 55,
+3 -1
View File
@@ -96,7 +96,9 @@ export class FrontendNode {
readonly exposedInputs!: NodeGraphInput[]; readonly exposedInputs!: NodeGraphInput[];
readonly outputs!: NodeGraphOutput[]; readonly primaryOutput!: NodeGraphOutput | undefined;
readonly exposedOutputs!: NodeGraphOutput[];
@TupleToVec2 @TupleToVec2
readonly position!: XY | undefined; readonly position!: XY | undefined;