Make texture and colour full kinds and drop the load-time migrations
- `Texture` and `Color` can be picked as a variable kind, a struct field kind and a declared port kind; a new `Texture` node hands a picture id into the graph. Declared, a texture is its bare id – the `$texture` tag stays a list-only thing. - The `String` node is labelled "Text" like its kind; the type id stays. - Whole-number inputs (`Item at`, `Set item`, `Sub list`, `Fill`, `Repeat`, `Substring`, `To text`) show no decimals (`int()` in shared.ts). - All server projects are reseeded, so `legacyNodes.ts`, `migrateDeclaredLists` and the `ownerId` fallback go, with their tests. - Note `reseed-because-of-list` moved to done/; new note `refactor-types`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
11c900c998
commit
8a7185948d
39 files changed
+378
-765
No files matched your search
+11
-70
@@ -13,9 +13,8 @@ import {
|
||||
} from '../graph';
|
||||
import type { Asset, TextureFrame } from '../model/Asset';
|
||||
import { isAssetType } from '../model/AssetTypes';
|
||||
import { emptyEdit, type InstanceEdit, isEmptyEdit, overrideSites } from '../model/instance';
|
||||
import { emptyEdit, type InstanceEdit, isEmptyEdit } from '../model/instance';
|
||||
import {
|
||||
type ColorItem,
|
||||
colorItem,
|
||||
colorItemValue,
|
||||
type FieldValue,
|
||||
@@ -23,18 +22,15 @@ import {
|
||||
textureItem,
|
||||
textureItemValue,
|
||||
isColorItem,
|
||||
isList,
|
||||
type PropertyBag,
|
||||
type PropertyValue,
|
||||
setBagValue,
|
||||
type StructValue,
|
||||
} from '../model/Properties';
|
||||
import { PROJECT_FORMAT_VERSION, type Project } from '../model/Project';
|
||||
import type { SceneObject } from '../model/SceneObject';
|
||||
import { isTextureFile, normalizeFrameRect } from '../model/texture';
|
||||
import { hasParentCycle } from '../model/walk';
|
||||
import { upgradePropertyNodes, upgradeVariableNodes } from './legacyNodes';
|
||||
import { makeItem } from '../properties/listItems';
|
||||
import type { FlatItem } from '../properties/listItems';
|
||||
import {
|
||||
coerceValue,
|
||||
COMMON_PROPS,
|
||||
@@ -42,12 +38,9 @@ import {
|
||||
type FieldKind,
|
||||
isValidFieldName,
|
||||
isValidVariableName,
|
||||
LIST_ITEM_KINDS,
|
||||
type ListItemKind,
|
||||
type PropertyDef,
|
||||
type PropertySchema,
|
||||
schemaFor,
|
||||
structOf,
|
||||
type StructFieldDef,
|
||||
VARIABLE_KINDS,
|
||||
variableDef,
|
||||
@@ -65,13 +58,10 @@ export function deserializeProject(raw: unknown, fallbackId = ''): Project {
|
||||
// Pass 1: asset ids and their effective schemas, needed to coerce overrides in pass 2.
|
||||
const schemas = new Map<string, PropertySchema>();
|
||||
const variables = new Map<string, PropertyDef[]>();
|
||||
const legacyLists = new Map<string, Map<string, DeclaredItems>>();
|
||||
for (const a of rawAssets) {
|
||||
if (typeof a.id !== 'string' || !a.id || !isAssetType(a.type)) continue;
|
||||
if (a.type === 'behavior') {
|
||||
const declared = new Map<string, DeclaredItems>();
|
||||
const vars = normalizeVariables(a.variables, declared);
|
||||
if (declared.size > 0) legacyLists.set(a.id, declared);
|
||||
const vars = normalizeVariables(a.variables);
|
||||
variables.set(a.id, vars);
|
||||
schemas.set(a.id, [...COMMON_PROPS, ...vars]);
|
||||
} else {
|
||||
@@ -99,9 +89,7 @@ export function deserializeProject(raw: unknown, fallbackId = ''): Project {
|
||||
name,
|
||||
props,
|
||||
variables: variables.get(id) ?? [],
|
||||
graph: normalizeGraph(
|
||||
upgradePropertyNodes(upgradeVariableNodes(a.graph, variables.get(id) ?? []), variables),
|
||||
),
|
||||
graph: normalizeGraph(a.graph),
|
||||
});
|
||||
} else if (type === 'texture') {
|
||||
assets.push({ id, type, name, props, ...normalizeTexture(a, takenIds) });
|
||||
@@ -133,43 +121,9 @@ export function deserializeProject(raw: unknown, fallbackId = ''): Project {
|
||||
assets,
|
||||
activeSceneId,
|
||||
};
|
||||
migrateDeclaredLists(project, legacyLists);
|
||||
return project;
|
||||
}
|
||||
|
||||
/** What a list variable declared before its items carried their own kind – see `migrateDeclaredLists`. */
|
||||
interface DeclaredItems {
|
||||
kind: ListItemKind;
|
||||
fields: readonly StructFieldDef[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A list used to declare what it held, so its stored items did not have to say: a `color` list held
|
||||
* plain numbers, and a list of structs could hold an item missing a declared field. Items carry
|
||||
* their own kind now, so every value of such a variable is converted here – the declaration is read
|
||||
* one last time and then gone.
|
||||
*/
|
||||
function migrateDeclaredLists(project: Project, legacy: Map<string, Map<string, DeclaredItems>>): void {
|
||||
const convert =
|
||||
({ kind, fields }: DeclaredItems) =>
|
||||
(value: PropertyValue | undefined): PropertyValue | undefined =>
|
||||
isList(value)
|
||||
? value.map((item) => (kind === 'struct' ? structOf(fields, item) : makeItem(kind, item)))
|
||||
: value;
|
||||
for (const [assetId, declared] of legacy) {
|
||||
const asset = project.assets.find((a) => a.id === assetId);
|
||||
if (asset?.type !== 'behavior') continue;
|
||||
for (const [key, items] of declared) {
|
||||
const as = convert(items);
|
||||
const def = asset.variables.find((v) => v.key === key);
|
||||
if (def) def.default = as(def.default) ?? def.default;
|
||||
setBagValue(asset.props, key, as(asset.props[key]));
|
||||
for (const site of overrideSites(project, (id) => id === assetId))
|
||||
setBagValue(site.bag, key, as(site.bag[key]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Plain JSON-ready copy of the project (deep clone, stable key order for readable diffs). */
|
||||
export function serializeProject(project: Project): unknown {
|
||||
return JSON.parse(JSON.stringify(project));
|
||||
@@ -191,8 +145,7 @@ function normalizeObjects(raw: unknown, schemas: Map<string, PropertySchema>): S
|
||||
position: { x: toNumber(p.x), y: toNumber(p.y), z: toNumber(p.z) },
|
||||
editorOffset: { x: toNumber(offset.x), y: toNumber(offset.y) },
|
||||
overrides: normalizeBag(o.overrides, schema, true),
|
||||
// `ownerId` is the pre-hierarchy name of the same link.
|
||||
parentId: typeof o.parentId === 'string' ? o.parentId : typeof o.ownerId === 'string' ? o.ownerId : '',
|
||||
parentId: typeof o.parentId === 'string' ? o.parentId : '',
|
||||
instanceEdits: normalizeInstanceEdits(o.instanceEdits),
|
||||
});
|
||||
}
|
||||
@@ -233,8 +186,7 @@ function normalizeInstanceEdits(raw: unknown): Record<string, InstanceEdit> {
|
||||
return edits;
|
||||
}
|
||||
|
||||
/** `legacy` collects what a list variable still declares about its items – see `migrateDeclaredLists`. */
|
||||
function normalizeVariables(raw: unknown, legacy?: Map<string, DeclaredItems>): PropertyDef[] {
|
||||
function normalizeVariables(raw: unknown): PropertyDef[] {
|
||||
const defs: PropertyDef[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const item of Array.isArray(raw) ? raw : []) {
|
||||
@@ -247,8 +199,6 @@ function normalizeVariables(raw: unknown, legacy?: Map<string, DeclaredItems>):
|
||||
if (!isValidVariableName(key) || seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
const fields = normalizeStructFields(v.fields);
|
||||
if (kind === 'list' && (LIST_ITEM_KINDS as readonly string[]).includes(String(v.items)))
|
||||
legacy?.set(key, { kind: v.items as ListItemKind, fields: primitivesOf(fields) });
|
||||
const def = variableDef(key, kind, undefined, fields);
|
||||
def.default = coerceValue(def, (v.default ?? def.default) as PropertyValue);
|
||||
defs.push(def);
|
||||
@@ -256,10 +206,6 @@ function normalizeVariables(raw: unknown, legacy?: Map<string, DeclaredItems>):
|
||||
return defs;
|
||||
}
|
||||
|
||||
/** A declared item shape held primitives only; a list field of one would have been a level too deep. */
|
||||
const primitivesOf = (fields: readonly StructFieldDef[]): StructFieldDef[] =>
|
||||
fields.filter((f) => f.kind !== 'list');
|
||||
|
||||
/** An unreadable field is dropped rather than defaulted: an invented key would claim a value. */
|
||||
function normalizeStructFields(raw: unknown): StructFieldDef[] {
|
||||
const fields: StructFieldDef[] = [];
|
||||
@@ -410,21 +356,16 @@ function asStoredValue(value: unknown): PropertyValue | undefined {
|
||||
const isPrimitive = (value: unknown): value is number | string | boolean =>
|
||||
typeof value === 'number' || typeof value === 'string' || typeof value === 'boolean';
|
||||
|
||||
/**
|
||||
* A primitive, or the one object a flat position may hold: a tagged color. A tagged texture is a
|
||||
* list item only, so here it keeps its id as text – the same degrade `flatItemOf` makes.
|
||||
*/
|
||||
const asFlatItem = (value: unknown): number | string | boolean | ColorItem | undefined => {
|
||||
/** A primitive, or the objects a flat position may hold: a tagged color or texture. */
|
||||
const asFlatItem = (value: unknown): FlatItem | undefined => {
|
||||
if (isPrimitive(value)) return value;
|
||||
if (isColorItem(value)) return colorItem(colorItemValue(value));
|
||||
return isTextureItem(value) ? textureItemValue(value) : undefined;
|
||||
return isTextureItem(value) ? textureItem(textureItemValue(value)) : undefined;
|
||||
};
|
||||
|
||||
/** A list item: a tagged texture, a flat one, or a struct of flat ones. */
|
||||
/** A list item: a flat one, or a struct of flat ones. */
|
||||
const asStoredItem = (value: unknown): number | string | boolean | StructValue | undefined =>
|
||||
isTextureItem(value)
|
||||
? textureItem(textureItemValue(value))
|
||||
: (asFlatItem(value) ?? asStoredStruct(value, asFlatItem));
|
||||
asFlatItem(value) ?? asStoredStruct(value, asFlatItem);
|
||||
|
||||
/** A struct field: a flat item or a flat list. A field that is itself a struct is dropped. */
|
||||
const asStoredField = (value: unknown): FieldValue | undefined =>
|
||||
|
||||
Reference in new issue
Block a user