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>
This commit is contained in:
rootandClaude Fable 5.1 committed 2026-09-07 11:13:08 +02:00
1 parent 5a46124454
commit aaa8154adf
11 files changed
+307 -142

No files matched your search

+5 -1
View File
@@ -18,7 +18,11 @@ 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: 'zoom', label: 'Zoom', kind: 'number', default: 1, min: 0.1, max: 10, step: 0.1 }],
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 },
+28
View File
@@ -0,0 +1,28 @@
import type { PropertyBag } from '../model/Properties';
import { defaultsFor } from './PropertySchema';
/** Axis-aligned rectangle in world units. */
export interface WorldRect {
left: number;
top: number;
right: number;
bottom: number;
}
/**
* Size of the world region a camera shows, in world units: its view size divided by its
* zoom. This is the frame drawn in edit mode and the only region rendered in play mode.
*/
export function cameraFrame(props: PropertyBag): { w: number; h: number } {
const d = defaultsFor('camera');
const zoom = Math.max(0.01, Number(props.zoom ?? d.zoom));
return { w: Number(props.width ?? d.width) / zoom, h: Number(props.height ?? d.height) / zoom };
}
export function rectAround(cx: number, cy: number, w: number, h: number): WorldRect {
return { left: cx - w / 2, top: cy - h / 2, right: cx + w / 2, bottom: cy + h / 2 };
}
export function rectsIntersect(a: WorldRect, b: WorldRect): boolean {
return a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top;
}