@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/dist/index.js +11 -10
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11 -10
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/node.ts +2 -10
- package/src/nodes/subsystem.ts +10 -0
- package/src/widgets/number.ts +1 -1
- package/src/widgets/slider.ts +1 -1
- package/test/hover_after_removal.test.ts +72 -0
- package/test/property_binding.test.ts +49 -0
package/package.json
CHANGED
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
|
-
|
|
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 = "";
|
package/src/nodes/subsystem.ts
CHANGED
|
@@ -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);
|
package/src/widgets/number.ts
CHANGED
package/src/widgets/slider.ts
CHANGED
|
@@ -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
|
+
});
|