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
@@ -13,7 +13,7 @@ import {
|
||||
import type { Asset, TextureFrame } from '../model/Asset';
|
||||
import { isAssetType } from '../model/AssetTypes';
|
||||
import { emptyEdit, type InstanceEdit, isEmptyEdit } from '../model/instance';
|
||||
import type { PropertyBag, PropertyValue } from '../model/Properties';
|
||||
import type { FieldValue, PropertyBag, PropertyValue } from '../model/Properties';
|
||||
import { PROJECT_FORMAT_VERSION, type Project } from '../model/Project';
|
||||
import type { SceneObject } from '../model/SceneObject';
|
||||
import { isTextureFile, normalizeFrameRect } from '../model/texture';
|
||||
@@ -209,10 +209,11 @@ function normalizeGraph(raw: unknown): Graph {
|
||||
const type = typeof n.type === 'string' ? n.type : '';
|
||||
if (!id || nodeIds.has(id) || !nodeDef(type)) continue;
|
||||
nodeIds.add(id);
|
||||
// Params carry both settings and inline input values, so there is no schema to coerce against.
|
||||
const params: PropertyBag = {};
|
||||
for (const [key, value] of Object.entries(asRecord(n.params))) {
|
||||
if (typeof value === 'number' || typeof value === 'string' || typeof value === 'boolean')
|
||||
params[key] = value;
|
||||
const stored = asStoredValue(value);
|
||||
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);
|
||||
@@ -296,10 +297,22 @@ function asRecord(value: unknown): Record<string, unknown> {
|
||||
}
|
||||
|
||||
/**
|
||||
* A value from an untyped slot (instance edits carry no schema to coerce against). Lists are kept
|
||||
* but flattened away one level, so nothing nested can enter the model behind the type's back.
|
||||
* A value from an untyped slot (node params, instance edits). Lists and structs are kept but
|
||||
* flattened one level, so nothing deeper than the model allows can enter behind the type's back.
|
||||
*/
|
||||
function asStoredValue(value: unknown): PropertyValue | undefined {
|
||||
const field = asFieldValue(value);
|
||||
if (field !== undefined) return field;
|
||||
if (!value || typeof value !== 'object') return undefined;
|
||||
const fields: Record<string, FieldValue> = {};
|
||||
for (const [key, item] of Object.entries(value)) {
|
||||
const stored = asFieldValue(item);
|
||||
if (stored !== undefined) fields[key] = stored;
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
function asFieldValue(value: unknown): FieldValue | undefined {
|
||||
if (typeof value === 'number' || typeof value === 'string' || typeof value === 'boolean') return value;
|
||||
if (Array.isArray(value))
|
||||
return value.filter(
|
||||
|
||||
Reference in new issue
Block a user