@elicdavis/node-flow 0.1.19 → 0.2.0

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.
Files changed (55) hide show
  1. package/dist/index.d.mts +386 -227
  2. package/dist/index.d.ts +386 -227
  3. package/dist/index.js +1884 -1076
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +1867 -1075
  6. package/dist/index.mjs.map +1 -1
  7. package/index.html +1134 -329
  8. package/package.json +1 -1
  9. package/src/connection.ts +30 -5
  10. package/src/contextMenu.ts +66 -34
  11. package/src/graph.ts +12 -10
  12. package/src/index.ts +26 -12
  13. package/src/markdown/entry.ts +11 -15
  14. package/src/markdown/syntaxParser.ts +2 -1
  15. package/src/node.ts +162 -111
  16. package/src/nodes/subsystem.ts +123 -29
  17. package/src/notes/note.ts +9 -1
  18. package/src/organize.ts +540 -111
  19. package/src/popup.ts +4 -2
  20. package/src/port.ts +83 -6
  21. package/src/styles/bevel.ts +31 -0
  22. package/src/styles/cursor.ts +1 -0
  23. package/src/styles/text.ts +4 -4
  24. package/src/theme.ts +179 -92
  25. package/src/types/text.ts +5 -1
  26. package/src/utils/color.ts +1 -1
  27. package/src/widgets/button.ts +52 -107
  28. package/src/widgets/color.ts +91 -150
  29. package/src/widgets/factory.ts +89 -46
  30. package/src/widgets/field.ts +99 -0
  31. package/src/widgets/image.ts +107 -165
  32. package/src/widgets/inplace.ts +84 -0
  33. package/src/widgets/number.ts +122 -154
  34. package/src/widgets/row.ts +138 -0
  35. package/src/widgets/slider.ts +101 -216
  36. package/src/widgets/string.ts +39 -130
  37. package/src/widgets/text.ts +81 -92
  38. package/src/widgets/toggle.ts +62 -207
  39. package/src/widgets/value.ts +127 -0
  40. package/src/widgets/widget.ts +180 -34
  41. package/test/context_menu.test.ts +82 -0
  42. package/test/cursor.test.ts +17 -0
  43. package/test/hidden_port.test.ts +125 -0
  44. package/test/hover_after_removal.test.ts +72 -0
  45. package/test/markdown.test.ts +2 -2
  46. package/test/markdown_code_block.test.ts +58 -0
  47. package/test/multi_input.test.ts +124 -0
  48. package/test/organize.test.ts +117 -1
  49. package/test/property_binding.test.ts +49 -0
  50. package/test/support.ts +175 -0
  51. package/test/theme.test.ts +86 -0
  52. package/test/widget_click.test.ts +111 -97
  53. package/test/widget_construction.test.ts +5 -4
  54. package/test/widgets.test.ts +333 -0
  55. package/test/wire_follows_camera.test.ts +57 -0
package/src/node.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Port, PortConfig, PortType } from "./port";
1
+ import { ARRAY_SLOT_PITCH, Port, PortConfig, PortType } from "./port";
2
2
  import { requestRender } from "./render_scheduler";
3
3
  import { FontWeight, TextStyle, TextStyleConfig, TextStyleFallback } from "./styles/text";
4
4
  import { Box, InBox } from "./types/box";
@@ -7,8 +7,9 @@ import { Widget } from './widgets/widget';
7
7
  import { List } from './types/list';
8
8
  import { BoxStyle, BoxStyleConfig, BoxStyleWithFallback } from "./styles/box";
9
9
  import { Text } from "./types/text";
10
- import { GlobalWidgetFactory } from "./widgets/factory";
11
- import { Theme } from "./theme";
10
+ import { GlobalWidgetFactory, WidgetConfig } from "./widgets/factory";
11
+ import { Theme, themeVersion } from "./theme";
12
+ import { drawBevel } from "./styles/bevel";
12
13
  import { TextAlign } from "./styles/canvasTextAlign";
13
14
  import { TextBaseline } from "./styles/canvasTextBaseline";
14
15
  import { CombineContextMenus, ContextMenuConfig, ContextMenuItemConfig } from "./contextMenu";
@@ -32,15 +33,11 @@ const MINIMUM_NODE_WIDTH = 150;
32
33
 
33
34
  type AnyPropertyChangeCallback = (propertyName: string, oldValue: any, newValue: any) => void
