@glissade/core 0.7.0 → 0.8.0-pre.1

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.
package/dist/index.d.ts CHANGED
@@ -72,6 +72,9 @@ interface ValueType<T> {
72
72
  scale?(a: T, k: number): T;
73
73
  /** Type-class handoff default (§B.1): spring for kinetic, cut for hold-only. */
74
74
  defaultHandoff?: HandoffKind;
75
+ /** Document (de)serialization; default identity for JSON-native types (§2.2). */
76
+ serialize?(value: T): unknown;
77
+ deserialize?(raw: unknown): T;
75
78
  }
76
79
  type ValueTypeId = 'number' | 'vec2' | 'color' | 'string' | 'boolean' | (string & {});
77
80
  declare function registerValueType<T>(vt: ValueType<T>): void;
@@ -82,6 +85,8 @@ declare function getValueType<T = unknown>(id: ValueTypeId): ValueType<T>;
82
85
  declare const numberType: ValueType<number>;
83
86
  declare const vec2Equals: (a: Vec2, b: Vec2) => boolean;
84
87
  declare const vec2Type: ValueType<Vec2>;
88
+ /** vec2 swept along a circular arc: polar lerp of radius + shortest-path angle (§2.2). */
89
+ declare const vec2ArcType: ValueType<Vec2>;
85
90
  declare const colorType: ValueType<string>;
86
91
  declare const stringType: ValueType<string>;
87
92
  declare const booleanType: ValueType<boolean>;
@@ -280,6 +285,8 @@ interface Key<T = unknown> {
280
285
  id?: string;
281
286
  /** Builder-resolved implicit from-value; re-resolved on merge (§2.6, §6.2). */
282
287
  derived?: boolean;
288
+ /** Reserved (§4.7): a v2 synthesized-transition leading key reads the live value. v1 accepts but ignores it. */
289
+ from?: 'live';
283
290
  }
284
291
  interface Track<T = unknown> {
285
292
  /** Canonical path: '<nodeId>/<prop.path>', e.g. 'circle/position.x'. */
@@ -289,6 +296,8 @@ interface Track<T = unknown> {
289
296
  keys: Key<T>[];
290
297
  /** Studio may own this track's keys via sidecar (§6.2). */
291
298
  editable?: boolean;
299
+ /** Reserved (§2.2/§B.6): v2 additive blending. v1 accepts but ignores it (coalesce stays last-wins). */
300
+ additive?: boolean;
292
301
  }
293
302
  declare class TrackValidationError extends Error {
294
303
  constructor(target: string, message: string);
@@ -477,6 +486,19 @@ interface TimelineBuilder {
477
486
  }): TimelineBuilder;
478
487
  /** Compiles to a marker; the callback is Player-registered, never serialized (§4.2). */
479
488
  call(fn: () => void, at?: Position): TimelineBuilder;
489
+ /**
490
+ * A named cue marker — the composer-signal substrate. Fired on crossing via
491
+ * `player.onCue(kind, …)` and emitted to the render-time `cues.json`. Every
492
+ * cue carries a `data.kind` (default `'cue'`); pass your own to group them
493
+ * (e.g. `{ kind: 'chapter', title: '…' }`). `data.title` becomes the chapter
494
+ * label in `--chapters vtt`.
495
+ */
496
+ cue(at: Position, name: string, data?: Json): TimelineBuilder;
497
+ /** An ad-break cue: a marker with `data.kind: 'ad-break'` + optional duration (§ad-break). */
498
+ adBreak(at: Position, opts?: {
499
+ id?: string;
500
+ duration?: number;
501
+ }): TimelineBuilder;
480
502
  /** Mark the preceding track editable for the studio (§6.2). */
481
503
  editable(): TimelineBuilder;
482
504
  }
