@cyberart-io/engine 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -254,11 +254,16 @@ type AnimationTiming = {
254
254
  deltaSinceLastUpdate: number;
255
255
  deltaSinceLastRender: number;
256
256
  };
257
+ type CartKind = 'render' | 'calculation';
257
258
  type AnimationCart<T = unknown, TFeatureState = undefined> = {
258
259
  getDefaultFeatureState?: (R: Random, dimensionContext: DimensionContext, rawParams: number[], keyboardManager: KeyboardManager, customState?: Partial<T>, pointerManager?: PointerManager, gameManager?: unknown, hostChannel?: HostChannel) => TFeatureState;
259
260
  getDefaultState: (R: Random, dimensionContext: DimensionContext, rawParams: number[], keyboardManager: KeyboardManager, customState?: Partial<T>, pointerManager?: PointerManager, gameManager?: unknown, featureState?: Readonly<TFeatureState>, hostChannel?: HostChannel) => T;
260
261
  update: (R: Random, framesElapsed: number, rawParams: number[], dimensionContext: DimensionContext, state: T, keyboardManager: KeyboardManager, pointerManager?: PointerManager, gameManager?: unknown, timing?: AnimationTiming, featureState?: Readonly<TFeatureState>, hostChannel?: HostChannel) => T;
261
- render: (R: Random, framesElapsed: number, rawParams: number[], dimensionContext: DimensionContext, state: T, drawingContext: CanvasRenderingContext2D, imageData: ImageData, pointerManager?: PointerManager, gameManager?: unknown, timing?: AnimationTiming, featureState?: Readonly<TFeatureState>, hostChannel?: HostChannel) => void;
262
+ /**
263
+ * Draw into the 2D context. Optional on calculation carts (`kind:
264
+ * 'calculation'`); the runtime does not call it and does not create a canvas.
265
+ */
266
+ render?: (R: Random, framesElapsed: number, rawParams: number[], dimensionContext: DimensionContext, state: T, drawingContext: CanvasRenderingContext2D, imageData: ImageData, pointerManager?: PointerManager, gameManager?: unknown, timing?: AnimationTiming, featureState?: Readonly<TFeatureState>, hostChannel?: HostChannel) => void;
262
267
  adjust?: Record<string, {
263
268
  type: 'switch';
264
269
  immediate?: boolean;
@@ -328,6 +333,76 @@ type CartStateBundle = {
328
333
  state: unknown;
329
334
  };
330
335
 
336
+ /**
337
+ * Copyright (c) 2026 Aaron Boyarsky
338
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
339
+ * See packages/engine/LICENSE
340
+ *
341
+ * Versioned snapshot envelope. Schema 1 is the previous engine-owned
342
+ * `CartStateBundle` (`CART_STATE_BUNDLE_VERSION = 1`). Schema 2 wraps that
343
+ * blob in `engineState` and separates host-owned payload. Hosts persist
344
+ * the JSON; this module is not a database.
345
+ */
346
+
347
+ /**
348
+ * Current envelope schema. Independent of `CART_STATE_BUNDLE_VERSION` (still
349
+ * 1 inside `engineState`) and of the npm package version.
350
+ */
351
+ declare const SNAPSHOT_SCHEMA_VERSION: 2;
352
+ type SnapshotCartRef = {
353
+ id: string;
354
+ version: string;
355
+ generative?: boolean;
356
+ };
357
+ type SnapshotModuleRef = {
358
+ id: string;
359
+ version: string;
360
+ };
361
+ type SnapshotClock = {
362
+ framesElapsed: number;
363
+ elapsedSinceStart?: number;
364
+ now?: number;
365
+ frameRate?: number;
366
+ };
367
+ type SnapshotAssetRef = {
368
+ id: string;
369
+ version?: string;
370
+ ref?: string;
371
+ };
372
+ type SnapshotIntegrity = {
373
+ alg: string;
374
+ hash: string;
375
+ };
376
+ type SnapshotProvenance = {
377
+ source?: string;
378
+ integrity?: SnapshotIntegrity;
379
+ };
380
+ type SnapshotEnvelope = {
381
+ schemaVersion: typeof SNAPSHOT_SCHEMA_VERSION;
382
+ runtimeVersion: string;
383
+ cart: SnapshotCartRef;
384
+ seed: string;
385
+ clock: SnapshotClock;
386
+ engineState: CartStateBundle;
387
+ createdAt: string;
388
+ modules?: SnapshotModuleRef[];
389
+ rng?: RandomState;
390
+ hostState?: unknown;
391
+ hostStateRef?: string;
392
+ assets?: SnapshotAssetRef[];
393
+ provenance?: SnapshotProvenance;
394
+ };
395
+ type ExportSnapshotOptions = {
396
+ cartVersion?: string;
397
+ modules?: SnapshotModuleRef[];
398
+ hostState?: unknown;
399
+ hostStateRef?: string;
400
+ assets?: SnapshotAssetRef[];
401
+ createdAt?: string;
402
+ provenance?: SnapshotProvenance;
403
+ runtimeVersion?: string;
404
+ };
405
+
331
406
  /**
332
407
  * Copyright (c) 2026 Aaron Boyarsky
333
408
  * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
@@ -475,6 +550,75 @@ type AssetPreloader = {
475
550
  dispose(): void;
476
551
  };
477
552
 
553
+ type AudioUnlockState = 'locked' | 'unlocking' | 'unlocked' | 'failed';
554
+ type AudioUnlockStatus = {
555
+ state: AudioUnlockState;
556
+ error?: string;
557
+ };
558
+ type AudioAssetStatus = 'ready' | 'failed';
559
+ type AudioChannelInspect = {
560
+ id: string;
561
+ participantId?: string;
562
+ gain: number;
563
+ muted: boolean;
564
+ duckGain: number;
565
+ priority: number;
566
+ effectiveGain: number;
567
+ };
568
+ type AudioBrokerInspect = {
569
+ status: AudioUnlockStatus;
570
+ reducedSensory: boolean;
571
+ muted: boolean;
572
+ authorized: string[];
573
+ channels: AudioChannelInspect[];
574
+ assets: Record<string, AudioAssetStatus>;
575
+ };
576
+ type AudioBrokerNotice = {
577
+ type: 'asset';
578
+ id: string;
579
+ status: AudioAssetStatus;
580
+ } | {
581
+ type: 'teardown';
582
+ participantId: string;
583
+ } | {
584
+ type: 'mute';
585
+ } | {
586
+ type: 'destroy';
587
+ };
588
+ type AudioBrokerListener = (notice: AudioBrokerNotice) => void;
589
+ type ActiveAudioCue = {
590
+ idempotencyKey: string;
591
+ channelId: string;
592
+ participantId?: string;
593
+ priority: number;
594
+ };
595
+ type AudioBroker = {
596
+ unlock(): Promise<AudioUnlockStatus>;
597
+ status(): AudioUnlockStatus;
598
+ authorize(participantId: string): void;
599
+ revoke(participantId: string): void;
600
+ isAuthorized(participantId: string | undefined): boolean;
601
+ setChannelGain(channelId: string, gain: number, participantId?: string): void;
602
+ setPriority(channelId: string, priority: number, participantId?: string): void;
603
+ mute(): void;
604
+ unmute(): void;
605
+ muteChannel(channelId: string, participantId?: string): void;
606
+ unmuteChannel(channelId: string): void;
607
+ duck(channelId: string, gain?: number): void;
608
+ unduck(channelId: string): void;
609
+ effectiveGain(channelId: string): number;
610
+ handleHostEvent(event: HostEvent): void;
611
+ assetStatus(id: string): AudioAssetStatus | undefined;
612
+ noteCueStarted(cue: ActiveAudioCue): void;
613
+ noteCueEnded(idempotencyKey: string): void;
614
+ teardown(participantId: string): void;
615
+ onNotice(listener: AudioBrokerListener): () => void;
616
+ inspect(): AudioBrokerInspect;
617
+ destroy(): void;
618
+ readonly reducedSensory: boolean;
619
+ readonly muted: boolean;
620
+ };
621
+
478
622
  /**
479
623
  * Copyright (c) 2026 Aaron Boyarsky
480
624
  * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
@@ -519,6 +663,22 @@ type CreateRuntimeOptions = {
519
663
  * scripted `{ type: 'asset' }` actions own delivery timing.
520
664
  */
521
665
  assets?: AssetRuntimeOptions;
666
+ /**
667
+ * Shared page-level unlock broker. Optional. Carts that only set
668
+ * `metadata.audio: 'tone'` keep the existing `start()` / `unlockAudio()`
669
+ * path when this is omitted.
670
+ */
671
+ audioBroker?: AudioBroker;
672
+ /**
673
+ * Runtime-group participant id to authorize on this runtime. Teardown of
674
+ * this id does not close Tone for remaining carts.
675
+ */
676
+ audioParticipantId?: string;
677
+ /**
678
+ * `'calculation'` skips canvas construction and paint. `getDefaultState`,
679
+ * `update`, and host-channel events still run. Default `'render'`.
680
+ */
681
+ kind?: CartKind;
522
682
  };
523
683
  type MountOptions<T = unknown> = {
524
684
  /** Boot overrides passed as `customState` into `getDefaultState`. Not a live-state replay. */
@@ -549,7 +709,12 @@ type CartHandle = {
549
709
  getCartState(): unknown;
550
710
  exportState(): Promise<CartStateBundle>;
551
711
  exportStateJSON(): Promise<string>;
552
- importState(bundle: CartStateBundle | string, extras?: {
712
+ importState(bundle: CartStateBundle | SnapshotEnvelope | string, extras?: {
713
+ framebuffer?: ImageData | null;
714
+ }): Promise<void>;
715
+ exportSnapshot(options?: ExportSnapshotOptions): Promise<SnapshotEnvelope>;
716
+ exportSnapshotJSON(options?: ExportSnapshotOptions): Promise<string>;
717
+ importSnapshot(input: SnapshotEnvelope | CartStateBundle | string, extras?: {
553
718
  framebuffer?: ImageData | null;
554
719
  }): Promise<void>;
555
720
  peekExportedFramebuffer(): ImageData | null;
@@ -590,6 +755,10 @@ type CyberArtRuntime = {
590
755
  * Survives cart remount; `destroy()` disposes it.
591
756
  */
592
757
  readonly assets: AssetPreloader | undefined;
758
+ /** Shared broker when `createRuntime({ audioBroker })` was set. */
759
+ readonly audioBroker: AudioBroker | undefined;
760
+ /** `'render'` (default) or `'calculation'` (no canvas / paint). */
761
+ readonly kind: CartKind;
593
762
  onError?: (error: unknown, info: FrameErrorInfo) => void;
594
763
  };
595
764
 
@@ -681,7 +850,7 @@ type EventRouter = {
681
850
  * locksteps a deterministic clock. Carts never receive the router object.
682
851
  */
683
852
 
684
- type RuntimeGroupKind = 'render' | 'calculation';
853
+ type RuntimeGroupKind = CartKind;
685
854
  /**
686
855
  * Optional capability-shaped attach hints. Explicit participant `emit` /
687
856
  * `subscribe` / `authoritative` win. Do not import the capability manifest
@@ -699,7 +868,7 @@ type RuntimeGroupFrameError = {
699
868
  type RuntimeGroupParticipantConfig<T = unknown> = {
700
869
  id: string;
701
870
  cart: AnimationCart<T>;
702
- /** Rendered surface vs calculation cart (still a real `AnimationCart`). */
871
+ /** Rendered surface vs calculation cart (update + events, no canvas / paint). */
703
872
  kind?: RuntimeGroupKind;
704
873
  seed?: CreateRuntimeOptions['seed'];
705
874
  container?: HTMLElement;
@@ -726,6 +895,8 @@ type CreateRuntimeGroupOptions = {
726
895
  router?: EventRouterOptions;
727
896
  /** Bound on the accepted-event trace (oldest dropped). Default 1024. */
728
897
  maxTrace?: number;
898
+ /** Shared page-level audio unlock broker. Optional. */
899
+ audioBroker?: AudioBroker;
729
900
  };
730
901
  type RuntimeGroupParticipantInspect = {
731
902
  state: unknown;
@@ -761,6 +932,7 @@ type RuntimeGroup = {
761
932
  readonly router: EventRouter;
762
933
  readonly origin: number;
763
934
  readonly paused: boolean;
935
+ readonly audioBroker: AudioBroker | undefined;
764
936
  participant(id: string): RuntimeGroupParticipantHandle;
765
937
  step(frames?: number): Promise<void>;
766
938
  pause(): void;
@@ -784,6 +956,20 @@ type CompositorBlendMode = (typeof COMPOSITOR_BLEND_MODES)[number];
784
956
  declare const COMPOSITOR_CLEAR_POLICIES: readonly ["transparent", "opaque"];
785
957
  type CompositorClearPolicy = (typeof COMPOSITOR_CLEAR_POLICIES)[number];
786
958
  type CompositorPointerEvents = 'auto' | 'none';
959
+ declare const COMPOSITOR_SOURCE_KINDS: readonly ["participant", "image", "canvas"];
960
+ type CompositorSourceKind = (typeof COMPOSITOR_SOURCE_KINDS)[number];
961
+ type CompositorParticipantSource = {
962
+ kind: 'participant';
963
+ };
964
+ type CompositorImageSource = {
965
+ kind: 'image';
966
+ image: ImageData;
967
+ };
968
+ type CompositorCanvasSource = {
969
+ kind: 'canvas';
970
+ canvas: HTMLCanvasElement;
971
+ };
972
+ type CompositorLayerSource = CompositorParticipantSource | CompositorImageSource | CompositorCanvasSource;
787
973
  type CompositorClip = {
788
974
  x: number;
789
975
  y: number;
@@ -799,6 +985,11 @@ type CompositorLayerConfig = {
799
985
  clip?: CompositorClip;
800
986
  pointerEvents?: CompositorPointerEvents;
801
987
  clearPolicy?: CompositorClearPolicy;
988
+ /**
989
+ * Pixel source. Default `participant` reads the runtime-group canvas.
990
+ * `image` / `canvas` swap atomically via `setLayer` / `addLayer`.
991
+ */
992
+ source?: CompositorLayerSource;
802
993
  };
803
994
  type CompositorLayerInspect = {
804
995
  id: string;
@@ -809,6 +1000,7 @@ type CompositorLayerInspect = {
809
1000
  clip: CompositorClip | null;
810
1001
  pointerEvents: CompositorPointerEvents;
811
1002
  clearPolicy: CompositorClearPolicy;
1003
+ sourceKind: CompositorSourceKind;
812
1004
  };
813
1005
  type ComposedFrame = {
814
1006
  imageData: ImageData;
@@ -841,6 +1033,7 @@ type Compositor = {
841
1033
  */
842
1034
  resize(width: number, height: number, dpr?: number): void;
843
1035
  setLayer(id: string, patch: Partial<Omit<CompositorLayerConfig, 'id'>>): void;
1036
+ addLayer(config: CompositorLayerConfig): void;
844
1037
  layer(id: string): CompositorLayerInspect;
845
1038
  layers(): CompositorLayerInspect[];
846
1039
  pointerTarget(x: number, y: number): string | undefined;
@@ -1213,6 +1406,289 @@ declare function replayExportedTrace(exported: ReplayInspectorExport, group: Run
1213
1406
  report: ReplayInspectorReport;
1214
1407
  }>;
1215
1408
 
1409
+ type CueEasing = 'linear' | 'ease-out';
1410
+ type CueDuplicatePolicy = 'ignore' | 'replace' | 'reject';
1411
+ type CueRepeatPolicy = {
1412
+ count: number;
1413
+ } | {
1414
+ forever: true;
1415
+ };
1416
+ type CueReducedMotionPolicy = 'skip' | 'complete' | {
1417
+ durationFrames: number;
1418
+ };
1419
+ type CueSpec = {
1420
+ name: string;
1421
+ idempotencyKey: string;
1422
+ /** Frame when the cue is eligible to start (before delay). Default: play frame. */
1423
+ startFrame?: number;
1424
+ durationFrames: number;
1425
+ delayFrames?: number;
1426
+ easing?: CueEasing;
1427
+ repeat?: CueRepeatPolicy;
1428
+ /**
1429
+ * When the timeline is in reduced-motion mode: skip (complete immediately),
1430
+ * complete (same), or a shorter duration. Default `complete`.
1431
+ */
1432
+ reducedMotion?: CueReducedMotionPolicy;
1433
+ onDuplicate?: CueDuplicatePolicy;
1434
+ };
1435
+ type CuePhase = 'scheduled' | 'active' | 'completed' | 'cancelled';
1436
+ type CueView = {
1437
+ name: string;
1438
+ idempotencyKey: string;
1439
+ phase: CuePhase;
1440
+ startFrame: number;
1441
+ durationFrames: number;
1442
+ delayFrames: number;
1443
+ easing: CueEasing;
1444
+ progress: number;
1445
+ repeatIndex: number;
1446
+ };
1447
+ type PlayCueResult = {
1448
+ ok: true;
1449
+ cue: CueView;
1450
+ } | {
1451
+ ok: false;
1452
+ reason: 'duplicate' | 'invalid';
1453
+ detail: string;
1454
+ };
1455
+
1456
+ /**
1457
+ * Copyright (c) 2026 Aaron Boyarsky
1458
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
1459
+ * See packages/engine/LICENSE
1460
+ *
1461
+ * Deterministic audio-cue timeline. Scheduling is frame-stepped via
1462
+ * `createPresentationTimeline`; PCM output is not part of the event trace.
1463
+ */
1464
+
1465
+ declare const AUDIO_CUE_EVENTS: readonly ["audio.cue.scheduled", "audio.cue.started", "audio.cue.skipped", "audio.cue.failed"];
1466
+ type AudioCueEventType = (typeof AUDIO_CUE_EVENTS)[number];
1467
+ type AudioCueSkipReason = 'reduced-sensory' | 'muted' | 'unauthorized';
1468
+ type AudioCueFailReason = 'asset-failed' | 'torn-down' | 'invalid';
1469
+ type AudioCueReason = AudioCueSkipReason | AudioCueFailReason;
1470
+ type AudioCueSpec = CueSpec & {
1471
+ assetId: string;
1472
+ channelId?: string;
1473
+ participantId?: string;
1474
+ priority?: number;
1475
+ gain?: number;
1476
+ };
1477
+ type AudioCueView = CueView & {
1478
+ assetId: string;
1479
+ channelId: string;
1480
+ participantId?: string;
1481
+ priority: number;
1482
+ audioPhase: 'scheduled' | 'started' | 'skipped' | 'failed' | 'completed';
1483
+ };
1484
+ type AudioCueEvent = {
1485
+ type: AudioCueEventType;
1486
+ atFrame: number;
1487
+ name: string;
1488
+ idempotencyKey: string;
1489
+ assetId: string;
1490
+ channelId: string;
1491
+ participantId?: string;
1492
+ reason?: AudioCueReason;
1493
+ progress: number;
1494
+ };
1495
+ type AudioCueTimelineSnapshot = {
1496
+ frame: number;
1497
+ reducedSensory: boolean;
1498
+ cues: AudioCueView[];
1499
+ events: AudioCueEvent[];
1500
+ };
1501
+ type PlayAudioCueResult = {
1502
+ ok: true;
1503
+ cue: AudioCueView;
1504
+ } | {
1505
+ ok: false;
1506
+ reason: 'duplicate' | 'invalid';
1507
+ detail: string;
1508
+ };
1509
+ type AudioCueTimeline = {
1510
+ play(spec: AudioCueSpec): PlayAudioCueResult;
1511
+ step(frames?: number): AudioCueEvent[];
1512
+ cancel(idempotencyKey: string): boolean;
1513
+ reset(): void;
1514
+ snapshot(): AudioCueTimelineSnapshot;
1515
+ get(idempotencyKey: string): AudioCueView | undefined;
1516
+ dispose(): void;
1517
+ readonly frame: number;
1518
+ readonly reducedSensory: boolean;
1519
+ };
1520
+ type HeadlessAudioAdapter = {
1521
+ readonly broker: AudioBroker;
1522
+ readonly timeline: AudioCueTimeline;
1523
+ unlock(): Promise<AudioUnlockStatus>;
1524
+ play(spec: AudioCueSpec): PlayAudioCueResult;
1525
+ step(frames?: number): AudioCueEvent[];
1526
+ handleHostEvent(event: HostEvent): void;
1527
+ snapshot(): AudioCueTimelineSnapshot & {
1528
+ unlock: AudioUnlockStatus;
1529
+ muted: boolean;
1530
+ };
1531
+ destroy(): void;
1532
+ };
1533
+ declare function createHeadlessAudioAdapter(options?: {
1534
+ reducedSensory?: boolean;
1535
+ originFrame?: number;
1536
+ }): HeadlessAudioAdapter;
1537
+
1538
+ declare const VISUAL_LAYER_SNAPSHOT_SCHEMA_VERSION: 1;
1539
+ declare const VISUAL_LAYER_KINDS: readonly ["layer", "mask", "sprite"];
1540
+ type VisualLayerKind = (typeof VISUAL_LAYER_KINDS)[number];
1541
+ declare const VISUAL_LAYER_TRANSITIONS: readonly ["show", "hide", "replace", "crossfade"];
1542
+ type VisualLayerTransitionKind = (typeof VISUAL_LAYER_TRANSITIONS)[number];
1543
+ declare const VISUAL_LAYER_EVENTS: readonly ["visual.layer.revealed", "visual.layer.hidden", "visual.layer.transition-started", "visual.layer.transition-completed", "visual.layer.failed"];
1544
+ type VisualLayerEventType = (typeof VISUAL_LAYER_EVENTS)[number];
1545
+ declare const VISUAL_LAYER_FAILURE_CODES: readonly ["asset-failed", "missing-source", "unknown-layer", "unknown-version", "invalid-snapshot"];
1546
+ type VisualLayerFailureCode = (typeof VISUAL_LAYER_FAILURE_CODES)[number];
1547
+ type VisualLayerAssetStatus = 'pending' | 'ready' | 'failed';
1548
+ type VisualLayerFallbackPolicy = 'keep-prior' | 'hide' | {
1549
+ version: string;
1550
+ };
1551
+ type VisualLayerVersionDeclaration = {
1552
+ id: string;
1553
+ assetId: string;
1554
+ provenance?: AssetProvenance;
1555
+ };
1556
+ type VisualLayerDeclaration = {
1557
+ id: string;
1558
+ kind: VisualLayerKind;
1559
+ versions: readonly VisualLayerVersionDeclaration[];
1560
+ initialVersion?: string;
1561
+ compositorLayerId?: string;
1562
+ incomingLayerId?: string;
1563
+ order?: number;
1564
+ blend?: CompositorBlendMode;
1565
+ clip?: CompositorClip;
1566
+ pointerEvents?: CompositorPointerEvents;
1567
+ clearPolicy?: CompositorClearPolicy;
1568
+ fallback?: VisualLayerFallbackPolicy;
1569
+ };
1570
+ type VisualLayerAcceptedBinding = {
1571
+ layerId: string;
1572
+ toVersion: string;
1573
+ kind?: VisualLayerTransitionKind;
1574
+ durationFrames?: number;
1575
+ delayFrames?: number;
1576
+ easing?: CueEasing;
1577
+ idempotencyKey?: string;
1578
+ };
1579
+ type VisualLayerCueSpec = CueSpec & {
1580
+ layerId: string;
1581
+ kind: VisualLayerTransitionKind;
1582
+ toVersion?: string;
1583
+ fromVersion?: string;
1584
+ };
1585
+ type VisualLayerTransitionInspect = {
1586
+ kind: VisualLayerTransitionKind;
1587
+ progress: number;
1588
+ fromVersion: string | null;
1589
+ toVersion: string | null;
1590
+ cueKey: string;
1591
+ startFrame: number;
1592
+ durationFrames: number;
1593
+ delayFrames: number;
1594
+ easing: CueEasing;
1595
+ };
1596
+ type VisualLayerInspect = {
1597
+ id: string;
1598
+ kind: VisualLayerKind;
1599
+ visible: boolean;
1600
+ activeVersion: string | null;
1601
+ pendingVersion: string | null;
1602
+ committedVersion: string | null;
1603
+ opacity: number;
1604
+ order: number;
1605
+ provenance: AssetProvenance | null;
1606
+ overrideVersion: string | null;
1607
+ transition: VisualLayerTransitionInspect | null;
1608
+ };
1609
+ type VisualLayerEvent = {
1610
+ type: VisualLayerEventType;
1611
+ atFrame: number;
1612
+ layerId: string;
1613
+ version?: string;
1614
+ kind?: VisualLayerTransitionKind;
1615
+ progress: number;
1616
+ code?: VisualLayerFailureCode;
1617
+ };
1618
+ type VisualLayerDiagnostic = {
1619
+ code: VisualLayerFailureCode;
1620
+ layerId: string;
1621
+ version?: string;
1622
+ assetId?: string;
1623
+ message: string;
1624
+ atFrame: number;
1625
+ failure?: AssetFailure;
1626
+ };
1627
+ type VisualLayerSnapshotRow = {
1628
+ id: string;
1629
+ kind: VisualLayerKind;
1630
+ visible: boolean;
1631
+ committedVersion: string | null;
1632
+ pendingVersion: string | null;
1633
+ activeVersion: string | null;
1634
+ opacity: number;
1635
+ order: number;
1636
+ overrideVersion: string | null;
1637
+ provenance: AssetProvenance | null;
1638
+ fallback: VisualLayerFallbackPolicy;
1639
+ transition: VisualLayerTransitionInspect | null;
1640
+ };
1641
+ type VisualLayerControllerSnapshot = {
1642
+ schemaVersion: typeof VISUAL_LAYER_SNAPSHOT_SCHEMA_VERSION;
1643
+ frame: number;
1644
+ sceneId: string | null;
1645
+ reducedMotion: boolean;
1646
+ layers: VisualLayerSnapshotRow[];
1647
+ assets: Record<string, VisualLayerAssetStatus>;
1648
+ events: VisualLayerEvent[];
1649
+ diagnostics: VisualLayerDiagnostic[];
1650
+ };
1651
+ type PlayVisualLayerResult = PlayCueResult;
1652
+ type RestoreVisualLayerResult = {
1653
+ ok: true;
1654
+ snapshot: VisualLayerControllerSnapshot;
1655
+ } | {
1656
+ ok: false;
1657
+ errors: VisualLayerDiagnostic[];
1658
+ };
1659
+ type CreateVisualLayerControllerOptions = {
1660
+ compositor: Compositor;
1661
+ layers: readonly VisualLayerDeclaration[];
1662
+ preloader?: AssetPreloader;
1663
+ originFrame?: number;
1664
+ reducedMotion?: boolean;
1665
+ sceneId?: string;
1666
+ fallback?: VisualLayerFallbackPolicy;
1667
+ onAccepted?: readonly VisualLayerAcceptedBinding[];
1668
+ dispatch?: (event: HostEvent) => void;
1669
+ };
1670
+ type VisualLayerController = {
1671
+ registerSource(assetId: string, source: ImageData | HTMLCanvasElement): void;
1672
+ handleHostEvent(event: HostEvent): void;
1673
+ override(layerId: string, version: string | null): void;
1674
+ play(spec: VisualLayerCueSpec): PlayVisualLayerResult;
1675
+ step(frames?: number): VisualLayerEvent[];
1676
+ snapshot(): VisualLayerControllerSnapshot;
1677
+ restore(input: unknown): RestoreVisualLayerResult;
1678
+ inspect(): VisualLayerInspect[];
1679
+ captureComposedFrame(): ComposedFrame;
1680
+ destroy(): void;
1681
+ readonly frame: number;
1682
+ readonly sceneId: string | null;
1683
+ readonly compositor: Compositor;
1684
+ };
1685
+ type VisualLayerCapture = {
1686
+ frame: ComposedFrame;
1687
+ layers: VisualLayerInspect[];
1688
+ };
1689
+ declare function captureVisualLayers(controller: VisualLayerController): VisualLayerCapture;
1690
+ declare function createVisualLayerController(options: CreateVisualLayerControllerOptions): VisualLayerController;
1691
+
1216
1692
  /**
1217
1693
  * Copyright (c) 2026 Aaron Boyarsky
1218
1694
  * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
@@ -1220,7 +1696,8 @@ declare function replayExportedTrace(exported: ReplayInspectorExport, group: Run
1220
1696
  *
1221
1697
  * Node/jsdom test helpers. Import from `@cyberart-io/engine/headless`.
1222
1698
  * Production carts and browser hosts must import `@cyberart-io/engine` instead
1223
- * so Vite never walks `node:fs/promises`.
1699
+ * so Vite never walks `node:fs/promises`. `createHeadlessMultiCartHarness`
1700
+ * is the same group API as `createRuntimeGroup`, including calculation carts.
1224
1701
  */
1225
1702
 
1226
1703
  type WriteComposedFrameResult = ComposedFrame & {
@@ -1232,4 +1709,4 @@ type WriteComposedFrameResult = ComposedFrame & {
1232
1709
  */
1233
1710
  declare function writeComposedFrame(compositor: Compositor, path?: string): Promise<WriteComposedFrameResult>;
1234
1711
 
1235
- export { type BoundReplaySession, type CausationTreeNode, type CreateHeadlessHarnessOptions, type CreateHeadlessMultiCartHarnessOptions, type CreateReplayInspectorOptions, DEFAULT_HEADLESS_HEIGHT, DEFAULT_HEADLESS_WIDTH, type GlyphAtlas, HEADLESS_PNG_DATA_URL, type HeadlessCanvas2DSettings, type HeadlessFrameError, type HeadlessHarness, type HeadlessImageFixture, type HeadlessInspect, type HeadlessMultiCartHarness, HeadlessUnsupportedOperationError, type InspectorRecord, type InstallHeadlessCanvasOptions, type ReplayCompareResult, type ReplayInspector, type ReplayInspectorExport, type ReplayInspectorReport, type ReplayParticipantSummary, type ReplayTapeAction, type ReplayTraceFilter, UPDATE_GOLDEN_ENV, type VisualArtifactPaths, type VisualArtifacts, type VisualCompareOptions, type VisualCompareResult, type WriteComposedFrameResult, assertPixelsEqual, assertPngDataUrlsEqual, attachHeadlessCanvas2D, compareImageData, compareReplayTraces, createDefaultGlyphAtlas, createHeadlessHarness, createHeadlessMultiCartHarness, createImageFixture, createReplayInspector, decodePng, encodePng, encodePngDataUrl, getHeadlessSurface, imageDataFromPngDataUrl, installHeadlessCanvas, makeImageData, replayExportedTrace, setDefaultGlyphAtlas, shouldUpdateGolden, writeComposedFrame, writeVisualArtifacts };
1712
+ export { type BoundReplaySession, type CausationTreeNode, type CreateHeadlessHarnessOptions, type CreateHeadlessMultiCartHarnessOptions, type CreateReplayInspectorOptions, DEFAULT_HEADLESS_HEIGHT, DEFAULT_HEADLESS_WIDTH, type GlyphAtlas, HEADLESS_PNG_DATA_URL, type HeadlessAudioAdapter, type HeadlessCanvas2DSettings, type HeadlessFrameError, type HeadlessHarness, type HeadlessImageFixture, type HeadlessInspect, type HeadlessMultiCartHarness, HeadlessUnsupportedOperationError, type InspectorRecord, type InstallHeadlessCanvasOptions, type ReplayCompareResult, type ReplayInspector, type ReplayInspectorExport, type ReplayInspectorReport, type ReplayParticipantSummary, type ReplayTapeAction, type ReplayTraceFilter, UPDATE_GOLDEN_ENV, type VisualArtifactPaths, type VisualArtifacts, type VisualCompareOptions, type VisualCompareResult, type VisualLayerCapture, type VisualLayerController, type VisualLayerInspect, type WriteComposedFrameResult, assertPixelsEqual, assertPngDataUrlsEqual, attachHeadlessCanvas2D, captureVisualLayers, compareImageData, compareReplayTraces, createDefaultGlyphAtlas, createHeadlessAudioAdapter, createHeadlessHarness, createHeadlessMultiCartHarness, createImageFixture, createReplayInspector, createVisualLayerController, decodePng, encodePng, encodePngDataUrl, getHeadlessSurface, imageDataFromPngDataUrl, installHeadlessCanvas, makeImageData, replayExportedTrace, setDefaultGlyphAtlas, shouldUpdateGolden, writeComposedFrame, writeVisualArtifacts };