Files
feuerwerk/src/core/properties/PropertySchema.ts
T
rootandClaude Fable 5.1 aaa8154adf Camera view frame, viewport clipping, overlay layer for editor symbols
- 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>
2026-09-07 11:13:08 +02:00

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);
}
}