34
35
  type PropertyChangeCallback = (oldValue: any, newValue: any) => void
36
+ type PropertyEditCallback = (newValue: any) => void
35
37
  type TitleChangeCallback = (node: FlowNode, oldTitle: string, newTitle: string) => void
36
38
  type InfoChangeCallback = (node: FlowNode, oldInfo: string, newInfo: string) => void
37
39
 
38
40
 
39
- export interface WidgetConfig {
40
- type?: string,
41
- config?: any
42
- }
43
-
44
41
  export interface FlowNodeTitleConfig {
45
42
  textStyle?: TextStyleConfig;
46
43
  color?: string;
@@ -138,6 +135,7 @@ export interface NodeIntersection {
138
135
  // Widget
139
136
  Widget?: Widget
140
137
  WidgetIndex?: number
138
+ WidgetBox?: Box
141
139
 
142
140
  }
143
141
 
@@ -218,15 +216,19 @@ export class FlowNode {
218
216
 
219
217
  // Styling ================================================================
220
218
 
221
- #titleColor: string;
219
+ #styleConfig: FlowNodeStyle | undefined;
220
+
221
+ #styledForTheme = -1;
222
222
 
223
- #selectedStyle: BoxStyle;
223
+ #titleColor!: string;
224
224
 
225
- #stateStyles: Map<NodeState, BoxStyle>;
225
+ #selectedStyle!: BoxStyle;
226
+
227
+ #stateStyles = new Map<NodeState, BoxStyle>();
226
228
 
227
229
  #padding: number;
228
230
 
229
- #portTextStyle: TextStyle;
231
+ #portTextStyle!: TextStyle;
230
232
 
231
233
  #elementSpacing: number;
232
234
 
@@ -246,6 +248,8 @@ export class FlowNode {
246
248
 
247
249
  #registeredPropertyChangeCallbacks: Map<string, Array<PropertyChangeCallback>>;
248
250
 
251
+ #registeredPropertyEditCallbacks = new Map<string, Array<PropertyEditCallback>>();
252
+
249
253
  constructor(config?: FlowNodeConfig) {
250
254
  this.#input = new Array<Port>();
251
255
  this.#output = new Array<Port>();
@@ -307,65 +311,21 @@ export class FlowNode {
307
311
  this.#position = config?.position === undefined ? { x: 0, y: 0 } : config.position;
308
312
  this.#title = new Text(
309
313
  config?.title === undefined ? "" : config.title,
310
- TextStyleFallback(config?.style?.title?.textStyle, {
311
- size: 16,
312
- weight: FontWeight.Bold,
313
- color: Theme.Node.FontColor
314
- })
314
+ TextStyleFallback(config?.style?.title?.textStyle, { size: 16, weight: FontWeight.Bold })
315
315
  );
316
316
 
317
317
  this.#subTitle = new Text(
318
318
  config?.subTitle === undefined ? "" : config.subTitle,
319
- TextStyleFallback(config?.style?.title?.textStyle, {
320
- size: 10,
321
- weight: FontWeight.Bold,
322
- color: Theme.Node.FontColor
323
- })
319
+ TextStyleFallback(config?.style?.title?.textStyle, { size: 10, weight: FontWeight.Bold })
324
320
  );
325
321
 
326
- this.#infoSymbol = new Text(
327
- "i",
328
- {
329
- size: 13,
330
- weight: FontWeight.Bold,
331
- color: Theme.Node.FontColor
332
- }
333
- );
322
+ this.#infoSymbol = new Text("i", { size: 13, weight: FontWeight.Bold });
334
323
  this.#infoText = config?.info ? config?.info : "";
335
324
 
336
- this.#titleColor = config?.style?.title?.color === undefined ? "#154050" : config?.style?.title?.color;
337
-
338
325
  this.#padding = config?.style?.title?.padding === undefined ? 15 : config?.style?.title?.padding;
339
326
 
340
- this.#stateStyles = new Map<NodeState, BoxStyle>();
341
- this.#stateStyles.set(NodeState.Idle, new BoxStyle(BoxStyleWithFallback(config?.style?.idle, {
342
- border: { color: Theme.Node.Border.Idle, size: 1 },
343
- radius: Theme.Node.BorderRadius,
344
- color: Theme.Node.BackgroundColor,
345
- })));
346
- this.#stateStyles.set(NodeState.MouseOver, new BoxStyle(BoxStyleWithFallback(config?.style?.mouseOver, {
347
- border: { color: Theme.Node.Border.MouseOver, size: 1.1, },
348
- radius: Theme.Node.BorderRadius,
349
- color: Theme.Node.BackgroundColor,
350
- })));
351
- this.#stateStyles.set(NodeState.Grabbed, new BoxStyle(BoxStyleWithFallback(config?.style?.grabbed, {
352
- border: { color: Theme.Node.Border.Grabbed, size: 2, },
353
- radius: Theme.Node.BorderRadius,
354
- color: Theme.Node.BackgroundColor,
355
- })));
356
-
357
- this.#selectedStyle = new BoxStyle(BoxStyleWithFallback(config?.style?.selected, {
358
- border: { color: Theme.Node.Border.Selected, size: 1, },
359
- radius: Theme.Node.BorderRadius,
360
- color: Theme.Node.BackgroundColor,
361
- }));
362
-
363
- this.#portTextStyle = new TextStyle({
364
- size: config?.style?.portText?.size === undefined ? 14 : config?.style?.portText?.size,
365
- color: config?.style?.portText?.color === undefined ? Theme.Node.Port.FontColor : config?.style?.portText?.color,
366
- font: config?.style?.portText?.font,
367
- weight: config?.style?.portText?.weight,
368
- });
327
+ this.#styleConfig = config?.style;
328
+ this.#applyStyle();
369
329
 
