Describe every value kind in one table instead of spreading it over the code
A kind like colour or texture used to be spelled out in five parallel lists, its port type, its empty value, its glyph, its label and its list tag - 22 literal places for texture alone. core/properties/kinds.ts now holds one row per kind and the rest is derived from it, so adding a kind is one row. The five literal nodes collapse into literals.ts, the six kind menus into one helper. Number, Text, Boolean is now the one order everywhere; the variable and port menus used to list Boolean before Text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8a7185948d
commit
3ed8b68ecf
39 files changed
+699
-565
No files matched your search
@@ -1,8 +1,10 @@
|
||||
import type { TaggedKind, ValueOf } from '../properties/kinds';
|
||||
|
||||
/** Depth is capped: `list → struct → primitive` is the deepest a stored value goes. */
|
||||
export type ListValue = readonly (number | string | boolean | StructValue)[];
|
||||
|
||||
/** What a struct field may hold. A field is never itself a struct – except a tagged colour or texture. */
|
||||
export type FieldValue = number | string | boolean | ListValue | ColorItem | TextureItem;
|
||||
/** What a struct field may hold. A field is never itself a struct – except a tagged item. */
|
||||
export type FieldValue = number | string | boolean | ListValue | TaggedItem;
|
||||
|
||||
/** Several named values travelling as one, so a bundle needs one port instead of one per part. */
|
||||
export type StructValue = { readonly [field: string]: FieldValue };
|
||||
@@ -15,42 +17,38 @@ export type PropertyBag = Record<string, PropertyValue>;
|
||||
export const isList = (value: unknown): value is ListValue => Array.isArray(value);
|
||||
|
||||
/**
|
||||
* Inside a list a color and a texture have to say so: a list declares nothing about its items, and
|
||||
* there 0xRRGGBB is a plain number and an asset id a plain text. A field name cannot start with `$`
|
||||
* (`isValidFieldName`), so a tag can never collide with a struct item of one field.
|
||||
* A value of a kind JSON cannot show, under its kind's tag: `{ "$color": 16711680 }`,
|
||||
* `{ "$texture": "walk-up" }`. Only a list needs it, where nothing is declared. A field name cannot
|
||||
* start with `$` (`isValidFieldName`), so a tag never collides with a struct item of one field.
|
||||
*/
|
||||
export const COLOR_TAG = '$color';
|
||||
export const TEXTURE_TAG = '$texture';
|
||||
export type TaggedItem = {
|
||||
[K in TaggedKind]: { readonly [P in `$${K}`]: ValueOf<K> } & TaggedFields;
|
||||
}[TaggedKind];
|
||||
|
||||
/** Indexable like any struct, so the one place that reads fields by name needs no special case. */
|
||||
export interface ColorItem {
|
||||
readonly $color: number;
|
||||
interface TaggedFields {
|
||||
readonly [field: string]: FieldValue;
|
||||
}
|
||||
|
||||
/** A picture a sprite can wear: a texture, one of its frames, or an animation – all one id. */
|
||||
export interface TextureItem {
|
||||
readonly $texture: string;
|
||||
readonly [field: string]: FieldValue;
|
||||
const TAG = '$';
|
||||
|
||||
/** The kind `value` wears as a tag, if it is a tagged item. */
|
||||
export function tagOf(value: unknown): string | undefined {
|
||||
if (!isStruct(value)) return undefined;
|
||||
const keys = Object.keys(value);
|
||||
const key = keys.length === 1 ? keys[0] : undefined;
|
||||
return key?.startsWith(TAG) ? key.slice(TAG.length) : undefined;
|
||||
}
|
||||
|
||||
const isTagged = (value: unknown, tag: string): boolean =>
|
||||
isStruct(value) && tag in value && Object.keys(value).length === 1;
|
||||
export function tagged(kind: TaggedKind, value: number | string): TaggedItem {
|
||||
return { [TAG + kind]: value } as TaggedItem;
|
||||
}
|
||||
|
||||
export const isColorItem = (value: unknown): value is ColorItem => isTagged(value, COLOR_TAG);
|
||||
|
||||
export const isTextureItem = (value: unknown): value is TextureItem =>
|
||||
isTagged(value, TEXTURE_TAG) && typeof (value as TextureItem).$texture === 'string';
|
||||
|
||||
export const colorItem = (color: number): ColorItem => ({ $color: color });
|
||||
|
||||
export const textureItem = (id: string): TextureItem => ({ $texture: id });
|
||||
|
||||
/** The number behind a tagged color. */
|
||||
export const colorItemValue = (item: ColorItem): number => colorOf(item.$color, 0);
|
||||
|
||||
/** The id behind a tagged texture. */
|
||||
export const textureItemValue = (item: TextureItem): string => item.$texture;
|
||||
/** A tagged item as what it wraps; anything else as itself. */
|
||||
export function untag(value: PropertyValue | undefined): PropertyValue | undefined {
|
||||
const tag = tagOf(value);
|
||||
return tag === undefined ? value : (value as StructValue)[TAG + tag];
|
||||
}
|
||||
|
||||
/**
|
||||
* Model-level only: a live entity handle is an object too, and the graph tells the two apart by
|
||||
@@ -67,8 +65,7 @@ export function textOf(value: PropertyValue | undefined): string {
|
||||
if (value === undefined) return '';
|
||||
if (isList(value)) return value.map((item) => textOf(item)).join(', ');
|
||||
// A tagged value reads as what it wraps, the same as the matching property does.
|
||||
if (isColorItem(value)) return String(colorItemValue(value));
|
||||
if (isTextureItem(value)) return textureItemValue(value);
|
||||
if (tagOf(value) !== undefined) return textOf(untag(value));
|
||||
if (isStruct(value))
|
||||
return Object.entries(value)
|
||||
.map(([field, item]) => `${field}: ${textOf(item)}`)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* `Project.ts` so that module stays free of the property schema and of container instancing:
|
||||
* both of those already import it, and answering this question needs both.
|
||||
*/
|
||||
import { isKindWith } from '../properties/kinds';
|
||||
import { schemaForAsset } from '../properties/PropertySchema';
|
||||
import type { AnimationAsset, Asset } from './Asset';
|
||||
import { overrideSites } from './instance';
|
||||
@@ -48,7 +49,7 @@ export function findPropertyUsages(project: Project, ids: string | readonly stri
|
||||
const matches = (value: unknown): boolean => typeof value === 'string' && wanted.has(value);
|
||||
for (const asset of project.assets) {
|
||||
const keys = schemaForAsset(asset)
|
||||
.filter((def) => def.kind === 'asset' || def.kind === 'texture')
|
||||
.filter((def) => isKindWith('reference', def.kind))
|
||||
.map((def) => def.key);
|
||||
refKeys.set(asset.id, keys);
|
||||
for (const key of keys) if (matches(asset.props[key])) result.push({ on: 'asset', asset, key });
|
||||
|
||||
Reference in new issue
Block a user