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:
1 parent
1eb28314cf
commit
f140c2fd77
58 files changed
+1969
-1171
No files matched your search
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user