@elicdavis/node-flow 0.2.2 → 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.2",
5
+ "version": "0.2.3",
6
6
  "license": "MIT",
7
7
  "keywords": [
8
8
  "nodes",
package/src/node.ts CHANGED
@@ -243,6 +243,8 @@ export class FlowNode {
243
243
 
244
244
  #widgetPositions: List<Box>;
245
245
 
246
+ #portWidgetPositions: List<Box> = new List<Box>();
247
+
246
248
  #data: Metadata;
247
249
 
248
250
  #registeredAnyPropertyChangeCallbacks: Array<AnyPropertyChangeCallback>;
@@ -802,7 +804,7 @@ export class FlowNode {
802
804
  }
803
805
  this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
804
806
  size.y += this.#portRowHeight(port, tempMeasurement.y) + this.#elementSpacing;
805
- size.x = Math.max(size.x, tempMeasurement.x + doublePadding)
807
+ size.x = Math.max(size.x, this.#portRowWidth(port, tempMeasurement.x) + doublePadding)
806
808
  }
807
809
 
808
810
  const widgets = this.#hasHiddenPortsToggle() ? [this.#hiddenPortsToggle, ...this.#widgets] : this.#widgets;
@@ -829,6 +831,9 @@ export class FlowNode {
829
831
 
830
832
  addInput(config: PortConfig): Port {
831
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
+ }
832
837
  this.#input.push(port);
833
838
  return port;
834
839
  }
@@ -980,6 +985,18 @@ export class FlowNode {
980
985
  }
981
986
  }
982
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
+
983
1000
  return intersection;
984
1001
  }
985
1002
 
@@ -1125,7 +1142,15 @@ export class FlowNode {
1125
1142
 
1126
1143
  #portRowHeight(port: Port, textHeight: number): number {
1127
1144
  const slots = port.slots();
1128
- 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;
1129
1154
  }
1130
1155
 
1131
1156
  public hasBeenLaidOut(): boolean {
@@ -1153,6 +1178,7 @@ export class FlowNode {
1153
1178
  this.#inputPortPositions.Clear();
1154
1179
  this.#outputPortPositions.Clear();
1155
1180
  this.#widgetPositions.Clear();
1181
+ this.#portWidgetPositions.Clear();
1156
1182
  const scaledPadding = this.#padding * camera.zoom;
1157
1183
  const scaledElementSpacing = this.#elementSpacing * camera.zoom;
1158
1184
 
@@ -1293,6 +1319,7 @@ export class FlowNode {
1293
1319
  const port = this.#input[i];
1294
1320
  if (port.hidden()) {
1295
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 } });
1296
1323
  continue;
1297
1324
  }
1298
1325
  this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
@@ -1306,6 +1333,21 @@ export class FlowNode {
1306
1333
  this.#portTextStyle.setupStyle(ctx, camera.zoom);
1307
1334
  ctx.fillText(port.getDisplayName(), leftSide, position.y);
1308
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
+
1309
1351
  // Port
1310
1352
  this.#inputPortPositions.Push(port.render(ctx, position, camera, mousePosition, postProcess));
1311
1353
 
@@ -1411,6 +1453,7 @@ export class FlowNode {
1411
1453
  return false;
1412
1454
  }
1413
1455
  this.#widgetPositions.Clear();
1456
+ this.#portWidgetPositions.Clear();
1414
1457
  this.#hiddenPortsTogglePosition = null;
1415
1458
  return true;
1416
1459
  }
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,11 +349,31 @@ 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
+
366
+ /**
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
+
347
377
  /**
348
378
  * A port asked to hide still shows while anything is connected to it,
349
379
  * and while its node is showing its hidden ports.