@elicdavis/node-flow 0.2.1 → 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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@elicdavis/node-flow",
3
3
  "description": "build node graphs",
4
4
  "author": "Eli C Davis",
5
- "version": "0.2.1",
5
+ "version": "0.2.2",
6
6
  "license": "MIT",
7
7
  "keywords": [
8
8
  "nodes",
package/src/graph.ts CHANGED
@@ -250,8 +250,8 @@ export class NodeFlowGraph {
250
250
  this.#clickEnd.bind(this),
251
251
  this.#openContextMenu.bind(this),
252
252
  this.#fileDrop.bind(this),
253
- (amount, anchor) => {
254
- this.zoom(amount, anchor);
253
+ (factor, anchor) => {
254
+ this.#zoomBy(factor, anchor);
255
255
  }
256
256
  );
257
257
 
@@ -417,6 +417,10 @@ export class NodeFlowGraph {
417
417
 
418
418
 
419
419
  zoom(amount: number, anchor?: Vector2): void {
420
+ this.#zoomBy(1 + amount * 0.05, anchor);
421
+ }
422
+
423
+ #zoomBy(factor: number, anchor?: Vector2): void {
420
424
  const anchorPos = anchor ?? this.#mousePosition;
421
425
 
422
426
  let oldPos: Vector2 | undefined = undefined;
@@ -424,7 +428,7 @@ export class NodeFlowGraph {
424
428
  oldPos = this.#sceenPositionToGraphPosition(anchorPos);
425
429
  }
426
430
 
427
- this.#camera.zoom += amount * this.#camera.zoom * 0.05;
431
+ this.#camera.zoom *= factor;
428
432
 
429
433
  if (!oldPos || anchorPos === undefined) {
430
434
  return;
package/src/input.ts CHANGED
@@ -3,7 +3,7 @@ import { CopyVector2, Distance, SubVector2, Vector2, Zero } from "./types/vector
3
3
 
4
4
  const LONG_PRESS_MS = 500;
5
5
  const LONG_PRESS_SLOP = 12;
6
- const PINCH_SENSITIVITY = 50;
6
+ const WHEEL_ZOOM_PER_PIXEL = 0.0005;
7
7
  const SUPPRESS_MOUSE_MS = 600;
8
8
 
9
9
  export class MouseObserver {
@@ -18,7 +18,7 @@ export class MouseObserver {
18
18
  #clickStart: (position: Vector2, shiftOrCtrl: boolean) => void;
19
19
  #clickStop: () => void;
20
20
  #contextMenu: (position: Vector2) => void;
21
- #zoomCallback: (amount: number, anchor: Vector2) => void;
21
+ #zoomCallback: (factor: number, anchor: Vector2) => void;
22
22
 
23
23
  #longPressTimer: ReturnType<typeof setTimeout> | null = null;
24
24
  #longPressOrigin: Vector2 = Zero();
@@ -34,7 +34,7 @@ export class MouseObserver {
34
34
  clickStop: () => void,
35
35
  contextMenu: (position: Vector2) => void,
36
36
  fileDrop: (file: File) => void,
37
- zoomCallback: (amount: number, anchor: Vector2) => void,
37
+ zoomCallback: (factor: number, anchor: Vector2) => void,
38
38
  ) {
39
39
  this.#ele = ele;
40
40
  this.#dragCallback = dragCallback;
@@ -132,14 +132,12 @@ export class MouseObserver {
132
132
  } else if (event.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
133
133
  dy *= this.#ele.clientHeight;
134
134
  }
135
- // Scroll up zooms in (matches Figma / Maps).
136
- const amount = -dy / 100;
137
- if (amount === 0) {
135
+ if (dy === 0) {
138
136
  return;
139
137
  }
140
138
  const anchor = this.#mousePos(event);
141
139
  this.#moveCallback(anchor);
142
- this.#zoomCallback(amount, anchor);
140
+ this.#zoomCallback(Math.exp(-dy * WHEEL_ZOOM_PER_PIXEL), anchor);
143
141
  }
144
142
 
145
143
  #move(event: MouseEvent): void {
@@ -226,7 +224,7 @@ export class MouseObserver {
226
224
  const anchor = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
227
225
  if (this.#pinchDist > 0) {
228
226
  this.#moveCallback(anchor);
229
- this.#zoomCallback((dist - this.#pinchDist) / PINCH_SENSITIVITY, anchor);
227
+ this.#zoomCallback(dist / this.#pinchDist, anchor);
230
228
  }
231
229
  this.#pinchDist = dist;
232
230
  return;
package/src/node.ts CHANGED
@@ -4,6 +4,7 @@ import { FontWeight, TextStyle, TextStyleConfig, TextStyleFallback } from "./sty
4
4
  import { Box, InBox } from "./types/box";
5
5
  import { CopyVector2, Distance, ScaleVector, Vector2, Zero } from "./types/vector2";
6
6
  import { Widget } from './widgets/widget';
7
+ import { HiddenPortsToggle } from './widgets/hidden_ports';
7
8
  import { List } from './types/list';
8
9
  import { BoxStyle, BoxStyleConfig, BoxStyleWithFallback } from "./styles/box";
9
10
  import { Text } from "./types/text";
@@ -804,7 +805,8 @@ export class FlowNode {
804
805
  size.x = Math.max(size.x, tempMeasurement.x + doublePadding)
805
806
  }
806
807
 
807
- for (const widget of this.#widgets) {
808
+ const widgets = this.#hasHiddenPortsToggle() ? [this.#hiddenPortsToggle, ...this.#widgets] : this.#widgets;
809
+ for (const widget of widgets) {
808
810
  if (widget.Hidden?.()) {
809
811
  continue;
810
812
  }
@@ -837,6 +839,38 @@ export class FlowNode {
837
839
  return port;
838
840
  }
839
841
 
842
+ #hiddenPortsShown = false;
843
+
844
+ #hiddenPortsToggle = new HiddenPortsToggle(this);
845
+
846
+ #hiddenPortsTogglePosition: Box | null = null;
847
+
848
+ hiddenPortsShown(): boolean {
849
+ return this.#hiddenPortsShown;
850
+ }
851
+
852
+ setHiddenPortsShown(shown: boolean): void {
853
+ if (shown === this.#hiddenPortsShown) {
854
+ return;
855
+ }
856
+ this.#hiddenPortsShown = shown;
857
+ this.invalidateLayout();
858
+ }
859
+
860
+ hiddenPortCount(): number {
861
+ let count = 0;
862
+ for (const port of [...this.#input, ...this.#output]) {
863
+ if (port.tucked()) {
864
+ count++;
865
+ }
866
+ }
867
+ return count;
868
+ }
869
+
870
+ #hasHiddenPortsToggle(): boolean {
871
+ return this.hiddenPortCount() > 0;
872
+ }
873
+
840
874
  addWidget(widget: Widget): void {
841
875
  this.#widgets.push(widget);
842
876
  requestRender();
@@ -930,6 +964,12 @@ export class FlowNode {
930
964
  }
931
965
  }
932
966
 
967
+ if (this.#hiddenPortsTogglePosition !== null && InBox(this.#hiddenPortsTogglePosition, position)) {
968
+ intersection.Node = this;
969
+ intersection.Widget = this.#hiddenPortsToggle;
970
+ intersection.WidgetBox = this.#hiddenPortsTogglePosition;
971
+ }
972
+
933
973
  for (let i = 0; i < this.#widgetPositions.Count(); i++) {
934
974
  const widget = this.#widgets[i];
935
975
  if (widget !== undefined && !widget.Hidden?.() && InBox(this.#widgetPositions.At(i), position)) {
@@ -1297,6 +1337,17 @@ export class FlowNode {
1297
1337
  startY += portHeight + scaledElementSpacing;
1298
1338
  }
1299
1339
 
1340
+ this.#hiddenPortsTogglePosition = null;
1341
+ if (this.#hasHiddenPortsToggle()) {
1342
+ const box: Box = {
1343
+ Position: { x: nodeBounds.Position.x + scaledPadding, y: startY },
1344
+ Size: { x: nodeBounds.Size.x - (scaledPadding * 2), y: this.#hiddenPortsToggle.Size().y * camera.zoom },
1345
+ };
1346
+ this.#hiddenPortsToggle.Draw(ctx, box, camera.zoom, mousePosition);
1347
+ this.#hiddenPortsTogglePosition = box;
1348
+ startY += box.Size.y + scaledElementSpacing;
1349
+ }
1350
+
1300
1351
  // Widgets
1301
1352
  for (let i = 0; i < this.#widgets.length; i++) {
1302
1353
  const widget = this.#widgets[i];
@@ -1360,6 +1411,7 @@ export class FlowNode {
1360
1411
  return false;
1361
1412
  }
1362
1413
  this.#widgetPositions.Clear();
1414
+ this.#hiddenPortsTogglePosition = null;
1363
1415
  return true;
1364
1416
  }
1365
1417
 
package/src/port.ts CHANGED
@@ -345,9 +345,14 @@ export class Port {
345
345
  }
346
346
 
347
347
  /**
348
- * A port asked to hide still shows while anything is connected to it.
348
+ * A port asked to hide still shows while anything is connected to it,
349
+ * and while its node is showing its hidden ports.
349
350
  */
350
351
  hidden(): boolean {
352
+ return this.tucked() && !(this.#node?.hiddenPortsShown() ?? false);
353
+ }
354
+
355
+ tucked(): boolean {
351
356
  return this.#hidden && this.#connections.length === 0;
352
357
  }
353
358
 
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 object | null
127
- ? (null extends T[K] ? T[K] : DeepPartial<T[K]>)
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
+ }
@@ -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 shown = scene(false);
54
- const hidden = scene(true);
53
+ const one = scene(true);
54
+ const two = scene(true);
55
+ two.node.addInput({ name: 'Iridescence', type: 'iridescence', hidden: true });
55
56
 
56
- const shownHeight = shown.node.calculateBounds(shown.ctx, shown.camera).Size.y;
57
- const hiddenHeight = hidden.node.calculateBounds(hidden.ctx, hidden.camera).Size.y;
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.ok(hiddenHeight < shownHeight);
60
+ assert.equal(twoHeight, oneHeight);
60
61
  });
61
62
 
62
63
  it('cannot be hovered where it would have been drawn', () => {