@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/index.html
CHANGED
|
@@ -192,7 +192,7 @@
|
|
|
192
192
|
import {
|
|
193
193
|
Theme, FlowNode, FlowNote, NodeFlowGraph, Publisher,
|
|
194
194
|
MessageType, ContextMenuItemState, DefaultConnectionRenderer,
|
|
195
|
-
|
|
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
|
-
|
|
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
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
|
-
(
|
|
254
|
-
this
|
|
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
|
|
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
|
|
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: (
|
|
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: (
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|