Skip to content

Commit ff523a2

Browse files
committed
ui: NodeGraph improvements
- Can insert node anywhere in a stack of nodes. - Ghost node showing where the node will land if dropped. - Snap to grid. - Edge pan while dragging a node. - Complete refactor of NodeGraph. - Ports are addressed using unique IDs. - Remove labels - they can just be styled nodes. - Remove hotkeys - wrap NodeGraph in HotkeyContext instead.
1 parent d97cd50 commit ff523a2

15 files changed

Lines changed: 2452 additions & 4052 deletions

File tree

ui/src/assets/widgets/nodegraph.scss

Lines changed: 120 additions & 397 deletions
Large diffs are not rendered by default.

ui/src/base/dom_utils.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,99 @@ export function bindEventListener<K extends keyof HTMLElementEventMap>(
137137
},
138138
};
139139
}
140+
141+
export interface DragEvent {
142+
// Movement delta since the previous event.
143+
delta: {readonly x: number; readonly y: number};
144+
// Absolute pointer position in client coordinates.
145+
client: {readonly x: number; readonly y: number};
146+
// Pointer position at the very start of the drag in client coordinates.
147+
startClient: {readonly x: number; readonly y: number};
148+
}
149+
150+
// Waits for a drag gesture to begin (pointer moved beyond `deadzone` px).
151+
// Returns an async iterable of DragEvents, or undefined if the pointer was
152+
// released before the deadzone was crossed (i.e. it was a click). The first yielded
153+
// event includes accumulated movement from the deadzone phase so callers never
154+
// lose movement that occurred before the drag was confirmed.
155+
export async function captureDrag(attrs: {
156+
el: HTMLElement;
157+
e: PointerEvent;
158+
deadzone?: number;
159+
}): Promise<AsyncIterable<DragEvent> | undefined> {
160+
const {el, e, deadzone = 0} = attrs;
161+
const pointerId = e.pointerId;
162+
163+
el.setPointerCapture(pointerId);
164+
165+
let resolveNext: ((e: PointerEvent | undefined) => void) | undefined;
166+
167+
const onMove = (e: PointerEvent) => {
168+
if (e.pointerId !== pointerId) return;
169+
resolveNext?.(e);
170+
resolveNext = undefined;
171+
};
172+
const onDone = (e: PointerEvent) => {
173+
if (e.pointerId !== pointerId) return;
174+
resolveNext?.(undefined);
175+
resolveNext = undefined;
176+
};
177+
178+
el.addEventListener('pointermove', onMove);
179+
el.addEventListener('pointerup', onDone);
180+
el.addEventListener('pointercancel', onDone);
181+
182+
const cleanup = () => {
183+
el.removeEventListener('pointermove', onMove);
184+
el.removeEventListener('pointerup', onDone);
185+
el.removeEventListener('pointercancel', onDone);
186+
};
187+
188+
const next = () =>
189+
new Promise<PointerEvent | undefined>((r) => {
190+
resolveNext = r;
191+
});
192+
193+
// Phase 1: wait for deadzone to be crossed, or pointerup (→ click).
194+
// Accumulate movement so the first yield includes the full delta.
195+
let accum = new Vector2D({x: 0, y: 0});
196+
const start = new Vector2D({x: e.clientX, y: e.clientY});
197+
let firstEvent: PointerEvent = e;
198+
while (deadzone > 0) {
199+
const ev = await next();
200+
if (ev === undefined) {
201+
cleanup();
202+
return undefined;
203+
}
204+
firstEvent = ev;
205+
accum = accum.add({x: ev.movementX, y: ev.movementY});
206+
if (start.sub({x: ev.clientX, y: ev.clientY}).magnitude >= deadzone) break;
207+
}
208+
209+
const startClient = {x: e.clientX, y: e.clientY};
210+
211+
// Phase 2: drag confirmed — stream events to the caller, leading with the
212+
// accumulated deadzone movement as the first yield.
213+
return (async function* () {
214+
try {
215+
if (deadzone > 0) {
216+
yield {
217+
delta: accum,
218+
client: {x: firstEvent.clientX, y: firstEvent.clientY},
219+
startClient,
220+
};
221+
}
222+
while (true) {
223+
const ev = await next();
224+
if (ev === undefined) return;
225+
yield {
226+
delta: {x: ev.movementX, y: ev.movementY},
227+
client: {x: ev.clientX, y: ev.clientY},
228+
startClient,
229+
};
230+
}
231+
} finally {
232+
cleanup();
233+
}
234+
})();
235+
}

0 commit comments

Comments
 (0)