From 4753fb6af29ba6661e8557d04985511d7beacf8b Mon Sep 17 00:00:00 2001 From: giswqs Date: Thu, 20 Aug 2026 00:02:15 -0400 Subject: [PATCH 1/3] Label a tool's input port even when it has only one Multi-input tools got a labelled row per port in #1983, but a single-input tool kept a bare dot. A bare dot says a connection goes here without saying what belongs on it, so a user facing Buffer had no more idea what to wire in than they had facing Raster Streams To Vector before its ports were named. Every input port on a tool node is now labelled. The synthetic `input` and `output` nodes stay bare: their single port carries the node's own kind as its name, so labelling it would print "Output" under a card already headed OUTPUT. Output ports are still labelled only when a tool has several, since one result port needs no telling apart. Two sizing corrections that go with it: - CARD_HEADER_HEIGHT was 36, but the rendered header (a 10px uppercase line over a 12px title inside the card's 8px padding) occupies 40. At 36 the first port row overlapped the tool name; that was invisible on a multi-input card because its rows start lower, and only showed up once one-row cards existed. - A labelled card is now always a little taller than a bare one. At the compact 64px a single row's text sat hard against the card's bottom edge. Verified in a browser: Buffer renders as a 70px card reading "Input layer", Clip and Raster Streams To Vector as 84px cards reading their two inputs, and the Input/Output nodes stay bare at 64px. Each label is centred on its dot to the pixel, clears the title, and sits inside the card. Wiring is unaffected: edges still terminate within 1px of their own port row. --- .../model-builder/ModelBuilderPanel.tsx | 50 +++++++++++++------ 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/apps/geolibre-desktop/src/components/processing/model-builder/ModelBuilderPanel.tsx b/apps/geolibre-desktop/src/components/processing/model-builder/ModelBuilderPanel.tsx index e3b1e592da..ba3fc34eda 100644 --- a/apps/geolibre-desktop/src/components/processing/model-builder/ModelBuilderPanel.tsx +++ b/apps/geolibre-desktop/src/components/processing/model-builder/ModelBuilderPanel.tsx @@ -4,6 +4,7 @@ import { useAppStore, type GeoLibreLayer, type ModelGraphNode, + type ModelGraphNodeKind, type ProcessingModel, type ProcessingModelGraph, } from "@geolibre/core"; @@ -157,30 +158,46 @@ function createId(): string { } /** Where a port's connector dot sits, in canvas coordinates. */ -/** Vertical room the provider line and the tool name take on a card. */ -const CARD_HEADER_HEIGHT = 36; +/** + * Vertical room the provider line and the tool name take on a card. Measured + * against the rendered header (a 10px uppercase line over a 12px title, inside + * the card's 8px padding), so the first port row starts below it rather than + * on top of the name. + */ +const CARD_HEADER_HEIGHT = 40; /** Height of one labelled port row. */ const PORT_ROW_HEIGHT = 18; /** * Card geometry for one node. * - * A side carrying more than one port gets a labelled row per port: two bare - * dots on the edge of a card are indistinguishable, so a tool like Raster - * Streams To Vector gave no way to tell its `d8_pntr` input from its `streams` - * one without hovering each in turn. Cards grow to fit those rows; a - * single-port side keeps the compact card and centres its dot as before. + * Every input port on a *tool* is labelled, including a lone one: a bare dot + * says a connection goes here but not what belongs on it, so a single-input + * tool left the user guessing what it wanted just as a multi-input one left + * them guessing which dot was which. Cards grow to fit the rows when they need + * to; one input row still fits the compact card. + * + * The synthetic `input` and `output` nodes are excluded: their single port + * carries the node's own kind as its name, so labelling it would print + * "Output" under a card already headed OUTPUT. Output ports are labelled only + * when a tool has several, since one result port needs no telling apart. */ -function cardLayout(ports: { - inputs: { id: string; label: string }[]; - outputs: { id: string; label: string }[]; -}): { height: number; labelIn: boolean; labelOut: boolean } { - const labelIn = ports.inputs.length > 1; +function cardLayout( + ports: { + inputs: { id: string; label: string }[]; + outputs: { id: string; label: string }[]; + }, + kind: ModelGraphNodeKind, +): { height: number; labelIn: boolean; labelOut: boolean } { + const labelIn = kind === "tool" && ports.inputs.length > 0; const labelOut = ports.outputs.length > 1; const rows = Math.max(ports.inputs.length, ports.outputs.length); + // A labelled card is always a little taller than a bare one: at the compact + // height a single row's text sits hard against the title above and the card + // edge below. const height = labelIn || labelOut - ? Math.max(NODE_HEIGHT, CARD_HEADER_HEIGHT + rows * PORT_ROW_HEIGHT + 6) + ? Math.max(NODE_HEIGHT + 6, CARD_HEADER_HEIGHT + rows * PORT_ROW_HEIGHT + 8) : NODE_HEIGHT; return { height, labelIn, labelOut }; } @@ -1130,7 +1147,8 @@ export function ModelBuilderPanel({ height: Math.max( acc.height, node.y + - cardLayout(portsOf(node, resolveDescriptor(node.provider, node.toolId))).height + + cardLayout(portsOf(node, resolveDescriptor(node.provider, node.toolId)), node.kind) + .height + 80, ), }), @@ -1729,7 +1747,7 @@ function GraphEdges({ if (index < 0) return null; // Same geometry the card uses, or a labelled multi-port node would draw its // curves to where the dots used to be. - const layout = cardLayout(ports); + const layout = cardLayout(ports, node.kind); return portPosition( node, index, @@ -1839,7 +1857,7 @@ const GraphNodeCard = memo(function GraphNodeCard({ }): ReactElement { const { t } = useTranslation(); const ports = portsOf(node, descriptor); - const layout = cardLayout(ports); + const layout = cardLayout(ports, node.kind); const title = node.kind === "input" ? (layers.find((layer) => layer.id === node.layerId)?.name ?? From ac50ba5c2bba2300e30f9535b7cb48f7da393c06 Mon Sep 17 00:00:00 2001 From: giswqs Date: Thu, 20 Aug 2026 00:14:24 -0400 Subject: [PATCH 2/3] Address Claude review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Keep a card's input and output dots on the same vertical band. Labelling every tool input moved the input side to row-based positions while an unlabelled single output stayed centred on the whole card, so a 1-in/1-out tool — the most common shape there is — had its two dots 14px apart and drew a visibly bent edge through a straight two-node pipeline. Both sides now share one port band: a labelled side fills it row by row, an unlabelled side spreads its dots down the same band instead of down the whole card. This goes slightly further than the suggestion of pinning the unlabelled side to the labelled side's first row. Centring on the band degenerates to exactly that for one row, and for a multi-input tool it puts the lone output midway between the inputs rather than level with the topmost one, which is how the edges want to meet it. Measured in a browser, dot offsets from the card top: before Buffer in=50 out=36 | Clip in=50,68 out=43 after Buffer in=50 out=50 | Clip in=50,68 out=59 The Input/Output data nodes are untouched at out=33. Edges still terminate within 1px of their own port row, and the labels still clear the title and sit inside the card. --- .../model-builder/ModelBuilderPanel.tsx | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/apps/geolibre-desktop/src/components/processing/model-builder/ModelBuilderPanel.tsx b/apps/geolibre-desktop/src/components/processing/model-builder/ModelBuilderPanel.tsx index ba3fc34eda..04720983aa 100644 --- a/apps/geolibre-desktop/src/components/processing/model-builder/ModelBuilderPanel.tsx +++ b/apps/geolibre-desktop/src/components/processing/model-builder/ModelBuilderPanel.tsx @@ -188,7 +188,7 @@ function cardLayout( outputs: { id: string; label: string }[]; }, kind: ModelGraphNodeKind, -): { height: number; labelIn: boolean; labelOut: boolean } { +): { height: number; labelIn: boolean; labelOut: boolean; band: PortBand } { const labelIn = kind === "tool" && ports.inputs.length > 0; const labelOut = ports.outputs.length > 1; const rows = Math.max(ports.inputs.length, ports.outputs.length); @@ -199,7 +199,21 @@ function cardLayout( labelIn || labelOut ? Math.max(NODE_HEIGHT + 6, CARD_HEADER_HEIGHT + rows * PORT_ROW_HEIGHT + 8) : NODE_HEIGHT; - return { height, labelIn, labelOut }; + // Both sides share one vertical band. A labelled side fills it row by row; an + // unlabelled side spreads its dots down the same band rather than down the + // whole card, so the lone output of a labelled tool still lines up with its + // inputs instead of floating up next to the header. + const band: PortBand = + labelIn || labelOut + ? { top: CARD_HEADER_HEIGHT, height: rows * PORT_ROW_HEIGHT } + : { top: 0, height: NODE_HEIGHT }; + return { height, labelIn, labelOut, band }; +} + +/** The vertical span of a card given over to its port connectors. */ +interface PortBand { + top: number; + height: number; } /** @@ -214,18 +228,15 @@ function portPosition( index: number, count: number, side: "in" | "out", - height: number = NODE_HEIGHT, + band: PortBand = { top: 0, height: NODE_HEIGHT }, labelled = false, ): { x: number; y: number } { const x = side === "in" ? node.x : node.x + NODE_WIDTH; if (labelled) { - return { - x, - y: node.y + CARD_HEADER_HEIGHT + PORT_ROW_HEIGHT * index + PORT_ROW_HEIGHT / 2, - }; + return { x, y: node.y + band.top + PORT_ROW_HEIGHT * index + PORT_ROW_HEIGHT / 2 }; } - const spacing = height / (count + 1); - return { x, y: node.y + spacing * (index + 1) }; + const spacing = band.height / (count + 1); + return { x, y: node.y + band.top + spacing * (index + 1) }; } interface ModelBuilderPanelProps { @@ -1753,7 +1764,7 @@ function GraphEdges({ index, list.length, side, - layout.height, + layout.band, side === "in" ? layout.labelIn : layout.labelOut, ); }; @@ -1914,7 +1925,7 @@ const GraphNodeCard = memo(function GraphNodeCard({ index, ports.inputs.length, "in", - layout.height, + layout.band, layout.labelIn, ); return ( @@ -1957,7 +1968,7 @@ const GraphNodeCard = memo(function GraphNodeCard({ index, ports.outputs.length, "out", - layout.height, + layout.band, layout.labelOut, ); return ( From b4110f98cae6236d0df5deeae797bf57fb76a07e Mon Sep 17 00:00:00 2001 From: giswqs Date: Thu, 20 Aug 2026 00:18:21 -0400 Subject: [PATCH 3/3] Address Claude review feedback - Delete the orphaned "Where a port's connector dot sits" line above CARD_HEADER_HEIGHT. It documented portPosition, but #1983 inserted the card geometry constants between the two and left the comment stranded on the wrong declaration. portPosition now carries its own doc block, so the stray line is redundant as well as misplaced. --- .../components/processing/model-builder/ModelBuilderPanel.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/geolibre-desktop/src/components/processing/model-builder/ModelBuilderPanel.tsx b/apps/geolibre-desktop/src/components/processing/model-builder/ModelBuilderPanel.tsx index 04720983aa..339cfefdfb 100644 --- a/apps/geolibre-desktop/src/components/processing/model-builder/ModelBuilderPanel.tsx +++ b/apps/geolibre-desktop/src/components/processing/model-builder/ModelBuilderPanel.tsx @@ -157,7 +157,6 @@ function createId(): string { : `id-${Math.random().toString(36).slice(2)}`; } -/** Where a port's connector dot sits, in canvas coordinates. */ /** * Vertical room the provider line and the tool name take on a card. Measured * against the rendered header (a 10px uppercase line over a 12px title, inside