@vosjs/studio-core 0.30.3 → 0.31.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
@@ -665,6 +665,527 @@ function projectFromArtifact(artifact, videoUrl, opts = {}) {
665
665
  return { doc, videoUrl };
666
666
  }
667
667
 
668
+ // src/ingest/traces.ts
669
+ var CLICK_HOLD_MS = 40;
670
+ function traceKindOf(name) {
671
+ const m = /\.([a-z0-9]+)$/i.exec(name);
672
+ const ext = m ? m[1].toLowerCase() : "";
673
+ if (ext === "zip") return "zip";
674
+ if (ext === "csv") return "csv";
675
+ if (ext === "jsonl" || ext === "ndjson") return "jsonl";
676
+ if (ext === "json") return "json";
677
+ if (ext === "trace") return "trace";
678
+ return null;
679
+ }
680
+ function cursorFromTrace(name, text, opts = {}) {
681
+ const kind = traceKindOf(name);
682
+ if (kind === "csv") return cursorFromCsv(text, opts);
683
+ if (kind === "trace" || looksLikePlaywright(text))
684
+ return cursorFromPlaywrightTrace(text, opts);
685
+ return cursorFromRecords(text, opts);
686
+ }
687
+ function looksLikePlaywright(text) {
688
+ const head = text.slice(0, 4e3);
689
+ return head.includes('"type":"context-options"') || head.includes('"type":"before"') && head.includes('"callId"');
690
+ }
691
+ function sortEvents(events) {
692
+ return events.map((e, i) => ({ e, i })).sort((a, b) => a.e.t - b.e.t || a.i - b.i).map((x) => x.e);
693
+ }
694
+ function round(n) {
695
+ return Math.round(n * 1e3) / 1e3;
696
+ }
697
+ var PW_PRESS = /* @__PURE__ */ new Set([
698
+ "click",
699
+ "dblclick",
700
+ "tap",
701
+ "check",
702
+ "uncheck",
703
+ "setChecked",
704
+ "selectOption"
705
+ ]);
706
+ var PW_TYPE = /* @__PURE__ */ new Set(["fill", "type", "press", "pressSequentially"]);
707
+ function cursorFromPlaywrightTrace(text, opts = {}) {
708
+ const befores = [];
709
+ const afters = /* @__PURE__ */ new Map();
710
+ const inputs = /* @__PURE__ */ new Map();
711
+ let viewport;
712
+ let wallTime;
713
+ let monotonicAtOptions;
714
+ for (const line of text.split("\n")) {
715
+ if (!line.trim()) continue;
716
+ let ev;
717
+ try {
718
+ ev = JSON.parse(line);
719
+ } catch {
720
+ continue;
721
+ }
722
+ const type = ev.type;
723
+ if (type === "context-options") {
724
+ const o = ev.options;
725
+ const v = o?.viewport;
726
+ if (v && typeof v.width === "number" && typeof v.height === "number")
727
+ viewport = { width: v.width, height: v.height };
728
+ if (typeof ev.wallTime === "number") wallTime = ev.wallTime;
729
+ if (typeof ev.monotonicTime === "number")
730
+ monotonicAtOptions = ev.monotonicTime;
731
+ } else if (type === "before" && typeof ev.startTime === "number") {
732
+ befores.push({
733
+ callId: String(ev.callId),
734
+ startTime: ev.startTime,
735
+ class: typeof ev.class === "string" ? ev.class : void 0,
736
+ method: typeof ev.method === "string" ? ev.method : void 0,
737
+ params: ev.params && typeof ev.params === "object" ? ev.params : void 0
738
+ });
739
+ } else if (type === "after") {
740
+ afters.set(String(ev.callId), {
741
+ endTime: typeof ev.endTime === "number" ? ev.endTime : void 0,
742
+ point: asPoint(ev.point)
743
+ });
744
+ } else if (type === "input") {
745
+ const p = asPoint(ev.point);
746
+ if (p) inputs.set(String(ev.callId), p);
747
+ }
748
+ }
749
+ const notes = [];
750
+ if (befores.length === 0) {
751
+ return {
752
+ events: [],
753
+ notes: ['not a Playwright trace: no "before" events with a startTime'],
754
+ viewport,
755
+ kind: "playwright",
756
+ placed: 0,
757
+ dropped: 0
758
+ };
759
+ }
760
+ const newPage = befores.find((b) => b.method === "newPage");
761
+ const origin = newPage?.startTime ?? monotonicAtOptions ?? befores[0].startTime;
762
+ const offset = opts.offsetMs ?? 0;
763
+ const t0 = wallTime !== void 0 && monotonicAtOptions !== void 0 ? Math.round(wallTime + (origin - monotonicAtOptions) - offset) : void 0;
764
+ const at = (ms) => round(ms - origin + offset);
765
+ const events = [];
766
+ const cur = { pos: null };
767
+ let placed = 0;
768
+ let dropped = 0;
769
+ const move2 = (t, p) => {
770
+ if (!cur.pos || cur.pos.x !== p.x || cur.pos.y !== p.y) {
771
+ events.push({ t, x: p.x, y: p.y, type: "move" });
772
+ }
773
+ cur.pos = p;
774
+ };
775
+ const press = (start, end, p, times = 1) => {
776
+ move2(start, p);
777
+ let downAt = Math.max(start, end - CLICK_HOLD_MS);
778
+ for (let i = 0; i < times; i++) {
779
+ events.push({ t: round(downAt), x: p.x, y: p.y, type: "down", button: 0 });
780
+ events.push({
781
+ t: round(downAt + CLICK_HOLD_MS),
782
+ x: p.x,
783
+ y: p.y,
784
+ type: "up",
785
+ button: 0
786
+ });
787
+ downAt += CLICK_HOLD_MS * 2;
788
+ }
789
+ };
790
+ for (const b of befores) {
791
+ const after = afters.get(b.callId);
792
+ const end = after?.endTime ?? b.startTime;
793
+ const point = after?.point ?? inputs.get(b.callId);
794
+ const method = b.method ?? "";
795
+ const cls = b.class ?? "";
796
+ const start = at(b.startTime);
797
+ const label = `${cls}.${method}`;
798
+ if (cls === "Page" && method === "mouseMove") {
799
+ const p = asPoint(b.params);
800
+ if (!p) {
801
+ notes.push(`${label} at ${start}ms has no x/y`);
802
+ dropped++;
803
+ continue;
804
+ }
805
+ move2(start, p);
806
+ placed++;
807
+ } else if (cls === "Page" && method === "mouseClick") {
808
+ const p = asPoint(b.params) ?? point;
809
+ if (!p) {
810
+ notes.push(`${label} at ${start}ms has no x/y`);
811
+ dropped++;
812
+ continue;
813
+ }
814
+ press(start, at(end), p, clickCount(b.params));
815
+ placed++;
816
+ } else if (cls === "Page" && (method === "mouseDown" || method === "mouseUp")) {
817
+ if (!cur.pos) {
818
+ notes.push(`${label} at ${start}ms before any pointer position`);
819
+ dropped++;
820
+ continue;
821
+ }
822
+ events.push({
823
+ t: start,
824
+ x: cur.pos.x,
825
+ y: cur.pos.y,
826
+ type: method === "mouseDown" ? "down" : "up",
827
+ button: 0
828
+ });
829
+ placed++;
830
+ } else if (cls === "Page" && method === "mouseWheel") {
831
+ if (!cur.pos) {
832
+ notes.push(`${label} at ${start}ms before any pointer position`);
833
+ dropped++;
834
+ continue;
835
+ }
836
+ events.push({ t: start, x: cur.pos.x, y: cur.pos.y, type: "scroll" });
837
+ placed++;
838
+ } else if (cls === "Frame" && method === "hover") {
839
+ if (!point) {
840
+ notes.push(
841
+ `${label} ${selectorOf(b)} at ${start}ms landed nowhere the trace recorded`
842
+ );
843
+ dropped++;
844
+ continue;
845
+ }
846
+ move2(start, point);
847
+ placed++;
848
+ } else if (cls === "Frame" && PW_PRESS.has(method)) {
849
+ if (!point) {
850
+ notes.push(
851
+ `${label} ${selectorOf(b)} at ${start}ms landed nowhere the trace recorded`
852
+ );
853
+ dropped++;
854
+ continue;
855
+ }
856
+ press(start, at(end), point, method === "dblclick" ? 2 : 1);
857
+ placed++;
858
+ } else if (cls === "Frame" && PW_TYPE.has(method)) {
859
+ if (!point && !cur.pos) {
860
+ notes.push(
861
+ `${label} ${selectorOf(b)} at ${start}ms typed into a field the trace never placed`
862
+ );
863
+ dropped++;
864
+ continue;
865
+ }
866
+ const p = point ?? cur.pos;
867
+ events.push({ t: start, x: p.x, y: p.y, type: "key" });
868
+ placed++;
869
+ } else if (cls === "Frame" && method === "dragAndDrop") {
870
+ notes.push(
871
+ `${label} at ${start}ms: a drag has two points the trace records as one; not placed`
872
+ );
873
+ dropped++;
874
+ }
875
+ }
876
+ const negative = events.filter((e) => e.t < 0).length;
877
+ if (negative)
878
+ notes.push(`${negative} event(s) before the video's start were dropped`);
879
+ return {
880
+ events: sortEvents(events.filter((e) => e.t >= 0)),
881
+ notes,
882
+ viewport,
883
+ t0,
884
+ kind: "playwright",
885
+ placed,
886
+ dropped
887
+ };
888
+ }
889
+ function asPoint(v) {
890
+ if (!v || typeof v !== "object") return void 0;
891
+ const o = v;
892
+ return typeof o.x === "number" && typeof o.y === "number" ? { x: o.x, y: o.y } : void 0;
893
+ }
894
+ function clickCount(params) {
895
+ const n = params?.clickCount;
896
+ return typeof n === "number" && n > 1 ? Math.min(3, Math.floor(n)) : 1;
897
+ }
898
+ function selectorOf(b) {
899
+ const s = b.params?.selector;
900
+ return typeof s === "string" ? s : "";
901
+ }
902
+ var CSV_TYPES = /* @__PURE__ */ new Set(["move", "down", "up", "click", "scroll"]);
903
+ function cursorFromCsv(text, opts = {}) {
904
+ const notes = [];
905
+ const events = [];
906
+ const offset = opts.offsetMs ?? 0;
907
+ let placed = 0;
908
+ let dropped = 0;
909
+ const lines = text.split(/\r?\n/);
910
+ let cols = ["t", "x", "y", "type"];
911
+ let start = 0;
912
+ const first = (lines[0] ?? "").split(",").map((c) => c.trim().toLowerCase());
913
+ if (first.length >= 3 && first.some((c) => c !== "" && Number.isNaN(Number(c)))) {
914
+ cols = first;
915
+ start = 1;
916
+ }
917
+ const ti = cols.indexOf("t");
918
+ const xi = cols.indexOf("x");
919
+ const yi = cols.indexOf("y");
920
+ const ki = cols.indexOf("type");
921
+ if (ti < 0 || xi < 0 || yi < 0) {
922
+ return {
923
+ events: [],
924
+ notes: [
925
+ "the CSV needs t, x and y columns (a header row names them; without one the order is t,x,y,type)"
926
+ ],
927
+ kind: "csv",
928
+ placed: 0,
929
+ dropped: 0
930
+ };
931
+ }
932
+ for (let i = start; i < lines.length; i++) {
933
+ const raw = lines[i];
934
+ if (!raw.trim()) continue;
935
+ const cells = raw.split(",").map((c) => c.trim());
936
+ const t = Number(cells[ti]);
937
+ const x = Number(cells[xi]);
938
+ const y = Number(cells[yi]);
939
+ const type = (ki >= 0 ? cells[ki] : "") || "move";
940
+ if (![t, x, y].every(Number.isFinite)) {
941
+ notes.push(`line ${i + 1}: t, x and y must be numbers`);
942
+ dropped++;
943
+ continue;
944
+ }
945
+ if (!CSV_TYPES.has(type)) {
946
+ notes.push(
947
+ `line ${i + 1}: unknown type "${type}" (move, down, up, click, scroll)`
948
+ );
949
+ dropped++;
950
+ continue;
951
+ }
952
+ const at = round(t + offset);
953
+ if (type === "click") {
954
+ events.push({ t: at, x, y, type: "move" });
955
+ events.push({ t: at, x, y, type: "down", button: 0 });
956
+ events.push({ t: round(at + CLICK_HOLD_MS), x, y, type: "up", button: 0 });
957
+ } else if (type === "down" || type === "up") {
958
+ events.push({ t: at, x, y, type, button: 0 });
959
+ } else {
960
+ events.push({ t: at, x, y, type });
961
+ }
962
+ placed++;
963
+ }
964
+ return { events: sortEvents(events), notes, kind: "csv", placed, dropped };
965
+ }
966
+ var RECORD_TYPES = {
967
+ move: "move",
968
+ hover: "move",
969
+ down: "down",
970
+ mousedown: "down",
971
+ up: "up",
972
+ mouseup: "up",
973
+ click: "click",
974
+ press: "click",
975
+ scroll: "scroll",
976
+ wheel: "scroll",
977
+ key: "key",
978
+ type: "key",
979
+ fill: "key",
980
+ focus: "focus"
981
+ };
982
+ function cursorFromRecords(input, opts = {}) {
983
+ const notes = [];
984
+ const events = [];
985
+ const offset = opts.offsetMs ?? 0;
986
+ let placed = 0;
987
+ let dropped = 0;
988
+ let records;
989
+ if (typeof input === "string") {
990
+ const trimmed = input.trim();
991
+ if (trimmed.startsWith("[")) {
992
+ try {
993
+ records = JSON.parse(trimmed);
994
+ } catch {
995
+ return {
996
+ events: [],
997
+ notes: ["not JSON: the records file could not be parsed"],
998
+ kind: "records",
999
+ placed: 0,
1000
+ dropped: 0
1001
+ };
1002
+ }
1003
+ } else {
1004
+ records = [];
1005
+ let n = 0;
1006
+ for (const line of trimmed.split("\n")) {
1007
+ n++;
1008
+ if (!line.trim()) continue;
1009
+ try {
1010
+ records.push(JSON.parse(line));
1011
+ } catch {
1012
+ notes.push(`line ${n}: not JSON`);
1013
+ dropped++;
1014
+ }
1015
+ }
1016
+ }
1017
+ } else records = input;
1018
+ let epochZero = null;
1019
+ let sawNoTime = 0;
1020
+ let sawNoPoint = 0;
1021
+ records.forEach((r, i) => {
1022
+ if (!r || typeof r !== "object") {
1023
+ notes.push(`record ${i + 1}: not an object`);
1024
+ dropped++;
1025
+ return;
1026
+ }
1027
+ const o = r;
1028
+ let t = numberOf(o.t ?? o.ts ?? o.time ?? o.timestamp);
1029
+ if (t !== void 0 && t > 1e12) {
1030
+ if (epochZero === null) epochZero = t;
1031
+ t -= epochZero;
1032
+ }
1033
+ const kind = recordKind(o);
1034
+ if (!kind) return;
1035
+ const p = asPoint(o) ?? asPoint(o.point) ?? centerOf(o.rect);
1036
+ if (t === void 0) {
1037
+ sawNoTime++;
1038
+ dropped++;
1039
+ return;
1040
+ }
1041
+ if (!p) {
1042
+ sawNoPoint++;
1043
+ dropped++;
1044
+ return;
1045
+ }
1046
+ const at = round(t + offset);
1047
+ const rect = asRect(o.rect);
1048
+ if (kind === "click") {
1049
+ events.push({ t: at, x: p.x, y: p.y, type: "move" });
1050
+ events.push({
1051
+ t: at,
1052
+ x: p.x,
1053
+ y: p.y,
1054
+ type: "down",
1055
+ button: 0,
1056
+ ...rect ? { rect } : {}
1057
+ });
1058
+ events.push({
1059
+ t: round(at + CLICK_HOLD_MS),
1060
+ x: p.x,
1061
+ y: p.y,
1062
+ type: "up",
1063
+ button: 0
1064
+ });
1065
+ } else if (kind === "down" || kind === "up") {
1066
+ events.push({
1067
+ t: at,
1068
+ x: p.x,
1069
+ y: p.y,
1070
+ type: kind,
1071
+ button: 0,
1072
+ ...rect ? { rect } : {}
1073
+ });
1074
+ } else {
1075
+ events.push({
1076
+ t: at,
1077
+ x: p.x,
1078
+ y: p.y,
1079
+ type: kind,
1080
+ ...rect ? { rect } : {}
1081
+ });
1082
+ }
1083
+ placed++;
1084
+ });
1085
+ if (sawNoTime)
1086
+ notes.push(
1087
+ `${sawNoTime} gesture record(s) carry no time (t, ts or time, in ms), so they cannot meet the video; agent-browser's own log has none, so stamp one per command when you keep it`
1088
+ );
1089
+ if (sawNoPoint)
1090
+ notes.push(
1091
+ `${sawNoPoint} gesture record(s) carry no point (x/y, point, or rect); a ref alone says which element, not where it was`
1092
+ );
1093
+ return { events: sortEvents(events), notes, kind: "records", placed, dropped };
1094
+ }
1095
+ function recordKind(o) {
1096
+ const explicit = typeof o.type === "string" ? o.type.toLowerCase() : "";
1097
+ if (explicit && explicit in RECORD_TYPES) return RECORD_TYPES[explicit];
1098
+ const cmd = commandWords(o.command ?? o.action ?? o.kind);
1099
+ const verb = cmd[0] === "mouse" ? `${cmd[0]}${cmd[1] ?? ""}` : cmd[0];
1100
+ if (!verb) return null;
1101
+ const key = verb.toLowerCase();
1102
+ if (key in RECORD_TYPES) return RECORD_TYPES[key];
1103
+ if (key === "mousemove") return "move";
1104
+ if (key === "mouseclick") return "click";
1105
+ if (key === "mousedown") return "down";
1106
+ if (key === "mouseup") return "up";
1107
+ if (key === "dblclick") return "click";
1108
+ return null;
1109
+ }
1110
+ function commandWords(v) {
1111
+ if (Array.isArray(v)) return v.map(String);
1112
+ if (typeof v === "string") return v.trim().split(/\s+/);
1113
+ return [];
1114
+ }
1115
+ function numberOf(v) {
1116
+ if (typeof v === "number" && Number.isFinite(v)) return v;
1117
+ if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v)))
1118
+ return Number(v);
1119
+ return void 0;
1120
+ }
1121
+ function asRect(v) {
1122
+ if (!v || typeof v !== "object") return void 0;
1123
+ const o = v;
1124
+ const x = numberOf(o.x);
1125
+ const y = numberOf(o.y);
1126
+ const w = numberOf(o.w ?? o.width);
1127
+ const h = numberOf(o.h ?? o.height);
1128
+ return x !== void 0 && y !== void 0 && w !== void 0 && h !== void 0 ? { x, y, w, h } : void 0;
1129
+ }
1130
+ function centerOf(v) {
1131
+ const r = asRect(v);
1132
+ return r ? { x: r.x + r.w / 2, y: r.y + r.h / 2 } : void 0;
1133
+ }
1134
+
1135
+ // src/ingest/zip.ts
1136
+ var EOCD = 101010256;
1137
+ var CENTRAL = 33639248;
1138
+ var LOCAL = 67324752;
1139
+ function zipEntries(bytes) {
1140
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1141
+ let eocd = -1;
1142
+ for (let i = bytes.length - 22; i >= Math.max(0, bytes.length - 22 - 65535); i--) {
1143
+ if (view.getUint32(i, true) === EOCD) {
1144
+ eocd = i;
1145
+ break;
1146
+ }
1147
+ }
1148
+ if (eocd < 0) throw new Error("not a zip: no end-of-central-directory record");
1149
+ const count = view.getUint16(eocd + 10, true);
1150
+ let p = view.getUint32(eocd + 16, true);
1151
+ const entries = [];
1152
+ const decoder = new TextDecoder();
1153
+ for (let i = 0; i < count; i++) {
1154
+ if (view.getUint32(p, true) !== CENTRAL)
1155
+ throw new Error("not a zip: central directory entry expected");
1156
+ const method = view.getUint16(p + 10, true);
1157
+ const compressedSize = view.getUint32(p + 20, true);
1158
+ const size = view.getUint32(p + 24, true);
1159
+ const nameLen = view.getUint16(p + 28, true);
1160
+ const extraLen = view.getUint16(p + 30, true);
1161
+ const commentLen = view.getUint16(p + 32, true);
1162
+ const local = view.getUint32(p + 42, true);
1163
+ const name = decoder.decode(bytes.subarray(p + 46, p + 46 + nameLen));
1164
+ if (view.getUint32(local, true) !== LOCAL)
1165
+ throw new Error(`not a zip: local header expected for ${name}`);
1166
+ const localNameLen = view.getUint16(local + 26, true);
1167
+ const localExtraLen = view.getUint16(local + 28, true);
1168
+ entries.push({
1169
+ name,
1170
+ method,
1171
+ compressedSize,
1172
+ size,
1173
+ dataOffset: local + 30 + localNameLen + localExtraLen
1174
+ });
1175
+ p += 46 + nameLen + extraLen + commentLen;
1176
+ }
1177
+ return entries;
1178
+ }
1179
+ function zipEntryBytes(bytes, entry) {
1180
+ return bytes.subarray(
1181
+ entry.dataOffset,
1182
+ entry.dataOffset + entry.compressedSize
1183
+ );
1184
+ }
1185
+ function playwrightTraceEntry(entries) {
1186
+ return entries.find((e) => e.name === "trace.trace") ?? entries.find((e) => /(^|\/)trace\.trace$/.test(e.name)) ?? null;
1187
+ }
1188
+
668
1189
  // src/lower/motion.ts
