mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-05 09:18:12 +08:00
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:
co-authored by
0hypercube
parent
94a0f8282e
commit
3450d638a6
@@ -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",
|
||||||
] }
|
] }
|
||||||
|
|||||||
+2
-2
@@ -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),
|
||||||
|
|||||||
+2
-2
@@ -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),
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user