Polish user-created subgraph nodes: imports in the Properties panel; reorder/delete/rename imports/exports (#2105)

* Remove imports/exports

* WIP: Autogenerated properties

* WIP: Input based properties

* WIP: Hashmap based input overrides

* Migrate noise pattern node to input properties

* Reorder exports

* Continue migrating properties

* WIP: Improve reorder exports

* Automatically populate all input properties for sub networks

* Complete reorder import and export

* Add widget override to node macro

* Migrate assign colors to input based properties

* WIP: Full node property override

* Node based properties override for proto nodes

* Migrate all node properties to be input based

* Rename imports/exports

* improve UI

* Protonode input valid implementations

* Valid type list

* Small formatting fixes

* Polishing small issues

* Document upgrade

* fix tests

* Upgrade noise pattern node

* remove console log

* Fix upgrade script for Noise Pattern

* Improve the Properties panel representation for graphical data

* Re-export demo art

* Code review

* code review improvements

* Cleanup for node properties overrides

* Reexport demo art

* Fix clippy lints

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Adam Gerhant
2025-01-20 21:13:14 -08:00
committed by GitHub
parent ad68b1e5c8
commit eec0ef761c
38 changed files with 3660 additions and 2006 deletions

View File

@@ -123,8 +123,8 @@
--color-data-vectordata-dim: #4b778c;
--color-data-number: #cbbab4;
--color-data-number-dim: #87736b;
--color-data-graphic: #6b84e8;
--color-data-graphic-dim: #4a557b;
--color-data-group: #6b84e8;
--color-data-group-dim: #4a557b;
--color-data-artboard: #70a898;
--color-data-artboard-dim: #3a6156;

View File

@@ -47,6 +47,69 @@
$: wirePaths = createWirePaths($nodeGraph.wirePathInProgress, nodeWirePaths);
let inputElement: HTMLInputElement;
let hoveringImportIndex: number | undefined = undefined;
let hoveringExportIndex: number | undefined = undefined;
let editingNameImportIndex: number | undefined = undefined;
let editingNameExportIndex: number | undefined = undefined;
let editingNameText = "";
function exportsToEdgeTextInputWidth() {
let exportTextDivs = document.querySelectorAll(`[data-export-text-edge]`);
let exportTextDiv = Array.from(exportTextDivs).find((div) => {
return div.getAttribute("data-index") === String(editingNameExportIndex);
});
if (!graph || !exportTextDiv) return "50px";
let distance = graph.getBoundingClientRect().right - exportTextDiv.getBoundingClientRect().right;
return distance - 15 + "px";
}
function importsToEdgeTextInputWidth() {
let importTextDivs = document.querySelectorAll(`[data-import-text-edge]`);
let importTextDiv = Array.from(importTextDivs).find((div) => {
return div.getAttribute("data-index") === String(editingNameImportIndex);
});
if (!graph || !importTextDiv) return "50px";
let distance = importTextDiv.getBoundingClientRect().left - graph.getBoundingClientRect().left;
return distance - 15 + "px";
}
function setEditingImportNameIndex(index: number, currentName: string) {
focusInput(currentName);
editingNameImportIndex = index;
}
function setEditingExportNameIndex(index: number, currentName: string) {
focusInput(currentName);
editingNameExportIndex = index;
}
function focusInput(currentName: string) {
editingNameText = currentName;
setTimeout(() => {
if (inputElement) {
inputElement.focus();
}
}, 0);
}
function setEditingImportName(event: Event) {
if (editingNameImportIndex !== undefined) {
let text = (event.target as HTMLInputElement)?.value;
editor.handle.setImportName(editingNameImportIndex, text);
editingNameImportIndex = undefined;
}
}
function setEditingExportName(event: Event) {
if (editingNameExportIndex !== undefined) {
let text = (event.target as HTMLInputElement)?.value;
editor.handle.setExportName(editingNameExportIndex, text);
editingNameExportIndex = undefined;
}
}
function calculateGridSpacing(scale: number): number {
const dense = scale * GRID_SIZE;
let sparse = dense;
@@ -267,6 +330,10 @@
return value.resolvedType ? `Resolved Data: ${value.resolvedType}` : `Unresolved Data: ${value.dataType}`;
}
function validTypesText(value: FrontendGraphInput): string {
return `Valid Types:\n${value.validTypes.join(",\n ")}`;
}
function outputConnectedToText(output: FrontendGraphOutput): string {
if (output.connectedTo.length === 0) {
return "Connected to nothing";
@@ -396,6 +463,9 @@
{/each}
<path class="all-nodes-bounding-box" d={$nodeGraph.clickTargets.allNodesBoundingBox} />
<path class="all-nodes-bounding-box" d={$nodeGraph.clickTargets.importExportsBoundingBox} />
{#each $nodeGraph.clickTargets.modifyImportExport as pathString}
<path class="modify-import-export" d={pathString} />
{/each}
</svg>
</div>
{/if}
@@ -439,15 +509,54 @@
<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>
<p class="import-text" style:--offset-left={position.x / 24} style:--offset-top={position.y / 24}>{outputMetadata.name}</p>
<div
class="edit-import-export import"
on:pointerenter={() => (hoveringImportIndex = index)}
on:pointerleave={() => (hoveringImportIndex = undefined)}
style:--offset-left={position.x / 24}
style:--offset-top={position.y / 24}
>
{#if editingNameImportIndex == index}
<input
class="import-text-input"
type="text"
style:width={importsToEdgeTextInputWidth()}
bind:this={inputElement}
bind:value={editingNameText}
on:blur={setEditingImportName}
on:keydown={(e) => e.key === "Enter" && setEditingImportName(e)}
/>
{:else}
<p class="import-text" on:dblclick={() => setEditingImportNameIndex(index, outputMetadata.name)}>{outputMetadata.name}</p>
{/if}
{#if hoveringImportIndex === index || editingNameImportIndex === index}
<IconButton
size={16}
icon={"Remove"}
class="remove-button-import"
data-index={index}
data-import-text-edge
action={() => {
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
}}
/>
<div class="reorder-drag-grip" title="Reorder this import"></div>
{/if}
</div>
{/each}
{#if $nodeGraph.reorderImportIndex !== undefined}
{@const position = {
x: Number($nodeGraph.imports[0].position.x),
y: Number($nodeGraph.imports[0].position.y) + Number($nodeGraph.reorderImportIndex) * 24,
}}
<div class="reorder-bar" style:--offset-left={(position.x - 48) / 24} style:--offset-top={(position.y - 4) / 24} />
{/if}
{#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"}
icon="Add"
action={() => {
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
}}
@@ -474,13 +583,50 @@
<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>
<p class="export-text" style:--offset-left={position.x / 24} style:--offset-top={position.y / 24}>{inputMetadata.name}</p>
<div
class="edit-import-export export"
on:pointerenter={() => (hoveringExportIndex = index)}
on:pointerleave={() => (hoveringExportIndex = undefined)}
style:--offset-left={position.x / 24}
style:--offset-top={position.y / 24}
>
{#if hoveringExportIndex === index || editingNameExportIndex === index}
<div class="reorder-drag-grip" title="Reorder this export"></div>
<IconButton
size={16}
icon={"Remove"}
class="remove-button-export"
data-index={index}
data-export-text-edge
action={() => {
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
}}
/>
{/if}
{#if editingNameExportIndex === index}
<input
type="text"
style:width={exportsToEdgeTextInputWidth()}
bind:this={inputElement}
bind:value={editingNameText}
on:blur={setEditingExportName}
on:keydown={(e) => e.key === "Enter" && setEditingExportName(e)}
/>
{:else}
<p class="export-text" on:dblclick={() => setEditingExportNameIndex(index, inputMetadata.name)}>{inputMetadata.name}</p>
{/if}
</div>
{/each}
{#if $nodeGraph.reorderExportIndex !== undefined}
{@const position = {
x: Number($nodeGraph.exports[0].position.x),
y: Number($nodeGraph.exports[0].position.y) + Number($nodeGraph.reorderExportIndex) * 24,
}}
<div class="reorder-bar" style:--offset-left={position.x / 24} style:--offset-top={(position.y - 4) / 24} />
{/if}
{#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={() => {
@@ -566,7 +712,7 @@
bind:this={inputs[nodeIndex + 1][0]}
>
{#if node.primaryInput}
<title>{`${dataTypeTooltip(node.primaryInput)}\n${inputConnectedToText(node.primaryInput)}`}</title>
<title>{`${dataTypeTooltip(node.primaryInput)}\n${validTypesText(node.primaryInput)}\n${inputConnectedToText(node.primaryInput)}`}</title>
{/if}
{#if node.primaryInput?.connectedTo !== undefined}
<path d="M0,0H8V8L5.479,6.319a2.666,2.666,0,0,0-2.959,0L0,8Z" fill="var(--data-color)" />
@@ -591,7 +737,7 @@
style:--data-color-dim={`var(--color-data-${stackDataInput.dataType.toLowerCase()}-dim)`}
bind:this={inputs[nodeIndex + 1][1]}
>
<title>{`${dataTypeTooltip(stackDataInput)}\n${inputConnectedToText(stackDataInput)}`}</title>
<title>{`${dataTypeTooltip(stackDataInput)}\n${validTypesText(stackDataInput)}\n${inputConnectedToText(stackDataInput)}`}</title>
{#if stackDataInput.connectedTo !== undefined}
<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}
@@ -698,7 +844,7 @@
style:--data-color-dim={`var(--color-data-${node.primaryInput.dataType.toLowerCase()}-dim)`}
bind:this={inputs[nodeIndex + 1][0]}
>
<title>{`${dataTypeTooltip(node.primaryInput)}\n${inputConnectedToText(node.primaryInput)}`}</title>
<title>{`${dataTypeTooltip(node.primaryInput)}\n${validTypesText(node.primaryInput)}\n${inputConnectedToText(node.primaryInput)}`}</title>
{#if node.primaryInput.connectedTo !== undefined}
<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}
@@ -718,7 +864,7 @@
style:--data-color-dim={`var(--color-data-${secondary.dataType.toLowerCase()}-dim)`}
bind:this={inputs[nodeIndex + 1][index + (node.primaryInput ? 1 : 0)]}
>
<title>{`${dataTypeTooltip(secondary)}\n${inputConnectedToText(secondary)}`}</title>
<title>{`${dataTypeTooltip(secondary)}\n${validTypesText(secondary)}\n${inputConnectedToText(secondary)}`}</title>
{#if secondary.connectedTo !== undefined}
<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}
@@ -880,6 +1026,10 @@
.all-nodes-bounding-box {
stroke: purple;
}
.modify-import-export {
stroke: orange;
}
}
}
@@ -918,30 +1068,66 @@
left: calc(var(--offset-left) * 24px);
}
.reorder-bar {
position: absolute;
top: calc(var(--offset-top) * 24px);
left: calc(var(--offset-left) * 24px);
width: 50px;
height: 2px;
background: white;
}
.plus {
margin-top: -4px;
margin-left: -4px;
position: absolute;
top: calc(var(--offset-top) * 24px);
left: calc(var(--offset-left) * 24px);
}
.export-text {
.edit-import-export {
position: absolute;
margin-top: 0;
margin-left: 20px;
display: flex;
align-items: center;
top: calc(var(--offset-top) * 24px);
left: calc(var(--offset-left) * 24px);
}
margin-top: -5px;
height: 24px;
.import-text {
position: absolute;
text-align: right;
top: calc(var(--offset-top) * 24px);
left: calc(var(--offset-left) * 24px);
margin-top: 0;
margin-left: calc(-100px - 2px);
width: 100px;
&.import {
right: calc(100% - var(--offset-left) * 24px);
}
&.export {
left: calc(var(--offset-left) * 24px + 17px);
}
.import-text {
text-align: right;
text-wrap: nowrap;
}
.export-text {
text-wrap: nowrap;
}
.import-text-input {
text-align: right;
}
.remove-button-import {
margin-left: 3px;
}
.remove-button-export {
margin-right: 3px;
}
.reorder-drag-grip {
width: 8px;
height: 24px;
background-position: 2px 8px;
border-radius: 2px;
margin: -6px 0;
background-image: var(--icon-drag-grip-hover);
}
}
}

View File

@@ -73,6 +73,14 @@ export class UpdateInSelectedNetwork extends JsMessage {
readonly inSelectedNetwork!: boolean;
}
export class UpdateImportReorderIndex extends JsMessage {
readonly importIndex!: number | undefined;
}
export class UpdateExportReorderIndex extends JsMessage {
readonly exportIndex!: number | undefined;
}
const LayerWidths = Transform(({ obj }) => obj.layerWidths);
const ChainWidths = Transform(({ obj }) => obj.chainWidths);
const HasLeftInputWire = Transform(({ obj }) => obj.hasLeftInputWire);
@@ -170,6 +178,7 @@ export type FrontendClickTargets = {
readonly iconClickTargets: string[];
readonly allNodesBoundingBox: string;
readonly importExportsBoundingBox: string;
readonly modifyImportExport: string[];
};
export type ContextMenuInformation = {
@@ -178,7 +187,7 @@ export type ContextMenuInformation = {
contextMenuData: "CreateNode" | { nodeId: bigint; currentlyIsNode: boolean };
};
export type FrontendGraphDataType = "General" | "Raster" | "VectorData" | "Number" | "Graphic" | "Artboard";
export type FrontendGraphDataType = "General" | "Raster" | "VectorData" | "Number" | "Group" | "Artboard";
export class Node {
readonly index!: bigint;
@@ -210,6 +219,8 @@ export class FrontendGraphInput {
readonly resolvedType!: string | undefined;
readonly validTypes!: string[];
@CreateOutputConnectorOptional
connectedTo!: Node | undefined;
}
@@ -1592,6 +1603,8 @@ export const messageMakers: Record<string, MessageMaker> = {
UpdateImportsExports,
UpdateInputHints,
UpdateInSelectedNetwork,
UpdateExportReorderIndex,
UpdateImportReorderIndex,
UpdateLayersPanelControlBarLayout,
UpdateLayerWidths,
UpdateMenuBarLayout,

View File

@@ -15,6 +15,8 @@ import {
UpdateClickTargets,
UpdateContextMenuInformation,
UpdateInSelectedNetwork,
UpdateImportReorderIndex,
UpdateExportReorderIndex,
UpdateImportsExports,
UpdateLayerWidths,
UpdateNodeGraph,
@@ -47,6 +49,8 @@ export function createNodeGraphState(editor: Editor) {
selected: [] as bigint[],
transform: { scale: 1, x: 0, y: 0 },
inSelectedNetwork: true,
reorderImportIndex: undefined as number | undefined,
reorderExportIndex: undefined as number | undefined,
});
// Set up message subscriptions on creation
@@ -76,6 +80,18 @@ export function createNodeGraphState(editor: Editor) {
return state;
});
});
editor.subscriptions.subscribeJsMessage(UpdateImportReorderIndex, (updateImportReorderIndex) => {
update((state) => {
state.reorderImportIndex = updateImportReorderIndex.importIndex;
return state;
});
});
editor.subscriptions.subscribeJsMessage(UpdateExportReorderIndex, (updateExportReorderIndex) => {
update((state) => {
state.reorderExportIndex = updateExportReorderIndex.exportIndex;
return state;
});
});
editor.subscriptions.subscribeJsMessage(UpdateImportsExports, (updateImportsExports) => {
update((state) => {
state.imports = updateImportsExports.imports;