Separate graph error diagnostics from frontend node metadata (#3385)

* Separate error popup from node

* Improve context menu data

* Code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Adam Gerhant
2025-11-24 08:55:30 +00:00
committed by GitHub
co-authored by Keavon Chambers
parent 548e0df1a1
commit 7afbeaa1f9
7 changed files with 193 additions and 157 deletions
+90 -95
View File
@@ -27,9 +27,6 @@
let graph: HTMLDivElement | undefined;
// Key value is node id + input/output index
// Imports/Export are stored at a key value of 0
$: gridSpacing = calculateGridSpacing($nodeGraph.transform.scale);
$: gridDotRadius = 1 + Math.floor($nodeGraph.transform.scale - 0.5 + 0.001) / 2;
@@ -115,15 +112,6 @@
return iconMap[icon] || "NodeNodes";
}
function toggleLayerDisplay(displayAsLayer: boolean, toggleId: bigint) {
let node = $nodeGraph.nodes.get(toggleId);
if (node) editor.handle.setToNodeOrLayer(node.id, displayAsLayer);
}
function canBeToggledBetweenNodeAndLayer(toggleDisplayAsLayerNodeId: bigint) {
return $nodeGraph.nodes.get(toggleDisplayAsLayerNodeId)?.canBeLayer || false;
}
function createNode(nodeType: string) {
if ($nodeGraph.contextMenuInformation === undefined) return;
@@ -224,29 +212,30 @@
top: `${$nodeGraph.contextMenuInformation.contextMenuCoordinates.y * $nodeGraph.transform.scale + $nodeGraph.transform.y}px`,
}}
>
{#if typeof $nodeGraph.contextMenuInformation.contextMenuData === "string" && $nodeGraph.contextMenuInformation.contextMenuData === "CreateNode"}
<NodeCatalog on:selectNodeType={(e) => createNode(e.detail)} />
{:else if $nodeGraph.contextMenuInformation.contextMenuData && "compatibleType" in $nodeGraph.contextMenuInformation.contextMenuData}
<NodeCatalog initialSearchTerm={$nodeGraph.contextMenuInformation.contextMenuData.compatibleType || ""} on:selectNodeType={(e) => createNode(e.detail)} />
{:else}
{@const contextMenuData = $nodeGraph.contextMenuInformation.contextMenuData}
{#if $nodeGraph.contextMenuInformation.contextMenuData.type === "CreateNode"}
<NodeCatalog initialSearchTerm={$nodeGraph.contextMenuInformation.contextMenuData.data.compatibleType || ""} on:selectNodeType={(e) => createNode(e.detail)} />
{:else if $nodeGraph.contextMenuInformation.contextMenuData.type === "ModifyNode"}
<LayoutRow class="toggle-layer-or-node">
<TextLabel>Display as</TextLabel>
<RadioInput
selectedIndex={contextMenuData.currentlyIsNode ? 0 : 1}
selectedIndex={$nodeGraph.contextMenuInformation.contextMenuData.data.currentlyIsNode ? 0 : 1}
entries={[
{
value: "node",
label: "Node",
action: () => toggleLayerDisplay(false, contextMenuData.nodeId),
action: () =>
$nodeGraph.contextMenuInformation?.contextMenuData.type === "ModifyNode" &&
editor.handle.setToNodeOrLayer($nodeGraph.contextMenuInformation.contextMenuData.data.nodeId, false),
},
{
value: "layer",
label: "Layer",
action: () => toggleLayerDisplay(true, contextMenuData.nodeId),
action: () =>
$nodeGraph.contextMenuInformation?.contextMenuData.type === "ModifyNode" &&
editor.handle.setToNodeOrLayer($nodeGraph.contextMenuInformation.contextMenuData.data.nodeId, true),
},
]}
disabled={!canBeToggledBetweenNodeAndLayer(contextMenuData.nodeId)}
disabled={!$nodeGraph.contextMenuInformation.contextMenuData.data.canBeLayer}
/>
</LayoutRow>
<Separator type="Section" direction="Vertical" />
@@ -257,6 +246,17 @@
</LayoutCol>
{/if}
{#if $nodeGraph.error}
<div class="node-error-container" style:transform-origin="0 0" style:transform={`translate(${$nodeGraph.transform.x}px, ${$nodeGraph.transform.y}px) scale(${$nodeGraph.transform.scale})`}>
<span class="node-error faded" style:left={`${$nodeGraph.error.position.x}px`} style:top={`${$nodeGraph.error.position.y}px`} transition:fade={FADE_TRANSITION}>
{$nodeGraph.error.error}
</span>
<span class="node-error hover" style:left={`${$nodeGraph.error.position.x}px`} style:top={`${$nodeGraph.error.position.y}px`} transition:fade={FADE_TRANSITION}>
{$nodeGraph.error.error}
</span>
</div>
{/if}
<!-- Click target debug visualizations -->
{#if $nodeGraph.clickTargets}
<div class="click-targets" style:transform-origin="0 0" style:transform={`translate(${$nodeGraph.transform.x}px, ${$nodeGraph.transform.y}px) scale(${$nodeGraph.transform.scale})`}>
@@ -333,7 +333,7 @@
style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 8) / 24}
style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 8) / 24 + index}
>
{#if editingNameImportIndex == index}
{#if editingNameImportIndex === index}
<input
class="import-text-input"
type="text"
@@ -445,7 +445,7 @@
{/if}
{/each}
{#if $nodeGraph.updateImportsExports.addImportExport == true}
{#if $nodeGraph.updateImportsExports.addImportExport}
<div
class="plus"
style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 12) / 24}
@@ -508,10 +508,6 @@
title={`${node.displayName}\n\n${description || ""}`.trim() + (editor.handle.inDevelopmentMode() ? `\n\nNode ID: ${node.id}` : "")}
data-node={node.id}
>
{#if node.errors}
<span class="node-error faded" transition:fade={FADE_TRANSITION} title="" data-node-error>{node.errors}</span>
<span class="node-error hover" transition:fade={FADE_TRANSITION} title="" data-node-error>{node.errors}</span>
{/if}
<div class="thumbnail">
{#if $nodeGraph.thumbnails.has(node.id)}
{@html $nodeGraph.thumbnails.get(node.id)}
@@ -657,10 +653,6 @@
title={`${node.displayName}\n\n${description || ""}`.trim() + (editor.handle.inDevelopmentMode() ? `\n\nNode ID: ${node.id}` : "")}
data-node={node.id}
>
{#if node.errors}
<span class="node-error faded" transition:fade={FADE_TRANSITION} title="" data-node-error>{node.errors}</span>
<span class="node-error hover" transition:fade={FADE_TRANSITION} title="" data-node-error>{node.errors}</span>
{/if}
<!-- Primary row -->
<div class="primary" class:in-selected-network={$nodeGraph.inSelectedNetwork} class:no-secondary-section={exposedInputsOutputs.length === 0}>
<IconLabel icon={nodeIcon(node.reference)} />
@@ -775,7 +767,6 @@
</div>
<!-- Box selection widget -->
<!-- TODO: Make its initial corner stay put (in graph space) when panning around -->
{#if $nodeGraph.box}
<div
class="box-selection"
@@ -837,6 +828,72 @@
}
}
.node-error-container {
position: absolute;
z-index: 1;
.node-error {
position: absolute;
width: max-content;
white-space: pre-wrap;
max-width: 600px;
line-height: 18px;
color: var(--color-2-mildblack);
background: var(--color-error-red);
padding: 8px;
border-radius: 4px;
transition: opacity 0.2s;
opacity: 0.5;
transform: translateY(-100%);
// Tail
&::after {
content: "";
position: absolute;
left: 6px;
bottom: -8px;
width: 0;
height: 0;
border-style: solid;
border-width: 8px 6px 0 6px;
border-color: var(--color-error-red) transparent transparent transparent;
}
&.hover {
opacity: 0;
z-index: 1;
pointer-events: none;
}
&.faded:hover + .hover {
opacity: 1;
}
&.faded:hover {
z-index: 2;
opacity: 1;
-webkit-user-select: text;
user-select: text;
transition:
opacity 0.2s,
z-index 0s 0.2s;
&::selection {
background-color: var(--color-e-nearwhite);
// Target only Safari
@supports (background: -webkit-named-image(i)) {
& {
// Setting an alpha value opts out of Safari's "fancy" (but not visible on dark backgrounds) selection highlight rendering
// https://stackoverflow.com/a/71753552/775283
background-color: rgba(var(--color-e-nearwhite-rgb), calc(254 / 255));
}
}
}
}
}
}
.click-targets {
position: absolute;
pointer-events: none;
@@ -1016,68 +1073,6 @@
// backdrop-filter: blur(4px);
background: rgba(var(--color-0-black-rgb), 0.33);
.node-error {
position: absolute;
width: max-content;
white-space: pre-wrap;
max-width: 600px;
line-height: 18px;
color: var(--color-2-mildblack);
background: var(--color-error-red);
padding: 8px;
border-radius: 4px;
bottom: calc(100% + 12px);
z-index: -1;
transition: opacity 0.2s;
opacity: 0.5;
// Tail
&::after {
content: "";
position: absolute;
left: 6px;
bottom: -8px;
width: 0;
height: 0;
border-style: solid;
border-width: 8px 6px 0 6px;
border-color: var(--color-error-red) transparent transparent transparent;
}
&.hover {
opacity: 0;
z-index: 1;
pointer-events: none;
}
&.faded:hover + .hover {
opacity: 1;
}
&.faded:hover {
z-index: 2;
opacity: 1;
-webkit-user-select: text;
user-select: text;
transition:
opacity 0.2s,
z-index 0s 0.2s;
&::selection {
background-color: var(--color-e-nearwhite);
// Target only Safari
@supports (background: -webkit-named-image(i)) {
& {
// Setting an alpha value opts out of Safari's "fancy" (but not visible on dark backgrounds) selection highlight rendering
// https://stackoverflow.com/a/71753552/775283
background-color: rgba(var(--color-e-nearwhite-rgb), calc(254 / 255));
}
}
}
}
}
&::after {
content: "";
position: absolute;
+18 -23
View File
@@ -33,23 +33,6 @@ export class UpdateClickTargets extends JsMessage {
readonly clickTargets!: FrontendClickTargets | undefined;
}
const ContextTupleToVec2 = Transform((data) => {
if (data.obj.contextMenuInformation === undefined) return undefined;
const contextMenuCoordinates = { x: data.obj.contextMenuInformation.contextMenuCoordinates[0], y: data.obj.contextMenuInformation.contextMenuCoordinates[1] };
let contextMenuData = data.obj.contextMenuInformation.contextMenuData;
if (contextMenuData.ToggleLayer !== undefined) {
contextMenuData = { nodeId: contextMenuData.ToggleLayer.nodeId, currentlyIsNode: contextMenuData.ToggleLayer.currentlyIsNode };
} else if (contextMenuData.CreateNode !== undefined) {
contextMenuData = { type: "CreateNode", compatibleType: contextMenuData.CreateNode.compatibleType };
}
return { contextMenuCoordinates, contextMenuData };
});
export class UpdateContextMenuInformation extends JsMessage {
@ContextTupleToVec2
readonly contextMenuInformation!: ContextMenuInformation | undefined;
}
export class UpdateImportsExports extends JsMessage {
readonly imports!: (FrontendGraphOutput | undefined)[];
@@ -94,6 +77,15 @@ export class UpdateNodeGraphNodes extends JsMessage {
readonly nodes!: FrontendNode[];
}
export class UpdateNodeGraphErrorDiagnostic extends JsMessage {
readonly error!: NodeGraphError | undefined;
}
export class NodeGraphError {
readonly position!: XY;
readonly error!: string;
}
export class UpdateVisibleNodes extends JsMessage {
readonly nodes!: bigint[];
}
@@ -173,9 +165,13 @@ export type FrontendClickTargets = {
export type ContextMenuInformation = {
contextMenuCoordinates: XY;
contextMenuData: "CreateNode" | { type: "CreateNode"; compatibleType: string } | { nodeId: bigint; currentlyIsNode: boolean };
contextMenuData: { type: "CreateNode"; data: { compatibleType: string | undefined } } | { type: "ModifyNode"; data: { canBeLayer: boolean; currentlyIsNode: boolean; nodeId: bigint } };
};
export class UpdateContextMenuInformation extends JsMessage {
readonly contextMenuInformation!: ContextMenuInformation | undefined;
}
export type FrontendGraphDataType = "General" | "Number" | "Artboard" | "Graphic" | "Raster" | "Vector" | "Color" | "Invalid";
export class FrontendGraphInput {
@@ -205,12 +201,12 @@ export class FrontendGraphOutput {
}
export class FrontendNode {
readonly id!: bigint;
readonly isLayer!: boolean;
readonly canBeLayer!: boolean;
readonly id!: bigint;
readonly reference!: string | undefined;
readonly displayName!: string;
@@ -236,9 +232,7 @@ export class FrontendNode {
readonly visible!: boolean;
readonly unlocked!: boolean;
readonly errors!: string | undefined;
readonly locked!: boolean;
}
export class FrontendNodeType {
@@ -1700,6 +1694,7 @@ export const messageMakers: Record<string, MessageMaker> = {
UpdateMouseCursor,
UpdateNodeGraphControlBarLayout,
UpdateNodeGraphNodes,
UpdateNodeGraphErrorDiagnostic,
UpdateNodeGraphSelection,
UpdateNodeGraphTransform,
UpdateNodeGraphWires,
@@ -1,6 +1,7 @@
import { writable } from "svelte/store";
import { type Editor } from "@graphite/editor";
import type { NodeGraphError } from "@graphite/messages";
import {
type Box,
type FrontendClickTargets,
@@ -25,6 +26,7 @@ import {
UpdateNodeGraphTransform,
UpdateNodeThumbnail,
UpdateWirePathInProgress,
UpdateNodeGraphErrorDiagnostic,
} from "@graphite/messages";
export function createNodeGraphState(editor: Editor) {
@@ -32,6 +34,7 @@ export function createNodeGraphState(editor: Editor) {
box: undefined as Box | undefined,
clickTargets: undefined as FrontendClickTargets | undefined,
contextMenuInformation: undefined as ContextMenuInformation | undefined,
error: undefined as NodeGraphError | undefined,
layerWidths: new Map<bigint, number>(),
chainWidths: new Map<bigint, number>(),
hasLeftInputWire: new Map<bigint, boolean>(),
@@ -118,6 +121,12 @@ export function createNodeGraphState(editor: Editor) {
return state;
});
});
editor.subscriptions.subscribeJsMessage(UpdateNodeGraphErrorDiagnostic, (updateNodeGraphErrorDiagnostic) => {
update((state) => {
state.error = updateNodeGraphErrorDiagnostic.error;
return state;
});
});
editor.subscriptions.subscribeJsMessage(UpdateVisibleNodes, (updateVisibleNodes) => {
update((state) => {
state.visibleNodes = new Set<bigint>(updateVisibleNodes.nodes);