mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 13:30:48 +00:00
Generalize layers as merge nodes to enable adjustment layers (#1712)
* WIP, backward traversal issues * Fix some tool issues * Remove debugging * Change some indices * WIP: new artboard node * WIP: add artboard node * WIP: Artboard node and create_artboard * WIP: Artboard node implementation complete * WIP: Artboards input for output node * Complete Artboard node * Generalize LayerNodeIdentifier, monitor_nodes support for Artboard node, adjust ResizeArtboard/ClearArtboards, move alias validation to Rust * Fix misaligned artboard click targets * Generalize/clarify create_layer and insert_between * non-negative dimensions for resize_artboard * Show artboards in layer panel * Generalize create_layer for layer output node * Generalize delete_layer/delete_artboard to NodeGraphMessage::DeleteNodes. Fixed upstream flow Iter * remove old primary_input function * Vertical node visuals, remove is_layer function, rename Layer node to Merge node, toggle display as layer exposed_value_count type fix Vertical node visuals, remove is_layer function, rename Layer node to Merge node, toggle display as layer * Fix demo artwork * Layer display context menu * Automatically select artboard, fix warnings * Improvements to context menu and layer invariant enforcement * Remove display_as_layer and update load_structure * Improve load_structure to show more layers, improve FlowIter, improve layer naming, layer rearrangement validation. * Clean up demo artwork using generalized layers * Improve design of Layers panel and graph nodes * MoveSelectedLayersTo rewrite to support generalized layer nodes * Include artboards in deepest_common_ancestor, fix resize_artboard/delete_artboard, sync artboard tool to layer panel * MoveSelectedLayersTo adjustments * Sync non layer node visibility with metadata * Include non layer nodes when moving/creating layer * Fix group layers and get_post_node_with_index * Include non layer nodes in UngroupSelectedLayers * GroupSelected for all selected nodes, UnGroupSelected position adjustments * Add grouping for layers in different folders * Fix hidden layers * Prevent node from connecting to itself, fix undo automatic node insertion, * Fix undo CreateEmptyFolder, fix grouping nested layer nodes * Formatting * Remove test and check if node is layer from network * Fix undo group layers * Check off roadmap * MoveUpstreamSiblingsToChild adjustments * Replace tabs with spaces, remove mut from argument * Final code review pass --------- Co-authored-by: 0hypercube <0hypercube@gmail.com> Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
beb88d280c
commit
8d83fa7079
41 changed files with 1712 additions and 825 deletions
4
frontend/assets/icon-16px-solid/stack.svg
Normal file
4
frontend/assets/icon-16px-solid/stack.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.4,9.7l-1.9-1-1.2.7,2,1.1-6,3.3c-.3.1-.6.1-.9,0l-6-3.3,2-1.1-1.2-.7-1.9,1c-.7.4-.7,1.1,0,1.5l6.6,3.6c.6.3,1.2.3,1.8,0l6.6-3.6c.7-.4.7-1.1,0-1.5Z" />
|
||||
<path d="M7.1,9.9L.6,6.3c-.7-.4-.7-1.1,0-1.5L7.1,1.2c.6-.3,1.2-.3,1.8,0l6.6,3.6c.7.4.7,1.1,0,1.5l-6.6,3.6c-.6.3-1.2.3-1.8,0Z" />
|
||||
</svg>
|
After Width: | Height: | Size: 360 B |
|
@ -34,7 +34,7 @@ TypeScript files which serve as the JS interface to the WASM bindings for the ed
|
|||
|
||||
Instantiates the WASM and editor backend instances. The function `initWasm()` asynchronously constructs and initializes an instance of the WASM bindings JS module provided by wasm-bindgen/wasm-pack. The function `createEditor()` constructs an instance of the editor backend. In theory there could be multiple editor instances sharing the same WASM module instance. The function returns an object where `raw` is the WASM module, `instance` is the editor, and `subscriptions` is the subscription router (described below).
|
||||
|
||||
`initWasm()` occurs in `main.ts` right before the Svelte application exists, then `createEditor()` is run in `Editor.svelte` during the Svelte app's creation. Similarly to the state providers described above, the editor is given via `setContext()` so other components can get it via `getContext` and call functions on `editor.raw`, `editor.instance`, or `editor.subscriptions`.
|
||||
`initWasm()` occurs in `main.ts` right before the Svelte application exists, then `createEditor()` is run in `Editor.svelte` during the Svelte app's creation. Similarly to the state providers described above, the editor is given via `setContext()` so other components can get it via `getContext` and call functions on `editor.raw`, `editor.handle`, or `editor.subscriptions`.
|
||||
|
||||
### Message definitions: `messages.ts`
|
||||
|
||||
|
|
|
@ -138,6 +138,8 @@
|
|||
linear-gradient(45deg, #cccccc 25%, transparent 25%, transparent 75%, #cccccc 75%), linear-gradient(#ffffff, #ffffff);
|
||||
--color-transparent-checkered-background-size: 16px 16px;
|
||||
--color-transparent-checkered-background-position: 0 0, 8px 8px;
|
||||
--color-transparent-checkered-background-size-mini: 8px 8px;
|
||||
--color-transparent-checkered-background-position-mini: 0 0, 4px 4px;
|
||||
|
||||
--background-inactive-stripes: repeating-linear-gradient(
|
||||
-45deg,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import { platformIsMac } from "@graphite/utility-functions/platform";
|
||||
import type { Editor } from "@graphite/wasm-communication/editor";
|
||||
import { defaultWidgetLayout, patchWidgetLayout, UpdateDocumentLayerDetails, UpdateDocumentLayerStructureJs, UpdateLayersPanelOptionsLayout } from "@graphite/wasm-communication/messages";
|
||||
import type { DataBuffer, LayerClassification, LayerPanelEntry } from "@graphite/wasm-communication/messages";
|
||||
import type { DataBuffer, LayerPanelEntry } from "@graphite/wasm-communication/messages";
|
||||
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
|
@ -165,7 +165,7 @@
|
|||
|
||||
const name = (e.target instanceof HTMLInputElement && e.target.value) || "";
|
||||
editor.handle.setLayerName(listing.entry.id, name);
|
||||
listing.entry.name = name;
|
||||
listing.entry.alias = name;
|
||||
}
|
||||
|
||||
async function onEditLayerNameDeselect(listing: LayerListingInfo) {
|
||||
|
@ -174,7 +174,7 @@
|
|||
layers = layers;
|
||||
|
||||
// Set it back to the original name if the user didn't enter a new name
|
||||
if (document.activeElement instanceof HTMLInputElement) document.activeElement.value = listing.entry.name;
|
||||
if (document.activeElement instanceof HTMLInputElement) document.activeElement.value = listing.entry.alias;
|
||||
|
||||
// Deselect the text so it doesn't appear selected while the input field becomes disabled and styled to look like regular text
|
||||
window.getSelection()?.removeAllRanges();
|
||||
|
@ -203,10 +203,6 @@
|
|||
editor.handle.deselectAllLayers();
|
||||
}
|
||||
|
||||
function isNestingLayer(layerClassification: LayerClassification) {
|
||||
return layerClassification === "Folder" || layerClassification === "Artboard";
|
||||
}
|
||||
|
||||
function calculateDragIndex(tree: LayoutCol, clientY: number, select?: () => void): DraggingData {
|
||||
const treeChildren = tree.div()?.children;
|
||||
const treeOffset = tree.div()?.getBoundingClientRect().top;
|
||||
|
@ -248,7 +244,7 @@
|
|||
}
|
||||
// Inserting below current row
|
||||
else if (distance > -closest && distance > -RANGE_TO_INSERT_WITHIN_BOTTOM_FOLDER_NOT_ROOT && distance < 0) {
|
||||
if (isNestingLayer(layer.layerClassification)) {
|
||||
if (layer.childrenAllowed) {
|
||||
insertParentId = layer.id;
|
||||
insertDepth = layer.depth;
|
||||
insertIndex = 0;
|
||||
|
@ -381,7 +377,6 @@
|
|||
classes={{
|
||||
selected: fakeHighlight !== undefined ? fakeHighlight === listing.entry.id : $nodeGraph.selected.includes(listing.entry.id),
|
||||
"insert-folder": (draggingData?.highlightFolder || false) && draggingData?.insertParentId === listing.entry.id,
|
||||
"nesting-layer": isNestingLayer(listing.entry.layerClassification),
|
||||
}}
|
||||
styles={{ "--layer-indent-levels": `${listing.entry.depth - 1}` }}
|
||||
data-layer
|
||||
|
@ -391,32 +386,29 @@
|
|||
on:dragstart={(e) => draggable && dragStart(e, listing)}
|
||||
on:click={(e) => selectLayerWithModifiers(e, listing)}
|
||||
>
|
||||
{#if isNestingLayer(listing.entry.layerClassification)}
|
||||
{#if listing.entry.childrenAllowed}
|
||||
<button
|
||||
class="expand-arrow"
|
||||
class:expanded={listing.entry.expanded}
|
||||
disabled={!listing.entry.hasChildren}
|
||||
disabled={!listing.entry.childrenPresent}
|
||||
on:click|stopPropagation={() => handleExpandArrowClick(listing.entry.id)}
|
||||
tabindex="0"
|
||||
/>
|
||||
{#if listing.entry.layerClassification === "Artboard"}
|
||||
<IconLabel icon="Artboard" class={"layer-type-icon"} />
|
||||
{:else if listing.entry.layerClassification === "Folder"}
|
||||
<IconLabel icon="Folder" class={"layer-type-icon"} />
|
||||
{/if}
|
||||
<div class="thumbnail">
|
||||
{#if $nodeGraph.thumbnails.has(listing.entry.id)}
|
||||
{@html $nodeGraph.thumbnails.get(listing.entry.id)}
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="thumbnail">
|
||||
{#if $nodeGraph.thumbnails.has(listing.entry.id)}
|
||||
{@html $nodeGraph.thumbnails.get(listing.entry.id)}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if listing.entry.name === "Artboard"}
|
||||
<IconLabel icon="Artboard" class={"layer-type-icon"} />
|
||||
{/if}
|
||||
<LayoutRow class="layer-name" on:dblclick={() => onEditLayerName(listing)}>
|
||||
<input
|
||||
data-text-input
|
||||
type="text"
|
||||
value={listing.entry.name}
|
||||
placeholder={listing.entry.layerClassification}
|
||||
value={listing.entry.alias}
|
||||
placeholder={listing.entry.name}
|
||||
disabled={!listing.editingName}
|
||||
on:blur={() => onEditLayerNameDeselect(listing)}
|
||||
on:keydown={(e) => e.key === "Escape" && onEditLayerNameDeselect(listing)}
|
||||
|
@ -503,11 +495,7 @@
|
|||
border-radius: 2px;
|
||||
height: 32px;
|
||||
margin: 0 4px;
|
||||
padding-left: calc(4px + var(--layer-indent-levels) * 16px);
|
||||
|
||||
&.nesting-layer {
|
||||
padding-left: calc(var(--layer-indent-levels) * 16px);
|
||||
}
|
||||
padding-left: calc(var(--layer-indent-levels) * 16px);
|
||||
|
||||
&.selected {
|
||||
background: var(--color-4-dimgray);
|
||||
|
@ -557,17 +545,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
.layer-type-icon {
|
||||
flex: 0 0 auto;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
width: 36px;
|
||||
height: 24px;
|
||||
background: white;
|
||||
margin-left: 4px;
|
||||
border-radius: 2px;
|
||||
flex: 0 0 auto;
|
||||
background: var(--color-transparent-checkered-background);
|
||||
background-size: var(--color-transparent-checkered-background-size-mini);
|
||||
background-position: var(--color-transparent-checkered-background-position-mini);
|
||||
|
||||
&:first-child {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: calc(100% - 4px);
|
||||
|
@ -576,6 +566,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.layer-type-icon {
|
||||
flex: 0 0 auto;
|
||||
margin-left: 8px;
|
||||
margin-right: -4px;
|
||||
}
|
||||
|
||||
.layer-name {
|
||||
flex: 1 1 100%;
|
||||
margin: 0 8px;
|
||||
|
|
|
@ -9,8 +9,10 @@
|
|||
import type { FrontendNodeLink, FrontendNodeType, FrontendNode, FrontendGraphInput, FrontendGraphOutput } from "@graphite/wasm-communication/messages";
|
||||
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
|
||||
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
|
||||
import RadioInput from "@graphite/components/widgets/inputs/RadioInput.svelte";
|
||||
import TextInput from "@graphite/components/widgets/inputs/TextInput.svelte";
|
||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
|
@ -47,7 +49,9 @@
|
|||
let disconnecting: { nodeId: bigint; inputIndex: number; linkIndex: number } | undefined = undefined;
|
||||
let nodeLinkPaths: LinkPath[] = [];
|
||||
let searchTerm = "";
|
||||
let nodeListLocation: { x: number; y: number } | undefined = undefined;
|
||||
let contextMenuOpenCoordinates: { x: number; y: number } | undefined = undefined;
|
||||
let toggleDisplayAsLayerNodeId: bigint | undefined = undefined;
|
||||
let toggleDisplayAsLayerCurrentlyIsNode: boolean = false;
|
||||
|
||||
let inputs: SVGSVGElement[][] = [];
|
||||
let outputs: SVGSVGElement[][] = [];
|
||||
|
@ -58,8 +62,8 @@
|
|||
$: gridSpacing = calculateGridSpacing(transform.scale);
|
||||
$: dotRadius = 1 + Math.floor(transform.scale - 0.5 + 0.001) / 2;
|
||||
$: nodeCategories = buildNodeCategories($nodeGraph.nodeTypes, searchTerm);
|
||||
$: nodeListX = ((nodeListLocation?.x || 0) + transform.x) * transform.scale;
|
||||
$: nodeListY = ((nodeListLocation?.y || 0) + transform.y) * transform.scale;
|
||||
$: contextMenuX = ((contextMenuOpenCoordinates?.x || 0) + transform.x) * transform.scale;
|
||||
$: contextMenuY = ((contextMenuOpenCoordinates?.y || 0) + transform.y) * transform.scale;
|
||||
|
||||
let appearAboveMouse = false;
|
||||
let appearRightOfMouse = false;
|
||||
|
@ -69,8 +73,8 @@
|
|||
if (!bounds) return;
|
||||
const { width, height } = bounds;
|
||||
|
||||
appearRightOfMouse = nodeListX > width - ADD_NODE_MENU_WIDTH;
|
||||
appearAboveMouse = nodeListY > height - ADD_NODE_MENU_HEIGHT;
|
||||
appearRightOfMouse = contextMenuX > width - ADD_NODE_MENU_WIDTH;
|
||||
appearAboveMouse = contextMenuY > height - ADD_NODE_MENU_HEIGHT;
|
||||
})();
|
||||
|
||||
$: linkPathInProgress = createLinkPathInProgress(linkInProgressFromConnector, linkInProgressToConnector);
|
||||
|
@ -127,7 +131,7 @@
|
|||
const to = linkInProgressToConnector instanceof SVGSVGElement ? connectorToNodeIndex(linkInProgressToConnector) : undefined;
|
||||
|
||||
const linkStart = $nodeGraph.nodes.find((n) => n.id === from?.nodeId)?.isLayer || false;
|
||||
const linkEnd = ($nodeGraph.nodes.find((n) => n.id === to?.nodeId)?.isLayer && to?.index !== 0) || false;
|
||||
const linkEnd = ($nodeGraph.nodes.find((n) => n.id === to?.nodeId)?.isLayer && to?.index == 0) || false;
|
||||
return createWirePath(linkInProgressFromConnector, linkInProgressToConnector, linkStart, linkEnd);
|
||||
}
|
||||
return undefined;
|
||||
|
@ -169,7 +173,7 @@
|
|||
if (disconnecting?.linkIndex === index) return [];
|
||||
|
||||
const linkStart = $nodeGraph.nodes.find((n) => n.id === link.linkStart)?.isLayer || false;
|
||||
const linkEnd = ($nodeGraph.nodes.find((n) => n.id === link.linkEnd)?.isLayer && link.linkEndInputIndex !== 0n) || false;
|
||||
const linkEnd = ($nodeGraph.nodes.find((n) => n.id === link.linkEnd)?.isLayer && Number(link.linkEndInputIndex) == 0) || false;
|
||||
|
||||
return [createWirePath(nodeOutput, nodeInput.getBoundingClientRect(), linkStart, linkEnd)];
|
||||
});
|
||||
|
@ -253,8 +257,9 @@
|
|||
|
||||
function createWirePath(outputPort: SVGSVGElement, inputPort: SVGSVGElement | DOMRect, verticalOut: boolean, verticalIn: boolean): LinkPath {
|
||||
const inputPortRect = inputPort instanceof DOMRect ? inputPort : inputPort.getBoundingClientRect();
|
||||
const outputPortRect = outputPort.getBoundingClientRect();
|
||||
|
||||
const pathString = buildWirePathString(outputPort.getBoundingClientRect(), inputPortRect, verticalOut, verticalIn);
|
||||
const pathString = buildWirePathString(outputPortRect, inputPortRect, verticalOut, verticalIn);
|
||||
const dataType = outputPort.getAttribute("data-datatype") || "general";
|
||||
|
||||
return { pathString, dataType, thick: verticalIn && verticalOut };
|
||||
|
@ -310,7 +315,7 @@
|
|||
|
||||
function keydown(e: KeyboardEvent) {
|
||||
if (e.key.toLowerCase() === "escape") {
|
||||
nodeListLocation = undefined;
|
||||
contextMenuOpenCoordinates = undefined;
|
||||
document.removeEventListener("keydown", keydown);
|
||||
linkInProgressFromConnector = undefined;
|
||||
// linkInProgressFromLayerTop = undefined;
|
||||
|
@ -319,7 +324,7 @@
|
|||
}
|
||||
|
||||
function loadNodeList(e: PointerEvent, graphBounds: DOMRect) {
|
||||
nodeListLocation = {
|
||||
contextMenuOpenCoordinates = {
|
||||
x: (e.clientX - graphBounds.x) / transform.scale - transform.x,
|
||||
y: (e.clientY - graphBounds.y) / transform.scale - transform.y,
|
||||
};
|
||||
|
@ -340,23 +345,33 @@
|
|||
const node = (e.target as HTMLElement).closest("[data-node]") as HTMLElement | undefined;
|
||||
const nodeIdString = node?.getAttribute("data-node") || undefined;
|
||||
const nodeId = nodeIdString ? BigInt(nodeIdString) : undefined;
|
||||
const nodeList = (e.target as HTMLElement).closest("[data-node-list]") as HTMLElement | undefined;
|
||||
const contextMenu = (e.target as HTMLElement).closest("[data-context-menu]") as HTMLElement | undefined;
|
||||
|
||||
// Create the add node popup on right click, then exit
|
||||
if (rmb) {
|
||||
toggleDisplayAsLayerNodeId = undefined;
|
||||
|
||||
if (node) {
|
||||
toggleDisplayAsLayerNodeId = nodeId;
|
||||
toggleDisplayAsLayerCurrentlyIsNode = !($nodeGraph.nodes.find((node) => node.id === nodeId)?.isLayer || false);
|
||||
}
|
||||
|
||||
const graphBounds = graph?.getBoundingClientRect();
|
||||
if (!graphBounds) return;
|
||||
|
||||
loadNodeList(e, graphBounds);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user is clicking on the add nodes list, exit here
|
||||
if (lmb && nodeList) return;
|
||||
// If the user is clicking on the add nodes list or context menu, exit here
|
||||
if (lmb && contextMenu) return;
|
||||
|
||||
// Since the user is clicking elsewhere in the graph, ensure the add nodes list is closed
|
||||
if (lmb) {
|
||||
nodeListLocation = undefined;
|
||||
contextMenuOpenCoordinates = undefined;
|
||||
linkInProgressFromConnector = undefined;
|
||||
toggleDisplayAsLayerNodeId = undefined;
|
||||
// linkInProgressFromLayerTop = undefined;
|
||||
// linkInProgressFromLayerBottom = undefined;
|
||||
}
|
||||
|
@ -474,7 +489,7 @@
|
|||
if (panning) {
|
||||
transform.x += e.movementX / transform.scale;
|
||||
transform.y += e.movementY / transform.scale;
|
||||
} else if (linkInProgressFromConnector && !nodeListLocation) {
|
||||
} else if (linkInProgressFromConnector && !contextMenuOpenCoordinates) {
|
||||
const target = e.target as Element | undefined;
|
||||
const dot = (target?.closest(`[data-port="input"]`) || undefined) as SVGSVGElement | undefined;
|
||||
if (dot) {
|
||||
|
@ -545,6 +560,20 @@
|
|||
editor.handle.toggleLayerVisibility(id);
|
||||
}
|
||||
|
||||
function toggleLayerDisplay(displayAsLayer: boolean) {
|
||||
let node = $nodeGraph.nodes.find((node) => node.id === toggleDisplayAsLayerNodeId);
|
||||
if (node !== undefined) {
|
||||
contextMenuOpenCoordinates = undefined;
|
||||
editor.handle.setToNodeOrLayer(node.id, displayAsLayer);
|
||||
toggleDisplayAsLayerCurrentlyIsNode = !($nodeGraph.nodes.find((node) => node.id === toggleDisplayAsLayerNodeId)?.isLayer || false);
|
||||
toggleDisplayAsLayerNodeId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function canBeToggledBetweenNodeAndLayer(toggleDisplayAsLayerNodeId: bigint) {
|
||||
return $nodeGraph.nodes.find((node) => node.id === toggleDisplayAsLayerNodeId)?.canBeLayer || false;
|
||||
}
|
||||
|
||||
function connectorToNodeIndex(svg: SVGSVGElement): { nodeId: bigint; index: number } | undefined {
|
||||
const node = svg.closest("[data-node]");
|
||||
|
||||
|
@ -568,7 +597,7 @@
|
|||
const selectedNodeId = $nodeGraph.selected[0];
|
||||
const selectedNode = nodesContainer?.querySelector(`[data-node="${String(selectedNodeId)}"]`) || undefined;
|
||||
|
||||
// Check that neither the input or output of the selected node are already connected.
|
||||
// Check that neither the primary input or output of the selected node are already connected.
|
||||
const notConnected = $nodeGraph.links.findIndex((link) => link.linkStart === selectedNodeId || (link.linkEnd === selectedNodeId && link.linkEndInputIndex === BigInt(0))) === -1;
|
||||
const input = selectedNode?.querySelector(`[data-port="input"]`) || undefined;
|
||||
const output = selectedNode?.querySelector(`[data-port="output"]`) || undefined;
|
||||
|
@ -589,13 +618,16 @@
|
|||
const selectedNodeBounds = selectedNode.getBoundingClientRect();
|
||||
const containerBoundsBounds = theNodesContainer.getBoundingClientRect();
|
||||
|
||||
return editor.handle.rectangleIntersects(
|
||||
new Float64Array(wireCurveLocations.map((loc) => loc.x)),
|
||||
new Float64Array(wireCurveLocations.map((loc) => loc.y)),
|
||||
selectedNodeBounds.top - containerBoundsBounds.y,
|
||||
selectedNodeBounds.left - containerBoundsBounds.x,
|
||||
selectedNodeBounds.bottom - containerBoundsBounds.y,
|
||||
selectedNodeBounds.right - containerBoundsBounds.x,
|
||||
return (
|
||||
link.linkEnd != selectedNodeId &&
|
||||
editor.handle.rectangleIntersects(
|
||||
new Float64Array(wireCurveLocations.map((loc) => loc.x)),
|
||||
new Float64Array(wireCurveLocations.map((loc) => loc.y)),
|
||||
selectedNodeBounds.top - containerBoundsBounds.y,
|
||||
selectedNodeBounds.left - containerBoundsBounds.x,
|
||||
selectedNodeBounds.bottom - containerBoundsBounds.y,
|
||||
selectedNodeBounds.right - containerBoundsBounds.x,
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -603,8 +635,7 @@
|
|||
if (link) {
|
||||
const isLayer = $nodeGraph.nodes.find((n) => n.id === selectedNodeId)?.isLayer;
|
||||
|
||||
editor.handle.connectNodesByLink(link.linkStart, 0, selectedNodeId, isLayer ? 1 : 0);
|
||||
editor.handle.connectNodesByLink(selectedNodeId, 0, link.linkEnd, Number(link.linkEndInputIndex));
|
||||
editor.handle.insertNodeBetween(link.linkEnd, Number(link.linkEndInputIndex), 0, selectedNodeId, 0, Number(link.linkStartOutputIndex), link.linkStart);
|
||||
if (!isLayer) editor.handle.shiftNode(selectedNodeId);
|
||||
}
|
||||
}
|
||||
|
@ -629,17 +660,17 @@
|
|||
}
|
||||
} else if (linkInProgressFromConnector && !initialDisconnecting) {
|
||||
// If the add node menu is already open, we don't want to open it again
|
||||
if (nodeListLocation) return;
|
||||
if (contextMenuOpenCoordinates) return;
|
||||
|
||||
const graphBounds = graph?.getBoundingClientRect();
|
||||
if (!graphBounds) return;
|
||||
|
||||
// Create the node list, which should set nodeListLocation to a valid value
|
||||
loadNodeList(e, graphBounds);
|
||||
if (!nodeListLocation) return;
|
||||
let nodeListLocation2: { x: number; y: number } = nodeListLocation;
|
||||
if (!contextMenuOpenCoordinates) return;
|
||||
let contextMenuLocation2: { x: number; y: number } = contextMenuOpenCoordinates;
|
||||
|
||||
linkInProgressToConnector = new DOMRect((nodeListLocation2.x + transform.x) * transform.scale + graphBounds.x, (nodeListLocation2.y + transform.y) * transform.scale + graphBounds.y);
|
||||
linkInProgressToConnector = new DOMRect((contextMenuLocation2.x + transform.x) * transform.scale + graphBounds.x, (contextMenuLocation2.y + transform.y) * transform.scale + graphBounds.y);
|
||||
|
||||
return;
|
||||
} else if (draggingNodes) {
|
||||
|
@ -665,13 +696,13 @@
|
|||
}
|
||||
|
||||
function createNode(nodeType: string) {
|
||||
if (!nodeListLocation) return;
|
||||
if (!contextMenuOpenCoordinates) return;
|
||||
|
||||
const inputNodeConnectionIndex = 0;
|
||||
const x = Math.round(nodeListLocation.x / GRID_SIZE);
|
||||
const y = Math.round(nodeListLocation.y / GRID_SIZE) - 1;
|
||||
const x = Math.round(contextMenuOpenCoordinates.x / GRID_SIZE);
|
||||
const y = Math.round(contextMenuOpenCoordinates.y / GRID_SIZE) - 1;
|
||||
const inputConnectedNodeID = editor.handle.createNode(nodeType, x, y);
|
||||
nodeListLocation = undefined;
|
||||
contextMenuOpenCoordinates = undefined;
|
||||
|
||||
if (!linkInProgressFromConnector) return;
|
||||
const from = connectorToNodeIndex(linkInProgressFromConnector);
|
||||
|
@ -702,17 +733,22 @@
|
|||
return borderMask(boxes, nodeWidth, nodeHeight);
|
||||
}
|
||||
|
||||
function layerBorderMask(nodeWidth: number): string {
|
||||
function layerBorderMask(nodeWidthFromThumbnail: number, nodeChainAreaLeftExtension: number): string {
|
||||
const NODE_HEIGHT = 2 * 24;
|
||||
const THUMBNAIL_WIDTH = 72 + 8 * 2;
|
||||
const FUDGE_HEIGHT_BEYOND_LAYER_HEIGHT = 2;
|
||||
|
||||
const nodeWidth = nodeWidthFromThumbnail + nodeChainAreaLeftExtension;
|
||||
|
||||
const boxes: { x: number; y: number; width: number; height: number }[] = [];
|
||||
|
||||
// Left input
|
||||
boxes.push({ x: -8, y: 16, width: 16, height: 16 });
|
||||
if (nodeChainAreaLeftExtension > 0) {
|
||||
boxes.push({ x: -8, y: 16, width: 16, height: 16 });
|
||||
}
|
||||
|
||||
// Thumbnail
|
||||
boxes.push({ x: 28, y: -FUDGE_HEIGHT_BEYOND_LAYER_HEIGHT, width: THUMBNAIL_WIDTH, height: NODE_HEIGHT + FUDGE_HEIGHT_BEYOND_LAYER_HEIGHT * 2 });
|
||||
boxes.push({ x: nodeChainAreaLeftExtension - 8, y: -FUDGE_HEIGHT_BEYOND_LAYER_HEIGHT, width: THUMBNAIL_WIDTH, height: NODE_HEIGHT + FUDGE_HEIGHT_BEYOND_LAYER_HEIGHT * 2 });
|
||||
|
||||
// Right visibility button
|
||||
boxes.push({ x: nodeWidth - 12, y: (NODE_HEIGHT - 24) / 2, width: 24, height: 24 });
|
||||
|
@ -745,33 +781,63 @@
|
|||
style:--dot-radius={`${dotRadius}px`}
|
||||
>
|
||||
<!-- Right click menu for adding nodes -->
|
||||
{#if nodeListLocation}
|
||||
{#if contextMenuOpenCoordinates}
|
||||
<LayoutCol
|
||||
class="node-list"
|
||||
data-node-list
|
||||
class="context-menu"
|
||||
data-context-menu
|
||||
styles={{
|
||||
transform: `translate(${appearRightOfMouse ? -100 : 0}%, ${appearAboveMouse ? -100 : 0}%)`,
|
||||
left: `${nodeListX}px`,
|
||||
top: `${nodeListY}px`,
|
||||
width: `${ADD_NODE_MENU_WIDTH}px`,
|
||||
height: `${ADD_NODE_MENU_HEIGHT}px`,
|
||||
left: `${contextMenuX}px`,
|
||||
top: `${contextMenuY}px`,
|
||||
...(toggleDisplayAsLayerNodeId === undefined
|
||||
? {
|
||||
transform: `translate(${appearRightOfMouse ? -100 : 0}%, ${appearAboveMouse ? -100 : 0}%)`,
|
||||
width: `${ADD_NODE_MENU_WIDTH}px`,
|
||||
height: `${ADD_NODE_MENU_HEIGHT}px`,
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
<TextInput placeholder="Search Nodes..." value={searchTerm} on:value={({ detail }) => (searchTerm = detail)} bind:this={nodeSearchInput} />
|
||||
<div class="list-results" on:wheel|passive|stopPropagation>
|
||||
{#each nodeCategories as nodeCategory}
|
||||
<details open={nodeCategory[1].open}>
|
||||
<summary>
|
||||
<TextLabel>{nodeCategory[0]}</TextLabel>
|
||||
</summary>
|
||||
{#each nodeCategory[1].nodes as nodeType}
|
||||
<TextButton label={nodeType.name} action={() => createNode(nodeType.name)} />
|
||||
{/each}
|
||||
</details>
|
||||
{:else}
|
||||
<TextLabel>No search results</TextLabel>
|
||||
{/each}
|
||||
</div>
|
||||
{#if toggleDisplayAsLayerNodeId === undefined}
|
||||
<TextInput placeholder="Search Nodes..." value={searchTerm} on:value={({ detail }) => (searchTerm = detail)} bind:this={nodeSearchInput} />
|
||||
<div class="list-results" on:wheel|passive|stopPropagation>
|
||||
{#each nodeCategories as nodeCategory}
|
||||
<details open={nodeCategory[1].open}>
|
||||
<summary>
|
||||
<TextLabel>{nodeCategory[0]}</TextLabel>
|
||||
</summary>
|
||||
{#each nodeCategory[1].nodes as nodeType}
|
||||
<TextButton label={nodeType.name} action={() => createNode(nodeType.name)} />
|
||||
{/each}
|
||||
</details>
|
||||
{:else}
|
||||
<TextLabel>No search results</TextLabel>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<LayoutRow class="toggle-layer-or-node">
|
||||
<TextLabel>Display as</TextLabel>
|
||||
<RadioInput
|
||||
selectedIndex={toggleDisplayAsLayerCurrentlyIsNode ? 0 : 1}
|
||||
entries={[
|
||||
{
|
||||
value: "node",
|
||||
label: "Node",
|
||||
action: () => {
|
||||
toggleLayerDisplay(false);
|
||||
},
|
||||
},
|
||||
{
|
||||
value: "layer",
|
||||
label: "Layer",
|
||||
action: () => {
|
||||
toggleLayerDisplay(true);
|
||||
},
|
||||
},
|
||||
]}
|
||||
disabled={!canBeToggledBetweenNodeAndLayer(toggleDisplayAsLayerNodeId)}
|
||||
/>
|
||||
</LayoutRow>
|
||||
{/if}
|
||||
</LayoutCol>
|
||||
{/if}
|
||||
<!-- Node connection links -->
|
||||
|
@ -801,6 +867,7 @@
|
|||
style:--data-color={`var(--color-data-${node.primaryOutput?.dataType || "general"})`}
|
||||
style:--data-color-dim={`var(--color-data-${node.primaryOutput?.dataType || "general"}-dim)`}
|
||||
style:--label-width={labelWidthGridCells}
|
||||
style:--node-chain-area-left-extension={node.exposedInputs.length === 0 ? 0 : 1.5}
|
||||
data-node={node.id}
|
||||
bind:this={nodeElements[nodeIndex]}
|
||||
>
|
||||
|
@ -808,29 +875,6 @@
|
|||
<span class="node-error faded" transition:fade={FADE_TRANSITION} data-node-error>{node.errors}</span>
|
||||
<span class="node-error hover" transition:fade={FADE_TRANSITION} data-node-error>{node.errors}</span>
|
||||
{/if}
|
||||
<div class="node-chain" />
|
||||
<!-- Layer input port (from left) -->
|
||||
<div class="input ports">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 8 8"
|
||||
class="port"
|
||||
data-port="input"
|
||||
data-datatype={node.primaryInput?.dataType}
|
||||
style:--data-color={`var(--color-data-${node.primaryInput?.dataType})`}
|
||||
style:--data-color-dim={`var(--color-data-${node.primaryInput?.dataType}-dim)`}
|
||||
bind:this={inputs[nodeIndex][0]}
|
||||
>
|
||||
{#if node.primaryInput}
|
||||
<title>{`${dataTypeTooltip(node.primaryInput)}\nConnected to ${node.primaryInput?.connected || "nothing"}`}</title>
|
||||
{/if}
|
||||
{#if node.primaryInput?.connected}
|
||||
<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" fill="var(--data-color)" />
|
||||
{: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" fill="var(--data-color-dim)" />
|
||||
{/if}
|
||||
</svg>
|
||||
</div>
|
||||
<div class="thumbnail">
|
||||
{#if $nodeGraph.thumbnails.has(node.id)}
|
||||
{@html $nodeGraph.thumbnails.get(node.id)}
|
||||
|
@ -847,10 +891,10 @@
|
|||
style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType}-dim)`}
|
||||
bind:this={outputs[nodeIndex][0]}
|
||||
>
|
||||
<title>{`${dataTypeTooltip(node.primaryOutput)}\nConnected to ${node.primaryOutput.connected || "nothing"}`}</title>
|
||||
<title>{`${dataTypeTooltip(node.primaryOutput)}\nConnected to ${`${node.primaryOutput.connected}, port index ${node.primaryOutput.connectedIndex}` || "nothing"}`}</title>
|
||||
{#if node.primaryOutput.connected}
|
||||
<path d="M0,6.953l2.521,-1.694a2.649,2.649,0,0,1,2.959,0l2.52,1.694v5.047h-8z" fill="var(--data-color)" />
|
||||
{#if $nodeGraph.nodes.find((n) => n.id === node.primaryOutput?.connected)?.isLayer}
|
||||
{#if Number(node.primaryOutput?.connectedIndex) === 0 && $nodeGraph.nodes.find((n) => n.id === node.primaryOutput?.connected)?.isLayer}
|
||||
<path d="M0,-3.5h8v8l-2.521,-1.681a2.666,2.666,0,0,0,-2.959,0l-2.52,1.681z" fill="var(--data-color-dim)" />
|
||||
{/if}
|
||||
{:else}
|
||||
|
@ -864,15 +908,17 @@
|
|||
viewBox="0 0 8 12"
|
||||
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)`}
|
||||
bind:this={inputs[nodeIndex][1]}
|
||||
data-datatype={node.primaryInput?.dataType}
|
||||
style:--data-color={`var(--color-data-${node.primaryInput?.dataType})`}
|
||||
style:--data-color-dim={`var(--color-data-${node.primaryInput?.dataType}-dim)`}
|
||||
bind:this={inputs[nodeIndex][0]}
|
||||
>
|
||||
<title>{`${dataTypeTooltip(stackDataInput)}\nConnected to ${stackDataInput.connected || "nothing"}`}</title>
|
||||
{#if stackDataInput.connected}
|
||||
{#if node.primaryInput}
|
||||
<title>{`${dataTypeTooltip(node.primaryInput)}\nConnected to ${node.primaryInput?.connected || "nothing"}`}</title>
|
||||
{/if}
|
||||
{#if node.primaryInput?.connected}
|
||||
<path d="M0,0H8V8L5.479,6.319a2.666,2.666,0,0,0-2.959,0L0,8Z" fill="var(--data-color)" />
|
||||
{#if $nodeGraph.nodes.find((n) => n.id === stackDataInput.connected)?.isLayer}
|
||||
{#if $nodeGraph.nodes.find((n) => n.id === node.primaryInput?.connected)?.isLayer}
|
||||
<path d="M0,10.95l2.52,-1.69c0.89,-0.6,2.06,-0.6,2.96,0l2.52,1.69v5.05h-8v-5.05z" fill="var(--data-color-dim)" />
|
||||
{/if}
|
||||
{:else}
|
||||
|
@ -880,10 +926,32 @@
|
|||
{/if}
|
||||
</svg>
|
||||
</div>
|
||||
<!-- Layer input port (from left) -->
|
||||
{#if node.exposedInputs.length > 0}
|
||||
<div class="input ports">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 8 8"
|
||||
class="port"
|
||||
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)`}
|
||||
bind:this={inputs[nodeIndex][1]}
|
||||
>
|
||||
<title>{`${dataTypeTooltip(stackDataInput)}\nConnected to ${stackDataInput.connected || "nothing"}`}</title>
|
||||
{#if stackDataInput.connected}
|
||||
<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" fill="var(--data-color)" />
|
||||
{: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" fill="var(--data-color-dim)" />
|
||||
{/if}
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="details">
|
||||
<!-- TODO: Allow the user to edit the name, just like in the Layers panel -->
|
||||
<span title={editor.handle.inDevelopmentMode() ? `Node ID: ${node.id}` : undefined} bind:offsetWidth={layerNameLabelWidths[String(node.id)]}>
|
||||
{node.alias || "Layer"}
|
||||
{node.alias}
|
||||
</span>
|
||||
</div>
|
||||
<IconButton
|
||||
|
@ -898,7 +966,10 @@
|
|||
<defs>
|
||||
<clipPath id={clipPathId}>
|
||||
<!-- Keep this equation in sync with the equivalent one in the CSS rule for `.layer { width: ... }` below -->
|
||||
<path clip-rule="evenodd" d={layerBorderMask(36 + 72 + 8 + 24 * Math.max(3, labelWidthGridCells) + 8 + 12 + extraWidthToReachGridMultiple)} />
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d={layerBorderMask(72 + 8 + 24 * Math.max(3, labelWidthGridCells) + 8 + 12 + extraWidthToReachGridMultiple, node.exposedInputs.length === 0 ? 0 : 36)}
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
@ -997,7 +1068,7 @@
|
|||
style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType}-dim)`}
|
||||
bind:this={outputs[nodeIndex][0]}
|
||||
>
|
||||
<title>{`${dataTypeTooltip(node.primaryOutput)}\nConnected to ${node.primaryOutput.connected || "nothing"}`}</title>
|
||||
<title>{`${dataTypeTooltip(node.primaryOutput)}\nConnected to ${`${node.primaryOutput.connected}, port index ${node.primaryOutput.connectedIndex}` || "nothing"}`}</title>
|
||||
{#if node.primaryOutput.connected}
|
||||
<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" fill="var(--data-color)" />
|
||||
{:else}
|
||||
|
@ -1016,7 +1087,7 @@
|
|||
style:--data-color-dim={`var(--color-data-${parameter.dataType}-dim)`}
|
||||
bind:this={outputs[nodeIndex][outputIndex + (node.primaryOutput ? 1 : 0)]}
|
||||
>
|
||||
<title>{`${dataTypeTooltip(parameter)}\nConnected to ${parameter.connected || "nothing"}`}</title>
|
||||
<title>{`${dataTypeTooltip(parameter)}\nConnected to ${`${parameter.connected}, port index ${parameter.connectedIndex}` || "nothing"}`}</title>
|
||||
{#if parameter.connected}
|
||||
<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" fill="var(--data-color)" />
|
||||
{:else}
|
||||
|
@ -1077,13 +1148,14 @@
|
|||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
.node-list {
|
||||
.context-menu {
|
||||
width: max-content;
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
padding: 5px;
|
||||
z-index: 3;
|
||||
background-color: var(--color-3-darkgray);
|
||||
border-radius: 4px;
|
||||
|
||||
.text-input {
|
||||
flex: 0 0 auto;
|
||||
|
@ -1093,11 +1165,15 @@
|
|||
.list-results {
|
||||
overflow-y: auto;
|
||||
flex: 1 1 auto;
|
||||
// Together with the `margin-right: 4px;` on `details` below, this keeps a gap between the listings and the scrollbar
|
||||
margin-right: -4px;
|
||||
|
||||
details {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
// Together with the `margin-right: -4px;` on `.list-results` above, this keeps a gap between the listings and the scrollbar
|
||||
margin-right: 4px;
|
||||
|
||||
&[open] summary .text-label::before {
|
||||
transform: rotate(90deg);
|
||||
|
@ -1133,6 +1209,11 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-layer-or-node .text-label {
|
||||
line-height: 24px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.wires {
|
||||
|
@ -1294,10 +1375,12 @@
|
|||
|
||||
.layer {
|
||||
border-radius: 8px;
|
||||
--half-visibility-button: 12px;
|
||||
--extra-width-to-reach-grid-multiple: 8px;
|
||||
--node-chain-area-left-extension: 0;
|
||||
// Keep this equation in sync with the equivalent one in the Svelte template `<clipPath><path d="layerBorderMask(...)" /></clipPath>` above
|
||||
width: calc(36px + 72px + 8px + 24px * Max(3, var(--label-width)) + 8px + var(--half-visibility-button) + var(--extra-width-to-reach-grid-multiple));
|
||||
width: calc(72px + 8px + 24px * Max(3, var(--label-width)) + 8px + 12px + var(--extra-width-to-reach-grid-multiple));
|
||||
padding-left: calc(var(--node-chain-area-left-extension) * 24px);
|
||||
margin-left: calc((1.5 - var(--node-chain-area-left-extension)) * 24px);
|
||||
|
||||
&::after {
|
||||
border: 1px solid var(--color-5-dullgray);
|
||||
|
@ -1309,10 +1392,6 @@
|
|||
background: rgba(66, 66, 66, 0.4);
|
||||
}
|
||||
|
||||
.node-chain {
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
background: var(--color-2-mildblack);
|
||||
border: 1px solid var(--data-color-dim);
|
||||
|
@ -1368,7 +1447,7 @@
|
|||
|
||||
.visibility {
|
||||
position: absolute;
|
||||
right: calc(-1 * var(--half-visibility-button));
|
||||
right: -12px;
|
||||
}
|
||||
|
||||
.visibility,
|
||||
|
|
|
@ -144,6 +144,7 @@ import Reload from "@graphite-frontend/assets/icon-16px-solid/reload.svg";
|
|||
import Rescale from "@graphite-frontend/assets/icon-16px-solid/rescale.svg";
|
||||
import Reset from "@graphite-frontend/assets/icon-16px-solid/reset.svg";
|
||||
import Settings from "@graphite-frontend/assets/icon-16px-solid/settings.svg";
|
||||
import Stack from "@graphite-frontend/assets/icon-16px-solid/stack.svg";
|
||||
import Trash from "@graphite-frontend/assets/icon-16px-solid/trash.svg";
|
||||
import ViewModeNormal from "@graphite-frontend/assets/icon-16px-solid/view-mode-normal.svg";
|
||||
import ViewModeOutline from "@graphite-frontend/assets/icon-16px-solid/view-mode-outline.svg";
|
||||
|
@ -217,6 +218,7 @@ const SOLID_16PX = {
|
|||
Rescale: { svg: Rescale, size: 16 },
|
||||
Reset: { svg: Reset, size: 16 },
|
||||
Settings: { svg: Settings, size: 16 },
|
||||
Stack: { svg: Stack, size: 16 },
|
||||
Trash: { svg: Trash, size: 16 },
|
||||
ViewModeNormal: { svg: ViewModeNormal, size: 16 },
|
||||
ViewModeOutline: { svg: ViewModeOutline, size: 16 },
|
||||
|
|
|
@ -100,11 +100,15 @@ export class FrontendGraphOutput {
|
|||
readonly resolvedType!: string | undefined;
|
||||
|
||||
readonly connected!: bigint | undefined;
|
||||
|
||||
readonly connectedIndex!: bigint | undefined;
|
||||
}
|
||||
|
||||
export class FrontendNode {
|
||||
readonly isLayer!: boolean;
|
||||
|
||||
readonly canBeLayer!: boolean;
|
||||
|
||||
readonly id!: bigint;
|
||||
|
||||
readonly alias!: string;
|
||||
|
@ -606,16 +610,23 @@ export class UpdateDocumentLayerDetails extends JsMessage {
|
|||
}
|
||||
|
||||
export class LayerPanelEntry {
|
||||
id!: bigint;
|
||||
|
||||
name!: string;
|
||||
|
||||
alias!: string;
|
||||
|
||||
@Transform(({ value }: { value: string }) => value || undefined)
|
||||
tooltip!: string | undefined;
|
||||
|
||||
layerClassification!: LayerClassification;
|
||||
childrenAllowed!: boolean;
|
||||
|
||||
childrenPresent!: boolean;
|
||||
|
||||
expanded!: boolean;
|
||||
|
||||
hasChildren!: boolean;
|
||||
@Transform(({ value }: { value: bigint }) => Number(value))
|
||||
depth!: number;
|
||||
|
||||
visible!: boolean;
|
||||
|
||||
|
@ -626,15 +637,8 @@ export class LayerPanelEntry {
|
|||
parentsUnlocked!: boolean;
|
||||
|
||||
parentId!: bigint | undefined;
|
||||
|
||||
id!: bigint;
|
||||
|
||||
@Transform(({ value }: { value: bigint }) => Number(value))
|
||||
depth!: number;
|
||||
}
|
||||
|
||||
export type LayerClassification = "Folder" | "Artboard" | "Layer";
|
||||
|
||||
export class DisplayDialogDismiss extends JsMessage {}
|
||||
|
||||
export class Font {
|
||||
|
|
|
@ -35,6 +35,7 @@ export default defineConfig({
|
|||
svelte({
|
||||
preprocess: [sveltePreprocess()],
|
||||
onwarn(warning, defaultHandler) {
|
||||
// NOTICE: Keep this list in sync with the list in `.vscode/settings.json`
|
||||
const suppressed = ["css-unused-selector", "vite-plugin-svelte-css-no-scopable-elements", "a11y-no-static-element-interactions", "a11y-no-noninteractive-element-interactions"];
|
||||
if (suppressed.includes(warning.code)) return;
|
||||
|
||||
|
|
|
@ -523,8 +523,8 @@ impl EditorHandle {
|
|||
#[wasm_bindgen(js_name = moveLayerInTree)]
|
||||
pub fn move_layer_in_tree(&self, insert_parent_id: Option<u64>, insert_index: Option<usize>) {
|
||||
let insert_parent_id = insert_parent_id.map(NodeId);
|
||||
|
||||
let parent = insert_parent_id.map(LayerNodeIdentifier::new_unchecked).unwrap_or_default();
|
||||
|
||||
let message = DocumentMessage::MoveSelectedLayersTo {
|
||||
parent,
|
||||
insert_index: insert_index.map(|x| x as isize).unwrap_or(-1),
|
||||
|
@ -568,6 +568,30 @@ impl EditorHandle {
|
|||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Inserts node in-between two other nodes
|
||||
#[wasm_bindgen(js_name = insertNodeBetween)]
|
||||
pub fn insert_node_between(
|
||||
&self,
|
||||
post_node_id: u64,
|
||||
post_node_input_index: usize,
|
||||
insert_node_output_index: usize,
|
||||
insert_node_id: u64,
|
||||
insert_node_input_index: usize,
|
||||
pre_node_output_index: usize,
|
||||
pre_node_id: u64,
|
||||
) {
|
||||
let message = NodeGraphMessage::InsertNodeBetween {
|
||||
post_node_id: NodeId(post_node_id),
|
||||
post_node_input_index,
|
||||
insert_node_output_index,
|
||||
insert_node_id: NodeId(insert_node_id),
|
||||
insert_node_input_index,
|
||||
pre_node_output_index,
|
||||
pre_node_id: NodeId(pre_node_id),
|
||||
};
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Shifts the node and its children to stop nodes going on top of each other
|
||||
#[wasm_bindgen(js_name = shiftNode)]
|
||||
pub fn shift_node(&self, node_id: u64) {
|
||||
|
@ -686,6 +710,14 @@ impl EditorHandle {
|
|||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Toggle display type for a layer
|
||||
#[wasm_bindgen(js_name = setToNodeOrLayer)]
|
||||
pub fn set_to_node_or_layer(&self, id: u64, is_layer: bool) {
|
||||
let node_id = NodeId(id);
|
||||
let message = NodeGraphMessage::SetToNodeOrLayer { node_id, is_layer };
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = injectImaginatePollServerStatus)]
|
||||
pub fn inject_imaginate_poll_server_status(&self) {
|
||||
self.dispatch(PortfolioMessage::ImaginatePollServerStatus);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue