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:
0HyperCube
2022-11-12 21:23:28 +00:00
committed by Keavon Chambers
parent e8256dd350
commit 504136b61b
37 changed files with 1213 additions and 294 deletions

View File

@@ -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>

View File

@@ -25,10 +25,6 @@
.sections {
flex: 1 1 100%;
.widget-section + .widget-section {
margin-top: 1px;
}
}
}
</style>

View File

@@ -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,
},

View File

@@ -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);