From 98fa9907fdc90234e9933c518a632764581a85ba Mon Sep 17 00:00:00 2001 From: MelekTaus Date: Mon, 14 Sep 2026 19:14:57 +0200 Subject: [PATCH] A drop asks the area the pointer already uses, so a port at the very edge is still the node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `nodeUnder()` recomputed the node's face as `0 <= x <= w`, while the pointer events follow the `hitArea`, which reaches `PORT_HIT_RADIUS` past the edge. An input port sits at exactly x = 0, so the global point rounded back landed a hair outside: the drop counted as one on empty canvas, the node palette opened and swallowed every later click – four checks of step 6 at once. `NodeView` now answers from that same `hitArea`, and the step also checks that no palette is open. Co-Authored-By: Claude Opus 5 --- src/editor/graph/GraphCanvas.ts | 2 +- src/editor/graph/NodeView.ts | 9 +++++++++ tools/e2e/06-graph.mjs | 8 ++++++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/editor/graph/GraphCanvas.ts b/src/editor/graph/GraphCanvas.ts index 7c17946b..ab7a77c5 100644 --- a/src/editor/graph/GraphCanvas.ts +++ b/src/editor/graph/GraphCanvas.ts @@ -655,7 +655,7 @@ export class GraphCanvas extends Widget { private nodeUnder(globalX: number, globalY: number): string | null { for (const view of this.topDownViews()) { const p = view.toLocal({ x: globalX, y: globalY }); - if (p.x >= 0 && p.x <= view.w && p.y >= 0 && p.y <= view.h) return view.node.id; + if (view.containsLocal(p.x, p.y)) return view.node.id; } return null; } diff --git a/src/editor/graph/NodeView.ts b/src/editor/graph/NodeView.ts index 8c1a7530..9d22a198 100644 --- a/src/editor/graph/NodeView.ts +++ b/src/editor/graph/NodeView.ts @@ -137,6 +137,15 @@ export class NodeView extends Widget { this.hitArea = new Rectangle(-PORT_HIT_RADIUS, 0, w + PORT_HIT_RADIUS * 2, h); } + /** + * Whether a local point is on this node, by the same area its pointer events use. An input port + * sits at exactly x = 0, and rounding a global point back can land a hair outside it – which made + * a drop on such a port count as a drop on empty canvas and opened the palette. + */ + containsLocal(x: number, y: number): boolean { + return this.hitArea?.contains(x, y) ?? false; + } + /** `project` resolves asset references (Spawn, Is asset). */ sync( node: GraphNode, diff --git a/tools/e2e/06-graph.mjs b/tools/e2e/06-graph.mjs index cf4d4870..9caeeafa 100644 --- a/tools/e2e/06-graph.mjs +++ b/tools/e2e/06-graph.mjs @@ -128,9 +128,13 @@ export async function graph(t) { ); const dataPort = await json(`__gm.graph.portAt(${JSON.stringify(newNode.id)}, 'message', false)`); await drag(fromPort.x, fromPort.y, dataPort.x, dataPort.y, 16); + // Released on the port itself, which sits at the node's very edge: that is a drop on the node, not + // on empty canvas. An open palette here swallows every later click – it did, for four checks. check( - 'incompatible ports are rejected', - (await graphCounts()).edges === before13.edges + 1 && s.mode === 'edit', + 'incompatible ports are rejected without taking the drop for empty canvas', + (await graphCounts()).edges === before13.edges + 1 && + (await state()).mode === 'edit' && + !(await evaluate(`__gm.locate('Search nodes…')`)), ); await shot('18-connected');