@elicdavis/node-flow 0.1.19 → 0.1.20

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.1.19",
5
+ "version": "0.1.20",
6
6
  "license": "MIT",
7
7
  "keywords": [
8
8
  "nodes",
package/src/node.ts CHANGED
@@ -454,7 +454,8 @@ export class FlowNode {
454
454
 
455
455
  public setProperty(name: string, value: any): void {
456
456
  const oldValue = this.#data[name];
457
- if (oldValue === value) {
457
+ // Object.is, not ===: NaN never equals itsel.
458
+ if (Object.is(oldValue, value)) {
458
459
  return;
459
460
  }
460
461
 
@@ -974,11 +975,6 @@ export class FlowNode {
974
975
  }
975
976
 
976
977
  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
978
  let cleaned = newTitle;
983
979
  if (cleaned === null || cleaned === undefined) {
984
980
  cleaned = "";
@@ -999,10 +995,6 @@ export class FlowNode {
999
995
  }
1000
996
 
1001
997
  setInfo(newInfo: string): void {
1002
- if (!this.#canEditInfo) {
1003
- console.warn("setInfo instruction ignored, as node has been marked un-editable");
1004
- }
1005
-
1006
998
  let cleaned = newInfo;
1007
999
  if (cleaned === null || cleaned === undefined) {
1008
1000
  cleaned = "";
@@ -684,6 +684,16 @@ export class NodeSubsystem {
684
684
  this.#removeNodeConnections(nodeIndex);
685
685
  const node = this.#nodes[nodeIndex];
686
686
  this.#nodes.splice(nodeIndex, 1);
687
+
688
+ // Hover is only recomputed on render, which is lazy, so it must follow the splice.
689
+ if (this.#nodeHovering === nodeIndex) {
690
+ this.#nodeHovering = -1;
691
+ } else if (this.#nodeHovering > nodeIndex) {
692
+ this.#nodeHovering--;
693
+ }
694
+ if (this.#portHovering?.Node === node) {
695
+ this.#portHovering = null;
696
+ }
687
697
  requestRender();
688
698
  for (let i = 0; i < this.#nodeRemovedCallbacks.length; i++) {
689
699
  this.#nodeRemovedCallbacks[i](node);
@@ -79,7 +79,7 @@ export class NumberWidget {
79
79
  }
80
80
 
81
81
  Set(newNumber: number): void {
82
- if (this.#value === newNumber) {
82
+ if (Object.is(this.#value, newNumber)) {
83
83
  return;
84
84
  }
85
85
 
@@ -112,7 +112,7 @@ export class SliderWidget {
112
112
 
113
113
  SetValue(newValue: number): void {
114
114
  const cleanedValue = Clamp(newValue, this.#min, this.#max);
115
- if (this.#value === cleanedValue) {
115
+ if (Object.is(this.#value, cleanedValue)) {
116
116
  return;
117
117
  }
118
118
 
@@ -0,0 +1,72 @@
1
+ import { strict as assert } from 'assert';
2
+ import { NodeSubsystem } from '../src/nodes/subsystem';
3
+ import { PassSubsystem } from '../src/pass/subsystem';
4
+ import { FlowNode } from '../src/node';
5
+ import { Camera } from '../src/camera';
6
+ import { ContextMenuConfig } from '../src/contextMenu';
7
+ import { Vector2 } from '../src/types/vector2';
8
+
9
+ function stubCtx(): CanvasRenderingContext2D {
10
+ const ctx: any = {
11
+ canvas: { width: 800, height: 600 },
12
+ lineWidth: 1,
13
+ font: '',
14
+ textAlign: 'left',
15
+ textBaseline: 'top',
16
+ fillStyle: '',
17
+ strokeStyle: '',
18
+ globalAlpha: 1,
19
+ measureText: () => ({ width: 40 }),
20
+ save: () => { }, restore: () => { }, translate: () => { }, scale: () => { },
21
+ beginPath: () => { }, closePath: () => { }, roundRect: () => { }, rect: () => { },
22
+ moveTo: () => { }, lineTo: () => { }, arc: () => { }, ellipse: () => { },
23
+ bezierCurveTo: () => { }, quadraticCurveTo: () => { },
24
+ fill: () => { }, stroke: () => { }, fillText: () => { }, strokeText: () => { },
25
+ clip: () => { }, drawImage: () => { }, setLineDash: () => { },
26
+ createLinearGradient: () => ({ addColorStop: () => { } }),
27
+ };
28
+ return ctx as CanvasRenderingContext2D;
29
+ }
30
+
31
+ function hoverOver(graph: NodeSubsystem, node: FlowNode, ctx: CanvasRenderingContext2D, camera: Camera): Vector2 {
32
+ graph.render(ctx, camera, undefined);
33
+ const bounds = node.calculateBounds(ctx, camera);
34
+ const over = { x: bounds.Position.x + bounds.Size.x / 2, y: bounds.Position.y + bounds.Size.y / 2 };
35
+ graph.render(ctx, camera, over);
36
+ return over;
37
+ }
38
+
39
+ function deleteNodeItem(menu: ContextMenuConfig | null) {
40
+ return menu?.subMenus?.find((m) => m.name === 'Delete')?.items?.find((i) => i.name === 'Node');
41
+ }
42
+
43
+ describe('the hovered node going away before the next render', () => {
44
+ it('opens the context menu without it', () => {
45
+ const graph = new NodeSubsystem(new PassSubsystem());
46
+ const node = new FlowNode({ title: 'gone', position: { x: 100, y: 100 } });
47
+ graph.addNode(node);
48
+ const ctx = stubCtx();
49
+ const camera = new Camera();
50
+ const over = hoverOver(graph, node, ctx, camera);
51
+
52
+ graph.removeNode(node);
53
+
54
+ assert.equal(deleteNodeItem(graph.openContextMenu(ctx, over)), undefined);
55
+ });
56
+
57
+ it('keeps pointing at the same node when an earlier one is removed', () => {
58
+ const graph = new NodeSubsystem(new PassSubsystem());
59
+ const earlier = new FlowNode({ title: 'earlier', position: { x: 100, y: 100 } });
60
+ const hovered = new FlowNode({ title: 'hovered', position: { x: 400, y: 100 } });
61
+ graph.addNode(earlier);
62
+ graph.addNode(hovered);
63
+ const ctx = stubCtx();
64
+ const camera = new Camera();
65
+ const over = hoverOver(graph, hovered, ctx, camera);
66
+
67
+ graph.removeNode(earlier);
68
+ deleteNodeItem(graph.openContextMenu(ctx, over))?.callback?.();
69
+
70
+ assert.deepEqual(graph.getNodes(), []);
71
+ });
72
+ });
@@ -0,0 +1,49 @@
1
+ import { strict as assert } from 'assert';
2
+ import { FlowNode } from '../src/node';
3
+ import { NumberWidget } from '../src/widgets/number';
4
+ import { SliderWidget } from '../src/widgets/slider';
5
+
6
+ describe('property binding', () => {
7
+ it('settles when a number widget is bound to a property set to NaN', () => {
8
+ const node = new FlowNode({ title: 'test', position: { x: 0, y: 0 } });
9
+ new NumberWidget(node, { property: 'x' });
10
+
11
+ node.setProperty('x', NaN);
12
+
13
+ assert.ok(Number.isNaN(node.getProperty('x')));
14
+ });
15
+
16
+ it('settles when a slider is bound to a property set to NaN', () => {
17
+ const node = new FlowNode({ title: 'test', position: { x: 0, y: 0 } });
18
+ new SliderWidget(node, { property: 'x', min: 0, max: 1 });
19
+
20
+ node.setProperty('x', NaN);
21
+ });
22
+
23
+ it('does not report a NaN property as changed when it stays NaN', () => {
24
+ const node = new FlowNode({ title: 'test', position: { x: 0, y: 0 } });
25
+ let changes = 0;
26
+ node.addPropertyChangeListener('x', () => { changes++; });
27
+
28
+ node.setProperty('x', NaN);
29
+ node.setProperty('x', NaN);
30
+
31
+ assert.equal(changes, 1);
32
+ });
33
+ });
34
+
35
+ describe('programmatic title and info', () => {
36
+ it('sets the title of a node the user cannot retitle', () => {
37
+ const node = new FlowNode({ title: 'before', position: { x: 0, y: 0 }, canEditTitle: false });
38
+ node.setTitle('after');
39
+ assert.equal(node.title(), 'after');
40
+ });
41
+
42
+ it('sets the info of a node the user cannot edit the info of', () => {
43
+ const node = new FlowNode({ title: 'test', position: { x: 0, y: 0 }, canEditInfo: false });
44
+ let changed = 0;
45
+ node.addInfoChangeListener(() => { changed++; });
46
+ node.setInfo('carrying a mesh');
47
+ assert.equal(changed, 1);
48
+ });
49
+ });