In a graph of many nodes the frame rate dropped while panning, and more so while one node was dragged. Every pointer move rebuilt every box, every text and every curve, and a drag also announced a changed project, so the panels rebuilt along with it. - Panning and zooming move the picture. GraphCanvas.applyView sets position and scale and draws anew only the grid and the rubber band. It used to call redraw(). - A node draws its box and makes its texts only when what they show has changed. NodeView keeps a signature of what redraw reads and one of what rebuildTexts reads, and returns early while they hold. - A drag does not announce a changed project. GraphDrags writes the model on every pointer move and asks the canvas once a frame to follow (GraphCanvasHost.moved). state.preview() waits for the command of the release. - Nodes outside the picture are not drawn. inView in graphView.ts is the rule, cull() applies it after every redraw and every change of the view. - The graph is a render group, and box and pins go into the batch. The mask sits on a container above the group, on the group itself it would be panned and zoomed along. - The rubber band and the drop mark of a dragged behavior are drawn on their own (drawBand), without a redraw of the graph. Measured in the CI image with 388 nodes and 540 links, fitted to the canvas, 240 pointer moves 16 ms apart: - Pan, script per move: 25.2 ms before, 2.0 ms after - Pan: 4.4 frames per second before, 18.7 after - Drag a node: 2.7 frames per second before, 22.4 after - The still picture: 9 frames per second before, 21 after Not solved: the still picture of such a graph stays at 21 frames per second in the test browser. What is left is the drawing itself, and the frame rates are those of that browser, not of a desktop GPU. Tests: - Gate on melektaus-atx: 207 files, 3121 tests (3114 + 7), exit 0 - e2e 4,6,18,19,20,23,24,26,28,31,36,43,51,53 on melektaus-atx: 542 pass, 1 fail. The fail is module 51 after module 43, an order the suite does not have, and it fails the same way without this change. Module 51 alone: 12 checks, all pass. - No full run and no smokes. The sum of 1660 (1656 + 4) is reckoned, and the CI workflow is switched off. - Seven unit tests: six for inView in tests/editor/graph-view.test.ts and one in frame-preview.test.ts - Four e2e checks in block 6z-xii of module 06: pan, drag, zoom in and fit, a changed value - Seven browser counter-probes fail 2 to 3 checks each Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
67 lines
3.6 KiB
TypeScript
67 lines
3.6 KiB
TypeScript
/**
|
|
* A graph is mostly looked at in parts (#292): a node outside the picture is left out of the drawing,
|
|
* and what decides that is where its box, with what hangs over its edges, stands against the view.
|
|
*/
|
|
import { describe, expect, it } from 'vitest';
|
|
import { inView, NODE_OVERHANG } from '../../src/editor/graph/graphView';
|
|
|
|
const box = (x: number, y: number) => ({ x, y, w: 176, h: 80 });
|
|
const plain = { pan: { x: 0, y: 0 }, zoom: 1 };
|
|
|
|
describe('which nodes are in the picture', () => {
|
|
it('keeps a node inside the view and drops one far outside it, on every side', () => {
|
|
expect(inView(box(100, 100), plain, 800, 600)).toBe(true);
|
|
expect(inView(box(-400, 100), plain, 800, 600)).toBe(false);
|
|
expect(inView(box(1000, 100), plain, 800, 600)).toBe(false);
|
|
expect(inView(box(100, -300), plain, 800, 600)).toBe(false);
|
|
expect(inView(box(100, 800), plain, 800, 600)).toBe(false);
|
|
});
|
|
|
|
it('keeps a node that only reaches into the view with a corner', () => {
|
|
expect(inView(box(-170, -70), plain, 800, 600)).toBe(true);
|
|
expect(inView(box(790, 590), plain, 800, 600)).toBe(true);
|
|
});
|
|
|
|
it('keeps a node just outside for what it draws beyond its box', () => {
|
|
// The pins sit on the edges and the shadow falls below: a box that ends right at the border
|
|
// still has pixels inside it.
|
|
expect(inView(box(-176 - NODE_OVERHANG + 1, 100), plain, 800, 600)).toBe(true);
|
|
expect(inView(box(-176 - NODE_OVERHANG, 100), plain, 800, 600)).toBe(false);
|
|
expect(inView(box(800 + NODE_OVERHANG - 1, 100), plain, 800, 600)).toBe(true);
|
|
expect(inView(box(800 + NODE_OVERHANG, 100), plain, 800, 600)).toBe(false);
|
|
expect(inView(box(100, -80 - NODE_OVERHANG + 1), plain, 800, 600)).toBe(true);
|
|
expect(inView(box(100, -80 - NODE_OVERHANG), plain, 800, 600)).toBe(false);
|
|
expect(inView(box(100, 600 + NODE_OVERHANG - 1), plain, 800, 600)).toBe(true);
|
|
expect(inView(box(100, 600 + NODE_OVERHANG), plain, 800, 600)).toBe(false);
|
|
});
|
|
|
|
it('follows the pan', () => {
|
|
const far = box(2000, 1500);
|
|
expect(inView(far, plain, 800, 600)).toBe(false);
|
|
expect(inView(far, { pan: { x: -1800, y: -1300 }, zoom: 1 }, 800, 600)).toBe(true);
|
|
expect(inView(box(100, 100), { pan: { x: -1800, y: -1300 }, zoom: 1 }, 800, 600)).toBe(false);
|
|
// Each direction by itself: a node left behind on one axis is out, whatever the other says.
|
|
expect(inView(box(100, 100), { pan: { x: -1800, y: 0 }, zoom: 1 }, 800, 600)).toBe(false);
|
|
expect(inView(box(100, 100), { pan: { x: 0, y: -1300 }, zoom: 1 }, 800, 600)).toBe(false);
|
|
expect(inView(box(-500, -400), { pan: { x: 600, y: 500 }, zoom: 1 }, 800, 600)).toBe(true);
|
|
});
|
|
|
|
it('follows the zoom: zoomed out the view holds more, zoomed in less', () => {
|
|
const node = box(1500, 100);
|
|
expect(inView(node, plain, 800, 600)).toBe(false);
|
|
expect(inView(node, { pan: { x: 0, y: 0 }, zoom: 0.5 }, 800, 600)).toBe(true);
|
|
expect(inView(box(500, 100), { pan: { x: 0, y: 0 }, zoom: 2 }, 800, 600)).toBe(false);
|
|
// Panned and zoomed at once, the view is the graph from 500 to 900 across and 500 to 800 down.
|
|
const both = { pan: { x: -1000, y: -1000 }, zoom: 2 };
|
|
expect(inView(box(600, 560), both, 800, 600)).toBe(true);
|
|
expect(inView(box(300, 560), both, 800, 600)).toBe(false);
|
|
expect(inView(box(950, 560), both, 800, 600)).toBe(false);
|
|
expect(inView(box(600, 380), both, 800, 600)).toBe(false);
|
|
expect(inView(box(600, 900), both, 800, 600)).toBe(false);
|
|
});
|
|
|
|
it('holds nothing while the canvas has no size', () => {
|
|
expect(inView(box(100, 100), plain, 0, 0)).toBe(false);
|
|
});
|
|
});
|