Replace Move 'per second' setting with a dt data input
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
06b1007015
commit
9060da615a
8 files changed
+210
-40
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { edgesInto, edgesOutOf, type Graph, type GraphNode } from './Graph';
|
||||
import { hasExecInput, isEventNode, type NodeContext, requireNodeDef, type Value } from './NodeRegistry';
|
||||
import { hasExecInput, inlineValue, isEventNode, type NodeContext, requireNodeDef, type Value } from './NodeRegistry';
|
||||
|
||||
/**
|
||||
* Guards against runaway flows (a Sequence looping back into itself, …). The interpreter
|
||||
@@ -83,7 +83,7 @@ export class GraphInterpreter {
|
||||
if (port.type === 'exec') continue;
|
||||
const edge = edgesInto(this.graph, node.id, port.key)[0];
|
||||
if (edge) inputs[port.key] = this.readOutput(edge.from.node, edge.from.port, ctx);
|
||||
else inputs[port.key] = node.params[port.key] ?? null;
|
||||
else inputs[port.key] = inlineValue(node, port);
|
||||
}
|
||||
return inputs;
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ export interface PortDef {
|
||||
key: string;
|
||||
label: string;
|
||||
type: PortType;
|
||||
/** Inline value used while the input is unconnected and the node has no own value (default 0/''/false). */
|
||||
default?: PropertyValue;
|
||||
}
|
||||
|
||||
export type NodeCategory = 'event' | 'flow' | 'data' | 'variable' | 'action';
|
||||
@@ -117,15 +119,20 @@ export function inlineInputs(def: NodeDef): PortDef[] {
|
||||
export function inlineInputDef(port: PortDef): PropertyDef {
|
||||
switch (port.type) {
|
||||
case 'boolean':
|
||||
return { key: port.key, label: port.label, kind: 'boolean', default: false };
|
||||
return { key: port.key, label: port.label, kind: 'boolean', default: asBoolean(port.default ?? false) };
|
||||
case 'string':
|
||||
case 'any':
|
||||
return { key: port.key, label: port.label, kind: 'text', default: '' };
|
||||
return { key: port.key, label: port.label, kind: 'text', default: asString(port.default ?? '') };
|
||||
default:
|
||||
return { key: port.key, label: port.label, kind: 'number', default: 0, step: 1 };
|
||||
return { key: port.key, label: port.label, kind: 'number', default: asNumber(port.default ?? 0), step: 1 };
|
||||
}
|
||||
}
|
||||
|
||||
/** The value an unconnected input carries: the node's own inline value, else the port default. */
|
||||
export function inlineValue(node: { params: PropertyBag }, port: PortDef): Value {
|
||||
return node.params[port.key] ?? port.default ?? null;
|
||||
}
|
||||
|
||||
// --- value helpers for node implementations ---------------------------------------------
|
||||
|
||||
export function asNumber(value: Value | undefined): number {
|
||||
|
||||
+72
-5
@@ -166,6 +166,72 @@ const MATH: [string, string, string, (a: number, b: number) => number][] = [
|
||||
for (const [type, label, description, fn] of MATH) {
|
||||
registerNode({ type, label, category: 'data', description, inputs: [num('a'), num('b')], outputs: [num('result')], params: [], run: (_c, i) => ({ outputs: { result: fn(asNumber(i.a), asNumber(i.b)) } }) });
|
||||
}
|
||||
registerNode({ type: 'Min', label: 'Min', category: 'data', description: 'The smaller of a and b.', inputs: [num('a'), num('b')], outputs: [num('result')], params: [], run: (_c, i) => ({ outputs: { result: Math.min(asNumber(i.a), asNumber(i.b)) } }) });
|
||||
registerNode({ type: 'Max', label: 'Max', category: 'data', description: 'The larger of a and b.', inputs: [num('a'), num('b')], outputs: [num('result')], params: [], run: (_c, i) => ({ outputs: { result: Math.max(asNumber(i.a), asNumber(i.b)) } }) });
|
||||
registerNode({ type: 'Abs', label: 'Abs', category: 'data', description: 'Absolute value (drops the sign).', inputs: [num('value')], outputs: [num('result')], params: [], run: (_c, i) => ({ outputs: { result: Math.abs(asNumber(i.value)) } }) });
|
||||
registerNode({ type: 'Sqrt', label: 'Sqrt', category: 'data', description: 'Square root (0 for negative input).', inputs: [num('value')], outputs: [num('result')], params: [], run: (_c, i) => ({ outputs: { result: Math.sqrt(Math.max(0, asNumber(i.value))) } }) });
|
||||
registerNode({
|
||||
type: 'Clamp',
|
||||
label: 'Clamp',
|
||||
category: 'data',
|
||||
description: 'Limits a value to the range min…max.',
|
||||
inputs: [num('value'), { ...num('min'), default: 0 }, { ...num('max'), default: 1 }],
|
||||
outputs: [num('result')],
|
||||
params: [],
|
||||
run: (_c, i) => ({ outputs: { result: Math.min(Math.max(asNumber(i.value), asNumber(i.min)), asNumber(i.max)) } }),
|
||||
});
|
||||
|
||||
// --- data: vectors -------------------------------------------------------------------------
|
||||
|
||||
registerNode({ type: 'Length', label: 'Length', category: 'data', description: 'Length of the vector (x, y).', inputs: [num('x'), num('y')], outputs: [num('result')], params: [], run: (_c, i) => ({ outputs: { result: Math.hypot(asNumber(i.x), asNumber(i.y)) } }) });
|
||||
registerNode({
|
||||
type: 'Distance',
|
||||
label: 'Distance',
|
||||
category: 'data',
|
||||
description: 'Distance between two objects.',
|
||||
inputs: [obj('a'), obj('b')],
|
||||
outputs: [num('result')],
|
||||
params: [],
|
||||
run: (_c, i) => {
|
||||
const a = asEntity(i.a);
|
||||
const b = asEntity(i.b);
|
||||
return { outputs: { result: a && b ? Math.hypot(a.x - b.x, a.y - b.y) : 0 } };
|
||||
},
|
||||
});
|
||||
registerNode({
|
||||
type: 'Normalize',
|
||||
label: 'Normalize',
|
||||
category: 'data',
|
||||
description: 'Scales the vector (x, y) to length 1; (0, 0) stays (0, 0).',
|
||||
inputs: [num('x'), num('y')],
|
||||
outputs: [num('x'), num('y'), num('length')],
|
||||
params: [],
|
||||
run: (_c, i) => ({ outputs: normalize(asNumber(i.x), asNumber(i.y)) }),
|
||||
});
|
||||
registerNode({
|
||||
type: 'AxisInput',
|
||||
label: 'Axis input',
|
||||
category: 'data',
|
||||
description: 'Direction from four keys as a normalized vector (diagonals are not faster).',
|
||||
inputs: [],
|
||||
outputs: [num('x'), num('y'), bool('active')],
|
||||
params: [
|
||||
{ key: 'left', label: 'Left', kind: 'key', default: 'ArrowLeft' },
|
||||
{ key: 'right', label: 'Right', kind: 'key', default: 'ArrowRight' },
|
||||
{ key: 'up', label: 'Up', kind: 'key', default: 'ArrowUp' },
|
||||
{ key: 'down', label: 'Down', kind: 'key', default: 'ArrowDown' },
|
||||
],
|
||||
run: (ctx, _i, p) => {
|
||||
const held = (key: string, fallback: string) => (ctx.isKeyDown(String(p[key] ?? fallback)) ? 1 : 0);
|
||||
const { x, y, length } = normalize(held('right', 'ArrowRight') - held('left', 'ArrowLeft'), held('down', 'ArrowDown') - held('up', 'ArrowUp'));
|
||||
return { outputs: { x, y, active: length > 0 } };
|
||||
},
|
||||
});
|
||||
|
||||
function normalize(x: number, y: number): { x: number; y: number; length: number } {
|
||||
const length = Math.hypot(x, y);
|
||||
return length === 0 ? { x: 0, y: 0, length: 0 } : { x: x / length, y: y / length, length };
|
||||
}
|
||||
|
||||
const COMPARE: Record<string, (a: number, b: number) => boolean> = {
|
||||
'==': (a, b) => a === b,
|
||||
@@ -196,15 +262,16 @@ registerNode({
|
||||
type: 'Move',
|
||||
label: 'Move',
|
||||
category: 'action',
|
||||
description: 'Moves an object by dx/dy.',
|
||||
inputs: [exec(), obj('object'), num('dx'), num('dy')],
|
||||
description: 'Moves an object by (dx, dy) × scale × dt. Connect dt to an event\'s dt for per-second movement; leave it at 1 for per-frame.',
|
||||
inputs: [exec(), obj('object'), num('dx'), num('dy'), { ...num('scale'), default: 1 }, { ...num('dt'), default: 1 }],
|
||||
outputs: [exec()],
|
||||
params: [],
|
||||
run: (_c, i) => {
|
||||
run: (_ctx, i) => {
|
||||
const target = asEntity(i.object);
|
||||
if (target) {
|
||||
target.x += asNumber(i.dx);
|
||||
target.y += asNumber(i.dy);
|
||||
const factor = asNumber(i.scale) * asNumber(i.dt);
|
||||
target.x += asNumber(i.dx) * factor;
|
||||
target.y += asNumber(i.dy) * factor;
|
||||
}
|
||||
return { next: 'exec' };
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user