mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-17 07:18:04 +08:00
Add a basic API and rudimentary frontend for node graph layers (#846)
* Node graph API stub * Rename and fix SetInputValue * Get list of links from network * Test populating node graph UI * Node properties * Fix viewport bounds * Slightly change promise usage * A tiny bit of cleanup I did while reading code * Cleanup and work towards hooking up node links in Vue template * Add the brighten colour node * Run cargo fmt * Add to and from hsla * GrayscaleImage node with small perf improvement * Fix gutter panel resizing * Display node links from backend * Add support for connecting node links * Use existing message * Fix formatting error * Add a (currently crashing) brighten node * Replace brighten node with proto node implementation * Add support for connecting node links * Update watch dirs * Add hue shift node * Add create_node function to editor api * Basic insert node UI * Fix broken names * Add log * Fix positioning * Set connector index to 0 * Add properties for Heu shift / brighten * Allow deselecting nodes * Redesign Properties panel collapsible sections Co-authored-by: Keavon Chambers <keavon@keavon.com> Co-authored-by: Dennis Kobert <dennis@kobert.dev>
This commit is contained in:
committed by
Keavon Chambers
parent
e8256dd350
commit
504136b61b
@@ -1,10 +1,16 @@
|
||||
<template>
|
||||
<LayoutCol class="node-graph">
|
||||
<LayoutRow class="options-bar"></LayoutRow>
|
||||
<div class="node-list">
|
||||
<LayoutRow>Nodes:</LayoutRow>
|
||||
<LayoutRow>
|
||||
<TextButton v-for="nodeType in nodeTypes" v-bind:key="String(nodeType)" :label="nodeType.name + ' Node'" :action="() => createNode(nodeType.name)"></TextButton>
|
||||
</LayoutRow>
|
||||
</div>
|
||||
<LayoutRow
|
||||
class="graph"
|
||||
@wheel="(e: WheelEvent) => scroll(e)"
|
||||
ref="graph"
|
||||
@wheel="(e: WheelEvent) => scroll(e)"
|
||||
@pointerdown="(e: PointerEvent) => pointerDown(e)"
|
||||
@pointermove="(e: PointerEvent) => pointerMove(e)"
|
||||
@pointerup="(e: PointerEvent) => pointerUp(e)"
|
||||
@@ -23,21 +29,19 @@
|
||||
transformOrigin: `0 0`,
|
||||
}"
|
||||
>
|
||||
<div class="node" style="--offset-left: 3; --offset-top: 2; --data-color: var(--color-data-raster); --data-color-dim: var(--color-data-raster-dim)">
|
||||
<div class="primary">
|
||||
<div class="ports">
|
||||
<!-- <div class="input port" data-port="input" data-datatype="raster">
|
||||
<div></div>
|
||||
</div> -->
|
||||
<div class="output port" data-port="output" data-datatype="raster">
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeImage'" />
|
||||
<TextLabel>Image</TextLabel>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node" style="--offset-left: 9; --offset-top: 2; --data-color: var(--color-data-raster); --data-color-dim: var(--color-data-raster-dim)">
|
||||
<div
|
||||
v-for="node in nodes"
|
||||
:key="String(node.id)"
|
||||
class="node"
|
||||
:class="{ selected: selected.includes(node.id) }"
|
||||
:style="{
|
||||
'--offset-left': 8 + Number(node.id < 9n ? node.id : node.id - 9n) * 7,
|
||||
'--offset-top': 4 + Number(node.id < 9n ? node.id : node.id - 9n) * 2,
|
||||
'--data-color': 'var(--color-data-raster)',
|
||||
'--data-color-dim': 'var(--color-data-raster-dim)',
|
||||
}"
|
||||
:data-node="node.id"
|
||||
>
|
||||
<div class="primary">
|
||||
<div class="ports">
|
||||
<div class="input port" data-port="input" data-datatype="raster">
|
||||
@@ -47,118 +51,8 @@
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeMask'" />
|
||||
<TextLabel>Mask</TextLabel>
|
||||
</div>
|
||||
<div class="arguments">
|
||||
<div class="argument">
|
||||
<div class="ports">
|
||||
<div class="input port" data-port="input" data-datatype="raster" style="--data-color: var(--color-data-raster); --data-color-dim: var(--color-data-vector-dim)">
|
||||
<div></div>
|
||||
</div>
|
||||
<!-- <div class="output port" data-port="output" data-datatype="raster">
|
||||
<div></div>
|
||||
</div> -->
|
||||
</div>
|
||||
<TextLabel>Stencil</TextLabel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node" style="--offset-left: 15; --offset-top: 2; --data-color: var(--color-data-raster); --data-color-dim: var(--color-data-raster-dim)">
|
||||
<div class="primary">
|
||||
<div class="ports">
|
||||
<!-- <div class="input port" data-port="input" data-datatype="raster">
|
||||
<div></div>
|
||||
</div> -->
|
||||
<div class="output port" data-port="output" data-datatype="raster">
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeTransform'" />
|
||||
<TextLabel>Transform</TextLabel>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node" style="--offset-left: 21; --offset-top: 2; --data-color: var(--color-data-raster); --data-color-dim: var(--color-data-raster-dim)">
|
||||
<div class="primary">
|
||||
<div class="ports">
|
||||
<div class="input port" data-port="input" data-datatype="raster">
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="output port" data-port="output" data-datatype="raster">
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeMotionBlur'" />
|
||||
<TextLabel>Motion Blur</TextLabel>
|
||||
</div>
|
||||
<div class="arguments">
|
||||
<div class="argument">
|
||||
<div class="ports">
|
||||
<div class="input port" data-port="input" data-datatype="raster">
|
||||
<div></div>
|
||||
</div>
|
||||
<!-- <div class="output port" data-port="output" data-datatype="raster">
|
||||
<div></div>
|
||||
</div> -->
|
||||
</div>
|
||||
<TextLabel>Strength</TextLabel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node" style="--offset-left: 2; --offset-top: 5; --data-color: var(--color-data-vector); --data-color-dim: var(--color-data-vector-dim)">
|
||||
<div class="primary">
|
||||
<div class="ports">
|
||||
<!-- <div class="input port" data-port="input" data-datatype="vector">
|
||||
<div></div>
|
||||
</div> -->
|
||||
<div class="output port" data-port="output" data-datatype="vector">
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeShape'" />
|
||||
<TextLabel>Shape</TextLabel>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node" style="--offset-left: 6; --offset-top: 7; --data-color: var(--color-data-raster); --data-color-dim: var(--color-data-raster-dim)">
|
||||
<div class="primary">
|
||||
<div class="ports">
|
||||
<!-- <div class="input port" data-port="input" data-datatype="raster">
|
||||
<div></div>
|
||||
</div> -->
|
||||
<div class="output port" data-port="output" data-datatype="raster">
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeBrushwork'" />
|
||||
<TextLabel>Brushwork</TextLabel>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node" style="--offset-left: 12; --offset-top: 7; --data-color: var(--color-data-raster); --data-color-dim: var(--color-data-raster-dim)">
|
||||
<div class="primary">
|
||||
<div class="ports">
|
||||
<!-- <div class="input port" data-port="input" data-datatype="raster">
|
||||
<div></div>
|
||||
</div> -->
|
||||
<div class="output port" data-port="output" data-datatype="raster">
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeBlur'" />
|
||||
<TextLabel>Blur</TextLabel>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node" style="--offset-left: 12; --offset-top: 9; --data-color: var(--color-data-raster); --data-color-dim: var(--color-data-raster-dim)">
|
||||
<div class="primary">
|
||||
<div class="ports">
|
||||
<!-- <div class="input port" data-port="input" data-datatype="raster">
|
||||
<div></div>
|
||||
</div> -->
|
||||
<div class="output port" data-port="output" data-datatype="raster">
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeGradient'" />
|
||||
<TextLabel>Gradient</TextLabel>
|
||||
<IconLabel :icon="nodeIcon(node.displayName)" />
|
||||
<TextLabel>{{ node.displayName }}</TextLabel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -169,7 +63,14 @@
|
||||
transformOrigin: `0 0`,
|
||||
}"
|
||||
>
|
||||
<svg ref="wiresContainer"></svg>
|
||||
<svg>
|
||||
<path
|
||||
v-for="([pathString, dataType], index) in linkPaths"
|
||||
:key="index"
|
||||
:d="pathString"
|
||||
:style="{ '--data-color': `var(--color-data-${dataType})`, '--data-color-dim': `var(--color-data-${dataType}-dim)` }"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</LayoutRow>
|
||||
</LayoutCol>
|
||||
@@ -178,6 +79,16 @@
|
||||
<style lang="scss">
|
||||
.node-graph {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
.node-list {
|
||||
width: max-content;
|
||||
position: fixed;
|
||||
padding: 20px;
|
||||
margin: 40px 10px;
|
||||
z-index: 3;
|
||||
background-color: var(--color-4-dimgray);
|
||||
}
|
||||
|
||||
.options-bar {
|
||||
height: 32px;
|
||||
@@ -223,6 +134,7 @@
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
|
||||
path {
|
||||
fill: none;
|
||||
@@ -244,6 +156,11 @@
|
||||
left: calc((var(--offset-left) + 0.5) * 24px);
|
||||
top: calc((var(--offset-top) + 0.5) * 24px);
|
||||
|
||||
&.selected {
|
||||
border: 1px solid var(--color-e-nearwhite);
|
||||
margin: -1px;
|
||||
}
|
||||
|
||||
.primary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -344,23 +261,32 @@
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { defineComponent, nextTick } from "vue";
|
||||
|
||||
// import type { FrontendNode } from "@/wasm-communication/messages";
|
||||
|
||||
import type { IconName } from "@/utility-functions/icons";
|
||||
|
||||
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
import TextButton from "@/components/widgets/buttons/TextButton.vue";
|
||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
||||
|
||||
const WHEEL_RATE = 1 / 600;
|
||||
const WHEEL_RATE = (1 / 600) * 3;
|
||||
const GRID_COLLAPSE_SPACING = 10;
|
||||
const GRID_SIZE = 24;
|
||||
|
||||
export default defineComponent({
|
||||
inject: ["nodeGraph", "editor"],
|
||||
data() {
|
||||
return {
|
||||
transform: { scale: 1, x: 0, y: 0 },
|
||||
panning: false,
|
||||
drawing: undefined as { port: HTMLDivElement; output: boolean; path: SVGElement } | undefined,
|
||||
selected: [] as bigint[],
|
||||
linkInProgressFromConnector: undefined as HTMLDivElement | undefined,
|
||||
linkInProgressToConnector: undefined as HTMLDivElement | DOMRect | undefined,
|
||||
nodeLinkPaths: [] as [string, string][],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -377,8 +303,55 @@ export default defineComponent({
|
||||
dotRadius(): number {
|
||||
return 1 + Math.floor(this.transform.scale - 0.5 + 0.001) / 2;
|
||||
},
|
||||
nodes() {
|
||||
return this.nodeGraph.state.nodes;
|
||||
},
|
||||
nodeTypes() {
|
||||
return this.nodeGraph.state.nodeTypes;
|
||||
},
|
||||
linkPathInProgress(): [string, string] | undefined {
|
||||
if (this.linkInProgressFromConnector && this.linkInProgressToConnector) {
|
||||
return this.createWirePath(this.linkInProgressFromConnector, this.linkInProgressToConnector, false, false);
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
linkPaths(): [string, string][] {
|
||||
const linkPathInProgress = this.linkPathInProgress ? [this.linkPathInProgress] : [];
|
||||
return [...linkPathInProgress, ...this.nodeLinkPaths];
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
nodes: {
|
||||
immediate: true,
|
||||
async handler() {
|
||||
await nextTick();
|
||||
|
||||
const containerBounds = this.$refs.nodesContainer as HTMLDivElement | undefined;
|
||||
if (!containerBounds) return;
|
||||
|
||||
const links = this.nodeGraph.state.links;
|
||||
this.nodeLinkPaths = links.flatMap((link) => {
|
||||
const connectorIndex = 0;
|
||||
|
||||
const nodePrimaryOutput = (containerBounds.querySelector(`[data-node="${String(link.linkStart)}"] [data-port="output"]`) || undefined) as HTMLDivElement | undefined;
|
||||
|
||||
const nodeInputConnectors = containerBounds.querySelectorAll(`[data-node="${String(link.linkEnd)}"] [data-port="input"]`) || undefined;
|
||||
const nodePrimaryInput = nodeInputConnectors?.[connectorIndex] as HTMLDivElement | undefined;
|
||||
|
||||
if (!nodePrimaryInput || !nodePrimaryOutput) return [];
|
||||
return [this.createWirePath(nodePrimaryOutput, nodePrimaryInput.getBoundingClientRect(), false, false)];
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
nodeIcon(nodeName: string): IconName {
|
||||
const iconMap: Record<string, IconName> = {
|
||||
Grayscale: "NodeColorCorrection",
|
||||
"Map Image": "NodeOutput",
|
||||
};
|
||||
return iconMap[nodeName] || "NodeNodes";
|
||||
},
|
||||
buildWirePathString(outputBounds: DOMRect, inputBounds: DOMRect, verticalOut: boolean, verticalIn: boolean): string {
|
||||
const containerBounds = (this.$refs.nodesContainer as HTMLDivElement | undefined)?.getBoundingClientRect();
|
||||
if (!containerBounds) return "[error]";
|
||||
@@ -392,7 +365,6 @@ export default defineComponent({
|
||||
const inY = verticalIn ? inputBounds.y + inputBounds.height - 1 : inputBounds.y + inputBounds.height / 2;
|
||||
const inConnectorX = (inX - containerBounds.x) / this.transform.scale;
|
||||
const inConnectorY = (inY - containerBounds.y) / this.transform.scale;
|
||||
// debugger;
|
||||
const horizontalGap = Math.abs(outConnectorX - inConnectorX);
|
||||
const verticalGap = Math.abs(outConnectorY - inConnectorY);
|
||||
|
||||
@@ -408,89 +380,138 @@ export default defineComponent({
|
||||
verticalIn ? inConnectorX : inConnectorX - horizontalCurve
|
||||
},${verticalIn ? inConnectorY + verticalCurve : inConnectorY} ${inConnectorX},${inConnectorY}`;
|
||||
},
|
||||
createWirePath(outputPort: HTMLDivElement, inputPort: HTMLDivElement, verticalOut: boolean, verticalIn: boolean): SVGPathElement {
|
||||
const pathString = this.buildWirePathString(outputPort.getBoundingClientRect(), inputPort.getBoundingClientRect(), verticalOut, verticalIn);
|
||||
const dataType = outputPort.dataset.datatype;
|
||||
createWirePath(outputPort: HTMLDivElement, inputPort: HTMLDivElement | DOMRect, verticalOut: boolean, verticalIn: boolean): [string, string] {
|
||||
const inputPortRect = inputPort instanceof HTMLDivElement ? inputPort.getBoundingClientRect() : inputPort;
|
||||
|
||||
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||
path.setAttribute("d", pathString);
|
||||
path.setAttribute("style", `--data-color: var(--color-data-${dataType}); --data-color-dim: var(--color-data-${dataType}-dim)`);
|
||||
(this.$refs.wiresContainer as SVGSVGElement | undefined)?.appendChild(path);
|
||||
const pathString = this.buildWirePathString(outputPort.getBoundingClientRect(), inputPortRect, verticalOut, verticalIn);
|
||||
const dataType = outputPort.getAttribute("data-datatype") || "general";
|
||||
|
||||
return path;
|
||||
return [pathString, dataType];
|
||||
},
|
||||
scroll(e: WheelEvent) {
|
||||
const scroll = e.deltaY;
|
||||
let zoomFactor = 1 + Math.abs(scroll) * WHEEL_RATE;
|
||||
if (scroll > 0) zoomFactor = 1 / zoomFactor;
|
||||
const scrollX = e.deltaX;
|
||||
const scrollY = e.deltaY;
|
||||
|
||||
const graphDiv: HTMLDivElement | undefined = (this.$refs.graph as typeof LayoutCol | undefined)?.$el;
|
||||
if (!graphDiv) return;
|
||||
const { x, y, width, height } = graphDiv.getBoundingClientRect();
|
||||
// Zoom
|
||||
if (e.ctrlKey) {
|
||||
let zoomFactor = 1 + Math.abs(scrollY) * WHEEL_RATE;
|
||||
if (scrollY > 0) zoomFactor = 1 / zoomFactor;
|
||||
|
||||
this.transform.scale *= zoomFactor;
|
||||
const graphDiv: HTMLDivElement | undefined = (this.$refs.graph as typeof LayoutCol | undefined)?.$el;
|
||||
if (!graphDiv) return;
|
||||
const { x, y, width, height } = graphDiv.getBoundingClientRect();
|
||||
|
||||
const newViewportX = width / zoomFactor;
|
||||
const newViewportY = height / zoomFactor;
|
||||
this.transform.scale *= zoomFactor;
|
||||
|
||||
const deltaSizeX = width - newViewportX;
|
||||
const deltaSizeY = height - newViewportY;
|
||||
const newViewportX = width / zoomFactor;
|
||||
const newViewportY = height / zoomFactor;
|
||||
|
||||
const deltaX = deltaSizeX * ((e.x - x) / width);
|
||||
const deltaY = deltaSizeY * ((e.y - y) / height);
|
||||
const deltaSizeX = width - newViewportX;
|
||||
const deltaSizeY = height - newViewportY;
|
||||
|
||||
this.transform.x -= (deltaX / this.transform.scale) * zoomFactor;
|
||||
this.transform.y -= (deltaY / this.transform.scale) * zoomFactor;
|
||||
const deltaX = deltaSizeX * ((e.x - x) / width);
|
||||
const deltaY = deltaSizeY * ((e.y - y) / height);
|
||||
|
||||
this.transform.x -= (deltaX / this.transform.scale) * zoomFactor;
|
||||
this.transform.y -= (deltaY / this.transform.scale) * zoomFactor;
|
||||
|
||||
// Prevent actually zooming into the page when pinch-zooming on laptop trackpads
|
||||
e.preventDefault();
|
||||
}
|
||||
// Pan
|
||||
else if (!e.shiftKey) {
|
||||
this.transform.x -= scrollX / this.transform.scale;
|
||||
this.transform.y -= scrollY / this.transform.scale;
|
||||
} else {
|
||||
this.transform.x -= scrollY / this.transform.scale;
|
||||
}
|
||||
},
|
||||
pointerDown(e: PointerEvent) {
|
||||
const port = (e.target as HTMLDivElement).closest("[data-port]") as HTMLDivElement;
|
||||
const node = (e.target as HTMLElement).closest("[data-node]") as HTMLElement | undefined;
|
||||
|
||||
if (port) {
|
||||
const output = port.classList.contains("output");
|
||||
const path = this.createWirePath(port, port, false, false);
|
||||
this.drawing = { port, output, path };
|
||||
} else {
|
||||
this.panning = true;
|
||||
}
|
||||
const isOutput = Boolean(port.getAttribute("data-port") === "output");
|
||||
|
||||
const graphDiv: HTMLDivElement | undefined = (this.$refs.graph as typeof LayoutCol | undefined)?.$el;
|
||||
graphDiv?.setPointerCapture(e.pointerId);
|
||||
if (isOutput) this.linkInProgressFromConnector = port;
|
||||
} else {
|
||||
const nodeId = node?.getAttribute("data-node") || undefined;
|
||||
if (nodeId) {
|
||||
const id = BigInt(nodeId);
|
||||
this.editor.instance.selectNodes(new BigUint64Array([id]));
|
||||
this.selected = [id];
|
||||
} else {
|
||||
this.editor.instance.selectNodes(new BigUint64Array([]));
|
||||
this.selected = [];
|
||||
const graphDiv: HTMLDivElement | undefined = (this.$refs.graph as typeof LayoutCol | undefined)?.$el;
|
||||
graphDiv?.setPointerCapture(e.pointerId);
|
||||
|
||||
this.panning = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
pointerMove(e: PointerEvent) {
|
||||
if (this.panning) {
|
||||
this.transform.x += e.movementX / this.transform.scale;
|
||||
this.transform.y += e.movementY / this.transform.scale;
|
||||
} else if (this.drawing) {
|
||||
const mouse = new DOMRect(e.x, e.y);
|
||||
const port = this.drawing.port.getBoundingClientRect();
|
||||
const output = this.drawing.output ? port : mouse;
|
||||
const input = this.drawing.output ? mouse : port;
|
||||
|
||||
const pathString = this.buildWirePathString(output, input, false, false);
|
||||
this.drawing.path.setAttribute("d", pathString);
|
||||
} else if (this.linkInProgressFromConnector) {
|
||||
const target = e.target as Element | undefined;
|
||||
const dot = (target?.closest(`[data-port="input"]`) || undefined) as HTMLDivElement | undefined;
|
||||
if (dot) {
|
||||
this.linkInProgressToConnector = dot;
|
||||
} else {
|
||||
this.linkInProgressToConnector = new DOMRect(e.x, e.y);
|
||||
}
|
||||
}
|
||||
},
|
||||
pointerUp(e: PointerEvent) {
|
||||
const graph: HTMLDivElement | undefined = (this.$refs.graph as typeof LayoutCol | undefined)?.$el;
|
||||
graph?.releasePointerCapture(e.pointerId);
|
||||
|
||||
this.panning = false;
|
||||
this.drawing = undefined;
|
||||
|
||||
if (this.linkInProgressToConnector instanceof HTMLDivElement && this.linkInProgressFromConnector) {
|
||||
const outputNode = this.linkInProgressFromConnector.closest("[data-node]");
|
||||
const inputNode = this.linkInProgressToConnector.closest("[data-node]");
|
||||
|
||||
const outputConnectedNodeID = outputNode?.getAttribute("data-node") ?? undefined;
|
||||
const inputConnectedNodeID = inputNode?.getAttribute("data-node") ?? undefined;
|
||||
|
||||
if (outputNode && inputNode && outputConnectedNodeID && inputConnectedNodeID) {
|
||||
const inputNodeInPorts = Array.from(inputNode.querySelectorAll(`[data-port="input"]`));
|
||||
const inputNodeConnectionIndexSearch = inputNodeInPorts.indexOf(this.linkInProgressToConnector);
|
||||
const inputNodeConnectionIndex = inputNodeConnectionIndexSearch > -1 ? inputNodeConnectionIndexSearch : undefined;
|
||||
|
||||
if (inputNodeConnectionIndex !== undefined) {
|
||||
const oneBasedIndex = inputNodeConnectionIndex + 1;
|
||||
|
||||
this.editor.instance.connectNodesByLink(BigInt(outputConnectedNodeID), BigInt(inputConnectedNodeID), oneBasedIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.linkInProgressFromConnector = undefined;
|
||||
this.linkInProgressToConnector = undefined;
|
||||
},
|
||||
createNode(nodeType: string): void {
|
||||
this.editor.instance.createNode(nodeType);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
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) this.createWirePath(outputPort1, inputPort1, true, true);
|
||||
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) this.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) this.createWirePath(outputPort2, inputPort2, true, false);
|
||||
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) this.createWirePath(outputPort2, inputPort2.getBoundingClientRect(), true, false);
|
||||
},
|
||||
components: {
|
||||
IconLabel,
|
||||
LayoutCol,
|
||||
LayoutRow,
|
||||
TextLabel,
|
||||
TextButton,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -25,10 +25,6 @@
|
||||
|
||||
.sections {
|
||||
flex: 1 1 100%;
|
||||
|
||||
.widget-section + .widget-section {
|
||||
margin-top: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<!-- TODO: Implement collapsable sections with properties system -->
|
||||
<template>
|
||||
<LayoutCol class="widget-section">
|
||||
<button class="header" @click.stop="() => (expanded = !expanded)" tabindex="0">
|
||||
<div class="expand-arrow" :class="{ expanded }"></div>
|
||||
<Separator :type="'Related'" />
|
||||
<button class="header" :class="{ expanded }" @click.stop="() => (expanded = !expanded)" tabindex="0">
|
||||
<div class="expand-arrow"></div>
|
||||
<TextLabel :bold="true">{{ widgetData.name }}</TextLabel>
|
||||
</button>
|
||||
<LayoutCol class="body" v-if="expanded">
|
||||
@@ -17,18 +16,19 @@
|
||||
flex: 0 0 auto;
|
||||
|
||||
.header {
|
||||
text-align: left;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 0 0 24px;
|
||||
border: 0;
|
||||
text-align: left;
|
||||
padding: 0 8px;
|
||||
margin: 0 -4px;
|
||||
background: var(--color-4-dimgray);
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
background: var(--color-5-dullgray);
|
||||
|
||||
.expand-arrow {
|
||||
width: 6px;
|
||||
height: 100%;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
@@ -40,29 +40,61 @@
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 3px 0 3px 6px;
|
||||
border-color: transparent transparent transparent var(--color-e-nearwhite);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--icon-expand-collapse-arrow);
|
||||
}
|
||||
}
|
||||
|
||||
&.expanded::after {
|
||||
border-width: 6px 3px 0 3px;
|
||||
border-color: var(--color-e-nearwhite) transparent transparent transparent;
|
||||
&.expanded {
|
||||
border-radius: 4px 4px 0 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
.expand-arrow::after {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.text-label {
|
||||
height: 18px;
|
||||
margin-left: 8px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--color-6-lowergray);
|
||||
|
||||
.expand-arrow::after {
|
||||
background: var(--icon-expand-collapse-arrow-hover);
|
||||
}
|
||||
|
||||
.text-label {
|
||||
color: var(--color-f-white);
|
||||
}
|
||||
|
||||
+ .body {
|
||||
border: 1px solid var(--color-6-lowergray);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
margin: 0 4px;
|
||||
padding: 0 7px;
|
||||
padding-top: 1px;
|
||||
margin-top: -1px;
|
||||
margin-bottom: 4px;
|
||||
border: 1px solid var(--color-5-dullgray);
|
||||
border-radius: 0 0 4px 4px;
|
||||
|
||||
.widget-row {
|
||||
&:first-child {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
> .text-label:first-of-type {
|
||||
flex: 0 0 30%;
|
||||
text-align: right;
|
||||
@@ -83,7 +115,6 @@ import { isWidgetRow, isWidgetSection, type LayoutGroup, type WidgetSection as W
|
||||
|
||||
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
import Separator from "@/components/widgets/labels/Separator.vue";
|
||||
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
||||
import WidgetRow from "@/components/widgets/WidgetRow.vue";
|
||||
|
||||
@@ -113,7 +144,6 @@ const WidgetSection = defineComponent({
|
||||
components: {
|
||||
LayoutCol,
|
||||
LayoutRow,
|
||||
Separator,
|
||||
TextLabel,
|
||||
WidgetRow,
|
||||
},
|
||||
|
||||
@@ -14,17 +14,17 @@
|
||||
ref="documentPanel"
|
||||
/>
|
||||
</LayoutRow>
|
||||
<LayoutRow class="workspace-grid-resize-gutter" @pointerdown="(e: PointerEvent) => resizePanel(e)" v-if="nodeGraphVisible"></LayoutRow>
|
||||
<LayoutRow class="workspace-grid-resize-gutter" data-gutter-vertical @pointerdown="(e: PointerEvent) => resizePanel(e)" v-if="nodeGraphVisible"></LayoutRow>
|
||||
<LayoutRow class="workspace-grid-subdivision" v-if="nodeGraphVisible">
|
||||
<Panel :panelType="'NodeGraph'" :tabLabels="[{ name: 'Node Graph' }]" :tabActiveIndex="0" />
|
||||
</LayoutRow>
|
||||
</LayoutCol>
|
||||
<LayoutCol class="workspace-grid-resize-gutter" @pointerdown="(e: PointerEvent) => resizePanel(e)"></LayoutCol>
|
||||
<LayoutCol class="workspace-grid-resize-gutter" data-gutter-horizontal @pointerdown="(e: PointerEvent) => resizePanel(e)"></LayoutCol>
|
||||
<LayoutCol class="workspace-grid-subdivision" style="flex-grow: 0.2">
|
||||
<LayoutRow class="workspace-grid-subdivision" style="flex-grow: 402">
|
||||
<Panel :panelType="'Properties'" :tabLabels="[{ name: 'Properties' }]" :tabActiveIndex="0" />
|
||||
</LayoutRow>
|
||||
<LayoutRow class="workspace-grid-resize-gutter" @pointerdown="(e: PointerEvent) => resizePanel(e)"></LayoutRow>
|
||||
<LayoutRow class="workspace-grid-resize-gutter" data-gutter-vertical @pointerdown="(e: PointerEvent) => resizePanel(e)"></LayoutRow>
|
||||
<LayoutRow class="workspace-grid-subdivision" style="flex-grow: 590">
|
||||
<Panel :panelType="'LayerTree'" :tabLabels="[{ name: 'Layer Tree' }]" :tabActiveIndex="0" />
|
||||
</LayoutRow>
|
||||
@@ -95,24 +95,26 @@ export default defineComponent({
|
||||
},
|
||||
methods: {
|
||||
resizePanel(event: PointerEvent) {
|
||||
const gutter = event.target as HTMLDivElement;
|
||||
const nextSibling = gutter.nextElementSibling as HTMLDivElement;
|
||||
const previousSibling = gutter.previousElementSibling as HTMLDivElement;
|
||||
const gutter = (event.target || undefined) as HTMLDivElement | undefined;
|
||||
const nextSibling = (gutter?.nextElementSibling || undefined) as HTMLDivElement | undefined;
|
||||
const previousSibling = (gutter?.previousElementSibling || undefined) as HTMLDivElement | undefined;
|
||||
|
||||
if (!gutter || !nextSibling || !previousSibling) return;
|
||||
|
||||
// Are we resizing horizontally?
|
||||
const horizontal = gutter.classList.contains("layout-col");
|
||||
const isHorizontal = gutter.getAttribute("data-gutter-horizontal") !== null;
|
||||
|
||||
// Get the current size in px of the panels being resized
|
||||
const nextSiblingSize = horizontal ? nextSibling.getBoundingClientRect().width : nextSibling.getBoundingClientRect().height;
|
||||
const previousSiblingSize = horizontal ? previousSibling.getBoundingClientRect().width : previousSibling.getBoundingClientRect().height;
|
||||
const nextSiblingSize = isHorizontal ? nextSibling.getBoundingClientRect().width : nextSibling.getBoundingClientRect().height;
|
||||
const previousSiblingSize = isHorizontal ? previousSibling.getBoundingClientRect().width : previousSibling.getBoundingClientRect().height;
|
||||
|
||||
// Prevent cursor flicker as mouse temporarily leaves the gutter
|
||||
gutter.setPointerCapture(event.pointerId);
|
||||
|
||||
const mouseStart = horizontal ? event.clientX : event.clientY;
|
||||
const mouseStart = isHorizontal ? event.clientX : event.clientY;
|
||||
|
||||
function updatePosition(event: PointerEvent): void {
|
||||
const mouseCurrent = horizontal ? event.clientX : event.clientY;
|
||||
const updatePosition = (event: PointerEvent): void => {
|
||||
const mouseCurrent = isHorizontal ? event.clientX : event.clientY;
|
||||
let mouseDelta = mouseStart - mouseCurrent;
|
||||
|
||||
mouseDelta = Math.max(nextSiblingSize + mouseDelta, MIN_PANEL_SIZE) - nextSiblingSize;
|
||||
@@ -122,15 +124,15 @@ export default defineComponent({
|
||||
previousSibling.style.flexGrow = (previousSiblingSize - mouseDelta).toString();
|
||||
|
||||
window.dispatchEvent(new CustomEvent("resize"));
|
||||
}
|
||||
};
|
||||
|
||||
function cleanup(event: PointerEvent): void {
|
||||
const cleanup = (event: PointerEvent): void => {
|
||||
gutter.releasePointerCapture(event.pointerId);
|
||||
|
||||
document.removeEventListener("pointermove", updatePosition);
|
||||
document.removeEventListener("pointerleave", cleanup);
|
||||
document.removeEventListener("pointerup", cleanup);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("pointermove", updatePosition);
|
||||
document.addEventListener("pointerleave", cleanup);
|
||||
|
||||
Reference in New Issue
Block a user