@nakednous/tree 0.0.19 → 0.0.20

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/README.md CHANGED
@@ -166,6 +166,30 @@ For matrix-based capture use `track.add({ mat4Model: mat4Eye })` for full-fideli
166
166
 
167
167
  ---
168
168
 
169
+ ### Path sampling
170
+
171
+ The interpolated path of a track can be sampled without advancing the transport cursor or firing hooks. All samplers are zero-alloc — the caller owns the output buffers — and honour the track's interpolation mode (`hermite` / `linear` / `step`) and the same stored-tangent → auto-CR fallback chain used by `eval()`.
172
+
173
+ **`PoseTrack`:**
174
+
175
+ ```js
176
+ track.samplePos(out, seg, t) // interpolated pos at (seg, t ∈ [0, 1])
177
+ track.sampleTangents(outIn, outOut, i) // effective in/out tangents at keyframe i
178
+ ```
179
+
180
+ **`CameraTrack`:**
181
+
182
+ ```js
183
+ track.sampleEye(out, seg, t)
184
+ track.sampleCenter(out, seg, t)
185
+ track.sampleEyeTangents(outIn, outOut, i)
186
+ track.sampleCenterTangents(outIn, outOut, i)
187
+ ```
188
+
189
+ Tangent samplers mirror the missing side at boundary keyframes so the first and last keyframes produce visible tangent vectors. Intended uses: custom rendering of the path (polyline overlays, arclength-based placement), pedagogical visualisations of Hermite / Catmull-Rom, and gizmos — `p5.tree`'s `trackPath` is built on top of these.
190
+
191
+ ---
192
+
169
193
  ### Shared Track transport
170
194
 
171
195
  Both `PoseTrack` and `CameraTrack` extend `Track`, which holds all transport machinery:
