- Camera assets get width/height properties; play mode renders only the camera frame (letterboxed, masked, culled) and nothing without a camera - Remove demo arrow-key/camera-follow systems from play mode - Clip world and gizmos to the viewport (selection frame no longer leaks over the left panel) - Blur focused widget on pointerdown outside it so inspector edits commit before selection changes - Separate objects/overlay layers so camera symbols always render on top - Add tools/seed-demo.mjs for reproducible E2E runs Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
69 lines
2.8 KiB
TypeScript
69 lines
2.8 KiB
TypeScript
import type { AssetType } from '../model/AssetTypes';
|
|
import type { PropertyBag, PropertyValue } from '../model/Properties';
|
|
|
|
/**
|
|
* Declarative description of one editable property. The inspector is generated from
|
|
* these entries, so adding a property to a type is a single line in a schema below.
|
|
*/
|
|
export type PropertyDef =
|
|
| { key: string; label: string; kind: 'number'; default: number; min?: number; max?: number; step?: number }
|
|
| { key: string; label: string; kind: 'text'; default: string }
|
|
| { key: string; label: string; kind: 'color'; default: number }
|
|
| { key: string; label: string; kind: 'boolean'; default: boolean };
|
|
|
|
export type PropertyKind = PropertyDef['kind'];
|
|
|
|
export type PropertySchema = readonly PropertyDef[];
|
|
|
|
/** Property schema per asset type. Position is deliberately absent: it belongs to scene objects. */
|
|
export const ASSET_SCHEMAS: Record<AssetType, PropertySchema> = {
|
|
scene: [{ key: 'backgroundColor', label: 'Background', kind: 'color', default: 0x2a2d34 }],
|
|
camera: [
|
|
{ key: 'width', label: 'View width', kind: 'number', default: 640, min: 16, step: 1 },
|
|
{ key: 'height', label: 'View height', kind: 'number', default: 360, min: 16, step: 1 },
|
|
{ key: 'zoom', label: 'Zoom', kind: 'number', default: 1, min: 0.1, max: 10, step: 0.1 },
|
|
],
|
|
sprite: [
|
|
{ key: 'width', label: 'Width', kind: 'number', default: 100, min: 1, step: 1 },
|
|
{ key: 'height', label: 'Height', kind: 'number', default: 100, min: 1, step: 1 },
|
|
{ key: 'color', label: 'Color', kind: 'color', default: 0xe0553f },
|
|
],
|
|
};
|
|
|
|
/** Properties every scene object has regardless of its asset type. */
|
|
export const SCENE_OBJECT_SCHEMA: PropertySchema = [
|
|
{ key: 'x', label: 'X', kind: 'number', default: 0, step: 1 },
|
|
{ key: 'y', label: 'Y', kind: 'number', default: 0, step: 1 },
|
|
];
|
|
|
|
export function schemaFor(type: AssetType): PropertySchema {
|
|
return ASSET_SCHEMAS[type];
|
|
}
|
|
|
|
export function defaultsFor(type: AssetType): PropertyBag {
|
|
const bag: PropertyBag = {};
|
|
for (const def of ASSET_SCHEMAS[type]) bag[def.key] = def.default;
|
|
return bag;
|
|
}
|
|
|
|
/** Clamp / coerce a raw value so it satisfies the definition. Unknown kinds are returned as-is. */
|
|
export function coerceValue(def: PropertyDef, value: PropertyValue): PropertyValue {
|
|
switch (def.kind) {
|
|
case 'number': {
|
|
let n = typeof value === 'number' ? value : Number(value);
|
|
if (!Number.isFinite(n)) n = def.default;
|
|
if (def.min !== undefined) n = Math.max(def.min, n);
|
|
if (def.max !== undefined) n = Math.min(def.max, n);
|
|
return n;
|
|
}
|
|
case 'color': {
|
|
const n = typeof value === 'number' ? value : parseInt(String(value).replace('#', ''), 16);
|
|
return Number.isFinite(n) ? Math.max(0, Math.min(0xffffff, Math.floor(n))) : def.default;
|
|
}
|
|
case 'boolean':
|
|
return Boolean(value);
|
|
case 'text':
|
|
return String(value);
|
|
}
|
|
}
|