@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/index.html CHANGED
@@ -192,7 +192,7 @@
192
192
  import {
193
193
  Theme, FlowNode, FlowNote, NodeFlowGraph, Publisher,
194
194
  MessageType, ContextMenuItemState, DefaultConnectionRenderer,
195
- WidgetBase, ValueWidget, drawFieldBox, widgetFont,
195
+ ValueWidget, drawFieldBox, widgetFont,
196
196
  NumberWidget, StringWidget, ToggleWidget, ButtonWidget, TextWidget, ImageWidget, RowWidget,
197
197
  setTheme, onThemeChange,
198
198
  } from "./dist/web/NodeFlow.js";
@@ -352,46 +352,6 @@
352
352
  }
353
353
  }
354
354
 
355
- class MorePortsWidget extends WidgetBase {
356
- #ports;
357
- #open = false;
358
-
359
- constructor(ports) {
360
- super();
361
- this.#ports = ports;
362
- }
363
-
364
- Size() {
365
- return { x: 150, y: 16 };
366
- }
367
-
368
- render(ctx, box, scale) {
369
- const waiting = this.#ports.filter((p) => p.connections().length === 0).length;
370
- const label = this.#open ? "▴ less" : `▾ ${waiting} more`;
371
- const y = box.Position.y + box.Size.y / 2;
372
- const cx = box.Position.x + box.Size.x / 2;
373
- ctx.font = widgetFont(scale * 0.8);
374
- const half = ctx.measureText(label).width / 2 + 6 * scale;
375
- ctx.globalAlpha = this.hovered() ? 1 : 0.55;
376
- ctx.strokeStyle = ctx.fillStyle = Theme.Widget.FontColor;
377
- ctx.lineWidth = Math.max(1, scale);
378
- ctx.beginPath();
379
- ctx.moveTo(box.Position.x, y);
380
- ctx.lineTo(cx - half, y);
381
- ctx.moveTo(cx + half, y);
382
- ctx.lineTo(box.Position.x + box.Size.x, y);
383
- ctx.stroke();
384
- ctx.textAlign = "center";
385
- ctx.textBaseline = "middle";
386
- ctx.fillText(label, cx, y);
387
- }
388
-
389
- onClick() {
390
- this.#open = !this.#open;
391
- this.#ports.forEach((p) => p.setHidden(!this.#open));
392
- }
393
- }
394
-
395
355
  const mathPublisher = new Publisher({
396
356
  name: "Math",
397
357
  description: "Nodes the live example evaluates",
@@ -745,7 +705,7 @@ inputs: [
745
705
 
746
706
  A port can start **hidden**, to keep busy nodes small. A hidden port still shows while something is wired to it, so no wire ever points at nothing.
747
707
 
748
- The "more" line is a custom widget that shows and hides them.
708
+ A node with hidden ports gets a **more** line under them. Click it to show or hide them all.
749
709
 
750
710
  \`\`\`
751
711
  inputs: [
@@ -770,15 +730,52 @@ port.setHidden(false);
770
730
  ],
771
731
  outputs: [{ name: "material", type: "material" }],
772
732
  });
773
- const hiddenPorts = [];
774
- for (let i = 0; i < material.inputs(); i++) {
775
- if (["Alpha", "Clearcoat", "Sheen", "Specular", "Unlit"].includes(material.inputPort(i).getDisplayName())) {
776
- hiddenPorts.push(material.inputPort(i));
777
- }
778
- }
779
- material.insertWidget(0, new MorePortsWidget(hiddenPorts));
780
733
  graph.connectNodes(sheen, 0, material, 5);
781
734
 
735
+ note(at(4, 0), `
736
+ # Widgets on ports
737
+
738
+ An input can hold its own value in a **widget** on its row. The widget stands in for a wire, so it steps aside while something is connected and comes back when the wire is removed.
739
+
740
+ Try wiring **Size** into **Radius**.
741
+
742
+ \`\`\`
743
+ inputs: [
744
+ { name: "Radius", type: "float32",
745
+ widget: { type: "number", config:
746
+ { property: "radius", value: 1 } } },
747
+ ]
748
+ \`\`\`
749
+
750
+ The value lives in the node's **property**, so edit listeners and \`setProperty\` work as they do for any widget.
751
+ `);
752
+ const size = add({
753
+ title: "Size", position: at(4, 0, 0, 600),
754
+ outputs: [{ name: "value", type: "float32" }],
755
+ widgets: [{ type: "number", config: { value: 2.5, label: "Value" } }],
756
+ });
757
+ const circle = add({
758
+ title: "Circle", subTitle: "Widgets on ports", position: at(4, 0, 330, 540),
759
+ inputs: [
760
+ { name: "Radius", type: "float32", widget: { type: "number", config: { property: "radius", value: 1, width: 90 } } },
761
+ { name: "Segments", type: "int", widget: { type: "number", config: { property: "segments", value: 32, integer: true, min: 3, width: 90 } } },
762
+ {
763
+ name: "Center", type: "vector2", widget: {
764
+ type: "row", config: {
765
+ widgets: ["X", "Y"].map((axis) => ({ type: "number", config: { property: axis.toLowerCase(), label: axis, value: 0, width: 64 } })),
766
+ },
767
+ },
768
+ },
769
+ { name: "Fill", type: "color", widget: { type: "color", config: { property: "fill", value: "#3fa7d6", width: 90 } } },
770
+ { name: "Filled", type: "bool", widget: { type: "toggle", config: { property: "filled", value: true, label: "", width: 60 } } },
771
+ { name: "Name", type: "string", widget: { type: "string", config: { property: "name", value: "circle", width: 120 } } },
772
+ ],
773
+ outputs: [{ name: "shape", type: "shape" }],
774
+ });
775
+ logEdits(circle, "radius", "segments", "x", "y", "fill", "filled", "name");
776
+ circle.inputPort(0).addConnectionAddedListener(() => log("Circle", "Radius wired, its widget steps aside"));
777
+ circle.inputPort(0).addConnectionRemovedListener(() => log("Circle", "Radius unwired, its widget is back"));
778
+
782
779
  note(at(0, 1), `
783
780
  # Widgets
784
781
 
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.3",
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";
@@ -242,6 +243,8 @@ export class FlowNode {
242
243
 
243
244
  #widgetPositions: List<Box>;
244
245
 
246
+ #portWidgetPositions: List<Box> = new List<Box>();
247
+
245
248
  #data: Metadata;
246
249
 
247
250
  #registeredAnyPropertyChangeCallbacks: Array<AnyPropertyChangeCallback>;
@@ -801,10 +804,11 @@ export class FlowNode {
801
804
  }
802
805
  this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
803
806
  size.y += this.#portRowHeight(port, tempMeasurement.y) + this.#elementSpacing;
804
- size.x = Math.max(size.x, tempMeasurement.x + doublePadding)
807
+ size.x = Math.max(size.x, this.#portRowWidth(port, tempMeasurement.x) + doublePadding)
805
808
  }
806
809
 
807
- for (const widget of this.#widgets) {
810
+ const widgets = this.#hasHiddenPortsToggle() ? [this.#hiddenPortsToggle, ...this.#widgets] : this.#widgets;
811
+ for (const widget of widgets) {
808
812
  if (widget.Hidden?.()) {
809
813
  continue;
810
814
  }
@@ -827,6 +831,9 @@ export class FlowNode {
827
831
 
828
832
  addInput(config: PortConfig): Port {
829
833
  const port = new Port(this, config.array ? PortType.InputArray : PortType.Input, config);
834
+ if (config.widget !== undefined) {
835
+ port.setWidget(GlobalWidgetFactory.createFromConfig(this, config.widget));
836
+ }
830
837
  this.#input.push(port);
831
838
  return port;
832
839
  }
@@ -837,6 +844,38 @@ export class FlowNode {
837
844
  return port;
838
845
  }
839
846
 
847
+ #hiddenPortsShown = false;
848
+
849
+ #hiddenPortsToggle = new HiddenPortsToggle(this);
850
+
851
+ #hiddenPortsTogglePosition: Box | null = null;
852
+
853
+ hiddenPortsShown(): boolean {
854
+ return this.#hiddenPortsShown;
855
+ }
856
+
857
+ setHiddenPortsShown(shown: boolean): void {
858
+ if (shown === this.#hiddenPortsShown) {
859
+ return;
860
+ }
861
+ this.#hiddenPortsShown = shown;
862
+ this.invalidateLayout();
863
+ }
864
+
865
+ hiddenPortCount(): number {
866
+ let count = 0;
867
+ for (const port of [...this.#input, ...this.#output]) {
868
+ if (port.tucked()) {
869
+ count++;
870
+ }
871
+ }
872
+ return count;
873
+ }
874
+
875
+ #hasHiddenPortsToggle(): boolean {
876
+ return this.hiddenPortCount() > 0;
877
+ }
878
+
840
879
  addWidget(widget: Widget): void {
841
880
  this.#widgets.push(widget);
842
881
  requestRender();
@@ -930,6 +969,12 @@ export class FlowNode {
930
969
  }
931
970
  }
932
971
 
972
+ if (this.#hiddenPortsTogglePosition !== null && InBox(this.#hiddenPortsTogglePosition, position)) {
973
+ intersection.Node = this;
974
+ intersection.Widget = this.#hiddenPortsToggle;
975
+ intersection.WidgetBox = this.#hiddenPortsTogglePosition;
976
+ }
977
+
933
978
  for (let i = 0; i < this.#widgetPositions.Count(); i++) {
934
979
  const widget = this.#widgets[i];
935
980
  if (widget !== undefined && !widget.Hidden?.() && InBox(this.#widgetPositions.At(i), position)) {
@@ -940,6 +985,18 @@ export class FlowNode {
940
985
  }
941
986
  }
942
987
 
988
+ for (let i = 0; i < this.#portWidgetPositions.Count(); i++) {
989
+ const port = this.#input[i];
990
+ if (port !== undefined && port.widgetShown() && InBox(this.#portWidgetPositions.At(i), position)) {
991
+ intersection.Node = this;
992
+ intersection.Widget = port.widget() as Widget;
993
+ intersection.WidgetBox = this.#portWidgetPositions.At(i);
994
+ intersection.Port = undefined;
995
+ intersection.PortIndex = undefined;
996
+ intersection.PortIsInput = undefined;
997
+ }
998
+ }
999
+
943
1000
  return intersection;
944
1001
  }
945
1002
 
@@ -1085,7 +1142,15 @@ export class FlowNode {
1085
1142
 
1086
1143
  #portRowHeight(port: Port, textHeight: number): number {
1087
1144
  const slots = port.slots();
1088
- return slots > 1 ? Math.max(textHeight, slots * ARRAY_SLOT_PITCH) : textHeight;
1145
+ const widget = port.widgetShown() ? (port.widget() as Widget).Size().y : 0;
1146
+ return Math.max(slots > 1 ? slots * ARRAY_SLOT_PITCH : 0, textHeight, widget);
1147
+ }
1148
+
1149
+ #portRowWidth(port: Port, textWidth: number): number {
1150
+ if (!port.widgetShown()) {
1151
+ return textWidth;
1152
+ }
1153
+ return textWidth + this.#elementSpacing + (port.widget() as Widget).Size().x;
1089
1154
  }
1090
1155
 
1091
1156
  public hasBeenLaidOut(): boolean {
@@ -1113,6 +1178,7 @@ export class FlowNode {
1113
1178
  this.#inputPortPositions.Clear();
1114
1179
  this.#outputPortPositions.Clear();
1115
1180
  this.#widgetPositions.Clear();
1181
+ this.#portWidgetPositions.Clear();
1116
1182
  const scaledPadding = this.#padding * camera.zoom;
1117
1183
  const scaledElementSpacing = this.#elementSpacing * camera.zoom;
1118
1184
 
@@ -1253,6 +1319,7 @@ export class FlowNode {
1253
1319
  const port = this.#input[i];
1254
1320
  if (port.hidden()) {
1255
1321
  this.#inputPortPositions.Push({ Position: { x: nodeBounds.Position.x, y: startY }, Size: { x: 0, y: 0 } });
1322
+ this.#portWidgetPositions.Push({ Position: { x: nodeBounds.Position.x, y: startY }, Size: { x: 0, y: 0 } });
1256
1323
  continue;
1257
1324
  }
1258
1325
  this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
@@ -1266,6 +1333,21 @@ export class FlowNode {
1266
1333
  this.#portTextStyle.setupStyle(ctx, camera.zoom);
1267
1334
  ctx.fillText(port.getDisplayName(), leftSide, position.y);
1268
1335
 
1336
+ // Widget
1337
+ if (port.widgetShown()) {
1338
+ const widget = port.widget() as Widget;
1339
+ const widgetHeight = widget.Size().y * camera.zoom;
1340
+ const widgetLeft = leftSide + (tempMeasurement.x * camera.zoom) + scaledElementSpacing;
1341
+ const box: Box = {
1342
+ Position: { x: widgetLeft, y: startY + ((portHeight - widgetHeight) / 2) },
1343
+ Size: { x: nodeBounds.Position.x + nodeBounds.Size.x - scaledPadding - widgetLeft, y: widgetHeight },
1344
+ };
1345
+ widget.Draw(ctx, box, camera.zoom, mousePosition);
1346
+ this.#portWidgetPositions.Push(box);
1347
+ } else {
1348
+ this.#portWidgetPositions.Push({ Position: { x: nodeBounds.Position.x, y: startY }, Size: { x: 0, y: 0 } });
1349
+ }
1350
+
1269
1351
  // Port
1270
1352
  this.#inputPortPositions.Push(port.render(ctx, position, camera, mousePosition, postProcess));
1271
1353
 
@@ -1297,6 +1379,17 @@ export class FlowNode {
1297
1379
  startY += portHeight + scaledElementSpacing;
1298
1380
  }
1299
1381
 
1382
+ this.#hiddenPortsTogglePosition = null;
1383
+ if (this.#hasHiddenPortsToggle()) {
1384
+ const box: Box = {
1385
+ Position: { x: nodeBounds.Position.x + scaledPadding, y: startY },
1386
+ Size: { x: nodeBounds.Size.x - (scaledPadding * 2), y: this.#hiddenPortsToggle.Size().y * camera.zoom },
1387
+ };
1388
+ this.#hiddenPortsToggle.Draw(ctx, box, camera.zoom, mousePosition);
1389
+ this.#hiddenPortsTogglePosition = box;
1390
+ startY += box.Size.y + scaledElementSpacing;
1391
+ }
1392
+
1300
1393
  // Widgets
1301
1394
  for (let i = 0; i < this.#widgets.length; i++) {
1302
1395
  const widget = this.#widgets[i];
@@ -1360,6 +1453,8 @@ export class FlowNode {
1360
1453
  return false;
1361
1454
  }
1362
1455
  this.#widgetPositions.Clear();
1456
+ this.#portWidgetPositions.Clear();
1457
+ this.#hiddenPortsTogglePosition = null;
1363
1458
  return true;
1364
1459
  }
1365
1460
 
package/src/port.ts CHANGED
@@ -11,6 +11,8 @@ import { Box, InBox } from "./types/box";
11
11
  import { Text } from "./types/text";
12
12
  import { Vector2 } from "./types/vector2";
13
13
  import { Color, HSV, HSV2RGB, RgbToHex } from './utils/color';
14
+ import type { Widget } from "./widgets/widget";
15
+ import type { WidgetConfig } from "./widgets/factory";
14
16
 
15
17
  export enum PortType {
16
18
  Input = "INPUT",
@@ -56,6 +58,12 @@ export interface PortConfig {
56
58
  */
57
59
  hidden?: boolean;
58
60
 
61
+ /**
62
+ * A widget drawn on the port's own row while nothing is connected to
63
+ * it, for a value the port holds on its own. Input ports only.
64
+ */
65
+ widget?: WidgetConfig;
66
+
59
67
  emptyStyle?: PortStyle;
60
68
  filledStyle?: PortStyle;
61
69
  onConnectionAdded?: ConnectionChangeCallback;
@@ -167,6 +175,8 @@ export class Port {
167
175
 
168
176
  #onConnectionRemoved: Array<ConnectionChangeCallback>;
169
177
 
178
+ #widget: Widget | null = null;
179
+
170
180
  constructor(node: FlowNode, portType: PortType, config?: PortConfig) {
171
181
  this.#node = node;
172
182
  this.#connections = new Array<Connection>();
@@ -339,15 +349,40 @@ export class Port {
339
349
  }
340
350
 
341
351
  #visibilityMayHaveChanged(was: boolean): void {
342
- if (was !== this.hidden() || this.#portType === PortType.InputArray) {
352
+ if (was !== this.hidden() || this.#portType === PortType.InputArray || this.#widget !== null) {
343
353
  this.#node?.invalidateLayout();
344
354
  }
345
355
  }
346
356
 
357
+ setWidget(widget: Widget | null): void {
358
+ this.#widget = widget;
359
+ this.#node?.invalidateLayout();
360
+ }
361
+
362
+ widget(): Widget | null {
363
+ return this.#widget;
364
+ }
365
+
347
366
  /**
348
- * A port asked to hide still shows while anything is connected to it.
367
+ * The widget stands in for a wire, so it leaves once one is connected.
368
+ */
369
+ widgetShown(): boolean {
370
+ return this.#widget !== null
371
+ && this.#portType !== PortType.Output
372
+ && this.#connections.length === 0
373
+ && !this.hidden()
374
+ && !(this.#widget.Hidden?.() ?? false);
375
+ }
376
+
377
+ /**
378
+ * A port asked to hide still shows while anything is connected to it,
379
+ * and while its node is showing its hidden ports.
349
380
  */
350
381
  hidden(): boolean {
382
+ return this.tucked() && !(this.#node?.hiddenPortsShown() ?? false);
383
+ }
384
+
385
+ tucked(): boolean {
351
386
  return this.#hidden && this.#connections.length === 0;
352
387
  }
353
388