@@ -571,36 +593,78 @@ interface CheckpointedSim {
571
593
  declare function bakeCheckpointed<W, S = W>(cfg: CheckpointedBakeConfig<W, S>): CheckpointedSim;
572
594
  //#endregion
573
595
  //#region src/sidecar.d.ts
596
+ type OrphanReason = 'node-missing' | 'prop-missing' | 'type-changed';
597
+ interface SidecarTrackEntry {
598
+ /** value type, so an editor-created track binds without a code baseline. */
599
+ type: ValueTypeId;
600
+ /** hash of the code baseline this track branched from; null = editor-created. */
601
+ baseHash: string | null;
602
+ keys: Key[];
603
+ }
604
+ interface SidecarOrphan {
605
+ type: ValueTypeId;
606
+ keys: Key[];
607
+ reason: OrphanReason;
608
+ }
609
+ interface SidecarTimelineEntry {
610
+ /** editor-owned tracks, keyed by canonical target. */
611
+ tracks: Record<string, SidecarTrackEntry>;
612
+ /** editor-created labels. */
613
+ labels?: Record<string, number>;
614
+ /** tracks parked off the merge because their target drifted (§6.2 rule 3). */
615
+ orphans?: Record<string, SidecarOrphan>;
616
+ }
574
617
  interface SidecarDoc {
618
+ sidecarVersion: 2;
619
+ /** edits namespaced by timeline id; the linear timeline is 'main'. */
620
+ timelines: Record<string, SidecarTimelineEntry>;
621
+ }
622
+ /** The flat v1 shape, accepted on load and migrated forward. */
623
+ interface SidecarDocV1 {
575
624
  sidecarVersion: 1;
576
- /** Editor-owned tracks, replacing same-target code tracks wholesale. */
577
625
  tracks: Track[];
578
- /** Editor-created labels; code labels are authoritative and win on a name collision (§6.2). */
579
626
  labels?: Record<string, number>;
580
627
  }
581
628
  declare class SidecarVersionError extends Error {
582
629
  constructor(version: unknown);
583
630
  }
584
631
  declare function emptySidecar(): SidecarDoc;
632
+ /** Lift a v1 (or already-v2) document to the v2 shape. Throws on unknown versions. */
633
+ declare function migrateSidecar(doc: SidecarDoc | SidecarDocV1 | null | undefined): SidecarDoc | null;
634
+ /** Stable hash of a code track's keys — the baseline an editor branch records. */
635
+ declare function hashKeys(keys: readonly Key[]): string;
636
+ /** Assign stable monotonic `k<N>` ids to keys lacking one; existing ids preserved. */
637
+ declare function assignKeyIds(keys: readonly Key[]): Key[];
638
+ /**
639
+ * Set one editable track's keys in the sidecar (§6.2) — the studio write path.
640
+ * `codeBaselineKeys` is the code track this branches from (null = editor-created
641
+ * with no baseline). Keys get stable `k<N>` ids. Returns a new document.
642
+ */
643
+ declare function setSidecarTrack(doc: SidecarDoc, timelineId: string, target: string, type: ValueTypeId, keys: Key[], codeBaselineKeys: readonly Key[] | null): SidecarDoc;
644
+ interface MergeResult {
645
+ timeline: Timeline;
646
+ /** targets whose code baseline changed beneath the editor's keys (§6.2 rule 2). */
647
+ drift: string[];
648
+ /** sidecar tracks parked off the merge (§6.2 rule 3). */
649
+ orphans: Record<string, SidecarOrphan>;
650
+ }
651
+ /**
652
+ * Merge with the full §6.2 report: the bindable Timeline, the list of targets
653
+ * whose code baseline drifted, and the orphaned tracks (type-changed, or whose
654
+ * code track vanished). Orphans are NEVER merged, so a drifted edit can't make
655
+ * the whole overlay fail to bind — the good edits survive. Inputs unmutated.
656
+ */
657
+ declare function mergeSidecarDetailed(code: Timeline, sidecar: SidecarDoc | SidecarDocV1 | null | undefined): MergeResult;
658
+ /** Merge the sidecar overlay onto the code baseline → a bindable Timeline (§6.2). */
659
+ declare function mergeSidecar(code: Timeline, sidecar: SidecarDoc | SidecarDocV1 | null | undefined): Timeline;
585
660
  /**
586
661
  * Editor-edit normalization (§2.7 invariant): a spring-eased key's t is
587
662
  * intrinsic — prev.t + spring.duration(cfg) — so after any retime, sort and
588
663
  * re-pin spring keys to their predecessors. Dragging a spring key itself
589
664
  * therefore snaps back; retiming its predecessor carries it along. Returns a
590
- * new array. Colliding keys are NUDGED apart (+1ms), never deleted — an
591
- * editor must not silently destroy keyframe data on an exact-t collision.
665
+ * new array. Colliding keys are NUDGED apart (+1ms), never deleted — an editor
666
+ * must not silently destroy keyframe data on an exact-t collision.
592
667
  */
593
668
  declare function normalizeEditedKeys(keys: Key[]): Key[];
594
- /**
595
- * Merge rules (§6.2, schema-drift resolution):
596
- * - a sidecar track whose target exists in code REPLACES that track's keys
597
- * (the editor owns it; `editable` is preserved on the result);
598
- * - a sidecar track with no code counterpart is ADDED (editor-created track);
599
- * - sidecar tracks targeting nothing the scene can bind fail later at
600
- * bindTimeline with UnboundTargetError — surfaced, never silently dropped;
601
- * - code tracks without sidecar counterparts pass through untouched.
602
- * The input documents are not mutated.
603
- */
604
- declare function mergeSidecar(code: Timeline, sidecar: SidecarDoc | null | undefined): Timeline;
605
669
  //#endregion
606
- export { type AssetRef, type AudioClip, type BakeConfig, BakeError, type BindTarget, type BindableSignal, type BoundTimeline, type CheckpointedBakeConfig, type CheckpointedSim, type ChildEntry, CircularDependencyError, ColorParseError, type CompiledTimeline, type CurveSampler, DEFAULT_EASE, type DevWarning, type EaseSpec, type EasingFn, type Equals, type GainEnvelope, type HandoffKind, type Json, type Key, type KeyOpts, type Marker, type OkLab, type PathContour, type PathValue, type Playhead, type Position, PositionError, type ReadonlySignal, type RetargetSpring, type Rgba, type Rng, type SidecarDoc, SidecarVersionError, type Signal, type SignalOptions, type SpringConfig, type SpringEase, TARGET_PATH, type TargetCarrier, type Timeline, type TimelineBuilder, type TimelineInit, TimelineValidationError, type Track, TrackValidationError, type TweenOpts, type TweenTarget, UnboundTargetError, UnknownEasingError, UnknownValueTypeError, UnresolvableTargetError, type ValueType, type ValueTypeId, ValueTypeInferenceError, type Vec2, type Vec2Signal, WriteDuringEvaluationError, audioOffsetSamples, bake, bakeCheckpointed, beginReadPhase, bindTimeline, booleanType, buildTimeline, colorType, compileTimeline, computed, createPlayhead, cubicBezier, cubicBezierDerivative, easingDerivatives, easings, emitDevWarning, emptySidecar, endReadPhase, evaluateAt, formatColor, getTimelineCallbacks, getValueType, inReadPhase, inferValueType, key, lerpColor, mergeSidecar, namedEasing, normalizeEditedKeys, numberType, oklabToRgba, parseColor, pathType, random, registerValueType, resolveEase, resolveEaseDerivative, resolveTweenTarget, rgbaToOklab, sampleTrack, setDevWarning, signal, spring, springEasing, springEasingDerivative, springPresets, springTo, stagger, stringType, timeline, track, untracked, validateTrack, vec2Equals, vec2Signal, vec2Type, velocityAt };
670
+ export { type AssetRef, type AudioClip, type BakeConfig, BakeError, type BindTarget, type BindableSignal, type BoundTimeline, type CheckpointedBakeConfig, type CheckpointedSim, type ChildEntry, CircularDependencyError, ColorParseError, type CompiledTimeline, type CurveSampler, DEFAULT_EASE, type DevWarning, type EaseSpec, type EasingFn, type Equals, type GainEnvelope, type HandoffKind, type Json, type Key, type KeyOpts, type Marker, type MergeResult, type OkLab, type OrphanReason, type PathContour, type PathValue, type Playhead, type Position, PositionError, type ReadonlySignal, type RetargetSpring, type Rgba, type Rng, type SidecarDoc, type SidecarDocV1, type SidecarOrphan, type SidecarTimelineEntry, type SidecarTrackEntry, SidecarVersionError, type Signal, type SignalOptions, type SpringConfig, type SpringEase, TARGET_PATH, type TargetCarrier, type Timeline, type TimelineBuilder, type TimelineInit, TimelineValidationError, type Track, TrackValidationError, type TweenOpts, type TweenTarget, UnboundTargetError, UnknownEasingError, UnknownValueTypeError, UnresolvableTargetError, type ValueType, type ValueTypeId, ValueTypeInferenceError, type Vec2, type Vec2Signal, WriteDuringEvaluationError, assignKeyIds, audioOffsetSamples, bake, bakeCheckpointed, beginReadPhase, bindTimeline, booleanType, buildTimeline, colorType, compileTimeline, computed, createPlayhead, cubicBezier, cubicBezierDerivative, easingDerivatives, easings, emitDevWarning, emptySidecar, endReadPhase, evaluateAt, formatColor, getTimelineCallbacks, getValueType, hashKeys, inReadPhase, inferValueType, key, lerpColor, mergeSidecar, mergeSidecarDetailed, migrateSidecar, namedEasing, normalizeEditedKeys, numberType, oklabToRgba, parseColor, pathType, random, registerValueType, resolveEase, resolveEaseDerivative, resolveTweenTarget, rgbaToOklab, sampleTrack, setDevWarning, setSidecarTrack, signal, spring, springEasing, springEasingDerivative, springPresets, springTo, stagger, stringType, timeline, track, untracked, validateTrack, vec2ArcType, vec2Equals, vec2Signal, vec2Type, velocityAt };
package/dist/index.js CHANGED
@@ -341,6 +341,24 @@ const vec2Type = {
341
341
  scale: (a, k) => [a[0] * k, a[1] * k],
342
342
  defaultHandoff: "spring"
343
343
  };
344
+ /** vec2 swept along a circular arc: polar lerp of radius + shortest-path angle (§2.2). */
345
+ const vec2ArcType = {
346
+ id: "vec2-arc",
347
+ lerp: (a, b, t) => {
348
+ const ra = Math.hypot(a[0], a[1]);
349
+ const rb = Math.hypot(b[0], b[1]);
350
+ const angA = Math.atan2(a[1], a[0]);
351
+ let dAng = Math.atan2(b[1], b[0]) - angA;
352
+ while (dAng > Math.PI) dAng -= 2 * Math.PI;
353
+ while (dAng < -Math.PI) dAng += 2 * Math.PI;
354
+ const r = ra + (rb - ra) * t;
355
+ const ang = angA + dAng * t;
356
+ return [r * Math.cos(ang), r * Math.sin(ang)];
357
+ },
358
+ extrapolates: true,
359
+ equals: vec2Equals,
360
+ defaultHandoff: "blend-from-frozen"
361
+ };
344
362
  const colorType = {
345
363
  id: "color",
346
364
  lerp: lerpColor,
@@ -435,6 +453,7 @@ function inferValueType(value) {
435
453
  }
436
454
  registerValueType(numberType);
437
455
  registerValueType(vec2Type);
456
+ registerValueType(vec2ArcType);
438
457
  registerValueType(colorType);
439
458
  registerValueType(stringType);
440
459
  registerValueType(booleanType);
@@ -1365,6 +1384,30 @@ function buildTimeline(build, init = {}) {
1365
1384
  callbacks.set(name, fn);
1366
1385
  return builder;
1367
1386
  },
1387
+ cue(at, name, data) {
1388
+ const merged = data !== void 0 && data !== null && typeof data === "object" && !Array.isArray(data) ? {
1389
+ kind: "cue",
1390
+ ...data
1391
+ } : { kind: "cue" };
1392
+ markers.push({
1393
+ t: resolvePosition(at),
1394
+ name,
1395
+ data: merged
1396
+ });
1397
+ return builder;
1398
+ },
1399
+ adBreak(at, opts = {}) {
1400
+ const data = {
1401
+ kind: "ad-break",
1402
+ ...opts.duration !== void 0 ? { duration: opts.duration } : {}
1403
+ };
1404
+ markers.push({
1405
+ t: resolvePosition(at),
1406
+ name: opts.id ?? "ad-break",
1407
+ data
1408
+ });
1409
+ return builder;
1410
+ },
1368
1411
  editable() {
1369
1412
  const last = insertions[insertions.length - 1];
1370
1413
  if (!last) throw new TimelineValidationError(".editable() requires a preceding insertion");
@@ -1624,85 +1667,204 @@ function bakeCheckpointed(cfg) {
1624
1667
  //#region src/sidecar.ts
1625
1668
  /**
1626
1669
  * The editor sidecar (DESIGN.md §6.2): code declares scene structure and
1627
- * programmatic tracks; the studio owns keyframe data persisted as a sidecar
1628
- * document next to the scene module, merged at track granularity. Versioned
1629
- * independently of the API (§7.4) — breaking it orphans users' files.
1670
+ * programmatic tracks; the studio owns keyframe data persisted next to the
1671
+ * scene module and merged at track granularity. Versioned independently of the
1672
+ * API (§7.4) — breaking it orphans users' files, so v1 documents migrate.
1673
+ *
1674
+ * v2 namespaces edits by timeline id ('main' for the linear timeline; v2
1675
+ * machines add more), keys tracks by canonical target, records the code
1676
+ * baseline hash for drift detection, parks drifted tracks as `orphans`, and
1677
+ * gives keys stable `k<N>` ids.
1630
1678
  */
1679
+ const MAIN = "main";
1631
1680
  var SidecarVersionError = class extends Error {
1632
1681
  constructor(version) {
1633
- super(`unsupported sidecar version ${String(version)}; this build reads sidecarVersion 1`);
1682
+ super(`unsupported sidecar version ${String(version)}; this build reads sidecarVersion 1 or 2`);
1634
1683
  this.name = "SidecarVersionError";
1635
1684
  }
1636
1685
  };
1637
1686
  function emptySidecar() {
1638
1687
  return {
1639
- sidecarVersion: 1,
1640
- tracks: []
1688
+ sidecarVersion: 2,
1689
+ timelines: { [MAIN]: { tracks: {} } }
1641
1690
  };
1642
1691
  }
1692
+ /** Lift a v1 (or already-v2) document to the v2 shape. Throws on unknown versions. */
1693
+ function migrateSidecar(doc) {
1694
+ if (!doc) return null;
1695
+ if (doc.sidecarVersion === 2) return doc;
1696
+ if (doc.sidecarVersion === 1) {
1697
+ const tracks = {};
1698
+ for (const t of doc.tracks) tracks[t.target] = {
1699
+ type: t.type,
1700
+ baseHash: null,
1701
+ keys: t.keys
1702
+ };
1703
+ const main = { tracks };
1704
+ if (doc.labels) main.labels = doc.labels;
1705
+ return {
1706
+ sidecarVersion: 2,
1707
+ timelines: { [MAIN]: main }
1708
+ };
1709
+ }
1710
+ throw new SidecarVersionError(doc.sidecarVersion);
1711
+ }
1712
+ /** Stable hash of a code track's keys — the baseline an editor branch records. */
1713
+ function hashKeys(keys) {
1714
+ const s = JSON.stringify(keys.map((k) => [
1715
+ k.t,
1716
+ k.value,
1717
+ k.ease ?? null,
1718
+ k.interp ?? null
1719
+ ]));
1720
+ let h = 2166136261;
1721
+ for (let i = 0; i < s.length; i++) {
1722
+ h ^= s.charCodeAt(i);
1723
+ h = Math.imul(h, 16777619);
1724
+ }
1725
+ return (h >>> 0).toString(16);
1726
+ }
1727
+ /** Assign stable monotonic `k<N>` ids to keys lacking one; existing ids preserved. */
1728
+ function assignKeyIds(keys) {
1729
+ let max = -1;
1730
+ for (const k of keys) {
1731
+ const m = k.id ? /^k(\d+)$/.exec(k.id) : null;
1732
+ if (m) max = Math.max(max, Number(m[1]));
1733
+ }
1734
+ return keys.map((k) => k.id ? { ...k } : {
1735
+ ...k,
1736
+ id: `k${++max}`
1737
+ });
1738
+ }
1643
1739
  /**
1644
- * Editor-edit normalization (§2.7 invariant): a spring-eased key's t is
1645
- * intrinsic prev.t + spring.duration(cfg) so after any retime, sort and
1646
- * re-pin spring keys to their predecessors. Dragging a spring key itself
1647
- * therefore snaps back; retiming its predecessor carries it along. Returns a
1648
- * new array. Colliding keys are NUDGED apart (+1ms), never deleted — an
1649
- * editor must not silently destroy keyframe data on an exact-t collision.
1740
+ * Set one editable track's keys in the sidecar 6.2) the studio write path.
1741
+ * `codeBaselineKeys` is the code track this branches from (null = editor-created
1742
+ * with no baseline). Keys get stable `k<N>` ids. Returns a new document.
1650
1743
  */
1651
- function normalizeEditedKeys(keys) {
1652
- const out = keys.map((k) => ({ ...k })).sort((a, b) => a.t - b.t);
1653
- for (let pass = 0; pass < 2; pass++) {
1654
- for (let i = 1; i < out.length; i++) {
1655
- const ease = out[i].ease;
1656
- if (ease && typeof ease === "object" && ease.kind === "spring") out[i].t = out[i - 1].t + spring.duration(ease);
1744
+ function setSidecarTrack(doc, timelineId, target, type, keys, codeBaselineKeys) {
1745
+ const tl = doc.timelines[timelineId] ?? { tracks: {} };
1746
+ const entry = {
1747
+ type,
1748
+ baseHash: codeBaselineKeys ? hashKeys(codeBaselineKeys) : null,
1749
+ keys: assignKeyIds(keys)
1750
+ };
1751
+ return {
1752
+ ...doc,
1753
+ timelines: {
1754
+ ...doc.timelines,
1755
+ [timelineId]: {
1756
+ ...tl,
1757
+ tracks: {
1758
+ ...tl.tracks,
1759
+ [target]: entry
1760
+ }
1761
+ }
1657
1762
  }
1658
- out.sort((a, b) => a.t - b.t);
1659
- }
1660
- for (let i = 1; i < out.length; i++) if (out[i].t <= out[i - 1].t) out[i].t = out[i - 1].t + .001;
1661
- return out;
1763
+ };
1662
1764
  }
1663
1765
  /**
1664
- * Merge rules (§6.2, schema-drift resolution):
1665
- * - a sidecar track whose target exists in code REPLACES that track's keys
1666
- * (the editor owns it; `editable` is preserved on the result);
1667
- * - a sidecar track with no code counterpart is ADDED (editor-created track);
1668
- * - sidecar tracks targeting nothing the scene can bind fail later at
1669
- * bindTimeline with UnboundTargetError — surfaced, never silently dropped;
1670
- * - code tracks without sidecar counterparts pass through untouched.
1671
- * The input documents are not mutated.
1766
+ * Re-resolve `derived:true` leading keys against the merged track 2.6): a
1767
+ * derived from-key duplicates the preceding key's held value, so an upstream
1768
+ * edit must flow into it or the segment pops at its start. Build-time derived
1769
+ * keys are already correct; this fixes the ones an edit moved beneath.
1672
1770
  */
1673
- function mergeSidecar(code, sidecar) {
1674
- if (!sidecar) return code;
1675
- if (sidecar.sidecarVersion !== 1) throw new SidecarVersionError(sidecar.sidecarVersion);
1676
- const overlay = new Map(sidecar.tracks.map((t) => [t.target, t]));
1771
+ function reresolveDerived(keys) {
1772
+ if (!keys.some((k) => k.derived)) return keys;
1773
+ return keys.map((k, i) => k.derived && i > 0 ? {
1774
+ ...k,
1775
+ value: keys[i - 1].value
1776
+ } : k);
1777
+ }
1778
+ /**
1779
+ * Merge with the full §6.2 report: the bindable Timeline, the list of targets
1780
+ * whose code baseline drifted, and the orphaned tracks (type-changed, or whose
1781
+ * code track vanished). Orphans are NEVER merged, so a drifted edit can't make
1782
+ * the whole overlay fail to bind — the good edits survive. Inputs unmutated.
1783
+ */
1784
+ function mergeSidecarDetailed(code, sidecar) {
1785
+ const doc = migrateSidecar(sidecar);
1786
+ if (!doc) return {
1787
+ timeline: code,
1788
+ drift: [],
1789
+ orphans: {}
1790
+ };
1791
+ const main = doc.timelines[MAIN] ?? { tracks: {} };
1792
+ const overlay = new Map(Object.entries(main.tracks));
1793
+ const drift = [];
1794
+ const orphans = { ...main.orphans ?? {} };
1677
1795
  const tracks = code.tracks.map((t) => {
1678
- const replacement = overlay.get(t.target);
1679
- if (!replacement) return t;
1796
+ const entry = overlay.get(t.target);
1797
+ if (!entry) return t;
1680
1798
  overlay.delete(t.target);
1799
+ if (entry.type !== t.type) {
1800
+ orphans[t.target] = {
1801
+ type: entry.type,
1802
+ keys: entry.keys,
1803
+ reason: "type-changed"
1804
+ };
1805
+ return t;
1806
+ }
1807
+ if (entry.baseHash !== null && entry.baseHash !== hashKeys(t.keys)) drift.push(t.target);
1681
1808
  return {
1682
1809
  ...t,
1683
- keys: replacement.keys.map((k) => ({ ...k })),
1810
+ keys: reresolveDerived(entry.keys.map((k) => ({ ...k }))),
1684
1811
  editable: true
1685
1812
  };
1686
1813
  });
1687
- for (const added of overlay.values()) tracks.push({
1688
- ...added,
1689
- keys: added.keys.map((k) => ({ ...k })),
1814
+ for (const [target, entry] of overlay) if (entry.baseHash !== null) orphans[target] = {
1815
+ type: entry.type,
1816
+ keys: entry.keys,
1817
+ reason: "prop-missing"
1818
+ };
1819
+ else tracks.push({
1820
+ target,
1821
+ type: entry.type,
1822
+ keys: reresolveDerived(entry.keys.map((k) => ({ ...k }))),
1690
1823
  editable: true
1691
1824
  });
1692
1825
  const merged = {
1693
1826
  ...code,
1694
1827
  tracks
1695
1828
  };
1696
- if (sidecar.labels && Object.keys(sidecar.labels).length > 0) {
1829
+ if (main.labels && Object.keys(main.labels).length > 0) {
1697
1830
  const codeLabels = code.labels ?? {};
1698
- const shadowed = Object.keys(sidecar.labels).filter((n) => n in codeLabels);
1831
+ const shadowed = Object.keys(main.labels).filter((n) => n in codeLabels);
1699
1832
  if (shadowed.length) emitDevWarning(`sidecar label(s) ${shadowed.join(", ")} collide with code labels; code wins (§6.2)`);
1700
1833
  merged.labels = {
1701
- ...sidecar.labels,
1834
+ ...main.labels,
1702
1835
  ...codeLabels
1703
1836
  };
1704
1837
  }
1705
- return merged;
1838
+ if (drift.length) emitDevWarning(`sidecar: code baseline changed beneath edits on ${drift.join(", ")} (§6.2)`);
1839
+ return {
1840
+ timeline: merged,
1841
+ drift,
1842
+ orphans
1843
+ };
1844
+ }
1845
+ /** Merge the sidecar overlay onto the code baseline → a bindable Timeline (§6.2). */
1846
+ function mergeSidecar(code, sidecar) {
1847
+ return mergeSidecarDetailed(code, sidecar).timeline;
1848
+ }
1849
+ /**
1850
+ * Editor-edit normalization (§2.7 invariant): a spring-eased key's t is
1851
+ * intrinsic — prev.t + spring.duration(cfg) — so after any retime, sort and
1852
+ * re-pin spring keys to their predecessors. Dragging a spring key itself
1853
+ * therefore snaps back; retiming its predecessor carries it along. Returns a
1854
+ * new array. Colliding keys are NUDGED apart (+1ms), never deleted — an editor
1855
+ * must not silently destroy keyframe data on an exact-t collision.
1856
+ */
1857
+ function normalizeEditedKeys(keys) {
1858
+ const out = keys.map((k) => ({ ...k })).sort((a, b) => a.t - b.t);
1859
+ for (let pass = 0; pass < 2; pass++) {
1860
+ for (let i = 1; i < out.length; i++) {
1861
+ const ease = out[i].ease;
1862
+ if (ease && typeof ease === "object" && ease.kind === "spring") out[i].t = out[i - 1].t + spring.duration(ease);
1863
+ }
1864
+ out.sort((a, b) => a.t - b.t);
1865
+ }
1866
+ for (let i = 1; i < out.length; i++) if (out[i].t <= out[i - 1].t) out[i].t = out[i - 1].t + .001;
1867
+ return out;
1706
1868
  }
1707
1869
  //#endregion
1708
- export { BakeError, CircularDependencyError, ColorParseError, DEFAULT_EASE, PositionError, SidecarVersionError, TARGET_PATH, TimelineValidationError, TrackValidationError, UnboundTargetError, UnknownEasingError, UnknownValueTypeError, UnresolvableTargetError, ValueTypeInferenceError, WriteDuringEvaluationError, audioOffsetSamples, bake, bakeCheckpointed, beginReadPhase, bindTimeline, booleanType, buildTimeline, colorType, compileTimeline, computed, createPlayhead, cubicBezier, cubicBezierDerivative, easingDerivatives, easings, emitDevWarning, emptySidecar, endReadPhase, evaluateAt, formatColor, getTimelineCallbacks, getValueType, inReadPhase, inferValueType, key, lerpColor, mergeSidecar, namedEasing, normalizeEditedKeys, numberType, oklabToRgba, parseColor, pathType, random, registerValueType, resolveEase, resolveEaseDerivative, resolveTweenTarget, rgbaToOklab, sampleTrack, setDevWarning, signal, spring, springEasing, springEasingDerivative, springPresets, springTo, stagger, stringType, timeline, track, untracked, validateTrack, vec2Equals, vec2Signal, vec2Type, velocityAt };
1870
+ export { BakeError, CircularDependencyError, ColorParseError, DEFAULT_EASE, PositionError, SidecarVersionError, TARGET_PATH, TimelineValidationError, TrackValidationError, UnboundTargetError, UnknownEasingError, UnknownValueTypeError, UnresolvableTargetError, ValueTypeInferenceError, WriteDuringEvaluationError, assignKeyIds, audioOffsetSamples, bake, bakeCheckpointed, beginReadPhase, bindTimeline, booleanType, buildTimeline, colorType, compileTimeline, computed, createPlayhead, cubicBezier, cubicBezierDerivative, easingDerivatives, easings, emitDevWarning, emptySidecar, endReadPhase, evaluateAt, formatColor, getTimelineCallbacks, getValueType, hashKeys, inReadPhase, inferValueType, key, lerpColor, mergeSidecar, mergeSidecarDetailed, migrateSidecar, namedEasing, normalizeEditedKeys, numberType, oklabToRgba, parseColor, pathType, random, registerValueType, resolveEase, resolveEaseDerivative, resolveTweenTarget, rgbaToOklab, sampleTrack, setDevWarning, setSidecarTrack, signal, spring, springEasing, springEasingDerivative, springPresets, springTo, stagger, stringType, timeline, track, untracked, validateTrack, vec2ArcType, vec2Equals, vec2Signal, vec2Type, velocityAt };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/core",
3
- "version": "0.7.0",
3
+ "version": "0.8.0-pre.1",
4
4
  "description": "glissade core: signals, tracks, timeline document, evaluation, easing, springs, seeded RNG. Zero DOM/Node dependencies.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",