Let a value be a bundle of named fields
PropertyValue was number | string | boolean | list. A struct joins it, which costs two things the typechecker found rather than I did. An object on a data edge used to mean a live entity, so asEntity would have taken a struct for one and asString would have read .name off it. EntityHandle now carries a symbol brand – a symbol so no field name can impersonate it, and it never serializes, which suits a thing that exists only while the game runs. It is built in exactly one place. String(value) would have written a struct as [object Object] at 39 call sites. They go through textOf(), which is identical for primitives. normalizeGraph dropped everything but primitives from node params, so the List node lost its value on every save ([3,1,4] round-tripped to undefined). Variables were spared: they have a schema, params have none.
This commit is contained in:
1 parent
3b3f374e10
commit
2cf788eedd
34 files changed
+284
-82
No files matched your search
+59
-14
@@ -1,9 +1,22 @@
|
||||
import { isList, type ListValue, type PropertyBag, type PropertyValue } from '../model/Properties';
|
||||
import type { EntityHandle } from './NodeContext';
|
||||
import {
|
||||
type FieldValue,
|
||||
isList,
|
||||
isStruct,
|
||||
type ListValue,
|
||||
type PropertyBag,
|
||||
type PropertyValue,
|
||||
type StructValue,
|
||||
textOf,
|
||||
} from '../model/Properties';
|
||||
import { ENTITY_HANDLE, type EntityHandle } from './NodeContext';
|
||||
|
||||
/** A value travelling along a data edge. Objects are live entity handles (play mode only). */
|
||||
export type Value = PropertyValue | EntityHandle | null;
|
||||
|
||||
export function isEntity(value: Value | undefined): value is EntityHandle {
|
||||
return typeof value === 'object' && value !== null && ENTITY_HANDLE in value;
|
||||
}
|
||||
|
||||
export function asNumber(value: Value | undefined): number {
|
||||
if (typeof value === 'number') return value;
|
||||
if (typeof value === 'boolean') return value ? 1 : 0;
|
||||
@@ -12,6 +25,8 @@ export function asNumber(value: Value | undefined): number {
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
}
|
||||
if (isList(value)) return value.length;
|
||||
// A bundle counts as its parts, the same way a list counts as its items.
|
||||
if (isStructValue(value)) return Object.keys(value).length;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -19,7 +34,9 @@ export function asNumber(value: Value | undefined): number {
|
||||
export function asList(value: Value | undefined): ListValue {
|
||||
if (isList(value)) return value;
|
||||
if (value === null || value === undefined) return [];
|
||||
return typeof value === 'object' ? [value.name] : [value];
|
||||
if (isEntity(value)) return [value.name];
|
||||
if (isStructValue(value)) return Object.values(value).map(asListItem);
|
||||
return [value];
|
||||
}
|
||||
|
||||
/** A value narrowed to what a list may hold: handles and nested lists have no stored form. */
|
||||
@@ -29,6 +46,11 @@ export function asListItem(value: Value | undefined): number | string | boolean
|
||||
return asString(value);
|
||||
}
|
||||
|
||||
/** A value narrowed to what a struct field may hold: a list stays a list, anything deeper flattens. */
|
||||
export function asField(value: Value | undefined): FieldValue {
|
||||
return isList(value) ? value : asListItem(value);
|
||||
}
|
||||
|
||||
export function asBoolean(value: Value | undefined): boolean {
|
||||
if (typeof value === 'string') return value !== '' && value !== 'false' && value !== '0';
|
||||
return Boolean(value);
|
||||
@@ -36,15 +58,42 @@ export function asBoolean(value: Value | undefined): boolean {
|
||||
|
||||
/** Entities read as their name, so a handle can be logged or joined into text directly. */
|
||||
export function asString(value: Value | undefined): string {
|
||||
if (value === null || value === undefined) return '';
|
||||
if (isList(value)) return value.join(', ');
|
||||
if (typeof value === 'object') return value.name;
|
||||
return String(value);
|
||||
if (value === null) return '';
|
||||
return isEntity(value) ? value.name : textOf(value);
|
||||
}
|
||||
|
||||
export function asEntity(value: Value | undefined): EntityHandle | null {
|
||||
if (!value || typeof value !== 'object' || isList(value)) return null;
|
||||
return value;
|
||||
return isEntity(value) ? value : null;
|
||||
}
|
||||
|
||||
/** A struct value, or the empty one: a number has no fields to become, so nothing is invented. */
|
||||
export function asStruct(value: Value | undefined): StructValue {
|
||||
return isStructValue(value) ? value : {};
|
||||
}
|
||||
|
||||
function isStructValue(value: Value | undefined): value is StructValue {
|
||||
return !isEntity(value) && isStruct(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* A struct slot keeps the fields it was declared with: what arrives fills the ones it names and
|
||||
* the rest stay put. That is what lets one node set some values of a bundle without erasing it.
|
||||
*/
|
||||
export function mergeStruct(current: StructValue, value: Value | undefined): StructValue {
|
||||
const incoming = asStruct(value);
|
||||
const merged: Record<string, PropertyValue> = {};
|
||||
for (const [field, own] of Object.entries(current))
|
||||
merged[field] = field in incoming ? asTypeOf(own, incoming[field]) : own;
|
||||
return merged as StructValue;
|
||||
}
|
||||
|
||||
/** `value` converted to the type the slot already holds, so a number stays a number. */
|
||||
export function asTypeOf(current: PropertyValue | undefined, value: Value | undefined): PropertyValue {
|
||||
if (isList(current)) return asList(value);
|
||||
if (isStruct(current)) return mergeStruct(current, value);
|
||||
if (typeof current === 'number') return asNumber(value);
|
||||
if (typeof current === 'boolean') return asBoolean(value);
|
||||
return asString(value);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,9 +101,5 @@ export function asEntity(value: Value | undefined): EntityHandle | null {
|
||||
* variable stays a number no matter what arrives on an `any` input.
|
||||
*/
|
||||
export function writeTyped(bag: PropertyBag, key: string, value: Value | undefined): void {
|
||||
const current = bag[key];
|
||||
if (isList(current)) bag[key] = asList(value);
|
||||
else if (typeof current === 'number') bag[key] = asNumber(value);
|
||||
else if (typeof current === 'boolean') bag[key] = asBoolean(value);
|
||||
else bag[key] = asString(value);
|
||||
bag[key] = asTypeOf(bag[key], value);
|
||||
}
|
||||
Reference in new issue
Block a user