Fix findUsages/undo/node-lookup bugs, split Viewport and Editor, share command and inspector helpers

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-08 09:22:49 +02:00
1 parent 1eb28314cf
commit f140c2fd77
58 files changed
+1969 -1171

No files matched your search

+19 -15
View File
@@ -5,12 +5,18 @@ import type { BehaviorAsset } from '../../core/model/Asset';
import { findAsset } from '../../core/model/Project';
import type { Trace } from '../../runtime/systems/BehaviorSystem';
import { TRACE_FADE } from '../../runtime/systems/BehaviorSystem';
import { Menu, type Size, Theme, textStyle, Widget } from '../../ui';
import { clamp, drawGrid, Menu, type Size, Theme, textStyle, Widget, zoomFactor } from '../../ui';
import type { EditorState } from '../state';
import { NodePalette } from './NodePalette';
import { NodeView, portColor } from './NodeView';
import { NODE_WIDTH, NodeView, portColor, TITLE_HEIGHT } from './NodeView';
const GRID_STEP = 24;
/** Below this on-screen spacing the grid is noise rather than orientation, so it is dropped. */
const MIN_GRID_STEP = 8;
const MIN_ZOOM = 0.25;
const MAX_ZOOM = 2.5;
/** Stand-in node height for centring the view – real nodes vary with their port count. */
const NODE_HEIGHT_ESTIMATE = 80;
interface PortAnchor {
nodeId: string;
@@ -63,7 +69,7 @@ export class GraphEditor extends Widget {
this.on('wheel', (e: FederatedWheelEvent) => {
const local = this.toLocal(e.global);
const before = this.screenToGraph(local.x, local.y);
this.zoom = Math.max(0.25, Math.min(2.5, this.zoom * Math.exp(-e.deltaY * 0.0015)));
this.zoom = clamp(this.zoom * zoomFactor(e.deltaY), MIN_ZOOM, MAX_ZOOM);
const after = this.screenToGraph(local.x, local.y);
this.pan.x += (after.x - before.x) * this.zoom;
this.pan.y += (after.y - before.y) * this.zoom;
@@ -148,13 +154,13 @@ export class GraphEditor extends Widget {
this.needsFit = false;
const asset = this.asset();
if (!asset || asset.graph.nodes.length === 0) {
this.pan = { x: this.w / 2 - 80, y: this.h / 3 };
this.pan = { x: this.w / 2 - NODE_WIDTH / 2, y: this.h / 3 };
this.zoom = 1;
} else {
const xs = asset.graph.nodes.map((n) => n.x);
const ys = asset.graph.nodes.map((n) => n.y);
const cx = (Math.min(...xs) + Math.max(...xs) + 176) / 2;
const cy = (Math.min(...ys) + Math.max(...ys) + 80) / 2;
const cx = (Math.min(...xs) + Math.max(...xs) + NODE_WIDTH) / 2;
const cy = (Math.min(...ys) + Math.max(...ys) + NODE_HEIGHT_ESTIMATE) / 2;
this.zoom = 1;
this.pan = { x: this.w / 2 - cx, y: this.h / 2 - cy };
}
@@ -306,7 +312,7 @@ export class GraphEditor extends Widget {
const c = Theme.colors;
this.bg.clear().rect(0, 0, this.w, this.h).fill(c.graphBackground);
this.clip.clear().rect(0, 0, this.w, this.h).fill(0xffffff);
this.drawGrid();
this.syncGrid();
const asset = this.asset();
this.canvas.visible = asset !== null;
if (!asset) {
@@ -359,15 +365,13 @@ export class GraphEditor extends Widget {
this.edges.stroke({ width: hot ? 4 : type === 'exec' ? 2.5 : 2, color: hot ? Theme.colors.hot : valid ? portColor(type) : Theme.colors.danger, alpha: type === 'exec' || hot ? 1 : 0.85 });
}
private drawGrid(): void {
private syncGrid(): void {
this.grid.clear();
const step = GRID_STEP * this.zoom;
if (step < 8) return;
const startX = ((this.pan.x % step) + step) % step;
const startY = ((this.pan.y % step) + step) % step;
for (let x = startX; x < this.w; x += step) this.grid.moveTo(x, 0).lineTo(x, this.h);
for (let y = startY; y < this.h; y += step) this.grid.moveTo(0, y).lineTo(this.w, y);
this.grid.stroke({ width: 1, color: 0xffffff, alpha: 0.04 });
if (step < MIN_GRID_STEP) return;
// `pan` is where the graph origin sits on screen – but the graph has no origin worth
// marking, so no axis lines.
drawGrid(this.grid, { originX: this.pan.x, originY: this.pan.y, step, w: this.w, h: this.h, alpha: 0.04 });
}
// --- test hooks -----------------------------------------------------------------------------
@@ -385,7 +389,7 @@ export class GraphEditor extends Widget {
nodeGlobal(nodeId: string): { x: number; y: number } | null {
const view = this.views.get(nodeId);
if (!view) return null;
const g = view.toGlobal({ x: view.w / 2, y: 12 });
const g = view.toGlobal({ x: view.w / 2, y: TITLE_HEIGHT / 2 });
return { x: g.x, y: g.y };
}
}
+5 -5
View File
@@ -7,6 +7,7 @@ const CATEGORY_ORDER: NodeCategory[] = ['event', 'flow', 'variable', 'data', 'ac
export class NodePalette extends Popup {
private readonly search: TextField;
private readonly list = new ListView();
private readonly scroll = new ScrollContainer();
private filtered: NodeDef[] = [];
/** Guards against a second pick per opening (e.g. Enter and a row click racing). */
private picked = false;
@@ -25,10 +26,9 @@ export class NodePalette extends Popup {
},
});
this.content.add(this.search);
const scroll = new ScrollContainer();
scroll.minHeight = 260;
scroll.setContent(this.list);
this.content.add(scroll, 1);
this.scroll.minHeight = 260;
this.scroll.setContent(this.list);
this.content.add(this.scroll, 1);
this.list.rowHeight = Theme.rowHeight - 2;
this.list.emptyText = 'No matching node';
this.list.onSelect = (id) => {
@@ -50,7 +50,7 @@ export class NodePalette extends Popup {
.filter((d) => !query || d.label.toLowerCase().includes(query) || d.type.toLowerCase().includes(query) || d.description.toLowerCase().includes(query))
.sort((a, b) => CATEGORY_ORDER.indexOf(a.category) - CATEGORY_ORDER.indexOf(b.category) || a.label.localeCompare(b.label));
this.list.setItems(this.filtered.map((d) => ({ id: d.type, label: d.label, hint: d.category })));
(this.content.items[1] as ScrollContainer | undefined)?.relayoutContent();
this.scroll.relayoutContent();
}
private pick(def: NodeDef): void {
+5 -1
View File
@@ -6,7 +6,11 @@ import type { PropertyDef } from '../../core/properties/PropertySchema';
import { Theme, textStyle, type TextStyleToken, Widget } from '../../ui';
import { keyLabel } from '../inspector/fieldFactory';
/** Node geometry in graph units; the editor relies on it for edge anchors and hit tests. */
/**
* Node geometry in graph units – what the node lays itself out, draws and hit-tests with.
* Exported as the single source of these numbers: edge anchors have to land exactly on the
* ports drawn here.
*/
export const NODE_WIDTH = 176;
export const TITLE_HEIGHT = 24;
export const ROW_HEIGHT = 20;