@vosjs/studio-core 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -61,7 +61,13 @@ interface ZoomStyleParams {
61
61
  autoZoom: boolean;
62
62
  /** clicks within this many seconds merge into one span (session merge). */
63
63
  clusterGap: number;
64
- /** minimum clicks for a cluster to earn a zoom (Cursorful's ≥2 rule). */
64
+ /**
65
+ * Minimum clicks for a cluster to earn a zoom. Every style is 1: the
66
+ * Cursorful ≥2 rule glide and keynote carried was never in effect on a
67
+ * recorded take (the recorder counted each press twice, so a lone click
68
+ * was a pair on most pages and a stray on the rest), and the lone click on
69
+ * a button is the zoom people miss when it goes.
70
+ */
65
71
  minClusterClicks: number;
66
72
  /** zoom so the clicked element fills ~this fraction of the frame. */
67
73
  targetFill: number;
@@ -2023,11 +2029,18 @@ interface SmoothOptions {
2023
2029
  declare function smoothCursor(track: CursorTrack, options?: SmoothOptions): SmoothPoint[];
2024
2030
 
2025
2031
  /**
2026
- * A click cluster whose element FITS the frame at less than this level is
2027
- * not a target: it is a drag (aiming, scrubbing, moving a thing across the
2028
- * canvas) or a frame-sized surface, and a zoom on it says nothing. Five real
2029
- * takes (2026-08-25) each carried 1-4 such clusters, planned at the floor
2030
- * level for 10-25s; every one was dropped by hand. Now they plan nothing.
2032
+ * A press whose element FITS the frame at less than this level is not a
2033
+ * target: it is a drag (aiming, scrubbing, moving a thing across the canvas)
2034
+ * or a frame-sized surface (a canvas, a panel, a modal clicked to focus or
2035
+ * dismiss it), and a zoom on it says nothing. Five real takes (2026-08-25)
2036
+ * each carried 1-4 clusters of these, planned at the floor level for 10-25s;
2037
+ * every one was dropped by hand. Now they plan nothing.
2038
+ *
2039
+ * The rule is PER PRESS, never per cluster: a target cluster's level came
2040
+ * from its LARGEST element, so one press on a panel inside a chain of button
2041
+ * clicks read the whole chain as a drag and threw every zoom away — two real
2042
+ * takes (2026-09-11) lost 5 of 8 and 4 of 4 clicks that way. A surface press
2043
+ * is set aside before clustering; the targets around it keep their zoom.
2031
2044
  */
2032
2045
  declare const DRAG_FIT_LEVEL = 1.15;
2033
2046
  interface PlanOptions {
@@ -2117,6 +2130,12 @@ interface Moment {
2117
2130
  /** Normalized target bounds (element rect union), when the events had one. */
2118
2131
  rect: NormRect | null;
2119
2132
  clicks?: number;
2133
+ /**
2134
+ * A click cluster on a frame-sized element (a canvas, a panel, a drag):
2135
+ * the planner frames nothing on it (DRAG_FIT_LEVEL), so a zoom here is
2136
+ * the agent's call, never a proposal.
2137
+ */
2138
+ surface?: boolean;
2120
2139
  pings?: number;
2121
2140
  /** Motion in the window, 0..1 (fraction of changed pixels), null without frames. */
2122
2141
  activity: number | null;
package/dist/index.js CHANGED
@@ -1135,7 +1135,7 @@ var ZOOM_STYLES = {
1135
1135
  glide: {
1136
1136
  autoZoom: true,
1137
1137
  clusterGap: 3,
1138
- minClusterClicks: 2,
1138
+ minClusterClicks: 1,
1139
1139
  targetFill: 0.42,
1140
1140
  minLevel: 1.3,
1141
1141
  maxLevel: 1.8,
@@ -1308,7 +1308,7 @@ var ZOOM_STYLES = {
1308
1308
  keynote: {
1309
1309
  autoZoom: true,
1310
1310
  clusterGap: 3,
1311
- minClusterClicks: 2,
1311
+ minClusterClicks: 1,
1312
1312
  targetFill: 0.42,
1313
1313
  minLevel: 1.3,
1314
1314
  maxLevel: 1.8,
@@ -1417,13 +1417,17 @@ var DWELL_MIN = 0.45;
1417
1417
  var DWELL_MAX = 2.6;
1418
1418
  var DWELL_SPACING = 1.8;
1419
1419
  var DRAG_FIT_LEVEL = 1.15;
1420
+ var PRESS_ECHO_MS = 4;
1421
+ var PRESS_ECHO_PX = 2;
1420
1422
  var TYPING_MIN_PINGS = 2;
1421
1423
  var TYPING_MIN_DUR = 0.5;
1422
1424
  var TYPING_CLICK_ABSORB = 1.5;
1423
1425
  var TYPING_REFOCUS_FRAC = 0.08;
1424
1426
  function groupTrack(track, opts) {
1425
- const { width, height, clusterGap, typingGap, typingZoom } = opts;
1426
- const clicks = track.filter((e) => e.type === "down").map((e) => ({ t: e.t / 1e3, rect: e.rect, x: e.x, y: e.y }));
1427
+ const { width, height, clusterGap, typingGap, typingZoom, targetFill } = opts;
1428
+ const clicks = dedupePresses(
1429
+ track.filter((e) => e.type === "down")
1430
+ ).map((e) => ({ t: e.t / 1e3, rect: e.rect, x: e.x, y: e.y }));
1427
1431
  const sessions = typingZoom ? typingSessions(track, width, height, typingGap) : [];
1428
1432
  const absorbed = /* @__PURE__ */ new Set();
1429
1433
  for (const s of sessions) {
@@ -1441,15 +1445,43 @@ function groupTrack(track, opts) {
1441
1445
  s.events = [best, ...s.events];
1442
1446
  }
1443
1447
  }
1444
- const clusters = [];
1448
+ const targets = [];
1449
+ const surfaces = [];
1445
1450
  for (const c of clicks) {
1446
1451
  if (absorbed.has(c)) continue;
1447
- const last = clusters.at(-1);
1448
- const prev = last?.at(-1);
1449
- if (last && prev && c.t - prev.t <= clusterGap) last.push(c);
1450
- else clusters.push([c]);
1452
+ if (isSurfacePress(c, width, height, targetFill)) surfaces.push(c);
1453
+ else targets.push(c);
1454
+ }
1455
+ const chain = (list) => {
1456
+ const clusters = [];
1457
+ for (const c of list) {
1458
+ const last = clusters.at(-1);
1459
+ const prev = last?.at(-1);
1460
+ if (last && prev && c.t - prev.t <= clusterGap) last.push(c);
1461
+ else clusters.push([c]);
1462
+ }
1463
+ return clusters;
1464
+ };
1465
+ return { sessions, clusters: chain(targets), surfaces: chain(surfaces) };
1466
+ }
1467
+ function dedupePresses(downs) {
1468
+ const kept = [];
1469
+ for (const e of downs) {
1470
+ const prev = kept.at(-1);
1471
+ if (prev && e.t - prev.t <= PRESS_ECHO_MS && Math.abs(e.x - prev.x) <= PRESS_ECHO_PX && Math.abs(e.y - prev.y) <= PRESS_ECHO_PX) {
1472
+ continue;
1473
+ }
1474
+ kept.push(e);
1451
1475
  }
1452
- return { sessions, clusters };
1476
+ return kept;
1477
+ }
1478
+ function fitLevel(rect, width, height, targetFill) {
1479
+ if (!rect || rect.w <= 0 || rect.h <= 0) return null;
1480
+ return Math.min(width * targetFill / rect.w, height * targetFill / rect.h);
1481
+ }
1482
+ function isSurfacePress(c, width, height, targetFill) {
1483
+ const fit = fitLevel(c.rect, width, height, targetFill);
1484
+ return fit !== null && fit < DRAG_FIT_LEVEL;
1453
1485
  }
1454
1486
  function planAutoZoom(track, options) {
1455
1487
  const style = resolveZoomStyle(options.style, options.params);
@@ -1470,31 +1502,31 @@ function planAutoZoom(track, options) {
1470
1502
  typingHold = style.typingHold,
1471
1503
  typingMinLevel = style.typingMinLevel
1472
1504
  } = options;
1473
- const { sessions, clusters } = groupTrack(track, {
1505
+ const { sessions, clusters, surfaces } = groupTrack(track, {
1474
1506
  width,
1475
1507
  height,
1476
1508
  clusterGap,
1477
1509
  typingGap,
1478
- typingZoom
1510
+ typingZoom,
1511
+ targetFill
1479
1512
  });
1480
1513
  const eligible = clusters.filter((c) => c.length >= minClusterClicks);
1481
1514
  const clickSpans = [];
1482
- const dragReserved = [];
1515
+ const dragReserved = surfaces.map((cluster, i) => {
1516
+ const f = clusterFocus(cluster, width, height);
1517
+ return {
1518
+ id: `drag${i}`,
1519
+ in: Math.max(0, cluster[0].t - lead),
1520
+ out: cluster[cluster.length - 1].t + hold,
1521
+ level: 1,
1522
+ cx: f.cx,
1523
+ cy: f.cy
1524
+ };
1525
+ });
1483
1526
  for (const cluster of eligible) {
1484
1527
  const first = cluster[0];
1485
1528
  const last = cluster[cluster.length - 1];
1486
1529
  const f = focusFor(cluster, width, height, targetFill, minLevel, maxLevel);
1487
- if (f.fit !== null && f.fit < DRAG_FIT_LEVEL) {
1488
- dragReserved.push({
1489
- id: `drag${dragReserved.length}`,
1490
- in: Math.max(0, first.t - lead),
1491
- out: last.t + hold,
1492
- level: 1,
1493
- cx: f.cx,
1494
- cy: f.cy
1495
- });
1496
- continue;
1497
- }
1498
1530
  clickSpans.push({
1499
1531
  in: Math.max(0, first.t - lead),
1500
1532
  out: last.t + hold,
@@ -1668,15 +1700,8 @@ function focusFor(cluster, width, height, targetFill, minLevel, maxLevel) {
1668
1700
  const n = cluster.length;
1669
1701
  const cx = clamp012(sx / n / width);
1670
1702
  const cy = clamp012(sy / n / height);
1671
- let level = maxLevel;
1672
- let fit = null;
1673
- if (maxW > 0 && maxH > 0) {
1674
- const fitX = width * targetFill / maxW;
1675
- const fitY = height * targetFill / maxH;
1676
- level = Math.min(fitX, fitY);
1677
- fit = level;
1678
- }
1679
- return { cx, cy, level: clamp2(level, minLevel, maxLevel), fit };
1703
+ const level = fitLevel({ x: 0, y: 0, w: maxW, h: maxH }, width, height, targetFill) ?? maxLevel;
1704
+ return { cx, cy, level: clamp2(level, minLevel, maxLevel) };
1680
1705
  }
1681
1706
  function clusterFocus(events, width, height) {
1682
1707
  let sx = 0;
@@ -6962,14 +6987,15 @@ function momentsFromDoc(doc, plan, opts = {}) {
6962
6987
  rect: null
6963
6988
  });
6964
6989
  if (track.length) {
6965
- const { sessions, clusters } = groupTrack(track, {
6990
+ const { sessions, clusters, surfaces } = groupTrack(track, {
6966
6991
  width,
6967
6992
  height,
6968
6993
  clusterGap: style.clusterGap,
6969
6994
  typingGap: style.typingGap,
6970
- typingZoom: style.typingZoom
6995
+ typingZoom: style.typingZoom,
6996
+ targetFill: style.targetFill
6971
6997
  });
6972
- for (const c of clusters) {
6998
+ const pressed = (c, surface) => {
6973
6999
  const f = clusterFocus(c, width, height);
6974
7000
  drafts.push({
6975
7001
  kind: "click",
@@ -6980,9 +7006,12 @@ function momentsFromDoc(doc, plan, opts = {}) {
6980
7006
  at: c[0].t,
6981
7007
  focus: { cx: f.cx, cy: f.cy },
6982
7008
  rect: f.rect,
6983
- clicks: c.length
7009
+ clicks: c.length,
7010
+ ...surface ? { surface: true } : {}
6984
7011
  });
6985
- }
7012
+ };
7013
+ for (const c of clusters) pressed(c, false);
7014
+ for (const c of surfaces) pressed(c, true);
6986
7015
  for (const s of sessions) {
6987
7016
  const f = clusterFocus(s.events, width, height);
6988
7017
  drafts.push({
@@ -7102,6 +7131,7 @@ function momentsFromDoc(doc, plan, opts = {}) {
7102
7131
  h: round8(d.rect.h)
7103
7132
  } : null,
7104
7133
  ...d.clicks !== void 0 ? { clicks: d.clicks } : {},
7134
+ ...d.surface ? { surface: true } : {},
7105
7135
  ...d.pings !== void 0 ? { pings: d.pings } : {},
7106
7136
  activity: activityOf(opts.bins, d),
7107
7137
  proposed,