@vosjs/studio-core 0.12.0 → 0.13.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.js CHANGED
@@ -571,7 +571,7 @@ function entranceSeconds(e) {
571
571
  function exitSeconds(e) {
572
572
  if (!e || e.kind === "none") return 0;
573
573
  const house = e.kind === "fade" ? CARD_FADE_SECONDS : END_CARD_RECEDE;
574
- return Math.max(0.1, Math.min(3, e.seconds ?? house));
574
+ return Math.max(0.1, Math.min(8, e.seconds ?? house));
575
575
  }
576
576
  function placeFreezes(rated, freezes) {
577
577
  const marks = /* @__PURE__ */ new Map();
@@ -663,8 +663,9 @@ function prependEntrance(track, head) {
663
663
  return { keyframes: sortKeyframes([...head, ...rest]) };
664
664
  }
665
665
  function exitPose(e) {
666
- return e?.kind === "fade" ? [1, 0, 0] : [0.9, -0.02, 0.22];
666
+ return e?.kind === "fade" ? [1, 0, 0] : [0.9, -0.02, 0];
667
667
  }
668
+ var RECEDE_MID = [0.91, -0.018, 0.8];
668
669
  function cardPoseTrack(enter, exit, footageEnd, outputEnd2 = footageEnd) {
669
670
  const keyframes = [];
670
671
  const s = entranceSeconds(enter);
@@ -674,16 +675,36 @@ function cardPoseTrack(enter, exit, footageEnd, outputEnd2 = footageEnd) {
674
675
  keyframes.push({ t: s, value: [1, 0, 1], ease: "power3.out" });
675
676
  }
676
677
  const x = exitSeconds(exit);
678
+ let last = [1, 0, 1];
677
679
  if (x > 0 && footageEnd > 0) {
678
- const room = outputEnd2 - footageEnd;
679
- const t0 = Math.max(s, room > 1e-6 ? footageEnd : footageEnd - x);
680
- const t1 = room > 1e-6 ? t0 + Math.min(x, Math.max(0.1, room)) : t0 + x;
680
+ const t0 = round3(Math.max(s, footageEnd - x));
681
+ const t1 = round3(Math.max(t0 + 0.05, footageEnd));
682
+ last = exitPose(exit);
681
683
  keyframes.push({ t: t0, value: [1, 0, 1], ease: "none" });
682
- keyframes.push({ t: t1, value: exitPose(exit), ease: "power2.out" });
684
+ if (exit?.kind !== "fade") {
685
+ keyframes.push({
686
+ t: round3(t0 + (t1 - t0) * 0.45),
687
+ value: [...RECEDE_MID],
688
+ ease: "power2.out"
689
+ });
690
+ keyframes.push({ t: t1, value: last, ease: "power1.in" });
691
+ } else {
692
+ keyframes.push({ t: t1, value: last, ease: "power2.out" });
693
+ }
694
+ }
695
+ if (outputEnd2 > footageEnd + 1e-6 && footageEnd > 0) {
696
+ if (!keyframes.some((k) => Math.abs(k.t - footageEnd) < 1e-9))
697
+ keyframes.push({ t: footageEnd, value: [...last], ease: "none" });
698
+ keyframes.push({
699
+ t: round3(footageEnd + 1e-3),
700
+ value: [last[0], last[1], 0],
701
+ ease: "none"
702
+ });
683
703
  }
684
704
  if (!keyframes.length) return void 0;
685
705
  return { keyframes: sortKeyframes(keyframes) };
686
706
  }
707
+ var round3 = (v) => Math.round(v * 1e3) / 1e3;
687
708
  function endCardClips(card, doc, endStart) {
688
709
  const seconds = Math.max(1, Math.min(8, card.seconds ?? END_CARD_SECONDS));
689
710
  const anim = { enter: "rise", exit: "none" };
@@ -822,18 +843,25 @@ function migrateMotion(doc) {
822
843
  const card = doc.endCard;
823
844
  if (card && segments.length) {
824
845
  const seconds = Math.max(1, Math.min(8, card.seconds ?? END_CARD_SECONDS));
846
+ const segsNow = out.segments ?? segments;
847
+ const lastSeg = segsNow.at(-1);
848
+ const freezesNow = docFreezes({ segments: segsNow, freeze: out.freeze });
825
849
  const rated = withFreezes(
826
- splitBySpeed(segments, doc.speed ?? []),
827
- docFreezes(doc)
850
+ splitBySpeed(segsNow, doc.speed ?? []),
851
+ freezesNow
828
852
  );
829
853
  const endStart = totalDuration(rated);
854
+ const taken = new Set(freezesNow.map((f) => f.id));
855
+ let n = 0;
856
+ while (taken.has(`f${n}`)) n++;
857
+ out.freeze = [
858
+ ...freezesNow,
859
+ { id: `f${n}`, at: lastSeg.out, seconds, from: END_CARD_FROM }
860
+ ].sort((a, b) => a.at - b.at);
830
861
  nextOverlays = [...nextOverlays, ...endCardClips(card, doc, endStart)];
831
862
  nextFrame.anim = {
832
863
  ...nextFrame.anim ?? {},
833
- exit: {
834
- kind: "recede",
835
- seconds: Math.min(END_CARD_RECEDE, seconds * 0.4)
836
- }
864
+ exit: { kind: "recede", seconds }
837
865
  };
838
866
  }
839
867
  delete out.endCard;
@@ -1910,14 +1938,14 @@ function resolveOverlayFx(clip, clipDuration) {
1910
1938
  const maxTotal = Math.max(dur, clipDuration * 0.9);
1911
1939
  st = Math.min(st, Math.max(0, (maxTotal - dur) / (upperN - 1)));
1912
1940
  }
1913
- const round11 = (v) => Math.round(v * 1e4) / 1e4;
1941
+ const round12 = (v) => Math.round(v * 1e4) / 1e4;
1914
1942
  return {
1915
1943
  k: spec.fx,
1916
1944
  u: unit,
1917
1945
  d: FX_DIR[spec.direction ?? "forward"] ?? 0,
1918
- st: round11(st),
1919
- dur: round11(dur),
1920
- tt: round11(st * (upperN - 1) + dur),
1946
+ st: round12(st),
1947
+ dur: round12(dur),
1948
+ tt: round12(st * (upperN - 1) + dur),
1921
1949
  units,
1922
1950
  n
1923
1951
  };
@@ -2132,7 +2160,7 @@ var CURSOR_IDLE_HOLD = 1;
2132
2160
  var CURSOR_IDLE_FADE_OUT = 0.35;
2133
2161
  var CURSOR_IDLE_FADE_IN = 0.18;
2134
2162
  var CURSOR_IDLE_EPS_FRAC = 25e-4;
2135
- var round3 = (n) => Math.round(n * 1e3) / 1e3;
2163
+ var round4 = (n) => Math.round(n * 1e3) / 1e3;
2136
2164
  function cursorIdleFade(track, o) {
2137
2165
  const moves = track.filter((e) => e.type === "move" || e.type === "down" || e.type === "up").map((e) => ({ t: e.t / 1e3, x: e.x, y: e.y }));
2138
2166
  if (moves.length === 0) return [];
@@ -2184,7 +2212,7 @@ function emit(keys, s, e) {
2184
2212
  push(keys, e + CURSOR_IDLE_FADE_IN, 1);
2185
2213
  }
2186
2214
  function push(keys, t, a) {
2187
- const rt = round3(t);
2215
+ const rt = round4(t);
2188
2216
  if (keys.length > 0 && rt <= keys[keys.length - 1].t) {
2189
2217
  keys[keys.length - 1].a = a;
2190
2218
  return;
@@ -3242,7 +3270,7 @@ function gatedRect(rect, x, y, space) {
3242
3270
  return null;
3243
3271
  const m = RECT_CONTAIN_MARGIN;
3244
3272
  const inside = x >= rect.x - m && x <= rect.x + rect.w + m && y >= rect.y - m && y <= rect.y + rect.h + m;
3245
- return inside ? [round4(rect.x), round4(rect.y), round4(rect.w), round4(rect.h)] : null;
3273
+ return inside ? [round5(rect.x), round5(rect.y), round5(rect.w), round5(rect.h)] : null;
3246
3274
  }
3247
3275
  function extractClicks(track, segments, opts) {
3248
3276
  const out = [];
@@ -3267,11 +3295,11 @@ function extractClicks(track, segments, opts) {
3267
3295
  const upSrc = st + Math.max(pressLen, 0.02);
3268
3296
  const up = keptOutputEnd(segments, st, upSrc) ?? ot + CLICK_SYNTH_RELEASE;
3269
3297
  const click = {
3270
- ot: round4(ot),
3271
- up: round4(Math.max(up, ot + 0.02)),
3272
- st: round4(st),
3273
- x: round4(e.x),
3274
- y: round4(e.y),
3298
+ ot: round5(ot),
3299
+ up: round5(Math.max(up, ot + 0.02)),
3300
+ st: round5(st),
3301
+ x: round5(e.x),
3302
+ y: round5(e.y),
3275
3303
  b: button
3276
3304
  };
3277
3305
  if (opts.rects && e.rect) {
@@ -3290,7 +3318,7 @@ function hexToRgbTriplet(hex) {
3290
3318
  const n = parseInt(h, 16);
3291
3319
  return [n >> 16, n >> 8 & 255, n & 255];
3292
3320
  }
3293
- function round4(v) {
3321
+ function round5(v) {
3294
3322
  return Math.round(v * 1e3) / 1e3;
3295
3323
  }
3296
3324
 
@@ -3353,7 +3381,7 @@ function trackEmitter() {
3353
3381
  if (tt <= prev.t + 1e-6 && sameVec(prev.value, value)) return prev.t;
3354
3382
  if (tt <= prev.t + 1e-6) tt = prev.t + 1e-3;
3355
3383
  }
3356
- keyframes.push({ t: round5(tt), value: value.map(round5), ease });
3384
+ keyframes.push({ t: round6(tt), value: value.map(round6), ease });
3357
3385
  return tt;
3358
3386
  };
3359
3387
  return { keyframes, push: push2 };
@@ -4444,13 +4472,16 @@ var ON_FRAME = `(ctx, content, dt) => {
4444
4472
  // corners, drawn by the clip alone, hid theirs inside the arc.
4445
4473
  var shD = ceZs * (cardW + cardH) + 2 * (W + H)
4446
4474
  var shX = cardX - shD / ceZs
4447
- // The card shadow is LAYERED: three shadows whose blur and offset grow
4448
- // while each stays at a low alpha (shadow is the strength they share),
4449
- // so the card reads as lifted a little off the ground. One wide layer at
4450
- // the full strength was the old paint, and at the default 0.4 it read as
4451
- // a dark pool under the card.
4475
+ // The card shadow is LAYERED: four shadows whose blur and offset grow
4476
+ // while each stays at a low alpha (shadow is the strength they share):
4477
+ // a hairline that defines the edge, two that ground it, and a WIDE,
4478
+ // faint ambient bloom (120 design px of blur, dropped well below the
4479
+ // card) that is what lets a large card read as lifted off a light
4480
+ // ground instead of outlined on it. One wide layer at the full strength
4481
+ // was the old paint, and at the default 0.4 it read as a dark pool
4482
+ // under the card; three tight layers read as a rim.
4452
4483
  if (shadow > 0) {
4453
- var shL = [[3, 1, 0.2], [14, 6, 0.22], [48, 22, 0.3]]
4484
+ var shL = [[2, 1, 0.1], [12, 4, 0.1], [40, 16, 0.14], [120, 44, 0.3]]
4454
4485
  for (var shI = 0; shI < shL.length; shI++) {
4455
4486
  c.save()
4456
4487
  c.shadowColor = 'rgba(' + shRgb + ',' + +(shadow * shL[shI][2]).toFixed(3) + ')'
@@ -4849,10 +4880,13 @@ var ON_FRAME = `(ctx, content, dt) => {
4849
4880
  card.mesh.scale.x = cpv[0]; card.mesh.scale.y = cpv[0]
4850
4881
  card.mesh.position.y = cpv[1] * cpH
4851
4882
  if (card.mesh.material) card.mesh.material.opacity = cpv[2]
4852
- } else if (card.mesh.scale && card.mesh.position && ((card.mesh.scale.x !== undefined && card.mesh.scale.x !== 1) || (card.mesh.position.y !== undefined && card.mesh.position.y !== 0))) {
4883
+ // Gone past its clip: the mesh leaves the scene, not just its paint.
4884
+ card.mesh.visible = cpv[2] > 0.001
4885
+ } else if (card.mesh.scale && card.mesh.position && ((card.mesh.scale.x !== undefined && card.mesh.scale.x !== 1) || (card.mesh.position.y !== undefined && card.mesh.position.y !== 0) || card.mesh.visible === false)) {
4853
4886
  card.mesh.scale.x = 1; card.mesh.scale.y = 1
4854
4887
  card.mesh.position.y = 0
4855
4888
  if (card.mesh.material) card.mesh.material.opacity = 1
4889
+ card.mesh.visible = true
4856
4890
  }
4857
4891
  var tilted = rx * rx + ry * ry > 1e-6
4858
4892
  if (card.texture && card.texture.generateMipmaps !== tilted) {
@@ -4884,14 +4918,14 @@ function studioLayerData(layers, duration) {
4884
4918
  // needs a decoded recording, which a sync lowering can't produce.
4885
4919
  audio: (layers.audio ?? []).map((c) => ({
4886
4920
  key: c.key,
4887
- start: round5(c.start),
4888
- in: round5(c.in),
4889
- out: round5(c.out),
4890
- gain: round5(c.gain),
4921
+ start: round6(c.start),
4922
+ in: round6(c.in),
4923
+ out: round6(c.out),
4924
+ gain: round6(c.gain),
4891
4925
  loop: !!c.loop,
4892
- len: round5(clipLength(c)),
4926
+ len: round6(clipLength(c)),
4893
4927
  duck: !!c.duck,
4894
- env: clipEnvelope(c).map((p) => ({ t: round5(p.t), g: round5(p.g) }))
4928
+ env: clipEnvelope(c).map((p) => ({ t: round6(p.t), g: round6(p.g) }))
4895
4929
  })),
4896
4930
  // Object clips: numbers resolved HERE; shapes are the drafted engine spec.
4897
4931
  ...layers.objects && layers.objects.length ? {
@@ -4904,17 +4938,17 @@ function studioLayerData(layers, duration) {
4904
4938
  } : o.asset.kind === "text3d" ? resolveText3dAsset(o.asset) : { kind: "gltf", key: o.asset.key },
4905
4939
  ...o.span ? {
4906
4940
  span: {
4907
- start: round5(o.span.start),
4908
- duration: round5(o.span.duration)
4941
+ start: round6(o.span.start),
4942
+ duration: round6(o.span.duration)
4909
4943
  }
4910
4944
  } : {},
4911
- x: round5(o.transform3d.x),
4912
- y: round5(o.transform3d.y),
4913
- z: round5(o.transform3d.z),
4914
- rx: round5(o.transform3d.rx),
4915
- ry: round5(o.transform3d.ry),
4916
- rz: round5(o.transform3d.rz),
4917
- scale: round5(o.transform3d.scale || OBJECT_DEFAULT_SCALE),
4945
+ x: round6(o.transform3d.x),
4946
+ y: round6(o.transform3d.y),
4947
+ z: round6(o.transform3d.z),
4948
+ rx: round6(o.transform3d.rx),
4949
+ ry: round6(o.transform3d.ry),
4950
+ rz: round6(o.transform3d.rz),
4951
+ scale: round6(o.transform3d.scale || OBJECT_DEFAULT_SCALE),
4918
4952
  anim: idleKey(o),
4919
4953
  // Pose keyframes: clip-local [x,y,z,rx,ry,rz,scale] track,
4920
4954
  // sampled at t − span.start. Omitted when absent — parity.
@@ -4941,12 +4975,12 @@ function studioLayerData(layers, duration) {
4941
4975
  const base = {
4942
4976
  id: o.id,
4943
4977
  kind: o.kind,
4944
- start: round5(o.start),
4945
- dur: round5(Math.max(OVERLAY_MIN_DURATION, o.duration)),
4946
- x: round5(o.transform.x),
4947
- y: round5(o.transform.y),
4948
- scale: round5(o.transform.scale || 1),
4949
- rot: round5(o.transform.rotation || 0),
4978
+ start: round6(o.start),
4979
+ dur: round6(Math.max(OVERLAY_MIN_DURATION, o.duration)),
4980
+ x: round6(o.transform.x),
4981
+ y: round6(o.transform.y),
4982
+ scale: round6(o.transform.scale || 1),
4983
+ rot: round6(o.transform.rotation || 0),
4950
4984
  enter: enterKey(o),
4951
4985
  exit: exitKey(o),
4952
4986
  // Pose keyframes: a CLIP-LOCAL [x, y, scale, rot, opacity]
@@ -4967,7 +5001,7 @@ function studioLayerData(layers, duration) {
4967
5001
  return {
4968
5002
  ...base,
4969
5003
  key: o.key,
4970
- w: round5(o.width ?? OVERLAY_MEDIA_DEFAULT_WIDTH),
5004
+ w: round6(o.width ?? OVERLAY_MEDIA_DEFAULT_WIDTH),
4971
5005
  radius: o.radius ?? OVERLAY_MEDIA_DEFAULT_RADIUS,
4972
5006
  opacity: o.opacity ?? 1,
4973
5007
  loop: !!o.loop,
@@ -4977,7 +5011,7 @@ function studioLayerData(layers, duration) {
4977
5011
  ...o.shadow ? { shadow: o.shadow } : {},
4978
5012
  ...o.border && o.border.width > 0 ? {
4979
5013
  border: {
4980
- width: round5(o.border.width),
5014
+ width: round6(o.border.width),
4981
5015
  color: o.border.color
4982
5016
  }
4983
5017
  } : {}
@@ -4997,11 +5031,11 @@ function studioLayerData(layers, duration) {
4997
5031
  // Style-v2 fields bake only when non-default (byte parity for
4998
5032
  // older docs); ON_FRAME reads them unconditionally.
4999
5033
  ...st.fontStyle === "italic" ? { sty: "italic" } : {},
5000
- ...o.maxWidth ? { mw: round5(o.maxWidth) } : {},
5001
- ...st.letterSpacing ? { ls: round5(st.letterSpacing) } : {},
5002
- ...st.lineHeight !== OVERLAY_LINE_HEIGHT ? { lh: round5(st.lineHeight) } : {},
5034
+ ...o.maxWidth ? { mw: round6(o.maxWidth) } : {},
5035
+ ...st.letterSpacing ? { ls: round6(st.letterSpacing) } : {},
5036
+ ...st.lineHeight !== OVERLAY_LINE_HEIGHT ? { lh: round6(st.lineHeight) } : {},
5003
5037
  ...st.align !== "center" ? { align: st.align } : {},
5004
- ...st.stroke ? { stroke: { c: st.stroke.color, w: round5(st.stroke.width) } } : {},
5038
+ ...st.stroke ? { stroke: { c: st.stroke.color, w: round6(st.stroke.width) } } : {},
5005
5039
  // Hosted face behind an override: ON_FRAME lazy-loads it so a
5006
5040
  // live family/weight edit paints without a LOAD (SETUP only
5007
5041
  // runs on cold load).
@@ -5014,10 +5048,10 @@ function studioLayerData(layers, duration) {
5014
5048
  ...bx ? {
5015
5049
  box: {
5016
5050
  c: bx.color,
5017
- o: round5(bx.opacity),
5018
- px: round5(bx.padX),
5019
- py: round5(bx.padY),
5020
- r: round5(bx.radius)
5051
+ o: round6(bx.opacity),
5052
+ px: round6(bx.padX),
5053
+ py: round6(bx.padY),
5054
+ r: round6(bx.radius)
5021
5055
  }
5022
5056
  } : {},
5023
5057
  // Entrance animation: segmentation + timing normalized
@@ -5092,16 +5126,16 @@ function lowerToComposition(input) {
5092
5126
  // Rated segments (speed spans pre-intersected): ON_FRAME's mapTime and the
5093
5127
  // export's audio splice read the rate straight off each segment.
5094
5128
  segments: rated.map((seg) => ({
5095
- in: round5(seg.in),
5096
- out: round5(seg.out),
5097
- ...seg.rate !== void 0 && seg.rate !== 1 ? { rate: round5(seg.rate) } : {}
5129
+ in: round6(seg.in),
5130
+ out: round6(seg.out),
5131
+ ...seg.rate !== void 0 && seg.rate !== 1 ? { rate: round6(seg.rate) } : {}
5098
5132
  })),
5099
5133
  frame: doc.frame,
5100
5134
  micGain: doc.micGain ?? 1,
5101
5135
  cursor: smoothed.map((p) => ({
5102
- t: round5(p.t),
5103
- x: round5(p.x),
5104
- y: round5(p.y)
5136
+ t: round6(p.t),
5137
+ x: round6(p.x),
5138
+ y: round6(p.y)
5105
5139
  })),
5106
5140
  cursorStyle: doc.cursor,
5107
5141
  cursorSpace: { w: doc.source.meta.width, h: doc.source.meta.height },
@@ -5186,7 +5220,7 @@ function clickFxData(doc, rated) {
5186
5220
  }
5187
5221
  };
5188
5222
  }
5189
- function round5(v) {
5223
+ function round6(v) {
5190
5224
  return Math.round(v * 1e3) / 1e3;
5191
5225
  }
5192
5226
 
@@ -5276,8 +5310,8 @@ function planAutoSpeed(track, opts) {
5276
5310
  accepted.sort((a, b) => a.in - b.in);
5277
5311
  return accepted.map((c, i) => ({
5278
5312
  id: `s${i}`,
5279
- in: round6(c.in),
5280
- out: round6(c.out),
5313
+ in: round7(c.in),
5314
+ out: round7(c.out),
5281
5315
  rate: clampSpeedRate(c.rate),
5282
5316
  source: "auto"
5283
5317
  }));
@@ -5330,7 +5364,7 @@ function collectRuns(evs, gap, minLen, emit2) {
5330
5364
  }
5331
5365
  flush();
5332
5366
  }
5333
- function round6(v) {
5367
+ function round7(v) {
5334
5368
  return Math.round(v * 1e3) / 1e3;
5335
5369
  }
5336
5370
 
@@ -5507,16 +5541,16 @@ function momentsFromDoc(doc, plan, opts = {}) {
5507
5541
  return {
5508
5542
  id: `m${String(i + 1).padStart(2, "0")}`,
5509
5543
  kind: d.kind,
5510
- source: { in: round7(d.in), out: round7(d.out) },
5511
- output: output ? { in: round7(output.start), out: round7(output.end) } : null,
5512
- at: d.at === null ? null : round7(d.at),
5513
- outputAt: outAt ? round7(outAt.start) : null,
5514
- focus: d.focus ? { cx: round7(d.focus.cx), cy: round7(d.focus.cy) } : null,
5544
+ source: { in: round8(d.in), out: round8(d.out) },
5545
+ output: output ? { in: round8(output.start), out: round8(output.end) } : null,
5546
+ at: d.at === null ? null : round8(d.at),
5547
+ outputAt: outAt ? round8(outAt.start) : null,
5548
+ focus: d.focus ? { cx: round8(d.focus.cx), cy: round8(d.focus.cy) } : null,
5515
5549
  rect: d.rect ? {
5516
- x: round7(d.rect.x),
5517
- y: round7(d.rect.y),
5518
- w: round7(d.rect.w),
5519
- h: round7(d.rect.h)
5550
+ x: round8(d.rect.x),
5551
+ y: round8(d.rect.y),
5552
+ w: round8(d.rect.w),
5553
+ h: round8(d.rect.h)
5520
5554
  } : null,
5521
5555
  ...d.clicks !== void 0 ? { clicks: d.clicks } : {},
5522
5556
  ...d.pings !== void 0 ? { pings: d.pings } : {},
@@ -5536,7 +5570,7 @@ function activityOf(bins, d) {
5536
5570
  sum += bins[i];
5537
5571
  n++;
5538
5572
  }
5539
- return n ? round7(sum / n) : null;
5573
+ return n ? round8(sum / n) : null;
5540
5574
  }
5541
5575
  function saidOver(transcript, d) {
5542
5576
  if (!transcript?.length) return null;
@@ -5545,7 +5579,7 @@ function saidOver(transcript, d) {
5545
5579
  const text = transcript.filter((s) => s.start < hi && s.end > lo).map((s) => s.text.trim()).filter(Boolean).join(" ");
5546
5580
  return text || null;
5547
5581
  }
5548
- function round7(v) {
5582
+ function round8(v) {
5549
5583
  return Math.round(v * 1e3) / 1e3;
5550
5584
  }
5551
5585
 
@@ -5833,12 +5867,14 @@ function clipsFrom(doc, from) {
5833
5867
  }
5834
5868
  function dropTemplate(doc, from) {
5835
5869
  const has = clipsFrom(doc, from);
5836
- if (!has.overlays.length && !has.objects.length && !has.audio.length)
5870
+ const freezes = (doc.freeze ?? []).some((f) => f.from === from);
5871
+ if (!has.overlays.length && !has.objects.length && !has.audio.length && !freezes)
5837
5872
  return doc;
5838
5873
  const out = { ...doc };
5839
5874
  if (doc.overlays) out.overlays = doc.overlays.filter((o) => o.from !== from);
5840
5875
  if (doc.objects) out.objects = doc.objects.filter((o) => o.from !== from);
5841
5876
  out.audio = doc.audio.filter((a) => a.from !== from);
5877
+ if (doc.freeze) out.freeze = doc.freeze.filter((f) => f.from !== from);
5842
5878
  return out;
5843
5879
  }
5844
5880
  function applyTemplate(template, take, opts) {
@@ -5846,8 +5882,22 @@ function applyTemplate(template, take, opts) {
5846
5882
  const notes = [];
5847
5883
  const base = dropTemplate(take, from);
5848
5884
  const doc = structuredClone(base);
5885
+ const tail = trailingFreeze(template);
5886
+ const lastSeg = doc.segments.at(-1);
5887
+ if (tail && opts.at === "end" && lastSeg) {
5888
+ const own2 = (doc.freeze ?? []).filter(
5889
+ (f) => Math.abs(f.at - lastSeg.out) > 1e-9
5890
+ );
5891
+ const taken = new Set(own2.map((f) => f.id));
5892
+ let n = 0;
5893
+ while (taken.has(`f${n}`)) n++;
5894
+ doc.freeze = [
5895
+ ...own2,
5896
+ { id: `f${n}`, at: lastSeg.out, seconds: tail.seconds, from }
5897
+ ].sort((a, b) => a.at - b.at);
5898
+ }
5849
5899
  const templateEnd = totalDuration4(ratedSegments(template));
5850
- const takeEnd = totalDuration4(ratedSegments(take));
5900
+ const takeEnd = totalDuration4(ratedSegments(doc));
5851
5901
  const shift = shiftBy(opts.at, templateEnd, takeEnd);
5852
5902
  const own = new Map((take.overlays ?? []).map((o) => [o.id, o]));
5853
5903
  const overlays = (doc.overlays ?? []).filter(
@@ -6011,8 +6061,8 @@ function buildDigest(input) {
6011
6061
  return {
6012
6062
  digestVersion: DIGEST_VERSION,
6013
6063
  take: {
6014
- sourceDuration: round8(meta.durationMs / 1e3),
6015
- outputDuration: round8(input.outputDuration),
6064
+ sourceDuration: round9(meta.durationMs / 1e3),
6065
+ outputDuration: round9(input.outputDuration),
6016
6066
  width: meta.width,
6017
6067
  height: meta.height,
6018
6068
  captureWidth: meta.captureWidth ?? null,
@@ -6061,7 +6111,7 @@ function buildDigest(input) {
6061
6111
  function outputDurationOf(doc) {
6062
6112
  return docOutputDuration(doc);
6063
6113
  }
6064
- function round8(v) {
6114
+ function round9(v) {
6065
6115
  return Math.round(v * 1e3) / 1e3;
6066
6116
  }
6067
6117
 
@@ -6087,20 +6137,20 @@ function setSpeedInRange(spans, srcIn, srcOut, rate) {
6087
6137
  continue;
6088
6138
  }
6089
6139
  if (srcIn - s.in >= MIN_REMAINDER)
6090
- kept.push({ ...s, out: round9(srcIn), source: "manual" });
6140
+ kept.push({ ...s, out: round10(srcIn), source: "manual" });
6091
6141
  if (s.out - srcOut >= MIN_REMAINDER)
6092
6142
  kept.push({
6093
6143
  ...s,
6094
6144
  id: mintId(used),
6095
- in: round9(srcOut),
6145
+ in: round10(srcOut),
6096
6146
  source: "manual"
6097
6147
  });
6098
6148
  }
6099
6149
  if (rate != null)
6100
6150
  kept.push({
6101
6151
  id: mintId(used),
6102
- in: round9(srcIn),
6103
- out: round9(srcOut),
6152
+ in: round10(srcIn),
6153
+ out: round10(srcOut),
6104
6154
  rate: clampSpeedRate(rate),
6105
6155
  source: "manual"
6106
6156
  });
@@ -6116,9 +6166,9 @@ function removeSourceRange(segments, srcIn, srcOut) {
6116
6166
  next.push(seg);
6117
6167
  continue;
6118
6168
  }
6119
- if (srcIn - seg.in >= MIN_SEGMENT) next.push({ ...seg, out: round9(srcIn) });
6169
+ if (srcIn - seg.in >= MIN_SEGMENT) next.push({ ...seg, out: round10(srcIn) });
6120
6170
  if (seg.out - srcOut >= MIN_SEGMENT)
6121
- next.push({ ...seg, in: round9(srcOut) });
6171
+ next.push({ ...seg, in: round10(srcOut) });
6122
6172
  }
6123
6173
  return next.length ? next : [...segments];
6124
6174
  }
@@ -6136,8 +6186,8 @@ function zoomSpanForRange(zoom, srcIn, srcOut) {
6136
6186
  while (used.has(`u${n}`)) n++;
6137
6187
  return {
6138
6188
  id: `u${n}`,
6139
- in: round9(start),
6140
- out: round9(end),
6189
+ in: round10(start),
6190
+ out: round10(end),
6141
6191
  level: DEFAULT_ZOOM_LEVEL,
6142
6192
  cx: 0.5,
6143
6193
  cy: 0.5,
@@ -6151,7 +6201,7 @@ function mintId(used) {
6151
6201
  used.add(id);
6152
6202
  return id;
6153
6203
  }
6154
- function round9(v) {
6204
+ function round10(v) {
6155
6205
  return Math.round(v * 1e3) / 1e3;
6156
6206
  }
6157
6207
 
@@ -6822,8 +6872,8 @@ var videoLane = {
6822
6872
  return segments.map((s, i) => ({
6823
6873
  id: `seg-${i}`,
6824
6874
  kind: "clip",
6825
- t: round10(starts[i]),
6826
- duration: round10(
6875
+ t: round11(starts[i]),
6876
+ duration: round11(
6827
6877
  outputLen(s, speeds, freezes, i === 0 || !contiguous(segments, i))
6828
6878
  )
6829
6879
  }));
@@ -6906,7 +6956,7 @@ var videoLane = {
6906
6956
  if (local >= m.t - 1e-9 && local <= m.t + m.duration + 1e-9)
6907
6957
  return null;
6908
6958
  }
6909
- const sourceT = round10(
6959
+ const sourceT = round11(
6910
6960
  mapTime2(segmentPieces(s, speeds, freezes, leading), local)
6911
6961
  );
6912
6962
  if (sourceT - s.in < 0.05 || s.out - sourceT < 0.05) return null;
@@ -6947,7 +6997,7 @@ var videoLane = {
6947
6997
  n === 1 || !contiguous(segments, n - 1)
6948
6998
  )
6949
6999
  ] : []
6950
- ].map(round10);
7000
+ ].map(round11);
6951
7001
  }
6952
7002
  };
6953
7003
  function nextFreezeId(doc) {
@@ -6974,8 +7024,8 @@ var freezeLane = {
6974
7024
  {
6975
7025
  id: f.id,
6976
7026
  kind: "clip",
6977
- t: round10(m.t),
6978
- duration: round10(m.duration),
7027
+ t: round11(m.t),
7028
+ duration: round11(m.duration),
6979
7029
  label: `${trimZeros(f.seconds)}s`
6980
7030
  }
6981
7031
  ] : [];
@@ -6997,7 +7047,7 @@ var freezeLane = {
6997
7047
  if (g.t >= m.t - 1e-9 && g.t <= m.t + m.duration + 1e-9) return null;
6998
7048
  }
6999
7049
  if (g.t > totalDuration6(rated) + 1e-9) return null;
7000
- const srcT = round10(mapTime2(rated, Math.max(0, g.t)));
7050
+ const srcT = round11(mapTime2(rated, Math.max(0, g.t)));
7001
7051
  if (!kept(srcT) || !clear(srcT)) return null;
7002
7052
  const id = nextFreezeId(doc);
7003
7053
  return (d) => {
@@ -7033,7 +7083,7 @@ var freezeLane = {
7033
7083
  const marks = freezeOutputExtents(splitBySpeed3(segs, speeds), freezes);
7034
7084
  const mine = marks.get(f.id);
7035
7085
  const endOut = mine ? mine.t + mine.duration : null;
7036
- const srcT = round10(
7086
+ const srcT = round11(
7037
7087
  Math.min(sourceDuration, mapTime2(base, Math.max(0, g.t)))
7038
7088
  );
7039
7089
  if (!kept(srcT) || !clear(srcT, f.id)) return null;
@@ -7093,8 +7143,8 @@ var zoomLane = {
7093
7143
  {
7094
7144
  id: z.id,
7095
7145
  kind: "clip",
7096
- t: round10(ext.start),
7097
- duration: round10(ext.end - ext.start),
7146
+ t: round11(ext.start),
7147
+ duration: round11(ext.end - ext.start),
7098
7148
  label: `${z.level.toFixed(2)}\xD7`
7099
7149
  }
7100
7150
  ];
@@ -7118,8 +7168,8 @@ var zoomLane = {
7118
7168
  ...d.zoom,
7119
7169
  {
7120
7170
  id,
7121
- in: round10(srcT),
7122
- out: round10(srcT + len),
7171
+ in: round11(srcT),
7172
+ out: round11(srcT + len),
7123
7173
  level: DEFAULT_ZOOM_LEVEL,
7124
7174
  cx,
7125
7175
  cy,
@@ -7150,8 +7200,8 @@ var zoomLane = {
7150
7200
  return (d) => {
7151
7201
  const z = d.zoom.find((x) => x.id === g.id);
7152
7202
  if (!z) return;
7153
- z.in = round10(newIn);
7154
- z.out = round10(newIn + len);
7203
+ z.in = round11(newIn);
7204
+ z.out = round11(newIn + len);
7155
7205
  z.source = "manual";
7156
7206
  d.zoom.sort((a, b) => a.in - b.in);
7157
7207
  };
@@ -7180,8 +7230,8 @@ var zoomLane = {
7180
7230
  return (d) => {
7181
7231
  const z = d.zoom.find((x) => x.id === g.id);
7182
7232
  if (!z) return;
7183
- z.in = round10(next.in);
7184
- z.out = round10(next.out);
7233
+ z.in = round11(next.in);
7234
+ z.out = round11(next.out);
7185
7235
  z.source = "manual";
7186
7236
  };
7187
7237
  }
@@ -7203,8 +7253,8 @@ function cursorFocusAt(doc, srcT) {
7203
7253
  if (Math.abs(e.t - ms) < Math.abs(best.t - ms)) best = e;
7204
7254
  const clamp015 = (v) => Math.max(0, Math.min(1, v));
7205
7255
  return {
7206
- cx: round10(clamp015(best.x / width)),
7207
- cy: round10(clamp015(best.y / height))
7256
+ cx: round11(clamp015(best.x / width)),
7257
+ cy: round11(clamp015(best.y / height))
7208
7258
  };
7209
7259
  }
7210
7260
  var tiltLane = {
@@ -7218,8 +7268,8 @@ var tiltLane = {
7218
7268
  {
7219
7269
  id: z.id,
7220
7270
  kind: "clip",
7221
- t: round10(ext.start),
7222
- duration: round10(ext.end - ext.start),
7271
+ t: round11(ext.start),
7272
+ duration: round11(ext.end - ext.start),
7223
7273
  label: `${formatDeg(z.rx)}\xB0/${formatDeg(z.ry)}\xB0`
7224
7274
  }
7225
7275
  ];
@@ -7242,8 +7292,8 @@ var tiltLane = {
7242
7292
  ...d.tilt ?? [],
7243
7293
  {
7244
7294
  id,
7245
- in: round10(srcT),
7246
- out: round10(srcT + len),
7295
+ in: round11(srcT),
7296
+ out: round11(srcT + len),
7247
7297
  rx: DEFAULT_TILT_POSE.rx,
7248
7298
  ry: DEFAULT_TILT_POSE.ry,
7249
7299
  source: "manual"
@@ -7273,8 +7323,8 @@ var tiltLane = {
7273
7323
  return (d) => {
7274
7324
  const z = (d.tilt ?? []).find((x) => x.id === g.id);
7275
7325
  if (!z || !d.tilt) return;
7276
- z.in = round10(newIn);
7277
- z.out = round10(newIn + len);
7326
+ z.in = round11(newIn);
7327
+ z.out = round11(newIn + len);
7278
7328
  z.source = "manual";
7279
7329
  d.tilt.sort((a, b) => a.in - b.in);
7280
7330
  };
@@ -7303,8 +7353,8 @@ var tiltLane = {
7303
7353
  return (d) => {
7304
7354
  const z = (d.tilt ?? []).find((x) => x.id === g.id);
7305
7355
  if (!z) return;
7306
- z.in = round10(next.in);
7307
- z.out = round10(next.out);
7356
+ z.in = round11(next.in);
7357
+ z.out = round11(next.out);
7308
7358
  z.source = "manual";
7309
7359
  };
7310
7360
  }
@@ -7331,8 +7381,8 @@ var camMoveLane = {
7331
7381
  {
7332
7382
  id: z.id,
7333
7383
  kind: "clip",
7334
- t: round10(ext.start),
7335
- duration: round10(ext.end - ext.start),
7384
+ t: round11(ext.start),
7385
+ duration: round11(ext.end - ext.start),
7336
7386
  label: z.size != null ? `${Math.round(z.size * 100)}%` : "Move"
7337
7387
  }
7338
7388
  ];
@@ -7356,8 +7406,8 @@ var camMoveLane = {
7356
7406
  ...d.camMotion ?? [],
7357
7407
  {
7358
7408
  id,
7359
- in: round10(srcT),
7360
- out: round10(srcT + len),
7409
+ in: round11(srcT),
7410
+ out: round11(srcT + len),
7361
7411
  ...DEFAULT_CAM_POSE,
7362
7412
  source: "manual"
7363
7413
  }
@@ -7386,8 +7436,8 @@ var camMoveLane = {
7386
7436
  return (d) => {
7387
7437
  const z = (d.camMotion ?? []).find((x) => x.id === g.id);
7388
7438
  if (!z || !d.camMotion) return;
7389
- z.in = round10(newIn);
7390
- z.out = round10(newIn + len);
7439
+ z.in = round11(newIn);
7440
+ z.out = round11(newIn + len);
7391
7441
  z.source = "manual";
7392
7442
  d.camMotion.sort((a, b) => a.in - b.in);
7393
7443
  };
@@ -7416,8 +7466,8 @@ var camMoveLane = {
7416
7466
  return (d) => {
7417
7467
  const z = (d.camMotion ?? []).find((x) => x.id === g.id);
7418
7468
  if (!z) return;
7419
- z.in = round10(next.in);
7420
- z.out = round10(next.out);
7469
+ z.in = round11(next.in);
7470
+ z.out = round11(next.out);
7421
7471
  z.source = "manual";
7422
7472
  };
7423
7473
  }
@@ -7450,10 +7500,10 @@ var camLane = {
7450
7500
  items.push({
7451
7501
  id: `cam-${i}`,
7452
7502
  kind: "clip",
7453
- t: round10(
7503
+ t: round11(
7454
7504
  starts[i] + outputLen({ in: s.in, out: a }, speeds, freezes, leading)
7455
7505
  ),
7456
- duration: round10(
7506
+ duration: round11(
7457
7507
  outputLen({ in: a, out: b }, speeds, freezes, a === s.in && leading)
7458
7508
  )
7459
7509
  });
@@ -7482,7 +7532,7 @@ var camLane = {
7482
7532
  Math.max(first.in, last.out - span)
7483
7533
  );
7484
7534
  return (d) => {
7485
- d.cam.window = { in: round10(newIn), out: round10(newIn + span) };
7535
+ d.cam.window = { in: round11(newIn), out: round11(newIn + span) };
7486
7536
  };
7487
7537
  }
7488
7538
  const overlapping = eff.map((s, i) => ({
@@ -7495,8 +7545,8 @@ var camLane = {
7495
7545
  const next = g.edge === "start" ? { in: Math.min(sourceT, current.out - 0.05), out: current.out } : { in: current.in, out: Math.max(sourceT, current.in + 0.05) };
7496
7546
  return (d) => {
7497
7547
  d.cam.window = {
7498
- in: round10(Math.max(0, next.in)),
7499
- out: round10(Math.min(sourceDuration, next.out))
7548
+ in: round11(Math.max(0, next.in)),
7549
+ out: round11(Math.min(sourceDuration, next.out))
7500
7550
  };
7501
7551
  };
7502
7552
  },
@@ -7516,8 +7566,8 @@ var micLane = {
7516
7566
  return segments.map((s, i) => ({
7517
7567
  id: `mic-${i}`,
7518
7568
  kind: "clip",
7519
- t: round10(starts[i]),
7520
- duration: round10(
7569
+ t: round11(starts[i]),
7570
+ duration: round11(
7521
7571
  outputLen(s, speeds, freezes, i === 0 || !contiguous(segments, i))
7522
7572
  )
7523
7573
  }));
@@ -7573,8 +7623,8 @@ var speedLane = {
7573
7623
  {
7574
7624
  id: sp.id,
7575
7625
  kind: "clip",
7576
- t: round10(start),
7577
- duration: round10(end - start),
7626
+ t: round11(start),
7627
+ duration: round11(end - start),
7578
7628
  label: `${sp.rate}\xD7`
7579
7629
  }
7580
7630
  ];
@@ -7597,8 +7647,8 @@ var speedLane = {
7597
7647
  ...d.speed ?? [],
7598
7648
  {
7599
7649
  id,
7600
- in: round10(srcT),
7601
- out: round10(srcT + len),
7650
+ in: round11(srcT),
7651
+ out: round11(srcT + len),
7602
7652
  rate: DEFAULT_SPEED_RATE,
7603
7653
  source: "manual"
7604
7654
  }
@@ -7628,8 +7678,8 @@ var speedLane = {
7628
7678
  return (d) => {
7629
7679
  const s = d.speed?.find((x) => x.id === g.id);
7630
7680
  if (!s) return;
7631
- s.in = round10(newIn);
7632
- s.out = round10(newIn + len);
7681
+ s.in = round11(newIn);
7682
+ s.out = round11(newIn + len);
7633
7683
  s.source = "manual";
7634
7684
  d.speed.sort((a, b) => a.in - b.in);
7635
7685
  };
@@ -7658,8 +7708,8 @@ var speedLane = {
7658
7708
  return (d) => {
7659
7709
  const s = d.speed?.find((x) => x.id === g.id);
7660
7710
  if (!s) return;
7661
- s.in = round10(next.in);
7662
- s.out = round10(next.out);
7711
+ s.in = round11(next.in);
7712
+ s.out = round11(next.out);
7663
7713
  s.source = "manual";
7664
7714
  };
7665
7715
  }
@@ -7678,8 +7728,8 @@ var audioLane = {
7678
7728
  return doc.audio.map((c) => ({
7679
7729
  id: c.id,
7680
7730
  kind: "clip",
7681
- t: round10(c.start),
7682
- duration: round10(clipLength(c)),
7731
+ t: round11(c.start),
7732
+ duration: round11(clipLength(c)),
7683
7733
  label: c.name
7684
7734
  }));
7685
7735
  },
@@ -7690,7 +7740,7 @@ var audioLane = {
7690
7740
  const start = Math.max(0, g.t);
7691
7741
  return (d) => {
7692
7742
  const c = d.audio.find((x) => x.id === g.id);
7693
- if (c) c.start = round10(start);
7743
+ if (c) c.start = round11(start);
7694
7744
  };
7695
7745
  }
7696
7746
  if (g.type === "resize") {
@@ -7703,8 +7753,8 @@ var audioLane = {
7703
7753
  return (d) => {
7704
7754
  const c = d.audio.find((x) => x.id === g.id);
7705
7755
  if (!c) return;
7706
- c.start = round10(newStart2);
7707
- c.loopLen = round10(end - newStart2);
7756
+ c.start = round11(newStart2);
7757
+ c.loopLen = round11(end - newStart2);
7708
7758
  };
7709
7759
  }
7710
7760
  const delta = g.t - clip.start;
@@ -7713,22 +7763,22 @@ var audioLane = {
7713
7763
  return (d) => {
7714
7764
  const c = d.audio.find((x) => x.id === g.id);
7715
7765
  if (!c) return;
7716
- c.in = round10(newIn);
7717
- c.start = round10(newStart);
7766
+ c.in = round11(newIn);
7767
+ c.start = round11(newStart);
7718
7768
  };
7719
7769
  }
7720
7770
  if (clip.loop) {
7721
7771
  const newLen = Math.max(0.1, g.t - clip.start);
7722
7772
  return (d) => {
7723
7773
  const c = d.audio.find((x) => x.id === g.id);
7724
- if (c) c.loopLen = round10(newLen);
7774
+ if (c) c.loopLen = round11(newLen);
7725
7775
  };
7726
7776
  }
7727
7777
  const span = Math.max(0.05, g.t - clip.start);
7728
7778
  const newOut = Math.min(clip.in + span, clip.duration);
7729
7779
  return (d) => {
7730
7780
  const c = d.audio.find((x) => x.id === g.id);
7731
- if (c) c.out = round10(Math.max(c.in + 0.05, newOut));
7781
+ if (c) c.out = round11(Math.max(c.in + 0.05, newOut));
7732
7782
  };
7733
7783
  }
7734
7784
  if (g.type === "remove") {
@@ -7741,8 +7791,8 @@ var audioLane = {
7741
7791
  },
7742
7792
  magnets(doc) {
7743
7793
  return doc.audio.flatMap((c) => [
7744
- round10(c.start),
7745
- round10(c.start + clipLength(c))
7794
+ round11(c.start),
7795
+ round11(c.start + clipLength(c))
7746
7796
  ]);
7747
7797
  }
7748
7798
  };
@@ -7760,7 +7810,7 @@ function poseItems(clipId, start, duration, motion) {
7760
7810
  return (motion ?? []).map((p, i) => ({
7761
7811
  id: `${clipId}::k${i}`,
7762
7812
  kind: "keyframe",
7763
- t: round10(start + Math.min(Math.max(0, p.at), duration))
7813
+ t: round11(start + Math.min(Math.max(0, p.at), duration))
7764
7814
  }));
7765
7815
  }
7766
7816
  var overlaysLane = {
@@ -7771,8 +7821,8 @@ var overlaysLane = {
7771
7821
  {
7772
7822
  id: o.id,
7773
7823
  kind: "clip",
7774
- t: round10(o.start),
7775
- duration: round10(o.duration),
7824
+ t: round11(o.start),
7825
+ duration: round11(o.duration),
7776
7826
  label: o.kind === "text" ? o.text.split("\n")[0].slice(0, 24) || "Text" : o.kind === "image" ? "Image" : "Video"
7777
7827
  },
7778
7828
  // Pose diamonds render after the clips, so they sit on top.
@@ -7792,7 +7842,7 @@ var overlaysLane = {
7792
7842
  return (d) => {
7793
7843
  const o = (d.overlays ?? []).find((x) => x.id === kf.clipId);
7794
7844
  const p = o?.motion?.[kf.index];
7795
- if (p) p.at = round10(at);
7845
+ if (p) p.at = round11(at);
7796
7846
  };
7797
7847
  }
7798
7848
  if (g.type === "remove") {
@@ -7821,8 +7871,8 @@ var overlaysLane = {
7821
7871
  {
7822
7872
  id,
7823
7873
  kind: "text",
7824
- start: round10(start),
7825
- duration: round10(len),
7874
+ start: round11(start),
7875
+ duration: round11(len),
7826
7876
  text: "Title",
7827
7877
  preset: "title",
7828
7878
  // Frame fractions: centered, lower-third — at any aspect ratio.
@@ -7837,7 +7887,7 @@ var overlaysLane = {
7837
7887
  const start = Math.max(0, g.t);
7838
7888
  return (d) => {
7839
7889
  const o = (d.overlays ?? []).find((x) => x.id === g.id);
7840
- if (o) o.start = round10(start);
7890
+ if (o) o.start = round11(start);
7841
7891
  };
7842
7892
  }
7843
7893
  if (g.type === "resize") {
@@ -7847,14 +7897,14 @@ var overlaysLane = {
7847
7897
  return (d) => {
7848
7898
  const o = (d.overlays ?? []).find((x) => x.id === g.id);
7849
7899
  if (!o) return;
7850
- o.start = round10(newStart);
7851
- o.duration = round10(end - newStart);
7900
+ o.start = round11(newStart);
7901
+ o.duration = round11(end - newStart);
7852
7902
  };
7853
7903
  }
7854
7904
  const newLen = Math.max(OVERLAY_MIN_DURATION, g.t - clip.start);
7855
7905
  return (d) => {
7856
7906
  const o = (d.overlays ?? []).find((x) => x.id === g.id);
7857
- if (o) o.duration = round10(newLen);
7907
+ if (o) o.duration = round11(newLen);
7858
7908
  };
7859
7909
  }
7860
7910
  return (d) => {
@@ -7863,8 +7913,8 @@ var overlaysLane = {
7863
7913
  },
7864
7914
  magnets(doc) {
7865
7915
  return (doc.overlays ?? []).flatMap((o) => [
7866
- round10(o.start),
7867
- round10(o.start + o.duration)
7916
+ round11(o.start),
7917
+ round11(o.start + o.duration)
7868
7918
  ]);
7869
7919
  }
7870
7920
  };
@@ -7877,8 +7927,8 @@ var objectsLane = {
7877
7927
  {
7878
7928
  id: o.id,
7879
7929
  kind: "clip",
7880
- t: round10(o.span?.start ?? 0),
7881
- duration: round10(o.span?.duration ?? outDur),
7930
+ t: round11(o.span?.start ?? 0),
7931
+ duration: round11(o.span?.duration ?? outDur),
7882
7932
  label: o.asset.kind === "primitive" ? o.asset.shape : o.asset.kind === "text3d" ? o.asset.text : "model"
7883
7933
  },
7884
7934
  // Pose diamonds: clip-local `at` from the span start (0 span-less).
@@ -7906,7 +7956,7 @@ var objectsLane = {
7906
7956
  return (d) => {
7907
7957
  const o = (d.objects ?? []).find((x) => x.id === kf.clipId);
7908
7958
  const p = o?.motion?.[kf.index];
7909
- if (p) p.at = round10(at);
7959
+ if (p) p.at = round11(at);
7910
7960
  };
7911
7961
  }
7912
7962
  if (g.type === "remove") {
@@ -7927,7 +7977,7 @@ var objectsLane = {
7927
7977
  const start = Math.max(0, g.t);
7928
7978
  return (d) => {
7929
7979
  const o = (d.objects ?? []).find((x) => x.id === g.id);
7930
- if (o?.span) o.span.start = round10(start);
7980
+ if (o?.span) o.span.start = round11(start);
7931
7981
  };
7932
7982
  }
7933
7983
  if (g.type === "resize") {
@@ -7938,14 +7988,14 @@ var objectsLane = {
7938
7988
  return (d) => {
7939
7989
  const o = (d.objects ?? []).find((x) => x.id === g.id);
7940
7990
  if (!o?.span) return;
7941
- o.span.start = round10(newStart);
7942
- o.span.duration = round10(end - newStart);
7991
+ o.span.start = round11(newStart);
7992
+ o.span.duration = round11(end - newStart);
7943
7993
  };
7944
7994
  }
7945
7995
  const newLen = Math.max(OVERLAY_MIN_DURATION, g.t - clip.span.start);
7946
7996
  return (d) => {
7947
7997
  const o = (d.objects ?? []).find((x) => x.id === g.id);
7948
- if (o?.span) o.span.duration = round10(newLen);
7998
+ if (o?.span) o.span.duration = round11(newLen);
7949
7999
  };
7950
8000
  }
7951
8001
  return (d) => {
@@ -7954,7 +8004,7 @@ var objectsLane = {
7954
8004
  },
7955
8005
  magnets(doc) {
7956
8006
  return (doc.objects ?? []).flatMap(
7957
- (o) => o.span ? [round10(o.span.start), round10(o.span.start + o.span.duration)] : []
8007
+ (o) => o.span ? [round11(o.span.start), round11(o.span.start + o.span.duration)] : []
7958
8008
  );
7959
8009
  }
7960
8010
  };
@@ -7981,7 +8031,7 @@ function nextSpeedId(doc) {
7981
8031
  function segIndex(id) {
7982
8032
  return Number(id.replace("seg-", ""));
7983
8033
  }
7984
- function round10(v) {
8034
+ function round11(v) {
7985
8035
  return Math.round(v * 1e3) / 1e3;
7986
8036
  }
7987
8037