@elicdavis/node-flow 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +17 -4
- package/dist/index.d.ts +17 -4
- package/dist/index.js +344 -222
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +344 -222
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/graph.ts +14 -7
- package/src/index.ts +2 -2
- package/src/input.ts +6 -8
- package/src/node.ts +53 -1
- package/src/port.ts +6 -1
- package/src/theme.ts +179 -179
- package/src/widgets/hidden_ports.ts +49 -0
- package/src/widgets/inplace.ts +75 -23
- package/src/widgets/number.ts +5 -12
- package/src/widgets/string.ts +1 -8
- package/src/widgets/widget.ts +201 -180
- package/test/hidden_port.test.ts +6 -5
- package/test/hidden_ports_toggle.test.ts +102 -0
- package/test/pinch_zoom.test.ts +78 -0
- package/test/support.ts +15 -2
- package/test/widgets.test.ts +54 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { strict as assert } from 'assert';
|
|
2
|
+
import { NodeSubsystem } from '../src/nodes/subsystem';
|
|
3
|
+
import { PassSubsystem } from '../src/pass/subsystem';
|
|
4
|
+
import { FlowNode, NodeIntersection } from '../src/node';
|
|
5
|
+
import { Camera } from '../src/camera';
|
|
6
|
+
import { stubCtx } from './support';
|
|
7
|
+
|
|
8
|
+
function scene(inputs: Array<{ name: string, type: string, hidden?: boolean }>) {
|
|
9
|
+
const graph = new NodeSubsystem(new PassSubsystem());
|
|
10
|
+
const node = new FlowNode({
|
|
11
|
+
title: 'material',
|
|
12
|
+
position: { x: 100, y: 100 },
|
|
13
|
+
inputs,
|
|
14
|
+
outputs: [{ name: 'Out', type: 'material' }],
|
|
15
|
+
});
|
|
16
|
+
graph.addNode(node);
|
|
17
|
+
return { graph, node, camera: new Camera(), ctx: stubCtx() };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const material = [
|
|
21
|
+
{ name: 'Color', type: 'color' },
|
|
22
|
+
{ name: 'Sheen', type: 'sheen', hidden: true },
|
|
23
|
+
{ name: 'Clearcoat', type: 'clearcoat', hidden: true },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function findWidget(s: ReturnType<typeof scene>): NodeIntersection | undefined {
|
|
27
|
+
s.graph.render(s.ctx, s.camera, undefined);
|
|
28
|
+
const bounds = s.node.calculateBounds(s.ctx, s.camera);
|
|
29
|
+
for (let y = bounds.Position.y; y < bounds.Position.y + bounds.Size.y; y++) {
|
|
30
|
+
const hit = s.node.inBounds(s.ctx, s.camera, { x: bounds.Position.x + bounds.Size.x / 2, y });
|
|
31
|
+
if (hit.Widget !== undefined) {
|
|
32
|
+
return hit;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function click(hit: NodeIntersection) {
|
|
39
|
+
const pointer = { position: { x: 0, y: 0 }, inside: true } as any;
|
|
40
|
+
hit.Widget!.PointerDown!(pointer);
|
|
41
|
+
hit.Widget!.PointerUp!(pointer);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('a node with hidden ports', () => {
|
|
45
|
+
it('draws a row that shows them', () => {
|
|
46
|
+
const s = scene(material);
|
|
47
|
+
const toggle = findWidget(s);
|
|
48
|
+
assert.ok(toggle, 'no toggle row to click');
|
|
49
|
+
|
|
50
|
+
click(toggle);
|
|
51
|
+
|
|
52
|
+
assert.equal(s.node.inputPort(1).hidden(), false);
|
|
53
|
+
assert.equal(s.node.inputPort(2).hidden(), false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('hides them again on a second click', () => {
|
|
57
|
+
const s = scene(material);
|
|
58
|
+
click(findWidget(s)!);
|
|
59
|
+
click(findWidget(s)!);
|
|
60
|
+
|
|
61
|
+
assert.equal(s.node.inputPort(1).hidden(), true);
|
|
62
|
+
assert.equal(s.node.inputPort(2).hidden(), true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('grows when they are shown', () => {
|
|
66
|
+
const s = scene(material);
|
|
67
|
+
const collapsed = s.node.calculateBounds(s.ctx, s.camera).Size.y;
|
|
68
|
+
|
|
69
|
+
s.node.setHiddenPortsShown(true);
|
|
70
|
+
|
|
71
|
+
assert.ok(s.node.calculateBounds(s.ctx, s.camera).Size.y > collapsed);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('asks for a fresh layout when they are shown', () => {
|
|
75
|
+
const s = scene(material);
|
|
76
|
+
s.graph.render(s.ctx, s.camera, undefined);
|
|
77
|
+
|
|
78
|
+
s.node.setHiddenPortsShown(true);
|
|
79
|
+
|
|
80
|
+
assert.ok(!s.node.hasBeenLaidOut());
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('does not take the place of its own widgets', () => {
|
|
84
|
+
const s = scene(material);
|
|
85
|
+
assert.equal(s.node.widgetCount(), 0);
|
|
86
|
+
assert.equal(findWidget(s)!.WidgetIndex, undefined);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('has no row when nothing is hidden', () => {
|
|
90
|
+
assert.equal(findWidget(scene([{ name: 'Color', type: 'color' }])), undefined);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('has no row once every hidden port is wired', () => {
|
|
94
|
+
const s = scene([{ name: 'Color', type: 'color' }, { name: 'Sheen', type: 'sheen', hidden: true }]);
|
|
95
|
+
const source = new FlowNode({ title: 'sheen', outputs: [{ name: 'Out', type: 'sheen' }] });
|
|
96
|
+
s.graph.addNode(source);
|
|
97
|
+
|
|
98
|
+
s.graph.connectNodes(source, 0, s.node, 1);
|
|
99
|
+
|
|
100
|
+
assert.equal(findWidget(s), undefined);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { strict as assert } from 'assert';
|
|
2
|
+
import { MouseObserver } from '../src/input';
|
|
3
|
+
|
|
4
|
+
function observe(run: (fire: (name: string, event: any) => void, zoom: () => number) => void) {
|
|
5
|
+
const listeners = new Map<string, Array<(e: any) => void>>();
|
|
6
|
+
const ele: any = {
|
|
7
|
+
style: {},
|
|
8
|
+
clientHeight: 600,
|
|
9
|
+
addEventListener: (name: string, fn: (e: any) => void) => {
|
|
10
|
+
listeners.set(name, [...(listeners.get(name) ?? []), fn]);
|
|
11
|
+
},
|
|
12
|
+
getBoundingClientRect: () => ({ left: 0, top: 0 }),
|
|
13
|
+
};
|
|
14
|
+
const fire = (name: string, event: any) => {
|
|
15
|
+
event.preventDefault = () => { };
|
|
16
|
+
(listeners.get(name) ?? []).forEach((fn) => fn(event));
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
let zoom = 1;
|
|
20
|
+
const g = globalThis as any;
|
|
21
|
+
const previous = { document: g.document, WheelEvent: g.WheelEvent };
|
|
22
|
+
g.document = { addEventListener: () => { } };
|
|
23
|
+
g.WheelEvent = { DOM_DELTA_PIXEL: 0, DOM_DELTA_LINE: 1, DOM_DELTA_PAGE: 2 };
|
|
24
|
+
try {
|
|
25
|
+
new MouseObserver(ele, () => { }, () => { }, () => { }, () => { }, () => { }, () => { },
|
|
26
|
+
(factor) => { zoom *= factor; });
|
|
27
|
+
run(fire, () => zoom);
|
|
28
|
+
} finally {
|
|
29
|
+
g.document = previous.document;
|
|
30
|
+
g.WheelEvent = previous.WheelEvent;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function fingers(apart: number) {
|
|
35
|
+
return { touches: [{ clientX: 300 - apart / 2, clientY: 300 }, { clientX: 300 + apart / 2, clientY: 300 }] };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('pinch to zoom', () => {
|
|
39
|
+
it('doubles the zoom when the fingers spread to twice as far apart', () => {
|
|
40
|
+
observe((fire, zoom) => {
|
|
41
|
+
fire('touchstart', fingers(100));
|
|
42
|
+
fire('touchmove', fingers(125));
|
|
43
|
+
fire('touchmove', fingers(150));
|
|
44
|
+
fire('touchmove', fingers(200));
|
|
45
|
+
|
|
46
|
+
assert.ok(Math.abs(zoom() - 2) < 1e-9, `zoom was ${zoom()}`);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('halves the zoom when the fingers close to half as far apart', () => {
|
|
51
|
+
observe((fire, zoom) => {
|
|
52
|
+
fire('touchstart', fingers(300));
|
|
53
|
+
fire('touchmove', fingers(200));
|
|
54
|
+
fire('touchmove', fingers(150));
|
|
55
|
+
|
|
56
|
+
assert.ok(Math.abs(zoom() - 0.5) < 1e-9, `zoom was ${zoom()}`);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('the scroll wheel', () => {
|
|
62
|
+
it('zooms in scrolling up and out scrolling down', () => {
|
|
63
|
+
observe((fire, zoom) => {
|
|
64
|
+
fire('wheel', { deltaY: -100, deltaMode: 0, clientX: 0, clientY: 0 });
|
|
65
|
+
assert.ok(zoom() > 1);
|
|
66
|
+
|
|
67
|
+
fire('wheel', { deltaY: 200, deltaMode: 0, clientX: 0, clientY: 0 });
|
|
68
|
+
assert.ok(zoom() < 1);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('never flips the zoom however far a page scroll goes', () => {
|
|
73
|
+
observe((fire, zoom) => {
|
|
74
|
+
fire('wheel', { deltaY: 10, deltaMode: 2, clientX: 0, clientY: 0 });
|
|
75
|
+
assert.ok(zoom() > 0);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|
package/test/support.ts
CHANGED
|
@@ -94,7 +94,7 @@ export interface StubElement {
|
|
|
94
94
|
tagName: string;
|
|
95
95
|
type: string;
|
|
96
96
|
value: string;
|
|
97
|
-
style: { cssText: string };
|
|
97
|
+
style: { cssText: string, opacity?: string, pointerEvents?: string };
|
|
98
98
|
children: Array<StubElement>;
|
|
99
99
|
isConnected: boolean;
|
|
100
100
|
clicked: number;
|
|
@@ -115,6 +115,19 @@ export interface StubElement {
|
|
|
115
115
|
fire(name: string, event?: any): void;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
function stubStyle(): StubElement['style'] {
|
|
119
|
+
const style: any = {};
|
|
120
|
+
let css = '';
|
|
121
|
+
Object.defineProperty(style, 'cssText', {
|
|
122
|
+
get: () => css,
|
|
123
|
+
set: (value: string) => {
|
|
124
|
+
css = value;
|
|
125
|
+
Object.keys(style).forEach((key) => delete style[key]);
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
return style;
|
|
129
|
+
}
|
|
130
|
+
|
|
118
131
|
export function withDocument(run: (body: StubElement, created: Array<StubElement>) => void): void {
|
|
119
132
|
const created: Array<StubElement> = [];
|
|
120
133
|
const element = (tagName: string): StubElement => {
|
|
@@ -123,7 +136,7 @@ export function withDocument(run: (body: StubElement, created: Array<StubElement
|
|
|
123
136
|
tagName,
|
|
124
137
|
type: '',
|
|
125
138
|
value: '',
|
|
126
|
-
style:
|
|
139
|
+
style: stubStyle(),
|
|
127
140
|
children: [],
|
|
128
141
|
isConnected: false,
|
|
129
142
|
clicked: 0,
|
package/test/widgets.test.ts
CHANGED
|
@@ -11,7 +11,19 @@ import { ImageWidget } from '../src/widgets/image';
|
|
|
11
11
|
import { ColorWidget } from '../src/widgets/color';
|
|
12
12
|
import { RowWidget } from '../src/widgets/row';
|
|
13
13
|
import { WidgetBase } from '../src/widgets/widget';
|
|
14
|
-
import {
|
|
14
|
+
import { placeEditsDuring } from '../src/widgets/inplace';
|
|
15
|
+
import { CanvasRecord, center, StubElement, stubCtx, widgetScene, withDocument } from './support';
|
|
16
|
+
|
|
17
|
+
function cssOf(el: StubElement): Record<string, string> {
|
|
18
|
+
const style: Record<string, string> = {};
|
|
19
|
+
el.style.cssText.split(';').forEach((rule) => {
|
|
20
|
+
const at = rule.indexOf(':');
|
|
21
|
+
if (at > 0) {
|
|
22
|
+
style[rule.slice(0, at).trim()] = rule.slice(at + 1).trim();
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
return style;
|
|
26
|
+
}
|
|
15
27
|
|
|
16
28
|
describe('a number widget', () => {
|
|
17
29
|
it('follows a drag and reports one edit when let go', () => {
|
|
@@ -97,6 +109,47 @@ describe('a number widget', () => {
|
|
|
97
109
|
});
|
|
98
110
|
});
|
|
99
111
|
|
|
112
|
+
it('keeps its input over the widget as the camera moves and zooms', () => {
|
|
113
|
+
withDocument((body) => {
|
|
114
|
+
const { graph, camera, ctx, widgetBox, click } = widgetScene((n) => new NumberWidget(n, { value: 3 }));
|
|
115
|
+
click(center(widgetBox()));
|
|
116
|
+
const input = body.children[0];
|
|
117
|
+
|
|
118
|
+
camera.position.x += 137;
|
|
119
|
+
camera.position.y -= 41;
|
|
120
|
+
camera.zoom = 2;
|
|
121
|
+
graph.render(ctx, camera, undefined);
|
|
122
|
+
|
|
123
|
+
const box = widgetBox();
|
|
124
|
+
const style = cssOf(input);
|
|
125
|
+
assert.equal(style.left, `${10 + box.Position.x}px`);
|
|
126
|
+
assert.equal(style.top, `${20 + box.Position.y}px`);
|
|
127
|
+
assert.equal(style.width, `${box.Size.x}px`);
|
|
128
|
+
assert.equal(style.height, `${box.Size.y}px`);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('hides its input while the node is off the canvas', () => {
|
|
133
|
+
withDocument((body) => {
|
|
134
|
+
const { graph, camera, ctx, widgetBox, click } = widgetScene(
|
|
135
|
+
(n) => new NumberWidget(n, { value: 3 }),
|
|
136
|
+
{ inputs: [{ name: 'in', type: 'n' }] },
|
|
137
|
+
);
|
|
138
|
+
click(center(widgetBox()));
|
|
139
|
+
const input = body.children[0];
|
|
140
|
+
const frame =() => placeEditsDuring(ctx.canvas, () => graph.render(ctx, camera, undefined));
|
|
141
|
+
|
|
142
|
+
camera.position.x -= 5000;
|
|
143
|
+
frame();
|
|
144
|
+
assert.equal(input.style.opacity, '0');
|
|
145
|
+
|
|
146
|
+
camera.position.x += 5000;
|
|
147
|
+
frame();
|
|
148
|
+
assert.equal(input.style.opacity, undefined);
|
|
149
|
+
assert.ok(input.isConnected);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
100
153
|
it('does not open an input after a drag', () => {
|
|
101
154
|
withDocument((body) => {
|
|
102
155
|
const { widgetBox, press } = widgetScene((n) => new NumberWidget(n, { value: 0 }));
|