An edge to a vanished port needs a handle, so the port stays as a ghost (#60)
game-maker / suite (push) Successful in 10m7s
game-maker / suite (push) Successful in 10m7s
Four ways erase a derived port and take no edge with them: deleting a state, deleting the `State machine` literal, deleting `Behavior start`/`end`, deleting the asset. The common cause is `RemoveNodeCommand` clearing only its own graph, so the fix sits in `drawnDef` (graph/nested.ts) and touches no delete site. A right-click on an edge goes through `view.portAt` only — there is no curve hit-test. Without a port row such an edge is not merely invisible, it is unreachable, so the ghost row is the handle Disconnect hangs on. `resolveDef` stays untouched: it is the truth for interpreter, compiler, ConnectCommand and dropTarget, so nothing runs or connects through a ghost without a second guard.
This commit is contained in:
1 parent
d5e67cd80c
commit
a041322a0e
8 files changed
+372
-16
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import { textOf } from '../model/Properties';
|
||||
import { isTaggedKind } from '../properties/kinds';
|
||||
import { flatDef, isValidFieldName } from '../properties/PropertySchema';
|
||||
import { type DeclaredPort, declaredPortType, type Graph, type GraphNode } from './Graph';
|
||||
import { type DeclaredPort, declaredPortType, type Graph, type GraphEdge, type GraphNode } from './Graph';
|
||||
import type { GraphLookup } from './lookup';
|
||||
import { nodeDef, type NodeDef, type PortDef } from './NodeRegistry';
|
||||
|
||||
@@ -109,6 +109,56 @@ function execFirst(ports: PortDef[]): PortDef[] {
|
||||
return [...ports.filter((p) => p.type === 'exec'), ...ports.filter((p) => p.type !== 'exec')];
|
||||
}
|
||||
|
||||
/** One end of an edge naming a key its node no longer has. */
|
||||
interface GhostEnd {
|
||||
edge: GraphEdge;
|
||||
key: string;
|
||||
isOutput: boolean;
|
||||
}
|
||||
|
||||
/** Read from the edge list, not from the ports: what is looked for is exactly what has no port. */
|
||||
function ghostEnds(node: GraphNode, graph: Graph, def: NodeDef): GhostEnd[] {
|
||||
const inputs = new Set(def.inputs.map((p) => p.key));
|
||||
const outputs = new Set(def.outputs.map((p) => p.key));
|
||||
const ends: GhostEnd[] = [];
|
||||
for (const edge of graph.edges) {
|
||||
if (edge.to.node === node.id && !inputs.has(edge.to.port))
|
||||
ends.push({ edge, key: edge.to.port, isOutput: false });
|
||||
if (edge.from.node === node.id && !outputs.has(edge.from.port))
|
||||
ends.push({ edge, key: edge.from.port, isOutput: true });
|
||||
}
|
||||
return ends;
|
||||
}
|
||||
|
||||
/** `any` because the type the key once had is exactly what is no longer known. */
|
||||
function ghostPort(key: string): PortDef {
|
||||
return { key, label: key, type: 'any', inline: false, ghost: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* The definition as the canvas draws it: the real ports, then the keys only edges still name. The
|
||||
* ghosts go last and skip `execFirst`, so no real row moves because of one – and they stay out of
|
||||
* `resolveDef`, which the interpreter, the compilers and `ConnectCommand` read as the truth.
|
||||
*/
|
||||
export function drawnDef(node: GraphNode, graph: Graph, def: NodeDef): NodeDef {
|
||||
const ends = ghostEnds(node, graph, def);
|
||||
if (ends.length === 0) return def;
|
||||
const side = (isOutput: boolean) =>
|
||||
[...new Set(ends.filter((e) => e.isOutput === isOutput).map((e) => e.key))].map(ghostPort);
|
||||
return {
|
||||
...def,
|
||||
inputs: [...def.inputs, ...side(false)],
|
||||
outputs: [...def.outputs, ...side(true)],
|
||||
};
|
||||
}
|
||||
|
||||
/** Why a node warns although its own settings are sound: edges left hanging on keys that are gone. */
|
||||
export function ghostProblem(node: GraphNode, graph: Graph, def: NodeDef): string | null {
|
||||
const count = ghostEnds(node, graph, def).length;
|
||||
if (count === 0) return null;
|
||||
return count === 1 ? '1 edge on a port that is gone' : `${count} edges on ports that are gone`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Why this call cannot do anything, or null if it can. A call is the one node that depends on a
|
||||
* *different* graph, so it is the one node that can look perfectly healthy and still be dead.
|
||||
|
||||
Reference in new issue
Block a user