Add merging nodes into a subgraph with Ctrl+M and basic subgraph signature customization (#2097)

* Merge nodes

* Fix bugs/crashes

* WIP: Debugging

* Fix bugs, add button

* Add imports/exports

* Improve button

* Fix breadcrumbs

* Fix lints and change shortcut key

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Adam Gerhant 2024-11-12 14:27:42 -08:00 committed by GitHub
parent 4c4d559d97
commit 4250f291ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 735 additions and 238 deletions

View file

@ -13,8 +13,10 @@
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 IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
import Separator from "@graphite/components/widgets/labels/Separator.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
const GRID_COLLAPSE_SPACING = 10;
const GRID_SIZE = 24;
@ -309,6 +311,15 @@
});
return connectedNode?.isLayer || false;
}
function zipWithUndefined(arr1: FrontendGraphInput[], arr2: FrontendGraphOutput[]) {
const maxLength = Math.max(arr1.length, arr2.length);
const result = [];
for (let i = 0; i < maxLength; i++) {
result.push([arr1[i], arr2[i]]);
}
return result;
}
</script>
<div
@ -357,6 +368,10 @@
disabled={!canBeToggledBetweenNodeAndLayer(contextMenuData.nodeId)}
/>
</LayoutRow>
<Separator type="Section" direction="Vertical" />
<LayoutRow class="merge-selected-nodes">
<TextButton label="Merge Selected Nodes" action={() => editor.handle.mergeSelectedNodes()} />
</LayoutRow>
{/if}
</LayoutCol>
{/if}
@ -424,6 +439,19 @@
</svg>
<p class="import-text" style:--offset-left={position.x / 24} style:--offset-top={position.y / 24}>{outputMetadata.name}</p>
{/each}
{#if $nodeGraph.addImport !== undefined}
<div class="plus" style:--offset-left={$nodeGraph.addImport.x / 24} style:--offset-top={$nodeGraph.addImport.y / 24}>
<IconButton
class={"visibility"}
data-visibility-button
size={24}
icon={"Add"}
action={() => {
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
}}
/>
</div>
{/if}
{#each $nodeGraph.exports as { inputMetadata, position }, index}
<svg
xmlns="http://www.w3.org/2000/svg"
@ -446,6 +474,19 @@
</svg>
<p class="export-text" style:--offset-left={position.x / 24} style:--offset-top={position.y / 24}>{inputMetadata.name}</p>
{/each}
{#if $nodeGraph.addExport !== undefined}
<div class="plus" style:--offset-left={$nodeGraph.addExport.x / 24} style:--offset-top={$nodeGraph.addExport.y / 24}>
<IconButton
class={"visibility"}
data-visibility-button
size={24}
icon={"Add"}
action={() => {
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
}}
/>
</div>
{/if}
</div>
<!-- Layers and nodes -->
@ -603,7 +644,7 @@
<!-- Nodes -->
{#each Array.from($nodeGraph.nodes.values()).flatMap((node, nodeIndex) => (node.isLayer ? [] : [{ node, nodeIndex }])) as { node, nodeIndex } (nodeIndex)}
{@const exposedInputsOutputs = [...node.exposedInputs, ...node.exposedOutputs]}
{@const exposedInputsOutputs = zipWithUndefined(node.exposedInputs, node.exposedOutputs)}
{@const clipPathId = String(Math.random()).substring(2)}
{@const description = (node.reference && $nodeGraph.nodeDescriptions.get(node.reference)) || undefined}
<div
@ -633,9 +674,11 @@
<!-- Secondary rows -->
{#if exposedInputsOutputs.length > 0}
<div class="secondary" class:in-selected-network={$nodeGraph.inSelectedNetwork}>
{#each exposedInputsOutputs as secondary, index}
<div class={`secondary-row expanded ${index < node.exposedInputs.length ? "input" : "output"}`}>
<TextLabel tooltip={secondary.name}>{secondary.name}</TextLabel>
{#each exposedInputsOutputs as [input, output]}
<div class={`secondary-row expanded ${input !== undefined ? "input" : "output"}`}>
<TextLabel tooltip={input !== undefined ? input.name : output.name}>
{input !== undefined ? input.name : output.name}
</TextLabel>
</div>
{/each}
</div>
@ -796,6 +839,10 @@
line-height: 24px;
margin-right: 8px;
}
.merge-selected-nodes {
justify-content: center;
}
}
.click-targets {
@ -869,6 +916,14 @@
left: calc(var(--offset-left) * 24px);
}
.plus {
margin-top: -4px;
margin-left: -4px;
position: absolute;
top: calc(var(--offset-top) * 24px);
left: calc(var(--offset-left) * 24px);
}
.export-text {
position: absolute;
margin-top: 0;

View file

@ -174,7 +174,7 @@
{/if}
{@const breadcrumbTrailButtons = narrowWidgetProps(component.props, "BreadcrumbTrailButtons")}
{#if breadcrumbTrailButtons}
<BreadcrumbTrailButtons {...exclude(breadcrumbTrailButtons)} action={(index) => widgetValueCommitAndUpdate(index, index)} />
<BreadcrumbTrailButtons {...exclude(breadcrumbTrailButtons)} action={(breadcrumbIndex) => widgetValueCommitAndUpdate(index, breadcrumbIndex)} />
{/if}
{@const textInput = narrowWidgetProps(component.props, "TextInput")}
{#if textInput}

View file

@ -36,6 +36,8 @@ export function createNodeGraphState(editor: Editor) {
hasLeftInputWire: new Map<bigint, boolean>(),
imports: [] as { outputMetadata: FrontendGraphOutput; position: { x: number; y: number } }[],
exports: [] as { inputMetadata: FrontendGraphInput; position: { x: number; y: number } }[],
addImport: undefined as { x: number; y: number } | undefined,
addExport: undefined as { x: number; y: number } | undefined,
nodes: new Map<bigint, FrontendNode>(),
wires: [] as FrontendNodeWire[],
wirePathInProgress: undefined as WirePath | undefined,
@ -80,6 +82,8 @@ export function createNodeGraphState(editor: Editor) {
update((state) => {
state.imports = updateImportsExports.imports;
state.exports = updateImportsExports.exports;
state.addImport = updateImportsExports.addImport;
state.addExport = updateImportsExports.addExport;
return state;
});
});

View file

@ -61,6 +61,12 @@ export class UpdateImportsExports extends JsMessage {
@ExportsToVec2Array
readonly exports!: { inputMetadata: FrontendGraphInput; position: XY }[];
@TupleToVec2
readonly addImport!: XY | undefined;
@TupleToVec2
readonly addExport!: XY | undefined;
}
export class UpdateInSelectedNetwork extends JsMessage {

View file

@ -569,6 +569,13 @@ impl EditorHandle {
self.dispatch(message);
}
/// Merge a group of nodes into a subnetwork
#[wasm_bindgen(js_name = mergeSelectedNodes)]
pub fn merge_nodes(&self) {
let message = NodeGraphMessage::MergeSelectedNodes;
self.dispatch(message);
}
/// Creates a new document node in the node graph
#[wasm_bindgen(js_name = createNode)]
pub fn create_node(&self, node_type: String, x: i32, y: i32) {
@ -763,9 +770,7 @@ impl EditorHandle {
document
.network_interface
.replace_implementation(&node_id, &[], DocumentNodeImplementation::proto("graphene_core::ToArtboardNode"));
document
.network_interface
.add_input(&node_id, &[], TaggedValue::IVec2(glam::IVec2::default()), false, 2, "".to_string());
document.network_interface.add_import(TaggedValue::IVec2(glam::IVec2::default()), false, 2, "".to_string(), &[node_id]);
}
}
}