package/dist/index.js CHANGED
@@ -1182,6 +1182,19 @@ function mat4ToRotation(out4, m) {
1182
1182
  * hooks, rate semantics. Subclasses add only keyframe storage and
1183
1183
  * add() / eval() for their respective data shape.
1184
1184
  *
1185
+ * ── Path samplers (public, zero-alloc, no cursor side effects) ──────────────
1186
+ * PoseTrack
1187
+ * samplePos(out, seg, t) writes interpolated pos at (seg, t∈[0,1])
1188
+ * sampleTangents(outIn, outOut, i) effective in/out tangents at keyframe i
1189
+ * CameraTrack
1190
+ * sampleEye(out, seg, t)
1191
+ * sampleCenter(out, seg, t)
1192
+ * sampleEyeTangents(outIn, outOut, i)
1193
+ * sampleCenterTangents(outIn, outOut, i)
1194
+ *
1195
+ * Samplers honour the corresponding interpolation mode (hermite/linear/step)
1196
+ * and the stored-tangent / auto-CR fallback chain used by eval().
1197
+ *
1185
1198
  * ── Hook architecture ─────────────────────────────────────────────────────────
1186
1199
  * Lib-space hooks (underscore prefix — reserved for host layer / UI layer):
1187
1200
  * _onActivate / _onDeactivate — fire on playing transitions false→true / true→false.
@@ -1259,19 +1272,22 @@ const hermiteVec3 = (out, p0, m0, p1, m1, t) => {
1259
1272
  };
1260
1273
 
1261
1274
  // Centripetal CR outgoing tangent at p1 for segment p1→p2, scaled by dt1.
1275
+ // Signature: (out, p0, p1, p2). Returns tangent AT p1 (the middle point).
1262
1276
  const _crTanOut = (out, p0, p1, p2) => {
1263
1277
  const dt0=Math.pow(_dist3(p0,p1),0.5)||1, dt1=Math.pow(_dist3(p1,p2),0.5)||1;
1264
1278
  for (let i=0;i<3;i++) out[i]=((p1[i]-p0[i])/dt0-(p2[i]-p0[i])/(dt0+dt1)+(p2[i]-p1[i])/dt1)*dt1;
1265
1279
  return out;
1266
1280
  };
1267
1281
 
1282
+ // Centripetal CR incoming tangent at p2 for segment p1→p2, scaled by dt1.
1283
+ // Signature: (out, p1, p2, p3). Returns tangent AT p2 (the middle point).
1268
1284
  const _crTanIn = (out, p1, p2, p3) => {
1269
1285
  const dt1=Math.pow(_dist3(p1,p2),0.5)||1, dt2=Math.pow(_dist3(p2,p3),0.5)||1;
1270
1286
  for (let i=0;i<3;i++) out[i]=((p2[i]-p1[i])/dt1-(p3[i]-p1[i])/(dt1+dt2)+(p3[i]-p2[i])/dt2)*dt1;
1271
1287
  return out;
1272
1288
  };
1273
1289
 
1274
- // Module-level scratch — shared by eval() across all track instances (non-reentrant hot path).
1290
+ // Module-level scratch — shared across all track instances (non-reentrant hot path).
1275
1291
  const _m0=[0,0,0], _m1=[0,0,0];
1276
1292
 
1277
1293
  /**
@@ -1289,6 +1305,121 @@ const lerpVec3 = (out, a, b, t) => {
1289
1305
  return out;
1290
1306
  };
1291
1307
 
1308
+ // =========================================================================
1309
+ // S2b Path samplers — shared core
1310
+ // =========================================================================
1311
+ //
1312
+ // These helpers factor out the per-field interpolation from eval(), so that
1313
+ // the samplers (samplePos / sampleEye / sampleCenter / sampleTangents / ...)
1314
+ // can write into caller buffers without touching the cursor state.
1315
+ //
1316
+ // The field name and its associated tangent field names are passed as keys
1317
+ // so the same core serves 'pos'/'tanIn'/'tanOut' for PoseTrack and
1318
+ // 'eye'|'center' + matching tangent keys for CameraTrack.
1319
+
1320
+ /**
1321
+ * Sample interpolated vec3 path at (seg, t) into out.
1322
+ * @private
1323
+ * @param {number[]} out
1324
+ * @param {Array} kfs keyframe array
1325
+ * @param {string} interp 'hermite' | 'linear' | 'step'
1326
+ * @param {string} field keyframe property holding the vec3 path point
1327
+ * @param {string} tanInName keyframe property for incoming tangent
1328
+ * @param {string} tanOutName keyframe property for outgoing tangent
1329
+ * @param {number} seg segment index
1330
+ * @param {number} t local parameter in [0,1]
1331
+ */
1332
+ function _samplePathCore(out, kfs, interp, field, tanInName, tanOutName, seg, t) {
1333
+ const n = kfs.length;
1334
+ if (n === 0) { out[0]=0; out[1]=0; out[2]=0; return out; }
1335
+ if (n === 1) {
1336
+ const p = kfs[0][field];
1337
+ out[0]=p[0]; out[1]=p[1]; out[2]=p[2];
1338
+ return out;
1339
+ }
1340
+ const nSeg = n - 1;
1341
+ seg = _clampS(seg | 0, 0, nSeg - 1);
1342
+ t = _clamp01(t);
1343
+ const k0 = kfs[seg];
1344
+ const k1 = kfs[seg + 1];
1345
+
1346
+ if (interp === 'step') {
1347
+ const p = k0[field];
1348
+ out[0]=p[0]; out[1]=p[1]; out[2]=p[2];
1349
+ return out;
1350
+ }
1351
+ if (interp === 'linear') {
1352
+ return lerpVec3(out, k0[field], k1[field], t);
1353
+ }
1354
+
1355
+ // hermite (default)
1356
+ const p0 = seg > 0 ? kfs[seg - 1][field] : k0[field];
1357
+ const p3 = seg + 2 < n ? kfs[seg + 2][field] : k1[field];
1358
+ const m0 = k0[tanOutName] != null ? k0[tanOutName]
1359
+ : k0[tanInName] != null ? k0[tanInName]
1360
+ : _crTanOut(_m0, p0, k0[field], k1[field]);
1361
+ const m1 = k1[tanInName] != null ? k1[tanInName]
1362
+ : k1[tanOutName] != null ? k1[tanOutName]
1363
+ : _crTanIn(_m1, k0[field], k1[field], p3);
1364
+ return hermiteVec3(out, k0[field], m0, k1[field], m1, t);
1365
+ }
1366
+
1367
+ /**
1368
+ * Write the effective in/out tangents at keyframe i.
1369
+ *
1370
+ * Stored tanIn/tanOut take precedence, then each mirrors the other when only
1371
+ * one is stored, else centripetal Catmull-Rom tangents are auto-computed from
1372
+ * neighbours.
1373
+ *
1374
+ * At endpoints one side has no adjacent segment; that side mirrors the other
1375
+ * side's tangent. Callers drawing arrows at endpoints therefore see a vector
1376
+ * that matches the curve's derivative into / out of the curve's boundary.
1377
+ *
1378
+ * @private
1379
+ */
1380
+ function _sampleTangentsCore(outIn, outOut, kfs, field, tanInName, tanOutName, i) {
1381
+ const n = kfs.length;
1382
+ if (n === 0) {
1383
+ outIn[0]=outIn[1]=outIn[2]=0; outOut[0]=outOut[1]=outOut[2]=0;
1384
+ return;
1385
+ }
1386
+ i = _clampS(i | 0, 0, n - 1);
1387
+ const ki = kfs[i];
1388
+ const hasTI = ki[tanInName] != null;
1389
+ const hasTO = ki[tanOutName] != null;
1390
+
1391
+ // ── outgoing tangent at keyframe i (for segment i → i+1) ──────────────
1392
+ if (hasTO) {
1393
+ outOut[0]=ki[tanOutName][0]; outOut[1]=ki[tanOutName][1]; outOut[2]=ki[tanOutName][2];
1394
+ } else if (hasTI) {
1395
+ outOut[0]=ki[tanInName][0]; outOut[1]=ki[tanInName][1]; outOut[2]=ki[tanInName][2];
1396
+ } else if (i < n - 1) {
1397
+ const k1 = kfs[i + 1];
1398
+ const p0 = i > 0 ? kfs[i - 1][field] : ki[field];
1399
+ _crTanOut(outOut, p0, ki[field], k1[field]);
1400
+ } else {
1401
+ outOut[0]=0; outOut[1]=0; outOut[2]=0; // filled below by mirror
1402
+ }
1403
+
1404
+ // ── incoming tangent at keyframe i (for segment i-1 → i) ──────────────
1405
+ if (hasTI) {
1406
+ outIn[0]=ki[tanInName][0]; outIn[1]=ki[tanInName][1]; outIn[2]=ki[tanInName][2];
1407
+ } else if (hasTO) {
1408
+ outIn[0]=ki[tanOutName][0]; outIn[1]=ki[tanOutName][1]; outIn[2]=ki[tanOutName][2];
1409
+ } else if (i > 0) {
1410
+ const k0 = kfs[i - 1];
1411
+ const p3 = i + 1 < n ? kfs[i + 1][field] : ki[field];
1412
+ _crTanIn(outIn, k0[field], ki[field], p3);
1413
+ } else {
1414
+ outIn[0]=outOut[0]; outIn[1]=outOut[1]; outIn[2]=outOut[2];
1415
+ }
1416
+
1417
+ // Boundary mirror the other way: last keyframe with no stored tangents.
1418
+ if (i === n - 1 && !hasTO && !hasTI) {
1419
+ outOut[0]=outIn[0]; outOut[1]=outIn[1]; outOut[2]=outIn[2];
1420
+ }
1421
+ }
1422
+
1292
1423
  // =========================================================================
1293
1424
  // S3 Transform <-> Mat4
1294
1425
  // =========================================================================
@@ -1503,10 +1634,6 @@ function _sameTransform(a, b) {
1503
1634
  * eyeTanIn/Out and centerTanIn/Out are optional vec3 tangents for Hermite.
1504
1635
  * When absent, centripetal Catmull-Rom tangents are auto-computed at eval time.
1505
1636
  *
1506
- * Removed forms (task 2):
1507
- * { mat4View } and { mat4Eye } — use PoseTrack.add({ mat4Model: mat4Eye }) for
1508
- * full-fidelity capture including roll, or cam.capturePose() for lookat-style.
1509
- *
1510
1637
  * @param {Object} spec
1511
1638
  * @returns {{ eye:number[], center:number[], up:number[],
1512
1639
  * fov:number|null, halfHeight:number|null,
@@ -1526,12 +1653,12 @@ function _parseCameraSpec(spec) {
1526
1653
  return {
1527
1654
  eye, center,
1528
1655
  up: [up[0]/ul, up[1]/ul, up[2]/ul],
1529
- fov: typeof spec.fov === 'number' ? spec.fov : null,
1530
- halfHeight: typeof spec.halfHeight === 'number' ? spec.halfHeight : null,
1531
- eyeTanIn: _parseVec3(spec.eyeTanIn) || null,
1532
- eyeTanOut: _parseVec3(spec.eyeTanOut) || null,
1533
- centerTanIn: _parseVec3(spec.centerTanIn) || null,
1534
- centerTanOut:_parseVec3(spec.centerTanOut)|| null,
1656
+ fov: typeof spec.fov === 'number' ? spec.fov : null,
1657
+ halfHeight: typeof spec.halfHeight === 'number' ? spec.halfHeight : null,
1658
+ eyeTanIn: _parseVec3(spec.eyeTanIn) || null,
1659
+ eyeTanOut: _parseVec3(spec.eyeTanOut) || null,
1660
+ centerTanIn: _parseVec3(spec.centerTanIn) || null,
1661
+ centerTanOut: _parseVec3(spec.centerTanOut)|| null,
1535
1662
  };
1536
1663
  }
1537
1664
 
@@ -1558,7 +1685,7 @@ class Track {
1558
1685
  this.playing = false;
1559
1686
  /** Loop at boundaries. @type {boolean} */
1560
1687
  this.loop = false;
1561
- /** Ping-pong bounce (takes precedence over loop). @type {boolean} */
1688
+ /** Ping-pong bounce (independent of loop). @type {boolean} */
1562
1689
  this.bounce = false;
1563
1690
  /** Frames per segment (≥1). @type {number} */
1564
1691
  this.duration = 30;
@@ -1773,14 +1900,12 @@ class Track {
1773
1900
  // ── loop:false, bounce:true — bounce once, stop at origin ────────────
1774
1901
  if (!this.loop && this.bounce) {
1775
1902
  if (next >= total) {
1776
- // far boundary: reflect and flip direction once
1777
1903
  this._setCursorFromScalar(Math.min(total, 2 * total - next));
1778
1904
  this._dir = -this._dir;
1779
1905
  this._bounced = true;
1780
1906
  return true;
1781
1907
  }
1782
1908
  if (next <= 0) {
1783
- // origin: stop (whether we bounced or started backward)
1784
1909
  this._setCursorFromScalar(0);
1785
1910
  this.playing = false;
1786
1911
  this._dir = 1; this._bounced = false;
@@ -1907,6 +2032,39 @@ class PoseTrack extends Track {
1907
2032
  return true;
1908
2033
  }
1909
2034
 
2035
+ /**
2036
+ * Sample the position path at (seg, t).
2037
+ *
2038
+ * Pure function of the keyframes — does not read or modify the transport
2039
+ * cursor, fires no hooks, allocates nothing. seg is clamped to
2040
+ * [0, segments-1] and t to [0, 1].
2041
+ *
2042
+ * @param {number[]} out 3-element result buffer.
2043
+ * @param {number} seg Segment index.
2044
+ * @param {number} t Local parameter in [0, 1].
2045
+ * @returns {number[]} out
2046
+ */
2047
+ samplePos(out, seg, t) {
2048
+ return _samplePathCore(out, this.keyframes, this.posInterp, 'pos', 'tanIn', 'tanOut', seg, t);
2049
+ }
2050
+
2051
+ /**
2052
+ * Write the effective incoming / outgoing tangents at keyframe i.
2053
+ * Stored tanIn/tanOut take precedence, then each mirrors the other when
2054
+ * only one is stored, else centripetal Catmull-Rom tangents are auto-
2055
+ * computed from neighbours. Endpoint tangents mirror across the missing
2056
+ * side so drawing arrows at boundary keyframes produces a visible vector.
2057
+ *
2058
+ * @param {number[]} outIn 3-element result — incoming tangent at kf i.
2059
+ * @param {number[]} outOut 3-element result — outgoing tangent at kf i.
2060
+ * @param {number} i Keyframe index.
2061
+ * @returns {PoseTrack} this
2062
+ */
2063
+ sampleTangents(outIn, outOut, i) {
2064
+ _sampleTangentsCore(outIn, outOut, this.keyframes, 'pos', 'tanIn', 'tanOut', i);
2065
+ return this;
2066
+ }
2067
+
1910
2068
  /**
1911
2069
  * Evaluate interpolated TRS pose at current cursor.
1912
2070
  * @param {{ pos:number[], rot:number[], scl:number[] }} [out]
@@ -1932,22 +2090,8 @@ class PoseTrack extends Track {
1932
2090
  const k0 = this.keyframes[seg];
1933
2091
  const k1 = this.keyframes[seg + 1];
1934
2092
 
1935
- // pos — Hermite (auto-CR tangents when none stored), linear, or step
1936
- if (this.posInterp === 'step') {
1937
- out.pos[0]=k0.pos[0]; out.pos[1]=k0.pos[1]; out.pos[2]=k0.pos[2];
1938
- } else if (this.posInterp === 'linear') {
1939
- lerpVec3(out.pos, k0.pos, k1.pos, t);
1940
- } else {
1941
- const p0 = seg > 0 ? this.keyframes[seg - 1].pos : k0.pos;
1942
- seg + 2 < n ? this.keyframes[seg + 2].pos : k1.pos;
1943
- const m0 = k0.tanOut != null ? k0.tanOut
1944
- : k0.tanIn != null ? k0.tanIn
1945
- : _crTanOut(_m0, p0, k0.pos, k1.pos);
1946
- const m1 = k1.tanIn != null ? k1.tanIn
1947
- : k1.tanOut != null ? k1.tanOut
1948
- : _crTanIn(_m1, p0, k0.pos, k1.pos);
1949
- hermiteVec3(out.pos, k0.pos, m0, k1.pos, m1, t);
1950
- }
2093
+ // pos — shared sampler (respects posInterp + tangent fallback chain)
2094
+ _samplePathCore(out.pos, this.keyframes, this.posInterp, 'pos', 'tanIn', 'tanOut', seg, t);
1951
2095
 
1952
2096
  // rot — step, slerp, or nlerp
1953
2097
  if (this.rotInterp === 'step') {
@@ -2055,6 +2199,52 @@ class CameraTrack extends Track {
2055
2199
  return true;
2056
2200
  }
2057
2201
 
2202
+ /**
2203
+ * Sample the eye path at (seg, t). See PoseTrack.samplePos for semantics.
2204
+ * @param {number[]} out
2205
+ * @param {number} seg
2206
+ * @param {number} t
2207
+ * @returns {number[]} out
2208
+ */
2209
+ sampleEye(out, seg, t) {
2210
+ return _samplePathCore(out, this.keyframes, this.eyeInterp, 'eye', 'eyeTanIn', 'eyeTanOut', seg, t);
2211
+ }
2212
+
2213
+ /**
2214
+ * Sample the center path at (seg, t). See PoseTrack.samplePos for semantics.
2215
+ * @param {number[]} out
2216
+ * @param {number} seg
2217
+ * @param {number} t
2218
+ * @returns {number[]} out
2219
+ */
2220
+ sampleCenter(out, seg, t) {
2221
+ return _samplePathCore(out, this.keyframes, this.centerInterp, 'center', 'centerTanIn', 'centerTanOut', seg, t);
2222
+ }
2223
+
2224
+ /**
2225
+ * Effective in/out eye tangents at keyframe i. See PoseTrack.sampleTangents.
2226
+ * @param {number[]} outIn
2227
+ * @param {number[]} outOut
2228
+ * @param {number} i
2229
+ * @returns {CameraTrack} this
2230
+ */
2231
+ sampleEyeTangents(outIn, outOut, i) {
2232
+ _sampleTangentsCore(outIn, outOut, this.keyframes, 'eye', 'eyeTanIn', 'eyeTanOut', i);
2233
+ return this;
2234
+ }
2235
+
2236
+ /**
2237
+ * Effective in/out center tangents at keyframe i. See PoseTrack.sampleTangents.
2238
+ * @param {number[]} outIn
2239
+ * @param {number[]} outOut
2240
+ * @param {number} i
2241
+ * @returns {CameraTrack} this
2242
+ */
2243
+ sampleCenterTangents(outIn, outOut, i) {
2244
+ _sampleTangentsCore(outIn, outOut, this.keyframes, 'center', 'centerTanIn', 'centerTanOut', i);
2245
+ return this;
2246
+ }
2247
+
2058
2248
  /**
2059
2249
  * Evaluate interpolated camera pose at current cursor.
2060
2250
  *
@@ -2083,39 +2273,11 @@ class CameraTrack extends Track {
2083
2273
  const k0 = this.keyframes[seg];
2084
2274
  const k1 = this.keyframes[seg + 1];
2085
2275
 
2086
- // eye — Hermite (auto-CR tangents when none stored), linear, or step
2087
- if (this.eyeInterp === 'step') {
2088
- out.eye[0]=k0.eye[0]; out.eye[1]=k0.eye[1]; out.eye[2]=k0.eye[2];
2089
- } else if (this.eyeInterp === 'linear') {
2090
- lerpVec3(out.eye, k0.eye, k1.eye, t);
2091
- } else {
2092
- const p0 = seg > 0 ? this.keyframes[seg - 1].eye : k0.eye;
2093
- seg + 2 < n ? this.keyframes[seg + 2].eye : k1.eye;
2094
- const m0 = k0.eyeTanOut != null ? k0.eyeTanOut
2095
- : k0.eyeTanIn != null ? k0.eyeTanIn
2096
- : _crTanOut(_m0, p0, k0.eye, k1.eye);
2097
- const m1 = k1.eyeTanIn != null ? k1.eyeTanIn
2098
- : k1.eyeTanOut != null ? k1.eyeTanOut
2099
- : _crTanIn(_m1, p0, k0.eye, k1.eye);
2100
- hermiteVec3(out.eye, k0.eye, m0, k1.eye, m1, t);
2101
- }
2276
+ // eye — shared sampler
2277
+ _samplePathCore(out.eye, this.keyframes, this.eyeInterp, 'eye', 'eyeTanIn', 'eyeTanOut', seg, t);
2102
2278
 
2103
- // center — Hermite, linear, or step (independent lookat target)
2104
- if (this.centerInterp === 'step') {
2105
- out.center[0]=k0.center[0]; out.center[1]=k0.center[1]; out.center[2]=k0.center[2];
2106
- } else if (this.centerInterp === 'hermite') {
2107
- const c0 = seg > 0 ? this.keyframes[seg - 1].center : k0.center;
2108
- seg + 2 < n ? this.keyframes[seg + 2].center : k1.center;
2109
- const m0 = k0.centerTanOut != null ? k0.centerTanOut
2110
- : k0.centerTanIn != null ? k0.centerTanIn
2111
- : _crTanOut(_m0, c0, k0.center, k1.center);
2112
- const m1 = k1.centerTanIn != null ? k1.centerTanIn
2113
- : k1.centerTanOut != null ? k1.centerTanOut
2114
- : _crTanIn(_m1, c0, k0.center, k1.center);
2115
- hermiteVec3(out.center, k0.center, m0, k1.center, m1, t);
2116
- } else {
2117
- lerpVec3(out.center, k0.center, k1.center, t);
2118
- }
2279
+ // center — shared sampler
2280
+ _samplePathCore(out.center, this.keyframes, this.centerInterp, 'center', 'centerTanIn', 'centerTanOut', seg, t);
2119
2281
 
2120
2282
  // up — nlerp on unit sphere
2121
2283
  lerpVec3(out.up, k0.up, k1.up, t);