@elicdavis/node-flow 0.2.1 → 0.2.3
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 +117 -101
- package/dist/index.d.ts +117 -101
- package/dist/index.js +349 -215
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +349 -215
- package/dist/index.mjs.map +1 -1
- package/index.html +46 -49
- package/package.json +1 -1
- package/src/graph.ts +7 -3
- package/src/input.ts +6 -8
- package/src/node.ts +98 -3
- package/src/port.ts +37 -2
- package/src/theme.ts +179 -179
- package/src/widgets/hidden_ports.ts +49 -0
- 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/src/theme.ts
CHANGED
|
@@ -1,179 +1,179 @@
|
|
|
1
|
-
import { requestRender } from "./render_scheduler";
|
|
2
|
-
|
|
3
|
-
export interface BevelColors {
|
|
4
|
-
Light: string;
|
|
5
|
-
Dark: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const defaults = {
|
|
9
|
-
FontFamily: "Courier New",
|
|
10
|
-
Graph: {
|
|
11
|
-
BackgroundColor: "#07212a",
|
|
12
|
-
DotColor: "rgb(41, 54, 57)",
|
|
13
|
-
},
|
|
14
|
-
Node: {
|
|
15
|
-
FontColor: "#afb9bb",
|
|
16
|
-
BackgroundColor: "#0c2b35",
|
|
17
|
-
Title: {
|
|
18
|
-
Color: "#154050",
|
|
19
|
-
FontColor: "#afb9bb",
|
|
20
|
-
Padding: 5
|
|
21
|
-
},
|
|
22
|
-
BorderRadius: 15,
|
|
23
|
-
Border: {
|
|
24
|
-
Idle: "#1c1c1c",
|
|
25
|
-
MouseOver: "#8e8e8e",
|
|
26
|
-
Grabbed: "white",
|
|
27
|
-
Selected: "#6e6e6e"
|
|
28
|
-
},
|
|
29
|
-
Bevel: null as BevelColors | null,
|
|
30
|
-
Port: {
|
|
31
|
-
FontColor: "#afb9bb"
|
|
32
|
-
},
|
|
33
|
-
Message: {
|
|
34
|
-
InfoColor: "#afb9bb",
|
|
35
|
-
WarnColor: "#f1f663ff",
|
|
36
|
-
ErrorColor: "#FF0000"
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
BoxSelect: {
|
|
40
|
-
Color: "white",
|
|
41
|
-
Size: 1,
|
|
42
|
-
Radius: 2,
|
|
43
|
-
LineDashLength: 5,
|
|
44
|
-
},
|
|
45
|
-
Widget: {
|
|
46
|
-
FontColor: "#afb9bb",
|
|
47
|
-
LabelColor: "#7f9497",
|
|
48
|
-
FontSize: 14,
|
|
49
|
-
Padding: 6,
|
|
50
|
-
Gap: 4,
|
|
51
|
-
BackgroundColor: "#0f313c",
|
|
52
|
-
Border: {
|
|
53
|
-
Color: "#154050",
|
|
54
|
-
Size: 2,
|
|
55
|
-
Radius: 2
|
|
56
|
-
},
|
|
57
|
-
Bevel: null as BevelColors | null,
|
|
58
|
-
Hover: {
|
|
59
|
-
BackgroundColor: "#195366"
|
|
60
|
-
},
|
|
61
|
-
Input: {
|
|
62
|
-
BackgroundColor: "#0f313c",
|
|
63
|
-
HoverBackgroundColor: "#195366",
|
|
64
|
-
FontColor: "#afb9bb",
|
|
65
|
-
},
|
|
66
|
-
Slider: {
|
|
67
|
-
FillColor: "#07212A",
|
|
68
|
-
},
|
|
69
|
-
Button: {
|
|
70
|
-
Click: {
|
|
71
|
-
BackgroundColor: "#1f637a"
|
|
72
|
-
},
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
ContextMenu: {
|
|
76
|
-
BackgroundColor: "#07212A",
|
|
77
|
-
HighlightColor: "#205A6D",
|
|
78
|
-
HighlightFontColor: "#afb9bb",
|
|
79
|
-
FontColor: "#afb9bb",
|
|
80
|
-
DisabledFontColor: "#4a585c",
|
|
81
|
-
BorderRadius: 5,
|
|
82
|
-
},
|
|
83
|
-
Minimap: {
|
|
84
|
-
BackgroundColor: "rgba(10, 16, 18, 0.85)",
|
|
85
|
-
BorderColor: "#1c1c1c",
|
|
86
|
-
BorderRadius: 4,
|
|
87
|
-
Padding: 6,
|
|
88
|
-
NodeColor: "#6b7f85",
|
|
89
|
-
ViewportColor: "rgba(175, 185, 187, 0.25)",
|
|
90
|
-
},
|
|
91
|
-
Popup: {
|
|
92
|
-
BackgroundColor: "#000000",
|
|
93
|
-
FontColor: "#ffffff",
|
|
94
|
-
FontFamily: "Verdana",
|
|
95
|
-
BorderRadius: 8,
|
|
96
|
-
Backdrop: "#00000070",
|
|
97
|
-
},
|
|
98
|
-
Note: {
|
|
99
|
-
FontColor: "#afb9bb",
|
|
100
|
-
FontSize: 16,
|
|
101
|
-
EntrySpacing: 20,
|
|
102
|
-
LineSpacing: 5,
|
|
103
|
-
DotSize: 3,
|
|
104
|
-
HeaderLineWidth: 2,
|
|
105
|
-
H1: {
|
|
106
|
-
FontSize: 32,
|
|
107
|
-
},
|
|
108
|
-
H2: {
|
|
109
|
-
FontSize: 16,
|
|
110
|
-
},
|
|
111
|
-
H3: {
|
|
112
|
-
FontSize: 16,
|
|
113
|
-
},
|
|
114
|
-
CodeBlock: {
|
|
115
|
-
BackgroundColor: "#0c2b35",
|
|
116
|
-
FontColor: "#afb9bb",
|
|
117
|
-
Padding: 16,
|
|
118
|
-
BorderRadius: 4,
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
export type ThemeValues = typeof defaults;
|
|
124
|
-
|
|
125
|
-
type DeepPartial<T> = {
|
|
126
|
-
[K in keyof T]?: T[K] extends
|
|
127
|
-
?
|
|
128
|
-
: T[K];
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
export type ThemeConfig = DeepPartial<ThemeValues>;
|
|
132
|
-
|
|
133
|
-
function clone<T>(value: T): T {
|
|
134
|
-
return JSON.parse(JSON.stringify(value));
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function merge(target: any, source: any): any {
|
|
138
|
-
if (source === undefined) {
|
|
139
|
-
return target;
|
|
140
|
-
}
|
|
141
|
-
if (source === null || typeof source !== "object" || target === null || typeof target !== "object") {
|
|
142
|
-
return source;
|
|
143
|
-
}
|
|
144
|
-
for (const key of Object.keys(source)) {
|
|
145
|
-
target[key] = merge(target[key], source[key]);
|
|
146
|
-
}
|
|
147
|
-
return target;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export const DefaultTheme: Readonly<ThemeValues> = Object.freeze(clone(defaults));
|
|
151
|
-
|
|
152
|
-
export const Theme: ThemeValues = clone(defaults);
|
|
153
|
-
|
|
154
|
-
let version = 0;
|
|
155
|
-
|
|
156
|
-
const listeners = new Set<() => void>();
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Replaces the theme: the defaults, with anything in config laid over them.
|
|
160
|
-
* Everything already on screen picks the change up on its next draw.
|
|
161
|
-
*/
|
|
162
|
-
export function setTheme(config?: ThemeConfig): void {
|
|
163
|
-
const next = merge(clone(defaults), config);
|
|
164
|
-
for (const key of Object.keys(next) as Array<keyof ThemeValues>) {
|
|
165
|
-
(Theme as any)[key] = next[key];
|
|
166
|
-
}
|
|
167
|
-
version++;
|
|
168
|
-
listeners.forEach((listener) => listener());
|
|
169
|
-
requestRender();
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export function themeVersion(): number {
|
|
173
|
-
return version;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export function onThemeChange(listener: () => void): () => void {
|
|
177
|
-
listeners.add(listener);
|
|
178
|
-
return () => listeners.delete(listener);
|
|
179
|
-
}
|
|
1
|
+
import { requestRender } from "./render_scheduler";
|
|
2
|
+
|
|
3
|
+
export interface BevelColors {
|
|
4
|
+
Light: string;
|
|
5
|
+
Dark: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const defaults = {
|
|
9
|
+
FontFamily: "Courier New",
|
|
10
|
+
Graph: {
|
|
11
|
+
BackgroundColor: "#07212a",
|
|
12
|
+
DotColor: "rgb(41, 54, 57)",
|
|
13
|
+
},
|
|
14
|
+
Node: {
|
|
15
|
+
FontColor: "#afb9bb",
|
|
16
|
+
BackgroundColor: "#0c2b35",
|
|
17
|
+
Title: {
|
|
18
|
+
Color: "#154050",
|
|
19
|
+
FontColor: "#afb9bb",
|
|
20
|
+
Padding: 5
|
|
21
|
+
},
|
|
22
|
+
BorderRadius: 15,
|
|
23
|
+
Border: {
|
|
24
|
+
Idle: "#1c1c1c",
|
|
25
|
+
MouseOver: "#8e8e8e",
|
|
26
|
+
Grabbed: "white",
|
|
27
|
+
Selected: "#6e6e6e"
|
|
28
|
+
},
|
|
29
|
+
Bevel: null as BevelColors | null,
|
|
30
|
+
Port: {
|
|
31
|
+
FontColor: "#afb9bb"
|
|
32
|
+
},
|
|
33
|
+
Message: {
|
|
34
|
+
InfoColor: "#afb9bb",
|
|
35
|
+
WarnColor: "#f1f663ff",
|
|
36
|
+
ErrorColor: "#FF0000"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
BoxSelect: {
|
|
40
|
+
Color: "white",
|
|
41
|
+
Size: 1,
|
|
42
|
+
Radius: 2,
|
|
43
|
+
LineDashLength: 5,
|
|
44
|
+
},
|
|
45
|
+
Widget: {
|
|
46
|
+
FontColor: "#afb9bb",
|
|
47
|
+
LabelColor: "#7f9497",
|
|
48
|
+
FontSize: 14,
|
|
49
|
+
Padding: 6,
|
|
50
|
+
Gap: 4,
|
|
51
|
+
BackgroundColor: "#0f313c",
|
|
52
|
+
Border: {
|
|
53
|
+
Color: "#154050",
|
|
54
|
+
Size: 2,
|
|
55
|
+
Radius: 2
|
|
56
|
+
},
|
|
57
|
+
Bevel: null as BevelColors | null,
|
|
58
|
+
Hover: {
|
|
59
|
+
BackgroundColor: "#195366"
|
|
60
|
+
},
|
|
61
|
+
Input: {
|
|
62
|
+
BackgroundColor: "#0f313c",
|
|
63
|
+
HoverBackgroundColor: "#195366",
|
|
64
|
+
FontColor: "#afb9bb",
|
|
65
|
+
},
|
|
66
|
+
Slider: {
|
|
67
|
+
FillColor: "#07212A",
|
|
68
|
+
},
|
|
69
|
+
Button: {
|
|
70
|
+
Click: {
|
|
71
|
+
BackgroundColor: "#1f637a"
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
ContextMenu: {
|
|
76
|
+
BackgroundColor: "#07212A",
|
|
77
|
+
HighlightColor: "#205A6D",
|
|
78
|
+
HighlightFontColor: "#afb9bb",
|
|
79
|
+
FontColor: "#afb9bb",
|
|
80
|
+
DisabledFontColor: "#4a585c",
|
|
81
|
+
BorderRadius: 5,
|
|
82
|
+
},
|
|
83
|
+
Minimap: {
|
|
84
|
+
BackgroundColor: "rgba(10, 16, 18, 0.85)",
|
|
85
|
+
BorderColor: "#1c1c1c",
|
|
86
|
+
BorderRadius: 4,
|
|
87
|
+
Padding: 6,
|
|
88
|
+
NodeColor: "#6b7f85",
|
|
89
|
+
ViewportColor: "rgba(175, 185, 187, 0.25)",
|
|
90
|
+
},
|
|
91
|
+
Popup: {
|
|
92
|
+
BackgroundColor: "#000000",
|
|
93
|
+
FontColor: "#ffffff",
|
|
94
|
+
FontFamily: "Verdana",
|
|
95
|
+
BorderRadius: 8,
|
|
96
|
+
Backdrop: "#00000070",
|
|
97
|
+
},
|
|
98
|
+
Note: {
|
|
99
|
+
FontColor: "#afb9bb",
|
|
100
|
+
FontSize: 16,
|
|
101
|
+
EntrySpacing: 20,
|
|
102
|
+
LineSpacing: 5,
|
|
103
|
+
DotSize: 3,
|
|
104
|
+
HeaderLineWidth: 2,
|
|
105
|
+
H1: {
|
|
106
|
+
FontSize: 32,
|
|
107
|
+
},
|
|
108
|
+
H2: {
|
|
109
|
+
FontSize: 16,
|
|
110
|
+
},
|
|
111
|
+
H3: {
|
|
112
|
+
FontSize: 16,
|
|
113
|
+
},
|
|
114
|
+
CodeBlock: {
|
|
115
|
+
BackgroundColor: "#0c2b35",
|
|
116
|
+
FontColor: "#afb9bb",
|
|
117
|
+
Padding: 16,
|
|
118
|
+
BorderRadius: 4,
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export type ThemeValues = typeof defaults;
|
|
124
|
+
|
|
125
|
+
type DeepPartial<T> = {
|
|
126
|
+
[K in keyof T]?: T[K] extends BevelColors | null
|
|
127
|
+
? T[K]
|
|
128
|
+
: T[K] extends object ? DeepPartial<T[K]> : T[K];
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export type ThemeConfig = DeepPartial<ThemeValues>;
|
|
132
|
+
|
|
133
|
+
function clone<T>(value: T): T {
|
|
134
|
+
return JSON.parse(JSON.stringify(value));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function merge(target: any, source: any): any {
|
|
138
|
+
if (source === undefined) {
|
|
139
|
+
return target;
|
|
140
|
+
}
|
|
141
|
+
if (source === null || typeof source !== "object" || target === null || typeof target !== "object") {
|
|
142
|
+
return source;
|
|
143
|
+
}
|
|
144
|
+
for (const key of Object.keys(source)) {
|
|
145
|
+
target[key] = merge(target[key], source[key]);
|
|
146
|
+
}
|
|
147
|
+
return target;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export const DefaultTheme: Readonly<ThemeValues> = Object.freeze(clone(defaults));
|
|
151
|
+
|
|
152
|
+
export const Theme: ThemeValues = clone(defaults);
|
|
153
|
+
|
|
154
|
+
let version = 0;
|
|
155
|
+
|
|
156
|
+
const listeners = new Set<() => void>();
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Replaces the theme: the defaults, with anything in config laid over them.
|
|
160
|
+
* Everything already on screen picks the change up on its next draw.
|
|
161
|
+
*/
|
|
162
|
+
export function setTheme(config?: ThemeConfig): void {
|
|
163
|
+
const next = merge(clone(defaults), config);
|
|
164
|
+
for (const key of Object.keys(next) as Array<keyof ThemeValues>) {
|
|
165
|
+
(Theme as any)[key] = next[key];
|
|
166
|
+
}
|
|
167
|
+
version++;
|
|
168
|
+
listeners.forEach((listener) => listener());
|
|
169
|
+
requestRender();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function themeVersion(): number {
|
|
173
|
+
return version;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function onThemeChange(listener: () => void): () => void {
|
|
177
|
+
listeners.add(listener);
|
|
178
|
+
return () => listeners.delete(listener);
|
|
179
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Theme } from "../theme";
|
|
2
|
+
import { Box } from "../types/box";
|
|
3
|
+
import { Vector2 } from "../types/vector2";
|
|
4
|
+
import { WidgetBase } from "./widget";
|
|
5
|
+
|
|
6
|
+
export interface HiddenPortsOwner {
|
|
7
|
+
hiddenPortCount(): number;
|
|
8
|
+
hiddenPortsShown(): boolean;
|
|
9
|
+
setHiddenPortsShown(shown: boolean): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class HiddenPortsToggle extends WidgetBase {
|
|
13
|
+
|
|
14
|
+
constructor(private readonly owner: HiddenPortsOwner) {
|
|
15
|
+
super();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
Size(): Vector2 {
|
|
19
|
+
return { x: 150, y: 16 };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
protected render(ctx: CanvasRenderingContext2D, box: Box, scale: number): void {
|
|
23
|
+
const label = this.owner.hiddenPortsShown() ? "▴ less" : `▾ ${this.owner.hiddenPortCount()} more`;
|
|
24
|
+
|
|
25
|
+
const midY = box.Position.y + box.Size.y / 2;
|
|
26
|
+
const centerX = box.Position.x + box.Size.x / 2;
|
|
27
|
+
|
|
28
|
+
ctx.font = `${11 * scale}px ${Theme.FontFamily}`;
|
|
29
|
+
const half = ctx.measureText(label).width / 2 + 6 * scale;
|
|
30
|
+
|
|
31
|
+
ctx.globalAlpha = this.hovered() ? 1 : 0.5;
|
|
32
|
+
ctx.strokeStyle = Theme.Widget.FontColor;
|
|
33
|
+
ctx.fillStyle = Theme.Widget.FontColor;
|
|
34
|
+
ctx.lineWidth = Math.max(1, scale);
|
|
35
|
+
ctx.beginPath();
|
|
36
|
+
ctx.moveTo(box.Position.x, midY);
|
|
37
|
+
ctx.lineTo(centerX - half, midY);
|
|
38
|
+
ctx.moveTo(centerX + half, midY);
|
|
39
|
+
ctx.lineTo(box.Position.x + box.Size.x, midY);
|
|
40
|
+
ctx.stroke();
|
|
41
|
+
ctx.textAlign = "center";
|
|
42
|
+
ctx.textBaseline = "middle";
|
|
43
|
+
ctx.fillText(label, centerX, midY);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
protected onClick(): void {
|
|
47
|
+
this.owner.setHiddenPortsShown(!this.owner.hiddenPortsShown());
|
|
48
|
+
}
|
|
49
|
+
}
|
package/test/hidden_port.test.ts
CHANGED
|
@@ -50,13 +50,14 @@ function scene(hidden: boolean) {
|
|
|
50
50
|
|
|
51
51
|
describe('a hidden port', () => {
|
|
52
52
|
it('takes no room on its node', () => {
|
|
53
|
-
const
|
|
54
|
-
const
|
|
53
|
+
const one = scene(true);
|
|
54
|
+
const two = scene(true);
|
|
55
|
+
two.node.addInput({ name: 'Iridescence', type: 'iridescence', hidden: true });
|
|
55
56
|
|
|
56
|
-
const
|
|
57
|
-
const
|
|
57
|
+
const oneHeight = one.node.calculateBounds(one.ctx, one.camera).Size.y;
|
|
58
|
+
const twoHeight = two.node.calculateBounds(two.ctx, two.camera).Size.y;
|
|
58
59
|
|
|
59
|
-
assert.
|
|
60
|
+
assert.equal(twoHeight, oneHeight);
|
|
60
61
|
});
|
|
61
62
|
|
|
62
63
|
it('cannot be hovered where it would have been drawn', () => {
|
|
@@ -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
|
+
});
|