669
1190
  import { sortKeyframes } from "@vosjs/timeline";
670
1191
 
@@ -1752,7 +2273,7 @@ function planAutoZoom(track, options) {
1752
2273
  typingMinLevel = style.typingMinLevel,
1753
2274
  duration
1754
2275
  } = options;
1755
- const within = (spans2) => duration === void 0 ? spans2 : spans2.map((z) => ({ ...z, out: Math.min(z.out, round(duration)) })).filter((z) => z.out - z.in >= 0.3);
2276
+ const within = (spans2) => duration === void 0 ? spans2 : spans2.map((z) => ({ ...z, out: Math.min(z.out, round2(duration)) })).filter((z) => z.out - z.in >= 0.3);
1756
2277
  const grouped = groupTrack(track, {
1757
2278
  width,
1758
2279
  height,
@@ -1764,17 +2285,17 @@ function planAutoZoom(track, options) {
1764
2285
  });
1765
2286
  const { sessions } = grouped;
1766
2287
  const drags = dragsFromTrack(track, width, height);
1767
- const dragPress = new Set(drags.map((d) => round(d.t0)));
1768
- const withoutDrags = (list) => list.map((cl) => cl.filter((c) => !dragPress.has(round(c.t)))).filter((cl) => cl.length > 0);
2288
+ const dragPress = new Set(drags.map((d) => round2(d.t0)));
2289
+ const withoutDrags = (list) => list.map((cl) => cl.filter((c) => !dragPress.has(round2(c.t)))).filter((cl) => cl.length > 0);
1769
2290
  const clusters = withoutDrags(grouped.clusters);
1770
2291
  const surfaces = withoutDrags(grouped.surfaces);
1771
2292
  const dragSpans = drags.map((d, i) => ({
1772
2293
  id: `g${i}`,
1773
- in: round(Math.max(0, d.t0 - lead)),
1774
- out: round(d.t1 + hold),
2294
+ in: round2(Math.max(0, d.t0 - lead)),
2295
+ out: round2(d.t1 + hold),
1775
2296
  level: clampZoomLevel(Math.min(Math.max(dragLevel, 1.1), maxLevel)),
1776
- cx: round(d.nx),
1777
- cy: round(d.ny),
2297
+ cx: round2(d.nx),
2298
+ cy: round2(d.ny),
1778
2299
  focusMode: "auto",
1779
2300
  source: "auto"
1780
2301
  }));
@@ -1851,11 +2372,11 @@ function planAutoZoom(track, options) {
1851
2372
  typing = typing.filter((t) => t.out - t.in >= 0.3);
1852
2373
  const zSpans = clickSpans.filter((s) => !s.dead).sort((a, b) => a.in - b.in).map((s, i) => ({
1853
2374
  id: `z${i}`,
1854
- in: round(s.in),
1855
- out: round(s.out),
2375
+ in: round2(s.in),
2376
+ out: round2(s.out),
1856
2377
  level: clampZoomLevel(s.level),
1857
- cx: round(s.cx),
1858
- cy: round(s.cy),
2378
+ cx: round2(s.cx),
2379
+ cy: round2(s.cy),
1859
2380
  // Follow styles ride the cursor through the span (entry + dead-zone
1860
2381
  // recenters baked at lowering); typing/dwell spans stay fixed-focus.
1861
2382
  ...followByDefault ? { focusMode: "auto" } : {},
@@ -1863,11 +2384,11 @@ function planAutoZoom(track, options) {
1863
2384
  }));
1864
2385
  const kSpans = typing.map((s, i) => ({
1865
2386
  id: `k${i}`,
1866
- in: round(s.in),
1867
- out: round(s.out),
2387
+ in: round2(s.in),
2388
+ out: round2(s.out),
1868
2389
  level: clampZoomLevel(s.level),
1869
- cx: round(s.cx),
1870
- cy: round(s.cy),
2390
+ cx: round2(s.cx),
2391
+ cy: round2(s.cy),
1871
2392
  // Never focusMode:'auto': the dot is parked (and fading) while typing —
1872
2393
  // the FIELD is the anchor, and a follow would be a no-op at best.
1873
2394
  source: "auto"
@@ -1944,12 +2465,12 @@ function dwellSpans(track, width, height, maxLevel, reserved) {
1944
2465
  if (taken.some((z) => spanIn < z.out && spanOut > z.in)) continue;
1945
2466
  const span = {
1946
2467
  id: `d${accepted.length}`,
1947
- in: round(spanIn),
1948
- out: round(spanOut),
2468
+ in: round2(spanIn),
2469
+ out: round2(spanOut),
1949
2470
  // No element rect on moves → point zoom at the planner ceiling.
1950
2471
  level: clampZoomLevel(maxLevel),
1951
- cx: round(clamp012(c.cx)),
1952
- cy: round(clamp012(c.cy)),
2472
+ cx: round2(clamp012(c.cx)),
2473
+ cy: round2(clamp012(c.cy)),
1953
2474
  source: "auto"
1954
2475
  };
1955
2476
  accepted.push(span);
@@ -2018,7 +2539,7 @@ function clamp3(v, lo, hi) {
2018
2539
  function clamp012(v) {
2019
2540
  return clamp3(v, 0, 1);
2020
2541
  }
2021
- function round(v) {
2542
+ function round2(v) {
2022
2543
  return Math.round(v * 1e3) / 1e3;
2023
2544
  }
2024
2545
 
@@ -2858,14 +3379,14 @@ function resolveOverlayFx(clip, clipDuration) {
2858
3379
  const maxTotal = Math.max(dur, clipDuration * 0.9);
2859
3380
  st = Math.min(st, Math.max(0, (maxTotal - dur) / (upperN - 1)));
2860
3381
  }
2861
- const round13 = (v) => Math.round(v * 1e4) / 1e4;
3382
+ const round14 = (v) => Math.round(v * 1e4) / 1e4;
2862
3383
  return {
2863
3384
  k: spec.fx,
2864
3385
  u: unit,
2865
3386
  d: FX_DIR[spec.direction ?? "forward"] ?? 0,
2866
- st: round13(st),
2867
- dur: round13(dur),
2868
- tt: round13(st * (upperN - 1) + dur),
3387
+ st: round14(st),
3388
+ dur: round14(dur),
3389
+ tt: round14(st * (upperN - 1) + dur),
2869
3390
  units,
2870
3391
  n
2871
3392
  };
@@ -3199,10 +3720,10 @@ function followFocusEvents(span, cursor, space, layout, options = {}) {
3199
3720
  camera
3200
3721
  );
3201
3722
  pathEvents.push({
3202
- t: round2(at),
3723
+ t: round4(at),
3203
3724
  path: true,
3204
- cx: round2(f.cx),
3205
- cy: round2(f.cy)
3725
+ cx: round4(f.cx),
3726
+ cy: round4(f.cy)
3206
3727
  });
3207
3728
  if (at >= to) break;
3208
3729
  }
@@ -3226,7 +3747,7 @@ function followFocusEvents(span, cursor, space, layout, options = {}) {
3226
3747
  const target = lookahead > 0 ? sampleAt2(pts, Math.min(p.t + lookahead, span.out)) : p;
3227
3748
  const f = clampFocus(target.nx, target.ny, level, layout, camera);
3228
3749
  if (Math.abs(f.cx - cx) < 1e-3 && Math.abs(f.cy - cy) < 1e-3) continue;
3229
- events.push({ t: round2(p.t), cx: round2(f.cx), cy: round2(f.cy) });
3750
+ events.push({ t: round4(p.t), cx: round4(f.cx), cy: round4(f.cy) });
3230
3751
  cx = f.cx;
3231
3752
  cy = f.cy;
3232
3753
  nextAllowed = p.t + recenter;
@@ -3250,7 +3771,7 @@ function sampleAt2(pts, t) {
3250
3771
  function clamp014(v) {
3251
3772
  return Math.max(0, Math.min(1, v));
3252
3773
  }
3253
- function round2(v) {
3774
+ function round4(v) {
3254
3775
  return Math.round(v * 1e3) / 1e3;
3255
3776
  }
3256
3777
 
@@ -3540,7 +4061,7 @@ function pinPlacement(input) {
3540
4061
  side,
3541
4062
  track: { keyframes: simplify(poses, [W, H, 1, 1, 1]) },
3542
4063
  tip: { keyframes: simplify(tips, [W, H]) },
3543
- clamped: round4(clamped),
4064
+ clamped: round5(clamped),
3544
4065
  box
3545
4066
  };
3546
4067
  }
@@ -3569,13 +4090,13 @@ function simplify(keys, scalePx) {
3569
4090
  }
3570
4091
  }
3571
4092
  const last = keys[keys.length - 1];
3572
- if (out[out.length - 1].t !== round4(last.t)) out.push(roundKey(last));
4093
+ if (out[out.length - 1].t !== round5(last.t)) out.push(roundKey(last));
3573
4094
  return out;
3574
4095
  }
3575
4096
  function roundKey(k) {
3576
- return { t: round4(k.t), value: k.value.map(round42), ease: k.ease };
4097
+ return { t: round5(k.t), value: k.value.map(round42), ease: k.ease };
3577
4098
  }
3578
- function round4(v) {
4099
+ function round5(v) {
3579
4100
  return Math.round(v * 1e3) / 1e3;
3580
4101
  }
3581
4102
  function round42(v) {
@@ -3587,7 +4108,7 @@ var CURSOR_IDLE_HOLD = 1;
3587
4108
  var CURSOR_IDLE_FADE_OUT = 0.35;
3588
4109
  var CURSOR_IDLE_FADE_IN = 0.18;
3589
4110
  var CURSOR_IDLE_EPS_FRAC = 25e-4;
3590
- var round5 = (n) => Math.round(n * 1e3) / 1e3;
4111
+ var round6 = (n) => Math.round(n * 1e3) / 1e3;
3591
4112
  function cursorIdleFade(track, o) {
3592
4113
  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 }));
3593
4114
  if (moves.length === 0) return [];
@@ -3639,7 +4160,7 @@ function emit(keys, s, e) {
3639
4160
  push(keys, e + CURSOR_IDLE_FADE_IN, 1);
3640
4161
  }
3641
4162
  function push(keys, t, a) {
3642
- const rt = round5(t);
4163
+ const rt = round6(t);
3643
4164
  if (keys.length > 0 && rt <= keys[keys.length - 1].t) {
3644
4165
  keys[keys.length - 1].a = a;
3645
4166
  return;
@@ -5147,7 +5668,7 @@ function gatedRect(rect, x, y, space) {
5147
5668
  return null;
5148
5669
  const m = RECT_CONTAIN_MARGIN;
5149
5670
  const inside = x >= rect.x - m && x <= rect.x + rect.w + m && y >= rect.y - m && y <= rect.y + rect.h + m;
5150
- return inside ? [round6(rect.x), round6(rect.y), round6(rect.w), round6(rect.h)] : null;
5671
+ return inside ? [round7(rect.x), round7(rect.y), round7(rect.w), round7(rect.h)] : null;
5151
5672
  }
5152
5673
  function extractClicks(track, segments, opts) {
5153
5674
  const out = [];
@@ -5172,11 +5693,11 @@ function extractClicks(track, segments, opts) {
5172
5693
  const upSrc = st + Math.max(pressLen, 0.02);
5173
5694
  const up = keptOutputEnd(segments, st, upSrc, opts.media) ?? ot + CLICK_SYNTH_RELEASE;
5174
5695
  const click = {
5175
- ot: round6(ot),
5176
- up: round6(Math.max(up, ot + 0.02)),
5177
- st: round6(st),
5178
- x: round6(e.x),
5179
- y: round6(e.y),
5696
+ ot: round7(ot),
5697
+ up: round7(Math.max(up, ot + 0.02)),
5698
+ st: round7(st),
5699
+ x: round7(e.x),
5700
+ y: round7(e.y),
5180
5701
  b: button
5181
5702
  };
5182
5703
  if (opts.rects && e.rect) {
@@ -5195,7 +5716,7 @@ function hexToRgbTriplet(hex) {
5195
5716
  const n = parseInt(h, 16);
5196
5717
  return [n >> 16, n >> 8 & 255, n & 255];
5197
5718
  }
5198
- function round6(v) {
5719
+ function round7(v) {
5199
5720
  return Math.round(v * 1e3) / 1e3;
5200
5721
  }
5201
5722
 
@@ -5278,7 +5799,7 @@ function trackEmitter() {
5278
5799
  if (tt <= prev.t + 1e-6 && sameVec(prev.value, value)) return prev.t;
5279
5800
  if (tt <= prev.t + 1e-6) tt = prev.t + 1e-3;
5280
5801
  }
5281
- keyframes.push({ t: round7(tt), value: value.map(round7), ease });
5802
+ keyframes.push({ t: round8(tt), value: value.map(round8), ease });
5282
5803
  return tt;
5283
5804
  };
5284
5805
  return { keyframes, push: push2 };
@@ -7206,24 +7727,25 @@ var ON_FRAME = `(ctx, content, dt) => {
7206
7727
  var cpH = 2 * ${Math.abs(CARD_Z)} * Math.tan(${CARD_FOV} * Math.PI / 180 / 2)
7207
7728
  var cpv = null
7208
7729
  if (cpk && cpk.keyframes && cpk.keyframes.length) cpv = TL.sample(cpk, t, TL.lerpArray)
7209
- if ((cpv || trIn) && card.mesh.scale && card.mesh.position) {
7210
- // The pose track, then a boundary's enter on top: a slide is an
7211
- // offset in plane widths and heights, a fade an opacity, a scale both.
7212
- var cpS = (cpv ? cpv[0] : 1) * trSc
7213
- var cpO = (cpv ? cpv[2] : 1) * trOp
7214
- card.mesh.scale.x = cpS; card.mesh.scale.y = cpS
7730
+ // The pose track, then a boundary's enter on top: a slide is an offset
7731
+ // in plane widths and heights, a fade an opacity, a scale both. Outside
7732
+ // both, every term is its rest value.
7733
+ //
7734
+ // EVERY field is written EVERY frame, so the pose is a pure function of
7735
+ // t. A pose restored only when some field looked off-rest kept whatever
7736
+ // the last frame left in the fields it did not look at: a fade moves
7737
+ // opacity alone, so a scrub that jumped out of one mid-window left the
7738
+ // card translucent until something else moved it.
7739
+ var cpS = (cpv ? cpv[0] : 1) * trSc
7740
+ var cpO = (cpv ? cpv[2] : 1) * trOp
7741
+ if (card.mesh.scale) { card.mesh.scale.x = cpS; card.mesh.scale.y = cpS }
7742
+ if (card.mesh.position) {
7215
7743
  card.mesh.position.x = trOx * cpH * (W / H)
7216
7744
  card.mesh.position.y = ((cpv ? cpv[1] : 0) + trOy) * cpH
7217
- if (card.mesh.material) card.mesh.material.opacity = cpO
7218
- // Gone past its clip: the mesh leaves the scene, not just its paint.
7219
- card.mesh.visible = cpO > 0.001
7220
- } 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.position.x !== undefined && card.mesh.position.x !== 0) || card.mesh.visible === false)) {
7221
- card.mesh.scale.x = 1; card.mesh.scale.y = 1
7222
- card.mesh.position.x = 0
7223
- card.mesh.position.y = 0
7224
- if (card.mesh.material) card.mesh.material.opacity = 1
7225
- card.mesh.visible = true
7226
7745
  }
7746
+ if (card.mesh.material) card.mesh.material.opacity = cpO
7747
+ // Gone past its clip: the mesh leaves the scene, not just its paint.
7748
+ card.mesh.visible = cpO > 0.001
7227
7749
  var tilted = rx * rx + ry * ry > 1e-6
7228
7750
  if (card.texture && card.texture.generateMipmaps !== tilted) {
7229
7751
  var THREE2 = ctx.THREE
@@ -7278,14 +7800,14 @@ function studioLayerData(layers, duration, pins) {
7278
7800
  // needs a decoded recording, which a sync lowering can't produce.
7279
7801
  audio: (layers.audio ?? []).map((c) => ({
7280
7802
  key: c.key,
7281
- start: round7(c.start),
7282
- in: round7(c.in),
7283
- out: round7(c.out),
7284
- gain: round7(c.gain),
7803
+ start: round8(c.start),
7804
+ in: round8(c.in),
7805
+ out: round8(c.out),
7806
+ gain: round8(c.gain),
7285
7807
  loop: !!c.loop,
7286
- len: round7(clipLength(c)),
7808
+ len: round8(clipLength(c)),
7287
7809
  duck: !!c.duck,
7288
- env: clipEnvelope(c).map((p) => ({ t: round7(p.t), g: round7(p.g) }))
7810
+ env: clipEnvelope(c).map((p) => ({ t: round8(p.t), g: round8(p.g) }))
7289
7811
  })),
7290
7812
  // Object clips: numbers resolved HERE; shapes are the drafted engine spec.
7291
7813
  ...layers.objects && layers.objects.length ? {
@@ -7298,17 +7820,17 @@ function studioLayerData(layers, duration, pins) {
7298
7820
  } : o.asset.kind === "text3d" ? resolveText3dAsset(o.asset) : { kind: "gltf", key: o.asset.key },
7299
7821
  ...o.span ? {
7300
7822
  span: {
7301
- start: round7(o.span.start),
7302
- duration: round7(o.span.duration)
7823
+ start: round8(o.span.start),
7824
+ duration: round8(o.span.duration)
7303
7825
  }
7304
7826
  } : {},
7305
- x: round7(o.transform3d.x),
7306
- y: round7(o.transform3d.y),
7307
- z: round7(o.transform3d.z),
7308
- rx: round7(o.transform3d.rx),
7309
- ry: round7(o.transform3d.ry),
7310
- rz: round7(o.transform3d.rz),
7311
- scale: round7(o.transform3d.scale || OBJECT_DEFAULT_SCALE),
7827
+ x: round8(o.transform3d.x),
7828
+ y: round8(o.transform3d.y),
7829
+ z: round8(o.transform3d.z),
7830
+ rx: round8(o.transform3d.rx),
7831
+ ry: round8(o.transform3d.ry),
7832
+ rz: round8(o.transform3d.rz),
7833
+ scale: round8(o.transform3d.scale || OBJECT_DEFAULT_SCALE),
7312
7834
  anim: idleKey(o),
7313
7835
  // Pose keyframes: clip-local [x,y,z,rx,ry,rz,scale] track,
7314
7836
  // sampled at t − span.start. Omitted when absent — parity.
@@ -7335,12 +7857,12 @@ function studioLayerData(layers, duration, pins) {
7335
7857
  const base = {
7336
7858
  id: o.id,
7337
7859
  kind: o.kind,
7338
- start: round7(o.start),
7339
- dur: round7(Math.max(OVERLAY_MIN_DURATION, o.duration)),
7340
- x: round7(o.transform.x),
7341
- y: round7(o.transform.y),
7342
- scale: round7(o.transform.scale || 1),
7343
- rot: round7(o.transform.rotation || 0),
7860
+ start: round8(o.start),
7861
+ dur: round8(Math.max(OVERLAY_MIN_DURATION, o.duration)),
7862
+ x: round8(o.transform.x),
7863
+ y: round8(o.transform.y),
7864
+ scale: round8(o.transform.scale || 1),
7865
+ rot: round8(o.transform.rotation || 0),
7344
7866
  enter: enterKey(o),
7345
7867
  exit: exitKey(o),
7346
7868
  // Pose keyframes: a CLIP-LOCAL [x, y, scale, rot, opacity]
@@ -7365,10 +7887,10 @@ function studioLayerData(layers, duration, pins) {
7365
7887
  ...base,
7366
7888
  kind: "image",
7367
7889
  key: hl.key,
7368
- w: round7(hl.width),
7890
+ w: round8(hl.width),
7369
7891
  radius: 0,
7370
7892
  shadow: "none",
7371
- opacity: round7(o.opacity ?? 1),
7893
+ opacity: round8(o.opacity ?? 1),
7372
7894
  html: hl.html
7373
7895
  };
7374
7896
  }
@@ -7385,8 +7907,8 @@ function studioLayerData(layers, duration, pins) {
7385
7907
  ...o.frame.browserBar?.theme ? { theme: o.frame.browserBar.theme } : {}
7386
7908
  },
7387
7909
  lean: [
7388
- round7(o.frame.lean?.rx ?? 0),
7389
- round7(o.frame.lean?.ry ?? 0)
7910
+ round8(o.frame.lean?.rx ?? 0),
7911
+ round8(o.frame.lean?.ry ?? 0)
7390
7912
  ],
7391
7913
  shadow: o.frame.shadow ?? LAYER_CARD_SHADOW,
7392
7914
  shadowContact: o.frame.shadowContact ?? 0,
@@ -7402,17 +7924,17 @@ function studioLayerData(layers, duration, pins) {
7402
7924
  for (const ev of lm.cursor) {
7403
7925
  if (ev.type === "down")
7404
7926
  clicks.push({
7405
- t: round7(ev.t / 1e3),
7406
- x: round7(ev.x),
7407
- y: round7(ev.y)
7927
+ t: round8(ev.t / 1e3),
7928
+ x: round8(ev.x),
7929
+ y: round8(ev.y)
7408
7930
  });
7409
7931
  }
7410
7932
  return {
7411
7933
  cur: {
7412
7934
  pts: sm.map((p) => ({
7413
- t: round7(p.t),
7414
- x: round7(p.x),
7415
- y: round7(p.y)
7935
+ t: round8(p.t),
7936
+ x: round8(p.x),
7937
+ y: round8(p.y)
7416
7938
  })),
7417
7939
  space: { w: lm.meta.width, h: lm.meta.height },
7418
7940
  size: rec.cursor.size,
@@ -7427,7 +7949,7 @@ function studioLayerData(layers, duration, pins) {
7427
7949
  ...lm && lm.crop ? { crop: lm.crop } : {},
7428
7950
  ...lmCard ? { card: lmCard } : {},
7429
7951
  ...lmFacts,
7430
- w: round7(o.width ?? OVERLAY_MEDIA_DEFAULT_WIDTH),
7952
+ w: round8(o.width ?? OVERLAY_MEDIA_DEFAULT_WIDTH),
7431
7953
  radius: o.radius ?? OVERLAY_MEDIA_DEFAULT_RADIUS,
7432
7954
  opacity: o.opacity ?? 1,
7433
7955
  loop: !!o.loop,
@@ -7437,7 +7959,7 @@ function studioLayerData(layers, duration, pins) {
7437
7959
  ...o.shadow ? { shadow: o.shadow } : {},
7438
7960
  ...o.border && o.border.width > 0 ? {
7439
7961
  border: {
7440
- width: round7(o.border.width),
7962
+ width: round8(o.border.width),
7441
7963
  color: o.border.color
7442
7964
  }
7443
7965
  } : {}
@@ -7457,11 +7979,11 @@ function studioLayerData(layers, duration, pins) {
7457
7979
  // Style-v2 fields bake only when non-default (byte parity for
7458
7980
  // older docs); ON_FRAME reads them unconditionally.
7459
7981
  ...st.fontStyle === "italic" ? { sty: "italic" } : {},
7460
- ...o.maxWidth ? { mw: round7(o.maxWidth) } : {},
7461
- ...st.letterSpacing ? { ls: round7(st.letterSpacing) } : {},
7462
- ...st.lineHeight !== OVERLAY_LINE_HEIGHT ? { lh: round7(st.lineHeight) } : {},
7982
+ ...o.maxWidth ? { mw: round8(o.maxWidth) } : {},
7983
+ ...st.letterSpacing ? { ls: round8(st.letterSpacing) } : {},
7984
+ ...st.lineHeight !== OVERLAY_LINE_HEIGHT ? { lh: round8(st.lineHeight) } : {},
7463
7985
  ...st.align !== "center" ? { align: st.align } : {},
7464
- ...st.stroke ? { stroke: { c: st.stroke.color, w: round7(st.stroke.width) } } : {},
7986
+ ...st.stroke ? { stroke: { c: st.stroke.color, w: round8(st.stroke.width) } } : {},
7465
7987
  // Hosted face behind an override: ON_FRAME lazy-loads it so a
7466
7988
  // live family/weight edit paints without a LOAD (SETUP only
7467
7989
  // runs on cold load).
@@ -7474,10 +7996,10 @@ function studioLayerData(layers, duration, pins) {
7474
7996
  ...bx ? {
7475
7997
  box: {
7476
7998
  c: bx.color,
7477
- o: round7(bx.opacity),
7478
- px: round7(bx.padX),
7479
- py: round7(bx.padY),
7480
- r: round7(bx.radius)
7999
+ o: round8(bx.opacity),
8000
+ px: round8(bx.padX),
8001
+ py: round8(bx.padY),
8002
+ r: round8(bx.radius)
7481
8003
  }
7482
8004
  } : {},
7483
8005
  // Entrance animation: segmentation + timing normalized
@@ -7549,17 +8071,17 @@ function lowerToComposition(input) {
7549
8071
  // Rated segments (speed spans pre-intersected): ON_FRAME's mapTime and the
7550
8072
  // export's audio splice read the rate straight off each segment.
7551
8073
  segments: rated.map((seg) => ({
7552
- in: round7(seg.in),
7553
- out: round7(seg.out),
7554
- ...seg.rate !== void 0 && seg.rate !== 1 ? { rate: round7(seg.rate) } : {},
8074
+ in: round8(seg.in),
8075
+ out: round8(seg.out),
8076
+ ...seg.rate !== void 0 && seg.rate !== 1 ? { rate: round8(seg.rate) } : {},
7555
8077
  ...seg.media ? { media: seg.media } : {}
7556
8078
  })),
7557
8079
  frame: doc.frame,
7558
8080
  micGain: doc.micGain ?? 1,
7559
8081
  cursor: smoothed.map((p) => ({
7560
- t: round7(p.t),
7561
- x: round7(p.x),
7562
- y: round7(p.y)
8082
+ t: round8(p.t),
8083
+ x: round8(p.x),
8084
+ y: round8(p.y)
7563
8085
  })),
7564
8086
  cursorStyle: doc.cursor,
7565
8087
  cursorSpace: { w: doc.source.meta.width, h: doc.source.meta.height },
@@ -7582,9 +8104,9 @@ function lowerToComposition(input) {
7582
8104
  crop: m.crop ?? null,
7583
8105
  hasAudio: !!m.meta.hasAudio,
7584
8106
  cursor: sm.map((p) => ({
7585
- t: round7(p.t),
7586
- x: round7(p.x),
7587
- y: round7(p.y)
8107
+ t: round8(p.t),
8108
+ x: round8(p.x),
8109
+ y: round8(p.y)
7588
8110
  })),
7589
8111
  cursorSpace: { w: m.meta.width, h: m.meta.height },
7590
8112
  // The card this media wears (its own placement, bar, corner,
@@ -7766,8 +8288,8 @@ function pinCandidates(doc, clip) {
7766
8288
  step: s.id ?? s.step,
7767
8289
  do: s.do,
7768
8290
  ...s.selector ? { selector: s.selector } : {},
7769
- output: { start: round7(ext.start), end: round7(ext.end) },
7770
- overlap: round7(overlap)
8291
+ output: { start: round8(ext.start), end: round8(ext.end) },
8292
+ overlap: round8(overlap)
7771
8293
  });
7772
8294
  }
7773
8295
  return out.sort((x, y) => y.overlap - x.overlap);
@@ -7798,8 +8320,8 @@ function pinsDataOf(doc, pins) {
7798
8320
  if (!ref) continue;
7799
8321
  out.push({
7800
8322
  id: o.id,
7801
- start: round7(o.start),
7802
- dur: round7(Math.max(OVERLAY_MIN_DURATION, o.duration)),
8323
+ start: round8(o.start),
8324
+ dur: round8(Math.max(OVERLAY_MIN_DURATION, o.duration)),
7803
8325
  x: round43(ref.rect.x),
7804
8326
  y: round43(ref.rect.y),
7805
8327
  w: round43(ref.rect.w),
@@ -7812,8 +8334,8 @@ function pinsDataOf(doc, pins) {
7812
8334
  // leader's end with the pointer through [t0, t1], source seconds.
7813
8335
  ...ref.carry ? {
7814
8336
  carry: {
7815
- t0: round7(ref.carry.t0),
7816
- t1: round7(ref.carry.t1),
8337
+ t0: round8(ref.carry.t0),
8338
+ t1: round8(ref.carry.t1),
7817
8339
  x0: round43(ref.carry.x0),
7818
8340
  y0: round43(ref.carry.y0)
7819
8341
  }
@@ -7860,7 +8382,7 @@ function clickFxData(doc, rated) {
7860
8382
  }
7861
8383
  };
7862
8384
  }
7863
- function round7(v) {
8385
+ function round8(v) {
7864
8386
  return Math.round(v * 1e3) / 1e3;
7865
8387
  }
7866
8388
 
@@ -7950,8 +8472,8 @@ function planAutoSpeed(track, opts) {
7950
8472
  accepted.sort((a, b) => a.in - b.in);
7951
8473
  return accepted.map((c, i) => ({
7952
8474
  id: `s${i}`,
7953
- in: round8(c.in),
7954
- out: round8(c.out),
8475
+ in: round9(c.in),
8476
+ out: round9(c.out),
7955
8477
  rate: clampSpeedRate(c.rate),
7956
8478
  source: "auto"
7957
8479
  }));
@@ -8004,7 +8526,7 @@ function collectRuns(evs, gap, minLen, emit2) {
8004
8526
  }
8005
8527
  flush();
8006
8528
  }
8007
- function round8(v) {
8529
+ function round9(v) {
8008
8530
  return Math.round(v * 1e3) / 1e3;
8009
8531
  }
8010
8532
 
@@ -8186,16 +8708,16 @@ function momentsFromDoc(doc, plan, opts = {}) {
8186
8708
  return {
8187
8709
  id: `m${String(i + 1).padStart(2, "0")}`,
8188
8710
  kind: d.kind,
8189
- source: { in: round9(d.in), out: round9(d.out) },
8190
- output: output ? { in: round9(output.start), out: round9(output.end) } : null,
8191
- at: d.at === null ? null : round9(d.at),
8192
- outputAt: outAt ? round9(outAt.start) : null,
8193
- focus: d.focus ? { cx: round9(d.focus.cx), cy: round9(d.focus.cy) } : null,
8711
+ source: { in: round10(d.in), out: round10(d.out) },
8712
+ output: output ? { in: round10(output.start), out: round10(output.end) } : null,
8713
+ at: d.at === null ? null : round10(d.at),
8714
+ outputAt: outAt ? round10(outAt.start) : null,
8715
+ focus: d.focus ? { cx: round10(d.focus.cx), cy: round10(d.focus.cy) } : null,
8194
8716
  rect: d.rect ? {
8195
- x: round9(d.rect.x),
8196
- y: round9(d.rect.y),
8197
- w: round9(d.rect.w),
8198
- h: round9(d.rect.h)
8717
+ x: round10(d.rect.x),
8718
+ y: round10(d.rect.y),
8719
+ w: round10(d.rect.w),
8720
+ h: round10(d.rect.h)
8199
8721
  } : null,
8200
8722
  ...d.clicks !== void 0 ? { clicks: d.clicks } : {},
8201
8723
  ...d.surface ? { surface: true } : {},
@@ -8216,7 +8738,7 @@ function activityOf(bins, d) {
8216
8738
  sum += bins[i];
8217
8739
  n++;
8218
8740
  }
8219
- return n ? round9(sum / n) : null;
8741
+ return n ? round10(sum / n) : null;
8220
8742
  }
8221
8743
  function saidOver(transcript, d) {
8222
8744
  if (!transcript?.length) return null;
@@ -8225,7 +8747,7 @@ function saidOver(transcript, d) {
8225
8747
  const text = transcript.filter((s) => s.start < hi && s.end > lo).map((s) => s.text.trim()).filter(Boolean).join(" ");
8226
8748
  return text || null;
8227
8749
  }
8228
- function round9(v) {
8750
+ function round10(v) {
8229
8751
  return Math.round(v * 1e3) / 1e3;
8230
8752
  }
8231
8753
 
@@ -8732,8 +9254,8 @@ function buildDigest(input) {
8732
9254
  return {
8733
9255
  digestVersion: DIGEST_VERSION,
8734
9256
  take: {
8735
- sourceDuration: round10(meta.durationMs / 1e3),
8736
- outputDuration: round10(input.outputDuration),
9257
+ sourceDuration: round11(meta.durationMs / 1e3),
9258
+ outputDuration: round11(input.outputDuration),
8737
9259
  width: meta.width,
8738
9260
  height: meta.height,
8739
9261
  captureWidth: meta.captureWidth ?? null,
@@ -8785,7 +9307,7 @@ function buildDigest(input) {
8785
9307
  function outputDurationOf(doc) {
8786
9308
  return docOutputDuration(doc);
8787
9309
  }
8788
- function round10(v) {
9310
+ function round11(v) {
8789
9311
  return Math.round(v * 1e3) / 1e3;
8790
9312
  }
8791
9313
 
@@ -8811,20 +9333,20 @@ function setSpeedInRange(spans, srcIn, srcOut, rate) {
8811
9333
  continue;
8812
9334
  }
8813
9335
  if (srcIn - s.in >= MIN_REMAINDER)
8814
- kept.push({ ...s, out: round11(srcIn), source: "manual" });
9336
+ kept.push({ ...s, out: round12(srcIn), source: "manual" });
8815
9337
  if (s.out - srcOut >= MIN_REMAINDER)
8816
9338
  kept.push({
8817
9339
  ...s,
8818
9340
  id: mintId(used),
8819
- in: round11(srcOut),
9341
+ in: round12(srcOut),
8820
9342
  source: "manual"
8821
9343
  });
8822
9344
  }
8823
9345
  if (rate != null)
8824
9346
  kept.push({
8825
9347
  id: mintId(used),
8826
- in: round11(srcIn),
8827
- out: round11(srcOut),
9348
+ in: round12(srcIn),
9349
+ out: round12(srcOut),
8828
9350
  rate: clampSpeedRate(rate),
8829
9351
  source: "manual"
8830
9352
  });
@@ -8840,9 +9362,9 @@ function removeSourceRange(segments, srcIn, srcOut) {
8840
9362
  next.push(seg);
8841
9363
  continue;
8842
9364
  }
8843
- if (srcIn - seg.in >= MIN_SEGMENT) next.push({ ...seg, out: round11(srcIn) });
9365
+ if (srcIn - seg.in >= MIN_SEGMENT) next.push({ ...seg, out: round12(srcIn) });
8844
9366
  if (seg.out - srcOut >= MIN_SEGMENT)
8845
- next.push({ ...seg, in: round11(srcOut) });
9367
+ next.push({ ...seg, in: round12(srcOut) });
8846
9368
  }
8847
9369
  return next.length ? next : [...segments];
8848
9370
  }
@@ -8860,8 +9382,8 @@ function zoomSpanForRange(zoom, srcIn, srcOut) {
8860
9382
  while (used.has(`u${n}`)) n++;
8861
9383
  return {
8862
9384
  id: `u${n}`,
8863
- in: round11(start),
8864
- out: round11(end),
9385
+ in: round12(start),
9386
+ out: round12(end),
8865
9387
  level: DEFAULT_ZOOM_LEVEL,
8866
9388
  cx: 0.5,
8867
9389
  cy: 0.5,
@@ -8875,7 +9397,7 @@ function mintId(used) {
8875
9397
  used.add(id);
8876
9398
  return id;
8877
9399
  }
8878
- function round11(v) {
9400
+ function round12(v) {
8879
9401
  return Math.round(v * 1e3) / 1e3;
8880
9402
  }
8881
9403
 
@@ -9586,8 +10108,8 @@ var videoLane = {
9586
10108
  return segments.map((s, i) => ({
9587
10109
  id: `seg-${i}`,
9588
10110
  kind: "clip",
9589
- t: round12(starts[i]),
9590
- duration: round12(
10111
+ t: round13(starts[i]),
10112
+ duration: round13(
9591
10113
  outputLen(s, speeds, freezes, i === 0 || !contiguous(segments, i))
9592
10114
  )
9593
10115
  }));
@@ -9670,7 +10192,7 @@ var videoLane = {
9670
10192
  if (local >= m.t - 1e-9 && local <= m.t + m.duration + 1e-9)
9671
10193
  return null;
9672
10194
  }
9673
- const sourceT = round12(
10195
+ const sourceT = round13(
9674
10196
  mapTime3(segmentPieces(s, speeds, freezes, leading), local)
9675
10197
  );
9676
10198
  if (sourceT - s.in < 0.05 || s.out - sourceT < 0.05) return null;
@@ -9711,7 +10233,7 @@ var videoLane = {
9711
10233
  n === 1 || !contiguous(segments, n - 1)
9712
10234
  )
9713
10235
  ] : []
9714
- ].map(round12);
10236
+ ].map(round13);
9715
10237
  }
9716
10238
  };
9717
10239
  function nextFreezeId(doc) {
@@ -9738,8 +10260,8 @@ var freezeLane = {
9738
10260
  {
9739
10261
  id: f.id,
9740
10262
  kind: "clip",
9741
- t: round12(m.t),
9742
- duration: round12(m.duration),
10263
+ t: round13(m.t),
10264
+ duration: round13(m.duration),
9743
10265
  label: `${trimZeros(f.seconds)}s`
9744
10266
  }
9745
10267
  ] : [];
@@ -9761,7 +10283,7 @@ var freezeLane = {
9761
10283
  if (g.t >= m.t - 1e-9 && g.t <= m.t + m.duration + 1e-9) return null;
9762
10284
  }
9763
10285
  if (g.t > totalDuration7(rated) + 1e-9) return null;
9764
- const srcT = round12(mapTime3(rated, Math.max(0, g.t)));
10286
+ const srcT = round13(mapTime3(rated, Math.max(0, g.t)));
9765
10287
  if (!kept(srcT) || !clear(srcT)) return null;
9766
10288
  const id = nextFreezeId(doc);
9767
10289
  const mediaHere = mediaAtOutput(rated, Math.max(0, g.t));
@@ -9803,7 +10325,7 @@ var freezeLane = {
9803
10325
  const marks = freezeOutputExtents(splitBySpeed4(segs, speeds), freezes);
9804
10326
  const mine = marks.get(f.id);
9805
10327
  const endOut = mine ? mine.t + mine.duration : null;
9806
- const srcT = round12(
10328
+ const srcT = round13(
9807
10329
  Math.min(sourceDuration, mapTime3(base, Math.max(0, g.t)))
9808
10330
  );
9809
10331
  if (!kept(srcT) || !clear(srcT, f.id)) return null;
@@ -9863,8 +10385,8 @@ var zoomLane = {
9863
10385
  {
9864
10386
  id: z.id,
9865
10387
  kind: "clip",
9866
- t: round12(ext.start),
9867
- duration: round12(ext.end - ext.start),
10388
+ t: round13(ext.start),
10389
+ duration: round13(ext.end - ext.start),
9868
10390
  label: `${z.level.toFixed(2)}\xD7`
9869
10391
  }
9870
10392
  ];
@@ -9891,8 +10413,8 @@ var zoomLane = {
9891
10413
  ...d.zoom,
9892
10414
  {
9893
10415
  id,
9894
- in: round12(srcT),
9895
- out: round12(srcT + len),
10416
+ in: round13(srcT),
10417
+ out: round13(srcT + len),
9896
10418
  level: DEFAULT_ZOOM_LEVEL,
9897
10419
  cx,
9898
10420
  cy,
@@ -9924,8 +10446,8 @@ var zoomLane = {
9924
10446
  return (d) => {
9925
10447
  const z = d.zoom.find((x) => x.id === g.id);
9926
10448
  if (!z) return;
9927
- z.in = round12(newIn);
9928
- z.out = round12(newIn + len);
10449
+ z.in = round13(newIn);
10450
+ z.out = round13(newIn + len);
9929
10451
  z.source = "manual";
9930
10452
  d.zoom.sort((a, b) => a.in - b.in);
9931
10453
  };
@@ -9954,8 +10476,8 @@ var zoomLane = {
9954
10476
  return (d) => {
9955
10477
  const z = d.zoom.find((x) => x.id === g.id);
9956
10478
  if (!z) return;
9957
- z.in = round12(next.in);
9958
- z.out = round12(next.out);
10479
+ z.in = round13(next.in);
10480
+ z.out = round13(next.out);
9959
10481
  z.source = "manual";
9960
10482
  };
9961
10483
  }
@@ -9977,8 +10499,8 @@ function cursorFocusAt(doc, srcT) {
9977
10499
  if (Math.abs(e.t - ms) < Math.abs(best.t - ms)) best = e;
9978
10500
  const clamp015 = (v) => Math.max(0, Math.min(1, v));
9979
10501
  return {
9980
- cx: round12(clamp015(best.x / width)),
9981
- cy: round12(clamp015(best.y / height))
10502
+ cx: round13(clamp015(best.x / width)),
10503
+ cy: round13(clamp015(best.y / height))
9982
10504
  };
9983
10505
  }
9984
10506
  var tiltLane = {
@@ -9992,8 +10514,8 @@ var tiltLane = {
9992
10514
  {
9993
10515
  id: z.id,
9994
10516
  kind: "clip",
9995
- t: round12(ext.start),
9996
- duration: round12(ext.end - ext.start),
10517
+ t: round13(ext.start),
10518
+ duration: round13(ext.end - ext.start),
9997
10519
  label: `${formatDeg(z.rx)}\xB0/${formatDeg(z.ry)}\xB0`
9998
10520
  }
9999
10521
  ];
@@ -10019,8 +10541,8 @@ var tiltLane = {
10019
10541
  ...d.tilt ?? [],
10020
10542
  {
10021
10543
  id,
10022
- in: round12(srcT),
10023
- out: round12(srcT + len),
10544
+ in: round13(srcT),
10545
+ out: round13(srcT + len),
10024
10546
  rx: DEFAULT_TILT_POSE.rx,
10025
10547
  ry: DEFAULT_TILT_POSE.ry,
10026
10548
  source: "manual",
@@ -10051,8 +10573,8 @@ var tiltLane = {
10051
10573
  return (d) => {
10052
10574
  const z = (d.tilt ?? []).find((x) => x.id === g.id);
10053
10575
  if (!z || !d.tilt) return;
10054
- z.in = round12(newIn);
10055
- z.out = round12(newIn + len);
10576
+ z.in = round13(newIn);
10577
+ z.out = round13(newIn + len);
10056
10578
  z.source = "manual";
10057
10579
  d.tilt.sort((a, b) => a.in - b.in);
10058
10580
  };
@@ -10081,8 +10603,8 @@ var tiltLane = {
10081
10603
  return (d) => {
10082
10604
  const z = (d.tilt ?? []).find((x) => x.id === g.id);
10083
10605
  if (!z) return;
10084
- z.in = round12(next.in);
10085
- z.out = round12(next.out);
10606
+ z.in = round13(next.in);
10607
+ z.out = round13(next.out);
10086
10608
  z.source = "manual";
10087
10609
  };
10088
10610
  }
@@ -10109,8 +10631,8 @@ var camMoveLane = {
10109
10631
  {
10110
10632
  id: z.id,
10111
10633
  kind: "clip",
10112
- t: round12(ext.start),
10113
- duration: round12(ext.end - ext.start),
10634
+ t: round13(ext.start),
10635
+ duration: round13(ext.end - ext.start),
10114
10636
  label: z.size != null ? `${Math.round(z.size * 100)}%` : "Move"
10115
10637
  }
10116
10638
  ];
@@ -10134,8 +10656,8 @@ var camMoveLane = {
10134
10656
  ...d.camMotion ?? [],
10135
10657
  {
10136
10658
  id,
10137
- in: round12(srcT),
10138
- out: round12(srcT + len),
10659
+ in: round13(srcT),
10660
+ out: round13(srcT + len),
10139
10661
  ...DEFAULT_CAM_POSE,
10140
10662
  source: "manual"
10141
10663
  }
@@ -10164,8 +10686,8 @@ var camMoveLane = {
10164
10686
  return (d) => {
10165
10687
  const z = (d.camMotion ?? []).find((x) => x.id === g.id);
10166
10688
  if (!z || !d.camMotion) return;
10167
- z.in = round12(newIn);
10168
- z.out = round12(newIn + len);
10689
+ z.in = round13(newIn);
10690
+ z.out = round13(newIn + len);
10169
10691
  z.source = "manual";
10170
10692
  d.camMotion.sort((a, b) => a.in - b.in);
10171
10693
  };
@@ -10194,8 +10716,8 @@ var camMoveLane = {
10194
10716
  return (d) => {
10195
10717
  const z = (d.camMotion ?? []).find((x) => x.id === g.id);
10196
10718
  if (!z) return;
10197
- z.in = round12(next.in);
10198
- z.out = round12(next.out);
10719
+ z.in = round13(next.in);
10720
+ z.out = round13(next.out);
10199
10721
  z.source = "manual";
10200
10722
  };
10201
10723
  }
@@ -10228,10 +10750,10 @@ var camLane = {
10228
10750
  items.push({
10229
10751
  id: `cam-${i}`,
10230
10752
  kind: "clip",
10231
- t: round12(
10753
+ t: round13(
10232
10754
  starts[i] + outputLen({ in: s.in, out: a }, speeds, freezes, leading)
10233
10755
  ),
10234
- duration: round12(
10756
+ duration: round13(
10235
10757
  outputLen({ in: a, out: b }, speeds, freezes, a === s.in && leading)
10236
10758
  )
10237
10759
  });
@@ -10260,7 +10782,7 @@ var camLane = {
10260
10782
  Math.max(first.in, last.out - span)
10261
10783
  );
10262
10784
  return (d) => {
10263
- d.cam.window = { in: round12(newIn), out: round12(newIn + span) };
10785
+ d.cam.window = { in: round13(newIn), out: round13(newIn + span) };
10264
10786
  };
10265
10787
  }
10266
10788
  const overlapping = eff.map((s, i) => ({
@@ -10273,8 +10795,8 @@ var camLane = {
10273
10795
  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) };
10274
10796
  return (d) => {
10275
10797
  d.cam.window = {
10276
- in: round12(Math.max(0, next.in)),
10277
- out: round12(Math.min(sourceDuration, next.out))
10798
+ in: round13(Math.max(0, next.in)),
10799
+ out: round13(Math.min(sourceDuration, next.out))
10278
10800
  };
10279
10801
  };
10280
10802
  },
@@ -10294,8 +10816,8 @@ var micLane = {
10294
10816
  return segments.map((s, i) => ({
10295
10817
  id: `mic-${i}`,
10296
10818
  kind: "clip",
10297
- t: round12(starts[i]),
10298
- duration: round12(
10819
+ t: round13(starts[i]),
10820
+ duration: round13(
10299
10821
  outputLen(s, speeds, freezes, i === 0 || !contiguous(segments, i))
10300
10822
  )
10301
10823
  }));
@@ -10351,8 +10873,8 @@ var speedLane = {
10351
10873
  {
10352
10874
  id: sp.id,
10353
10875
  kind: "clip",
10354
- t: round12(start),
10355
- duration: round12(end - start),
10876
+ t: round13(start),
10877
+ duration: round13(end - start),
10356
10878
  label: `${sp.rate}\xD7`
10357
10879
  }
10358
10880
  ];
@@ -10376,8 +10898,8 @@ var speedLane = {
10376
10898
  ...d.speed ?? [],
10377
10899
  {
10378
10900
  id,
10379
- in: round12(srcT),
10380
- out: round12(srcT + len),
10901
+ in: round13(srcT),
10902
+ out: round13(srcT + len),
10381
10903
  rate: DEFAULT_SPEED_RATE,
10382
10904
  source: "manual",
10383
10905
  ...mediaHere ? { media: mediaHere } : {}
@@ -10408,8 +10930,8 @@ var speedLane = {
10408
10930
  return (d) => {
10409
10931
  const s = d.speed?.find((x) => x.id === g.id);
10410
10932
  if (!s) return;
10411
- s.in = round12(newIn);
10412
- s.out = round12(newIn + len);
10933
+ s.in = round13(newIn);
10934
+ s.out = round13(newIn + len);
10413
10935
  s.source = "manual";
10414
10936
  d.speed.sort((a, b) => a.in - b.in);
10415
10937
  };
@@ -10438,8 +10960,8 @@ var speedLane = {
10438
10960
  return (d) => {
10439
10961
  const s = d.speed?.find((x) => x.id === g.id);
10440
10962
  if (!s) return;
10441
- s.in = round12(next.in);
10442
- s.out = round12(next.out);
10963
+ s.in = round13(next.in);
10964
+ s.out = round13(next.out);
10443
10965
  s.source = "manual";
10444
10966
  };
10445
10967
  }
@@ -10458,8 +10980,8 @@ var audioLane = {
10458
10980
  return doc.audio.map((c) => ({
10459
10981
  id: c.id,
10460
10982
  kind: "clip",
10461
- t: round12(c.start),
10462
- duration: round12(clipLength(c)),
10983
+ t: round13(c.start),
10984
+ duration: round13(clipLength(c)),
10463
10985
  label: c.name
10464
10986
  }));
10465
10987
  },
@@ -10470,7 +10992,7 @@ var audioLane = {
10470
10992
  const start = Math.max(0, g.t);
10471
10993
  return (d) => {
10472
10994
  const c = d.audio.find((x) => x.id === g.id);
10473
- if (c) c.start = round12(start);
10995
+ if (c) c.start = round13(start);
10474
10996
  };
10475
10997
  }
10476
10998
  if (g.type === "resize") {
@@ -10483,8 +11005,8 @@ var audioLane = {
10483
11005
  return (d) => {
10484
11006
  const c = d.audio.find((x) => x.id === g.id);
10485
11007
  if (!c) return;
10486
- c.start = round12(newStart2);
10487
- c.loopLen = round12(end - newStart2);
11008
+ c.start = round13(newStart2);
11009
+ c.loopLen = round13(end - newStart2);
10488
11010
  };
10489
11011
  }
10490
11012
  const delta = g.t - clip.start;
@@ -10493,22 +11015,22 @@ var audioLane = {
10493
11015
  return (d) => {
10494
11016
  const c = d.audio.find((x) => x.id === g.id);
10495
11017
  if (!c) return;
10496
- c.in = round12(newIn);
10497
- c.start = round12(newStart);
11018
+ c.in = round13(newIn);
11019
+ c.start = round13(newStart);
10498
11020
  };
10499
11021
  }
10500
11022
  if (clip.loop) {
10501
11023
  const newLen = Math.max(0.1, g.t - clip.start);
10502
11024
  return (d) => {
10503
11025
  const c = d.audio.find((x) => x.id === g.id);
10504
- if (c) c.loopLen = round12(newLen);
11026
+ if (c) c.loopLen = round13(newLen);
10505
11027
  };
10506
11028
  }
10507
11029
  const span = Math.max(0.05, g.t - clip.start);
10508
11030
  const newOut = Math.min(clip.in + span, clip.duration);
10509
11031
  return (d) => {
10510
11032
  const c = d.audio.find((x) => x.id === g.id);
10511
- if (c) c.out = round12(Math.max(c.in + 0.05, newOut));
11033
+ if (c) c.out = round13(Math.max(c.in + 0.05, newOut));
10512
11034
  };
10513
11035
  }
10514
11036
  if (g.type === "remove") {
@@ -10521,8 +11043,8 @@ var audioLane = {
10521
11043
  },
10522
11044
  magnets(doc) {
10523
11045
  return doc.audio.flatMap((c) => [
10524
- round12(c.start),
10525
- round12(c.start + clipLength(c))
11046
+ round13(c.start),
11047
+ round13(c.start + clipLength(c))
10526
11048
  ]);
10527
11049
  }
10528
11050
  };
@@ -10540,7 +11062,7 @@ function poseItems(clipId, start, duration, motion) {
10540
11062
  return (motion ?? []).map((p, i) => ({
10541
11063
  id: `${clipId}::k${i}`,
10542
11064
  kind: "keyframe",
10543
- t: round12(start + Math.min(Math.max(0, p.at), duration))
11065
+ t: round13(start + Math.min(Math.max(0, p.at), duration))
10544
11066
  }));
10545
11067
  }
10546
11068
  var overlaysLane = {
@@ -10551,8 +11073,8 @@ var overlaysLane = {
10551
11073
  {
10552
11074
  id: o.id,
10553
11075
  kind: "clip",
10554
- t: round12(o.start),
10555
- duration: round12(o.duration),
11076
+ t: round13(o.start),
11077
+ duration: round13(o.duration),
10556
11078
  label: o.kind === "text" ? o.text.split("\n")[0].slice(0, 24) || "Text" : o.kind === "html" ? htmlLayerLabel(o) : o.kind === "image" ? "Image" : "Video"
10557
11079
  },
10558
11080
  // Pose diamonds render after the clips, so they sit on top.
@@ -10572,7 +11094,7 @@ var overlaysLane = {
10572
11094
  return (d) => {
10573
11095
  const o = (d.overlays ?? []).find((x) => x.id === kf.clipId);
10574
11096
  const p = o?.motion?.[kf.index];
10575
- if (p) p.at = round12(at);
11097
+ if (p) p.at = round13(at);
10576
11098
  };
10577
11099
  }
10578
11100
  if (g.type === "remove") {
@@ -10601,8 +11123,8 @@ var overlaysLane = {
10601
11123
  {
10602
11124
  id,
10603
11125
  kind: "text",
10604
- start: round12(start),
10605
- duration: round12(len),
11126
+ start: round13(start),
11127
+ duration: round13(len),
10606
11128
  text: "Title",
10607
11129
  preset: "title",
10608
11130
  // Frame fractions: centered, lower-third — at any aspect ratio.
@@ -10617,7 +11139,7 @@ var overlaysLane = {
10617
11139
  const start = Math.max(0, g.t);
10618
11140
  return (d) => {
10619
11141
  const o = (d.overlays ?? []).find((x) => x.id === g.id);
10620
- if (o) o.start = round12(start);
11142
+ if (o) o.start = round13(start);
10621
11143
  };
10622
11144
  }
10623
11145
  if (g.type === "resize") {
@@ -10627,14 +11149,14 @@ var overlaysLane = {
10627
11149
  return (d) => {
10628
11150
  const o = (d.overlays ?? []).find((x) => x.id === g.id);
10629
11151
  if (!o) return;
10630
- o.start = round12(newStart);
10631
- o.duration = round12(end - newStart);
11152
+ o.start = round13(newStart);
11153
+ o.duration = round13(end - newStart);
10632
11154
  };
10633
11155
  }
10634
11156
  const newLen = Math.max(OVERLAY_MIN_DURATION, g.t - clip.start);
10635
11157
  return (d) => {
10636
11158
  const o = (d.overlays ?? []).find((x) => x.id === g.id);
10637
- if (o) o.duration = round12(newLen);
11159
+ if (o) o.duration = round13(newLen);
10638
11160
  };
10639
11161
  }
10640
11162
  return (d) => {
@@ -10643,8 +11165,8 @@ var overlaysLane = {
10643
11165
  },
10644
11166
  magnets(doc) {
10645
11167
  return (doc.overlays ?? []).flatMap((o) => [
10646
- round12(o.start),
10647
- round12(o.start + o.duration)
11168
+ round13(o.start),
11169
+ round13(o.start + o.duration)
10648
11170
  ]);
10649
11171
  }
10650
11172
  };
@@ -10657,8 +11179,8 @@ var objectsLane = {
10657
11179
  {
10658
11180
  id: o.id,
10659
11181
  kind: "clip",
10660
- t: round12(o.span?.start ?? 0),
10661
- duration: round12(o.span?.duration ?? outDur),
11182
+ t: round13(o.span?.start ?? 0),
11183
+ duration: round13(o.span?.duration ?? outDur),
10662
11184
  label: o.asset.kind === "primitive" ? o.asset.shape : o.asset.kind === "text3d" ? o.asset.text : "model"
10663
11185
  },
10664
11186
  // Pose diamonds: clip-local `at` from the span start (0 span-less).
@@ -10686,7 +11208,7 @@ var objectsLane = {
10686
11208
  return (d) => {
10687
11209
  const o = (d.objects ?? []).find((x) => x.id === kf.clipId);
10688
11210
  const p = o?.motion?.[kf.index];
10689
- if (p) p.at = round12(at);
11211
+ if (p) p.at = round13(at);
10690
11212
  };
10691
11213
  }
10692
11214
  if (g.type === "remove") {
@@ -10707,7 +11229,7 @@ var objectsLane = {
10707
11229
  const start = Math.max(0, g.t);
10708
11230
  return (d) => {
10709
11231
  const o = (d.objects ?? []).find((x) => x.id === g.id);
10710
- if (o?.span) o.span.start = round12(start);
11232
+ if (o?.span) o.span.start = round13(start);
10711
11233
  };
10712
11234
  }
10713
11235
  if (g.type === "resize") {
@@ -10718,14 +11240,14 @@ var objectsLane = {
10718
11240
  return (d) => {
10719
11241
  const o = (d.objects ?? []).find((x) => x.id === g.id);
10720
11242
  if (!o?.span) return;
10721
- o.span.start = round12(newStart);
10722
- o.span.duration = round12(end - newStart);
11243
+ o.span.start = round13(newStart);
11244
+ o.span.duration = round13(end - newStart);
10723
11245
  };
10724
11246
  }
10725
11247
  const newLen = Math.max(OVERLAY_MIN_DURATION, g.t - clip.span.start);
10726
11248
  return (d) => {
10727
11249
  const o = (d.objects ?? []).find((x) => x.id === g.id);
10728
- if (o?.span) o.span.duration = round12(newLen);
11250
+ if (o?.span) o.span.duration = round13(newLen);
10729
11251
  };
10730
11252
  }
10731
11253
  return (d) => {
@@ -10734,7 +11256,7 @@ var objectsLane = {
10734
11256
  },
10735
11257
  magnets(doc) {
10736
11258
  return (doc.objects ?? []).flatMap(
10737
- (o) => o.span ? [round12(o.span.start), round12(o.span.start + o.span.duration)] : []
11259
+ (o) => o.span ? [round13(o.span.start), round13(o.span.start + o.span.duration)] : []
10738
11260
  );
10739
11261
  }
10740
11262
  };
@@ -10761,7 +11283,7 @@ function nextSpeedId(doc) {
10761
11283
  function segIndex(id) {
10762
11284
  return Number(id.replace("seg-", ""));
10763
11285
  }
10764
- function round12(v) {
11286
+ function round13(v) {
10765
11287
  return Math.round(v * 1e3) / 1e3;
10766
11288
  }
10767
11289
 
@@ -10929,6 +11451,7 @@ export {
10929
11451
  CLICK_FX_INTENSITY,
10930
11452
  CLICK_FX_PRE,
10931
11453
  CLICK_HIGHLIGHT_FADE,
11454
+ CLICK_HOLD_MS,
10932
11455
  CLICK_PAIR_MAX,
10933
11456
  CLICK_PULSE_DUR,
10934
11457
  CLICK_RECT_MAX_FRAC,
@@ -11111,6 +11634,10 @@ export {
11111
11634
  cropBox,
11112
11635
  cssFamilies,
11113
11636
  cssWeights,
11637
+ cursorFromCsv,
11638
+ cursorFromPlaywrightTrace,
11639
+ cursorFromRecords,
11640
+ cursorFromTrace,
11114
11641
  cursorIdleFade,
11115
11642
  deltaE,
11116
11643
  deriveViewportCrop,
@@ -11239,6 +11766,7 @@ export {
11239
11766
  planForDigest,
11240
11767
  planeSizeAtDepth,
11241
11768
  platformBarKind,
11769
+ playwrightTraceEntry,
11242
11770
  programDuration,
11243
11771
  projectFromArtifact,
11244
11772
  pumpFreeChainGap,
@@ -11274,6 +11802,7 @@ export {
11274
11802
  studioLayerData,
11275
11803
  tiltLane,
11276
11804
  tiltTrackFromDoc,
11805
+ traceKindOf,
11277
11806
  transitionMult,
11278
11807
  transitionSeconds,
11279
11808
  unionFitLevel,
@@ -11286,6 +11815,8 @@ export {
11286
11815
  withHolds,
11287
11816
  withoutRejected,
11288
11817
  wrapProgramLength,
11818
+ zipEntries,
11819
+ zipEntryBytes,
11289
11820
  zoomCoversRect,
11290
11821
  zoomLane,
11291
11822
  zoomSpanForRange,