370
330
  if (config?.inputs !== undefined) {
371
331
  for (let i = 0; i < config.inputs.length; i++) {
@@ -380,12 +340,8 @@ export class FlowNode {
380
340
  }
381
341
 
382
342
  if (config?.widgets !== undefined) {
383
- for (let i = 0; i < config.widgets.length; i++) {
384
- const widget = config.widgets[i];
385
- if (widget.type === undefined) {
386
- continue;
387
- }
388
- this.addWidget(GlobalWidgetFactory.create(this, widget.type, widget.config))
343
+ for (const widget of config.widgets) {
344
+ this.addWidget(GlobalWidgetFactory.createFromConfig(this, widget));
389
345
  }
390
346
  }
391
347
  }
@@ -454,7 +410,8 @@ export class FlowNode {
454
410
 
455
411
  public setProperty(name: string, value: any): void {
456
412
  const oldValue = this.#data[name];
457
- if (oldValue === value) {
413
+ // Object.is, not ===: NaN never equals itsel.
414
+ if (Object.is(oldValue, value)) {
458
415
  return;
459
416
  }
460
417
 
@@ -475,6 +432,34 @@ export class FlowNode {
475
432
  }
476
433
  }
477
434
 
435
+ /**
436
+ * Listens for a person changing the property through a widget, but not
437
+ * for the property being set from code.
438
+ */
439
+ public addPropertyEditListener(name: string, callback: PropertyEditCallback): void {
440
+ let callbacks = this.#registeredPropertyEditCallbacks.get(name);
441
+ if (callbacks === undefined) {
442
+ callbacks = [];
443
+ this.#registeredPropertyEditCallbacks.set(name, callbacks);
444
+ }
445
+ callbacks.push(callback);
446
+ }
447
+
448
+ /**
449
+ * Sets the property as a person's finished edit, so edit listeners hear
450
+ * it even when the value was already showing.
451
+ */
452
+ public editProperty(name: string, value: any): void {
453
+ this.setProperty(name, value);
454
+ const callbacks = this.#registeredPropertyEditCallbacks.get(name);
455
+ if (callbacks === undefined) {
456
+ return;
457
+ }
458
+ for (const callback of [...callbacks]) {
459
+ callback(value);
460
+ }
461
+ }
462
+
478
463
  public getProperty(name: string): any {
479
464
  return this.#data[name];
480
465
  }
@@ -788,6 +773,7 @@ export class FlowNode {
788
773
  // }
789
774
 
790
775
  public calculateBounds(ctx: CanvasRenderingContext2D, camera: Camera): Box {
776
+ this.#followTheme();
791
777
  const tempMeasurement = Zero();
792
778
 
793
779
  const doublePadding = this.#padding * 2;
@@ -807,29 +793,23 @@ export class FlowNode {
807
793
  }
808
794
 
809
795
  size.x += doublePadding;
810
- size.y += doublePadding + (this.#elementSpacing * this.#input.length);
796
+ size.y += doublePadding;
811
797
 
812
- for (let i = 0; i < this.#input.length; i++) {
813
- const port = this.#input[i];
814
- this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
815
- size.y += tempMeasurement.y;
816
- size.x = Math.max(size.x, tempMeasurement.x + doublePadding)
817
- }
818
-
819
- size.y += (this.#elementSpacing * this.#output.length);
820
-
821
- for (let i = 0; i < this.#output.length; i++) {
822
- const port = this.#output[i];
798
+ for (const port of [...this.#input, ...this.#output]) {
799
+ if (port.hidden()) {
800
+ continue;
801
+ }
823
802
  this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
824
- size.y += tempMeasurement.y;
803
+ size.y += this.#portRowHeight(port, tempMeasurement.y) + this.#elementSpacing;
825
804
  size.x = Math.max(size.x, tempMeasurement.x + doublePadding)
826
805
  }
827
806
 
828
- size.y += (this.#elementSpacing * this.#widgets.length);
829
- for (let i = 0; i < this.#widgets.length; i++) {
830
- const element = this.#widgets[i];
831
- const eleSize = element.Size();
832
- size.y += eleSize.y
807
+ for (const widget of this.#widgets) {
808
+ if (widget.Hidden?.()) {
809
+ continue;
810
+ }
811
+ const eleSize = widget.Size();
812
+ size.y += eleSize.y + this.#elementSpacing;
833
813
  size.x = Math.max(size.x, (eleSize.x) + doublePadding)
834
814
  }
835
815
 
@@ -933,7 +913,7 @@ export class FlowNode {
933
913
  }
934
914
 
935
915
  for (let i = 0; i < this.#inputPortPositions.Count(); i++) {
936
- if (InBox(this.#inputPortPositions.At(i), position)) {
916
+ if (!this.#input[i].hidden() && InBox(this.#inputPortPositions.At(i), position)) {
937
917
  intersection.Node = this;
938
918
  intersection.PortIndex = i;
939
919
  intersection.PortIsInput = true;
@@ -942,7 +922,7 @@ export class FlowNode {
942
922
  }
943
923
 
944
924
  for (let i = 0; i < this.#outputPortPositions.Count(); i++) {
945
- if (InBox(this.#outputPortPositions.At(i), position)) {
925
+ if (!this.#output[i].hidden() && InBox(this.#outputPortPositions.At(i), position)) {
946
926
  intersection.Node = this;
947
927
  intersection.PortIndex = i;
948
928
  intersection.PortIsInput = false;
@@ -951,10 +931,12 @@ export class FlowNode {
951
931
  }
952
932
 
953
933
  for (let i = 0; i < this.#widgetPositions.Count(); i++) {
954
- if (InBox(this.#widgetPositions.At(i), position)) {
934
+ const widget = this.#widgets[i];
935
+ if (widget !== undefined && !widget.Hidden?.() && InBox(this.#widgetPositions.At(i), position)) {
955
936
  intersection.Node = this;
956
937
  intersection.WidgetIndex = i;
957
- intersection.Widget = this.#widgets[i];
938
+ intersection.Widget = widget;
939
+ intersection.WidgetBox = this.#widgetPositions.At(i);
958
940
  }
959
941
  }
960
942
 
@@ -974,11 +956,6 @@ export class FlowNode {
974
956
  }
975
957
 
976
958
  setTitle(newTitle: string): void {
977
- if (!this.#canEditTitle) {
978
- console.warn("setTitle instruction ignored, as node has been marked un-editable");
979
- return;
980
- }
981
-
982
959
  let cleaned = newTitle;
983
960
  if (cleaned === null || cleaned === undefined) {
984
961
  cleaned = "";
@@ -999,10 +976,6 @@ export class FlowNode {
999
976
  }
1000
977
 
1001
978
  setInfo(newInfo: string): void {
1002
- if (!this.#canEditInfo) {
1003
- console.warn("setInfo instruction ignored, as node has been marked un-editable");
1004
- }
1005
-
1006
979
  let cleaned = newInfo;
1007
980
  if (cleaned === null || cleaned === undefined) {
1008
981
  cleaned = "";
@@ -1026,6 +999,13 @@ export class FlowNode {
1026
999
  return this.#inputPortPositions.At(index);
1027
1000
  }
1028
1001
 
1002
+ widgetPosition(index: number): Box | undefined {
1003
+ if (index < 0 || index >= this.#widgetPositions.Count()) {
1004
+ return undefined;
1005
+ }
1006
+ return this.#widgetPositions.At(index);
1007
+ }
1008
+
1029
1009
  inputPort(index: number): Port {
1030
1010
  return this.#input[index];
1031
1011
  }
@@ -1046,6 +1026,42 @@ export class FlowNode {
1046
1026
  return this.#output.length;
1047
1027
  }
1048
1028
 
1029
+ #applyStyle(): void {
1030
+ const style = this.#styleConfig;
1031
+ const titleText = style?.title?.textStyle?.color ?? Theme.Node.Title.FontColor;
1032
+ this.#title.style().setColor(titleText);
1033
+ this.#subTitle.style().setColor(titleText);
1034
+ this.#infoSymbol.style().setColor(titleText);
1035
+
1036
+ this.#titleColor = style?.title?.color ?? Theme.Node.Title.Color;
1037
+
1038
+ const box = (config: BoxStyleConfig | undefined, border: string, size: number) =>
1039
+ new BoxStyle(BoxStyleWithFallback(config, {
1040
+ border: { color: border, size },
1041
+ radius: Theme.Node.BorderRadius,
1042
+ color: Theme.Node.BackgroundColor,
1043
+ }));
1044
+ this.#stateStyles.set(NodeState.Idle, box(style?.idle, Theme.Node.Border.Idle, 1));
1045
+ this.#stateStyles.set(NodeState.MouseOver, box(style?.mouseOver, Theme.Node.Border.MouseOver, 1.1));
1046
+ this.#stateStyles.set(NodeState.Grabbed, box(style?.grabbed, Theme.Node.Border.Grabbed, 2));
1047
+ this.#selectedStyle = box(style?.selected, Theme.Node.Border.Selected, 1);
1048
+
1049
+ this.#portTextStyle = new TextStyle({
1050
+ size: style?.portText?.size ?? 14,
1051
+ color: style?.portText?.color ?? Theme.Node.Port.FontColor,
1052
+ font: style?.portText?.font,
1053
+ weight: style?.portText?.weight,
1054
+ });
1055
+
1056
+ this.#styledForTheme = themeVersion();
1057
+ }
1058
+
1059
+ #followTheme(): void {
1060
+ if (this.#styledForTheme !== themeVersion()) {
1061
+ this.#applyStyle();
1062
+ }
1063
+ }
1064
+
1049
1065
  #calculateStyle(state: NodeState): BoxStyle {
1050
1066
  // Let the node being selected override the idle style
1051
1067
  if (this.#selected && state === NodeState.Idle) {
@@ -1067,10 +1083,25 @@ export class FlowNode {
1067
1083
  this.#messages = [];
1068
1084
  }
1069
1085
 
1086
+ #portRowHeight(port: Port, textHeight: number): number {
1087
+ const slots = port.slots();
1088
+ return slots > 1 ? Math.max(textHeight, slots * ARRAY_SLOT_PITCH) : textHeight;
1089
+ }
1090
+
1070
1091
  public hasBeenLaidOut(): boolean {
1071
1092
  return this.#laidOut;
1072
1093
  }
1073
1094
 
1095
+ /**
1096
+ * Throws away where the ports were last placed, so wires wait for the
1097
+ * next draw instead of pointing at the old spots.
1098
+ */
1099
+ public invalidateLayout(): void {
1100
+ this.#laidOut = false;
1101
+ this.#portOffsets = { input: [], output: [] };
1102
+ requestRender();
1103
+ }
1104
+
1074
1105
  #laidOut: boolean = false;
1075
1106
 
1076
1107
  render(ctx: CanvasRenderingContext2D, camera: Camera, state: NodeState, mousePosition: Vector2 | undefined, postProcess: PassSubsystem): void {
@@ -1117,7 +1148,10 @@ export class FlowNode {
1117
1148
  [nodeStyle.radius() * camera.zoom, nodeStyle.radius() * camera.zoom, 0, 0]
1118
1149
  );
1119
1150
  ctx.fill();
1120
- // ctx.stroke();
1151
+
1152
+ if (Theme.Node.Bevel !== null) {
1153
+ drawBevel(ctx, nodeBounds, camera.zoom, Theme.Node.Bevel, false);
1154
+ }
1121
1155
 
1122
1156
  const titlePosition = VectorPool.get();
1123
1157
  titlePosition.x = nodeBounds.Position.x + (nodeBounds.Size.x / 2)
@@ -1217,8 +1251,12 @@ export class FlowNode {
1217
1251
  for (let i = 0; i < this.#input.length; i++) {
1218
1252
  ctx.textAlign = TextAlign.Left;
1219
1253
  const port = this.#input[i];
1254
+ if (port.hidden()) {
1255
+ this.#inputPortPositions.Push({ Position: { x: nodeBounds.Position.x, y: startY }, Size: { x: 0, y: 0 } });
1256
+ continue;
1257
+ }
1220
1258
  this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
1221
- const portHeight = tempMeasurement.y * camera.zoom;
1259
+ const portHeight = this.#portRowHeight(port, tempMeasurement.y) * camera.zoom;
1222
1260
  const position = VectorPool.get();
1223
1261
 
1224
1262
  position.x = nodeBounds.Position.x;
@@ -1239,6 +1277,10 @@ export class FlowNode {
1239
1277
  ctx.textAlign = TextAlign.Right;
1240
1278
  for (let i = 0; i < this.#output.length; i++) {
1241
1279
  const port = this.#output[i];
1280
+ if (port.hidden()) {
1281
+ this.#outputPortPositions.Push({ Position: { x: rightSide, y: startY }, Size: { x: 0, y: 0 } });
1282
+ continue;
1283
+ }
1242
1284
  this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
1243
1285
  const portHeight = tempMeasurement.y * camera.zoom;
1244
1286
  const position = VectorPool.get();
@@ -1258,15 +1300,18 @@ export class FlowNode {
1258
1300
  // Widgets
1259
1301
  for (let i = 0; i < this.#widgets.length; i++) {
1260
1302
  const widget = this.#widgets[i];
1303
+ if (widget.Hidden?.()) {
1304
+ this.#widgetPositions.Push({ Position: { x: nodeBounds.Position.x, y: startY }, Size: { x: 0, y: 0 } });
1305
+ continue;
1306
+ }
1261
1307
  const widgetSize = widget.Size();
1262
- const scaledWidgetWidth = widgetSize.x * camera.zoom;
1263
-
1264
- const position = VectorPool.get();
1265
- position.x = nodeBounds.Position.x + ((nodeBounds.Size.x - scaledWidgetWidth) / 2);
1266
- position.y = startY;
1267
-
1268
- this.#widgetPositions.Push(widget.Draw(ctx, position, camera.zoom, mousePosition));
1269
- startY += (widgetSize.y * camera.zoom) + scaledElementSpacing;
1308
+ const box: Box = {
1309
+ Position: { x: nodeBounds.Position.x + scaledPadding, y: startY },
1310
+ Size: { x: nodeBounds.Size.x - (scaledPadding * 2), y: widgetSize.y * camera.zoom },
1311
+ };
1312
+ widget.Draw(ctx, box, camera.zoom, mousePosition);
1313
+ this.#widgetPositions.Push(box);
1314
+ startY += box.Size.y + scaledElementSpacing;
1270
1315
  }
1271
1316
 
1272
1317
  // Messages
@@ -1311,6 +1356,14 @@ export class FlowNode {
1311
1356
  }
1312
1357
 
1313
1358
  public layoutPortsWithoutDrawing(bounds: Box, zoom: number): boolean {
1359
+ if (!this.placePorts(bounds, zoom)) {
1360
+ return false;
1361
+ }
1362
+ this.#widgetPositions.Clear();
1363
+ return true;
1364
+ }
1365
+
1366
+ public placePorts(bounds: Box, zoom: number): boolean {
1314
1367
  if (this.#portOffsets.input.length === 0 && this.#portOffsets.output.length === 0) {
1315
1368
  return false;
1316
1369
  }
@@ -1335,8 +1388,6 @@ export class FlowNode {
1335
1388
  project(this.#portOffsets.input, this.#inputPortPositions);
1336
1389
  project(this.#portOffsets.output, this.#outputPortPositions);
1337
1390
 
1338
- this.#widgetPositions.Clear();
1339
-
1340
1391
  return true;
1341
1392
  }
1342
1393
  }