Add Get/Set properties nodes and a breadcrumb for opened sub-behaviors
Get properties / Set properties work like their variable counterparts:
one port per chosen property, an unconnected input writes its inline
value. A port chosen from the built-in list keeps that property's field
and default (height starts at 100, color is a colour field). "object" is
reserved for the fixed input, checked by the command and on load.
The property picker names the owning type ("Sprite width"), and text
fields can now carry a suggestion list; Get/Set property use it.
Double-clicking a call node shows the path of opened behaviors under the
graph header. Clicking an entry jumps back and trims the path; choosing
a behavior any other way hides it. Reopening a behavior already on the
path steps back to it instead of repeating it. The bar shows before the
graph switches, otherwise it clips the graph that was just fitted.
The "Add node..." header button is gone; nodes come from the context menu.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e9fa592078
commit
854ae2e756
35 files changed
+1044
-101
No files matched your search
@@ -4,6 +4,7 @@ import {
|
||||
declaresVariablePorts,
|
||||
type DeclaredPort,
|
||||
findNode,
|
||||
fixedPortKeys,
|
||||
type Graph,
|
||||
type GraphEdge,
|
||||
type GraphNode,
|
||||
@@ -73,6 +74,12 @@ function carryPort(site: PortSite, from: string, to: string): void {
|
||||
const INVALID_NAME =
|
||||
'Port names must start with a letter, contain only letters, digits or _, and "exec" is taken by the flow port';
|
||||
|
||||
function checkPortName(node: GraphNode, key: string): void {
|
||||
if (!isValidPortName(key)) throw new CommandError(INVALID_NAME);
|
||||
if (fixedPortKeys(requireNodeDef(node.type)).includes(key))
|
||||
throw new CommandError(`"${key}" is taken by a port of the node itself`);
|
||||
}
|
||||
|
||||
/** Several ports as one step, so seeding a node from a struct is one undo rather than one per field. */
|
||||
export class AddPortsCommand implements Command {
|
||||
readonly label: string;
|
||||
@@ -91,7 +98,7 @@ export class AddPortsCommand implements Command {
|
||||
const node = requireNode(project, this.assetId, this.nodeId);
|
||||
const keys = new Set((node.ports ?? []).map((p) => p.key));
|
||||
for (const port of this.ports) {
|
||||
if (!isValidPortName(port.key)) throw new CommandError(INVALID_NAME);
|
||||
checkPortName(node, port.key);
|
||||
if (keys.has(port.key)) throw new CommandError(`"${port.key}" already exists`);
|
||||
keys.add(port.key);
|
||||
}
|
||||
@@ -187,7 +194,7 @@ export class RenamePortCommand implements Command {
|
||||
if (from === to) return;
|
||||
const node = requireNode(project, this.assetId, this.nodeId);
|
||||
const sites = portSites(project, this.assetId, node);
|
||||
if (!isValidPortName(to)) throw new CommandError(INVALID_NAME);
|
||||
checkPortName(node, to);
|
||||
const ports = node.ports ?? [];
|
||||
if (!ports.some((p) => p.key === from)) throw new CommandError(`Port "${from}" not found`);
|
||||
if (ports.some((p) => p.key === to)) throw new CommandError(`"${to}" already exists`);
|
||||
@@ -209,11 +216,11 @@ function renamePortOn(graph: Graph, node: GraphNode, from: string, to: string):
|
||||
}
|
||||
|
||||
/**
|
||||
* Points one declared port at another variable. Key and kind change together, because on a
|
||||
* Get/Set variables node the kind is the variable's rather than a choice of its own; the edges
|
||||
* Points one declared port at another variable or property. Key and kind change together, because
|
||||
* there the kind is the variable's (the property's) rather than a choice of its own; the edges
|
||||
* hanging on the port come along, so correcting a pick is not a rewiring.
|
||||
*/
|
||||
export class SetVariablePortCommand implements Command {
|
||||
export class RepointPortCommand implements Command {
|
||||
readonly label: string;
|
||||
private previous?: DeclaredPort;
|
||||
|
||||
@@ -231,6 +238,7 @@ export class SetVariablePortCommand implements Command {
|
||||
const ports = node.ports ?? [];
|
||||
const current = ports.find((p) => p.key === this.from);
|
||||
if (!current) throw new CommandError(`Port "${this.from}" not found`);
|
||||
checkPortName(node, this.port.key);
|
||||
if (this.from !== this.port.key && ports.some((p) => p.key === this.port.key))
|
||||
throw new CommandError(`"${this.port.key}" already exists`);
|
||||
this.previous = { ...current };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PropertyBag, PropertyValue } from '../model/Properties';
|
||||
import type { PropertyDef } from '../properties/PropertySchema';
|
||||
import type { Graph, GraphNode, PortType } from './Graph';
|
||||
import type { DeclaredPort, Graph, GraphNode, PortType } from './Graph';
|
||||
import type { BehaviorLookup } from './lookup';
|
||||
import type { NodeContext } from './NodeContext';
|
||||
import { asBoolean, asList, asNumber, asString, type Value } from './values';
|
||||
@@ -11,6 +11,8 @@ export interface PortDef {
|
||||
type: PortType;
|
||||
/** Inline value used while the input is unconnected and the node has no own value (default 0/''/false). */
|
||||
default?: PropertyValue;
|
||||
/** The property this port carries; its typed-in value is edited as that property (color, enum, …). */
|
||||
field?: PropertyDef;
|
||||
}
|
||||
|
||||
export type NodeCategory = 'event' | 'flow' | 'data' | 'variable' | 'action';
|
||||
@@ -76,6 +78,8 @@ export interface NodeDef {
|
||||
* is what gives a node the port editor in the inspector.
|
||||
*/
|
||||
declaresPorts?: 'inputs' | 'outputs';
|
||||
/** How a declared port becomes a port; without it, named after its key and typed by its kind. */
|
||||
declaredPort?(port: DeclaredPort): PortDef;
|
||||
/**
|
||||
* Ports this node computes beyond the fixed and the declared ones – how many outputs a Sequence
|
||||
* offers, which values the behavior a call node names takes. `resolveDef` applies it.
|
||||
@@ -132,6 +136,7 @@ export function inlineInputs(def: NodeDef): PortDef[] {
|
||||
}
|
||||
|
||||
export function inlineInputDef(port: PortDef): PropertyDef {
|
||||
if (port.field) return { ...port.field, key: port.key, label: port.label };
|
||||
switch (port.type) {
|
||||
case 'boolean':
|
||||
return { key: port.key, label: port.label, kind: 'boolean', default: asBoolean(port.default ?? false) };
|
||||
|
||||
@@ -7,6 +7,7 @@ export * from './lookup';
|
||||
export * from './nested';
|
||||
export * from './NodeContext';
|
||||
export * from './NodeRegistry';
|
||||
export * from './propertyPorts';
|
||||
export * from './values';
|
||||
export * from './variablePorts';
|
||||
export { GraphInterpreter, type TraceListener } from './GraphInterpreter';
|
||||
@@ -9,6 +9,8 @@ export const BEHAVIOR_END = 'BehaviorEnd';
|
||||
export const RUN_BEHAVIOR = 'RunBehavior';
|
||||
export const GET_VARIABLES = 'GetVariables';
|
||||
export const SET_VARIABLES = 'SetVariables';
|
||||
export const GET_PROPERTIES = 'GetProperties';
|
||||
export const SET_PROPERTIES = 'SetProperties';
|
||||
|
||||
export const BEHAVIOR_PARAM = 'behavior';
|
||||
|
||||
@@ -21,11 +23,21 @@ export function declaresVariablePorts(type: string): boolean {
|
||||
return type === GET_VARIABLES || type === SET_VARIABLES;
|
||||
}
|
||||
|
||||
/** Nodes whose declared ports are properties of the object they are handed. */
|
||||
export function declaresPropertyPorts(type: string): boolean {
|
||||
return type === GET_PROPERTIES || type === SET_PROPERTIES;
|
||||
}
|
||||
|
||||
/** Identifier-like, and not `exec`: that key belongs to the fixed flow port of every terminal node. */
|
||||
export function isValidPortName(name: string): boolean {
|
||||
return isValidFieldName(name) && name !== 'exec';
|
||||
}
|
||||
|
||||
/** Keys a declared port cannot take: the node's fixed ports on the side it declares (`object`). */
|
||||
export function fixedPortKeys(def: NodeDef): string[] {
|
||||
return (def.declaresPorts === 'inputs' ? def.inputs : def.outputs).map((p) => p.key);
|
||||
}
|
||||
|
||||
/** The single start (or end) node of a graph; a second one is ignored. */
|
||||
export function terminalNode(graph: Graph, type: string): GraphNode | undefined {
|
||||
return graph.nodes.find((n) => n.type === type);
|
||||
@@ -56,7 +68,7 @@ export function declaredPorts(graph: Graph | undefined, type: string): PortDef[]
|
||||
export function resolveDef(node: GraphNode, graph: Graph, lookup: BehaviorLookup): NodeDef | undefined {
|
||||
const def = nodeDef(node.type);
|
||||
if (!def || (!def.declaresPorts && !def.derivePorts)) return def;
|
||||
const own = def.declaresPorts ? (node.ports ?? []).map(portDefOf) : [];
|
||||
const own = def.declaresPorts ? (node.ports ?? []).map((p) => (def.declaredPort ?? portDefOf)(p)) : [];
|
||||
const side = (which: 'inputs' | 'outputs') => (def.declaresPorts === which ? own : []);
|
||||
const grown = def.derivePorts?.({ node, graph, behaviors: lookup }) ?? {};
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { SET_PROPERTIES } from '../../nested';
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { propertyPort } from '../../propertyPorts';
|
||||
import { asEntity, writeTyped } from '../../values';
|
||||
import { exec, obj } from '../shared';
|
||||
|
||||
/**
|
||||
* The plural of Set property: one input per property picked. Like Set variables it writes every
|
||||
* property it lists each time it runs – an unconnected input writes the value typed on the node.
|
||||
*/
|
||||
export const SetProperties: NodeDef = {
|
||||
type: SET_PROPERTIES,
|
||||
label: 'Set properties',
|
||||
category: 'action',
|
||||
description: 'Writes several properties (width, color, …) of an object at once, one input per property.',
|
||||
inputs: [exec(), obj('object')],
|
||||
outputs: [exec()],
|
||||
params: [],
|
||||
declaresPorts: 'inputs',
|
||||
declaredPort: propertyPort,
|
||||
run: (_ctx, inputs, _params, def) => {
|
||||
const target = asEntity(inputs.object);
|
||||
if (target)
|
||||
for (const port of def.inputs.slice(SetProperties.inputs.length))
|
||||
writeTyped(target.props, port.key, inputs[port.key]);
|
||||
return { next: 'exec' };
|
||||
},
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
import { textOf } from '../../../model/Properties';
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { asEntity, writeTyped } from '../../values';
|
||||
import { any, exec, obj } from '../shared';
|
||||
import { any, exec, obj, PROPERTY_PARAM } from '../shared';
|
||||
|
||||
import { textOf } from '../../../model/Properties';
|
||||
export const SetProperty: NodeDef = {
|
||||
type: 'SetProperty',
|
||||
label: 'Set property',
|
||||
@@ -10,7 +10,7 @@ export const SetProperty: NodeDef = {
|
||||
description: 'Writes a property (width, color, …) of an object.',
|
||||
inputs: [exec(), obj('object'), any('value')],
|
||||
outputs: [exec()],
|
||||
params: [{ key: 'property', label: 'Property', kind: 'text', default: 'width' }],
|
||||
params: [PROPERTY_PARAM],
|
||||
run: (_ctx, inputs, params) => {
|
||||
const target = asEntity(inputs.object);
|
||||
const key = textOf(params.property ?? '');
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { GET_PROPERTIES } from '../../nested';
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { propertyPort } from '../../propertyPorts';
|
||||
import { asEntity, type Value } from '../../values';
|
||||
import { obj } from '../shared';
|
||||
|
||||
/** The plural of Get property: one output per property picked, the way Get variables reads. */
|
||||
export const GetProperties: NodeDef = {
|
||||
type: GET_PROPERTIES,
|
||||
label: 'Get properties',
|
||||
category: 'data',
|
||||
description: 'Reads several properties (width, color, …) of an object at once, one output per property.',
|
||||
inputs: [obj('object')],
|
||||
outputs: [],
|
||||
params: [],
|
||||
declaresPorts: 'outputs',
|
||||
declaredPort: propertyPort,
|
||||
run: (_ctx, inputs, _params, def) => {
|
||||
const target = asEntity(inputs.object);
|
||||
const outputs: Record<string, Value> = {};
|
||||
for (const port of def.outputs) outputs[port.key] = target?.props[port.key] ?? null;
|
||||
return { outputs };
|
||||
},
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
import { textOf } from '../../../model/Properties';
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { asEntity } from '../../values';
|
||||
import { any, obj } from '../shared';
|
||||
import { any, obj, PROPERTY_PARAM } from '../shared';
|
||||
|
||||
import { textOf } from '../../../model/Properties';
|
||||
export const GetProperty: NodeDef = {
|
||||
type: 'GetProperty',
|
||||
label: 'Get property',
|
||||
@@ -10,7 +10,7 @@ export const GetProperty: NodeDef = {
|
||||
description: 'Reads a property (width, color, …) of an object.',
|
||||
inputs: [obj('object')],
|
||||
outputs: [any('value')],
|
||||
params: [{ key: 'property', label: 'Property', kind: 'text', default: 'width' }],
|
||||
params: [PROPERTY_PARAM],
|
||||
run: (_ctx, inputs, params) => ({
|
||||
outputs: { value: asEntity(inputs.object)?.props[textOf(params.property ?? '')] ?? null },
|
||||
}),
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Move } from './action/Move';
|
||||
import { RestartGame } from './action/RestartGame';
|
||||
import { SetEnabled } from './action/SetEnabled';
|
||||
import { SetPosition } from './action/SetPosition';
|
||||
import { SetProperties } from './action/SetProperties';
|
||||
import { SetProperty } from './action/SetProperty';
|
||||
import { SetText } from './action/SetText';
|
||||
import { Spawn } from './action/Spawn';
|
||||
@@ -23,6 +24,7 @@ import { Equals } from './data/Equals';
|
||||
import { FindText } from './data/FindText';
|
||||
import { Floor } from './data/Floor';
|
||||
import { GetPosition } from './data/GetPosition';
|
||||
import { GetProperties } from './data/GetProperties';
|
||||
import { GetProperty } from './data/GetProperty';
|
||||
import { IsAsset } from './data/IsAsset';
|
||||
import { IsEnabled } from './data/IsEnabled';
|
||||
@@ -125,6 +127,7 @@ export const BUILT_IN_NODES: readonly NodeDef[] = [
|
||||
IsAsset,
|
||||
IsEnabled,
|
||||
GetProperty,
|
||||
GetProperties,
|
||||
GetPosition,
|
||||
// data: literals and math
|
||||
NumberLiteral,
|
||||
@@ -182,6 +185,7 @@ export const BUILT_IN_NODES: readonly NodeDef[] = [
|
||||
Move,
|
||||
SetPosition,
|
||||
SetProperty,
|
||||
SetProperties,
|
||||
SetEnabled,
|
||||
SetText,
|
||||
Spawn,
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import { type PropertyBag, type PropertyValue, textOf } from '../../model/Properties';
|
||||
import type { PropertyDef } from '../../properties/PropertySchema';
|
||||
import { OBJECT_PROPERTY_SUGGESTIONS, type PropertyDef } from '../../properties/PropertySchema';
|
||||
import type { NodeContext } from '../NodeContext';
|
||||
import type { DerivedShape, NodeDef, PortDef, PortSource, RunResult } from '../NodeRegistry';
|
||||
import { asBoolean, asNumber, type Value, writeTyped } from '../values';
|
||||
|
||||
/** The property a Get/Set property node names: typed freely, or picked from the built-in ones. */
|
||||
export const PROPERTY_PARAM: PropertyDef = {
|
||||
key: 'property',
|
||||
label: 'Property',
|
||||
kind: 'text',
|
||||
default: 'width',
|
||||
suggestions: OBJECT_PROPERTY_SUGGESTIONS,
|
||||
};
|
||||
|
||||
export const exec = (key = 'exec', label = ''): PortDef => ({ key, label, type: 'exec' });
|
||||
export const num = (key: string, label = key): PortDef => ({ key, label, type: 'number' });
|
||||
export const bool = (key: string, label = key): PortDef => ({ key, label, type: 'boolean' });
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { OBJECT_PROPERTIES, type PropertyDef } from '../properties/PropertySchema';
|
||||
import type { DeclaredPort } from './Graph';
|
||||
import { portDefOf } from './nested';
|
||||
import type { PortDef } from './NodeRegistry';
|
||||
import { portKindOf } from './variablePorts';
|
||||
|
||||
export function objectPropertyDef(key: string): PropertyDef | undefined {
|
||||
return OBJECT_PROPERTIES.find((def) => def.key === key);
|
||||
}
|
||||
|
||||
export function propertyPortOf(def: PropertyDef): DeclaredPort {
|
||||
return { key: def.key, kind: portKindOf(def) };
|
||||
}
|
||||
|
||||
/**
|
||||
* A port naming a built-in property carries its def: a typed-in value starts at its default and is
|
||||
* edited with its field. A custom name, or a kind the property does not travel as, stays plain.
|
||||
*/
|
||||
export function propertyPort(port: DeclaredPort): PortDef {
|
||||
const plain = portDefOf(port);
|
||||
const field = objectPropertyDef(port.key);
|
||||
if (!field || portKindOf(field) !== port.kind) return plain;
|
||||
return { ...plain, field, default: field.default };
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { PropertyDef, StructFieldDef } from '../properties/PropertySchema';
|
||||
import { DECLARED_PORT_KINDS, type DeclaredPort, type DeclaredPortKind, type GraphNode } from './Graph';
|
||||
|
||||
/** Every variable kind is a port kind too; the fallback is for a project file that says otherwise. */
|
||||
export function variablePortKind(def: PropertyDef): DeclaredPortKind {
|
||||
/** The port a variable or property travels on. A color is a number; an enum or a texture is text. */
|
||||
export function portKindOf(def: PropertyDef): DeclaredPortKind {
|
||||
if (def.kind === 'color') return 'number';
|
||||
return (DECLARED_PORT_KINDS as readonly string[]).includes(def.kind)
|
||||
? (def.kind as DeclaredPortKind)
|
||||
: 'text';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Asset } from '../model/Asset';
|
||||
import { type AssetType, isPlaceableType } from '../model/AssetTypes';
|
||||
import { ASSET_TYPE_LABELS, type AssetType, isPlaceableType } from '../model/AssetTypes';
|
||||
import {
|
||||
type FieldValue,
|
||||
isList,
|
||||
@@ -13,6 +13,12 @@ import {
|
||||
colorOf,
|
||||
} from '../model/Properties';
|
||||
|
||||
/** A value a text field offers to fill in; the text stays free to type. */
|
||||
export interface TextSuggestion {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
/** The inspector is generated from these entries, so a new property is one line in a schema below. */
|
||||
export type PropertyDef =
|
||||
/**
|
||||
@@ -30,7 +36,7 @@ export type PropertyDef =
|
||||
step?: number;
|
||||
decimals?: number;
|
||||
}
|
||||
| { key: string; label: string; kind: 'text'; default: string }
|
||||
| { key: string; label: string; kind: 'text'; default: string; suggestions?: readonly TextSuggestion[] }
|
||||
| { key: string; label: string; kind: 'color'; default: number }
|
||||
| { key: string; label: string; kind: 'boolean'; default: boolean }
|
||||
| { key: string; label: string; kind: 'enum'; default: string; options: { value: string; label: string }[] }
|
||||
@@ -197,6 +203,32 @@ const FULL_SCHEMAS: Record<AssetType, PropertySchema> = Object.fromEntries(
|
||||
]),
|
||||
) as Record<AssetType, PropertySchema>;
|
||||
|
||||
const OBJECT_TYPES = ['sprite', 'text', 'collider', 'camera'] as const;
|
||||
|
||||
/** A built-in property with the type it belongs to ("Sprite width"); a shared key appears once per type. */
|
||||
export const OBJECT_PROPERTY_CHOICES: readonly { label: string; def: PropertyDef }[] = [
|
||||
...COMMON_PROPS.map((def) => ({ label: `Object ${def.key}`, def })),
|
||||
...OBJECT_TYPES.flatMap((type) =>
|
||||
ASSET_SCHEMAS[type].map((def) => ({ label: `${ASSET_TYPE_LABELS[type]} ${def.key}`, def })),
|
||||
),
|
||||
];
|
||||
|
||||
/**
|
||||
* Every built-in property once per key – what a Get/Set properties port resolves to. Sprite comes
|
||||
* first so a shared key (`width`) keeps its def, not the camera's whole-pixel view width.
|
||||
*/
|
||||
export const OBJECT_PROPERTIES: PropertySchema = firstPerKey(OBJECT_PROPERTY_CHOICES.map((c) => c.def));
|
||||
|
||||
export const OBJECT_PROPERTY_SUGGESTIONS: readonly TextSuggestion[] = OBJECT_PROPERTY_CHOICES.map((c) => ({
|
||||
value: c.def.key,
|
||||
label: c.label,
|
||||
}));
|
||||
|
||||
function firstPerKey(defs: readonly PropertyDef[]): PropertyDef[] {
|
||||
const seen = new Set<string>();
|
||||
return defs.filter((def) => !seen.has(def.key) && seen.add(def.key));
|
||||
}
|
||||
|
||||
export function schemaFor(type: AssetType): PropertySchema {
|
||||
return FULL_SCHEMAS[type];
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type GraphEdge,
|
||||
type GraphNode,
|
||||
isValidPortName,
|
||||
fixedPortKeys,
|
||||
nodeDef,
|
||||
} from '../graph';
|
||||
import type { Asset, TextureFrame } from '../model/Asset';
|
||||
@@ -210,9 +211,9 @@ function readField(key: string, f: Record<string, unknown>): StructFieldDef | nu
|
||||
return isFieldKind(f.kind) ? { key, kind: f.kind as FieldKind } : null;
|
||||
}
|
||||
|
||||
function normalizePorts(raw: unknown): DeclaredPort[] {
|
||||
function normalizePorts(raw: unknown, fixed: readonly string[]): DeclaredPort[] {
|
||||
const ports: DeclaredPort[] = [];
|
||||
const seen = new Set<string>();
|
||||
const seen = new Set(fixed);
|
||||
for (const item of Array.isArray(raw) ? raw : []) {
|
||||
const p = asRecord(item);
|
||||
const key = typeof p.key === 'string' ? p.key : '';
|
||||
@@ -234,7 +235,8 @@ function normalizeGraph(raw: unknown): Graph {
|
||||
const n = asRecord(item);
|
||||
const id = typeof n.id === 'string' ? n.id : '';
|
||||
const type = typeof n.type === 'string' ? n.type : '';
|
||||
if (!id || nodeIds.has(id) || !nodeDef(type)) continue;
|
||||
const def = nodeDef(type);
|
||||
if (!id || nodeIds.has(id) || !def) continue;
|
||||
nodeIds.add(id);
|
||||
// Params carry both settings and inline input values, so there is no schema to coerce against.
|
||||
const params: PropertyBag = {};
|
||||
@@ -243,7 +245,7 @@ function normalizeGraph(raw: unknown): Graph {
|
||||
if (stored !== undefined) params[key] = stored;
|
||||
}
|
||||
const node: GraphNode = { id, type, x: toNumber(n.x), y: toNumber(n.y), params };
|
||||
const ports = normalizePorts(n.ports);
|
||||
const ports = normalizePorts(n.ports, fixedPortKeys(def));
|
||||
if (ports.length > 0) node.ports = ports;
|
||||
if (n.collapsed === true) node.collapsed = true;
|
||||
graph.nodes.push(node);
|
||||
|
||||
Reference in new issue
Block a user