@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.
@@ -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: { cssText: '' },
139
+ style: stubStyle(),
127
140
  children: [],
128
141
  isConnected: false,
129
142
  clicked: 0,
@@ -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 { CanvasRecord, center, stubCtx, widgetScene, withDocument } from './support';
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 }));