spoint 0.1.658 → 0.1.659
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/package.json
CHANGED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Pure binary wire-format primitives for SnapshotEncoder.js: the fixed 23-byte numeric record
|
|
2
|
+
// (position/velocity/rotation/scale/flags) and the 32-bit packed-quaternion encode/decode. No
|
|
3
|
+
// closure/instance state -- split out as the one genuinely self-contained piece of that file.
|
|
4
|
+
// See SnapshotEncoder.js's own header comment for the full wire-layout rationale.
|
|
5
|
+
|
|
6
|
+
const Q1 = 100
|
|
7
|
+
const QSCALE = 511 * Math.SQRT2
|
|
8
|
+
|
|
9
|
+
// --- Binary numeric record (DataView, replaces JS-array-through-msgpackr for the fixed numeric
|
|
10
|
+
// fields: position/velocity/rotation/scale). id/model/bodyType/custom stay as native JS values
|
|
11
|
+
// alongside this buffer -- they are variable-shape (strings, arbitrary objects) and packing them
|
|
12
|
+
// into a fixed byte layout would be strictly worse (lossy or unbounded), not a real win. Position/
|
|
13
|
+
// velocity are int16 at the existing Q1=100 (1cm) scale, clamped to +-327.67m -- entity/player
|
|
14
|
+
// positions are always encoded relative to a region/session-local origin already (no planetary
|
|
15
|
+
// float32 range concern here; see AGENTS.md floating-origin-camera-relative-rendering row for the
|
|
16
|
+
// separate concern of >10km world coordinates, which is a rendering-layer issue, not a wire-format
|
|
17
|
+
// one). Scale uses uint16 unsigned at the same Q1 scale (0..655.35, entities are never negatively
|
|
18
|
+
// scaled). Rotation reuses packQuat's existing 32-bit packed representation verbatim -- not
|
|
19
|
+
// reinvented. Fixed player/entity record is 23 bytes: 3*i16 pos + 3*i16 vel + u32 quat + 3*u16
|
|
20
|
+
// scale + 1 flags byte = 6+6+4+6+1 = 23.
|
|
21
|
+
export const BIN_RECORD_BYTES = 23
|
|
22
|
+
export const POS_I16_MAX = 32767 / Q1 // 327.67
|
|
23
|
+
export const SCALE_U16_MAX = 65535 / Q1 // 655.35
|
|
24
|
+
|
|
25
|
+
export function clampI16(v) { return Math.max(-32767, Math.min(32767, Math.round((v || 0) * Q1))) }
|
|
26
|
+
export function clampU16Scale(v) { return Math.max(0, Math.min(65535, Math.round((v ?? 1) * Q1))) }
|
|
27
|
+
|
|
28
|
+
// Packs the fixed numeric fields of one entity/player record into a fresh 23-byte Uint8Array.
|
|
29
|
+
// flags: caller-supplied bitfield (onGround/sleeping/etc for players/entities respectively).
|
|
30
|
+
export function packBinRecord(px, py, pz, qrot, vx, vy, vz, sx, sy, sz, flags) {
|
|
31
|
+
const buf = new Uint8Array(BIN_RECORD_BYTES)
|
|
32
|
+
const dv = new DataView(buf.buffer)
|
|
33
|
+
dv.setInt16(0, clampI16(px), true); dv.setInt16(2, clampI16(py), true); dv.setInt16(4, clampI16(pz), true)
|
|
34
|
+
dv.setInt16(6, clampI16(vx), true); dv.setInt16(8, clampI16(vy), true); dv.setInt16(10, clampI16(vz), true)
|
|
35
|
+
dv.setUint32(12, qrot >>> 0, true)
|
|
36
|
+
dv.setUint16(16, clampU16Scale(sx), true); dv.setUint16(18, clampU16Scale(sy), true); dv.setUint16(20, clampU16Scale(sz), true)
|
|
37
|
+
dv.setUint8(22, flags & 0xFF)
|
|
38
|
+
return buf
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function unpackBinRecord(buf, out) {
|
|
42
|
+
const dv = buf instanceof DataView ? buf : new DataView(buf.buffer, buf.byteOffset, buf.byteLength)
|
|
43
|
+
out.px = dv.getInt16(0, true) / Q1; out.py = dv.getInt16(2, true) / Q1; out.pz = dv.getInt16(4, true) / Q1
|
|
44
|
+
out.vx = dv.getInt16(6, true) / Q1; out.vy = dv.getInt16(8, true) / Q1; out.vz = dv.getInt16(10, true) / Q1
|
|
45
|
+
out.qrot = dv.getUint32(12, true)
|
|
46
|
+
out.sx = dv.getUint16(16, true) / Q1; out.sy = dv.getUint16(18, true) / Q1; out.sz = dv.getUint16(20, true) / Q1
|
|
47
|
+
out.flags = dv.getUint8(22)
|
|
48
|
+
return out
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function packQuat(rx, ry, rz, rw) {
|
|
52
|
+
const arx = Math.abs(rx), ary = Math.abs(ry), arz = Math.abs(rz), arw = Math.abs(rw)
|
|
53
|
+
let maxIdx = 0, maxAbs = arx
|
|
54
|
+
if (ary > maxAbs) { maxIdx = 1; maxAbs = ary }
|
|
55
|
+
if (arz > maxAbs) { maxIdx = 2; maxAbs = arz }
|
|
56
|
+
if (arw > maxAbs) { maxIdx = 3; maxAbs = arw }
|
|
57
|
+
const mval = maxIdx === 0 ? rx : maxIdx === 1 ? ry : maxIdx === 2 ? rz : rw
|
|
58
|
+
const sign = mval < 0 ? -1 : 1
|
|
59
|
+
let packed = maxIdx
|
|
60
|
+
if (maxIdx !== 0) packed = (packed << 10) | Math.max(0, Math.min(1022, Math.round((rx * sign + Math.SQRT1_2) * QSCALE)))
|
|
61
|
+
if (maxIdx !== 1) packed = (packed << 10) | Math.max(0, Math.min(1022, Math.round((ry * sign + Math.SQRT1_2) * QSCALE)))
|
|
62
|
+
if (maxIdx !== 2) packed = (packed << 10) | Math.max(0, Math.min(1022, Math.round((rz * sign + Math.SQRT1_2) * QSCALE)))
|
|
63
|
+
if (maxIdx !== 3) packed = (packed << 10) | Math.max(0, Math.min(1022, Math.round((rw * sign + Math.SQRT1_2) * QSCALE)))
|
|
64
|
+
return packed >>> 0
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Unrolled per maxIdx branch (was a QUAT_IDX[] lookup + generic loop over `indices`) -- this runs
|
|
68
|
+
// once per entity/player per snapshot, client-side, the hottest per-frame decode path, so the extra
|
|
69
|
+
// array indirection through QUAT_IDX plus a 3-iteration loop with a data-dependent out-index write
|
|
70
|
+
// is worth trading for 4 flat, branch-predictable unpacks. Each branch reads the same three 10-bit
|
|
71
|
+
// fields off `packed` in the same bit order (most-significant first, j=2..0) as the original loop,
|
|
72
|
+
// just with the literal QUAT_IDX[maxIdx] destination slots inlined instead of indexed.
|
|
73
|
+
export function unpackQuat(packed, out) {
|
|
74
|
+
const maxIdx = (packed >>> 30) & 0x3
|
|
75
|
+
const c2 = (packed & 0x3FF) / QSCALE - Math.SQRT1_2; packed = packed >>> 10
|
|
76
|
+
const c1 = (packed & 0x3FF) / QSCALE - Math.SQRT1_2; packed = packed >>> 10
|
|
77
|
+
const c0 = (packed & 0x3FF) / QSCALE - Math.SQRT1_2
|
|
78
|
+
const sumSq = c0 * c0 + c1 * c1 + c2 * c2
|
|
79
|
+
const m = Math.sqrt(Math.max(0, 1 - sumSq))
|
|
80
|
+
switch (maxIdx) {
|
|
81
|
+
case 0: out[1] = c0; out[2] = c1; out[3] = c2; out[0] = m; break
|
|
82
|
+
case 1: out[0] = c0; out[2] = c1; out[3] = c2; out[1] = m; break
|
|
83
|
+
case 2: out[0] = c0; out[1] = c1; out[3] = c2; out[2] = m; break
|
|
84
|
+
default: out[0] = c0; out[1] = c1; out[2] = c2; out[3] = m; break
|
|
85
|
+
}
|
|
86
|
+
return out
|
|
87
|
+
}
|
|
@@ -1,90 +1,16 @@
|
|
|
1
1
|
import { getComponentSchema, encodeCustomFields, decodeCustomFields } from '../../apps/_lib/ComponentSchema.js'
|
|
2
|
+
import {
|
|
3
|
+
BIN_RECORD_BYTES, POS_I16_MAX, SCALE_U16_MAX, clampI16, clampU16Scale,
|
|
4
|
+
packBinRecord, unpackBinRecord, packQuat, unpackQuat
|
|
5
|
+
} from './SnapshotBinFormat.js'
|
|
6
|
+
|
|
7
|
+
// Re-exported from SnapshotBinFormat.js for backward compatibility -- AnimationClipCache.js,
|
|
8
|
+
// TickHandler.js, and edge/cf-do/do-client-probe.mjs all import these from this file's own path.
|
|
9
|
+
export { unpackBinRecord, packQuat, unpackQuat }
|
|
2
10
|
|
|
3
|
-
const Q1=100
|
|
4
11
|
const TAU = 2 * Math.PI, HALF_PI = Math.PI / 2
|
|
5
12
|
const VEL_ZERO = [0,0,0]
|
|
6
13
|
const SCALE_ONE = [1,1,1]
|
|
7
|
-
const QSCALE = 511 * Math.SQRT2
|
|
8
|
-
|
|
9
|
-
// --- Binary numeric record (DataView, replaces JS-array-through-msgpackr for the fixed numeric
|
|
10
|
-
// fields: position/velocity/rotation/scale). id/model/bodyType/custom stay as native JS values
|
|
11
|
-
// alongside this buffer -- they are variable-shape (strings, arbitrary objects) and packing them
|
|
12
|
-
// into a fixed byte layout would be strictly worse (lossy or unbounded), not a real win. Position/
|
|
13
|
-
// velocity are int16 at the existing Q1=100 (1cm) scale, clamped to +-327.67m -- entity/player
|
|
14
|
-
// positions are always encoded relative to a region/session-local origin already (no planetary
|
|
15
|
-
// float32 range concern here; see AGENTS.md floating-origin-camera-relative-rendering row for the
|
|
16
|
-
// separate concern of >10km world coordinates, which is a rendering-layer issue, not a wire-format
|
|
17
|
-
// one). Scale uses uint16 unsigned at the same Q1 scale (0..655.35, entities are never negatively
|
|
18
|
-
// scaled). Rotation reuses packQuat's existing 32-bit packed representation verbatim -- not
|
|
19
|
-
// reinvented. Fixed player/entity record is 23 bytes: 3*i16 pos + 3*i16 vel + u32 quat + 3*u16
|
|
20
|
-
// scale + 1 flags byte = 6+6+4+6+1 = 23.
|
|
21
|
-
const BIN_RECORD_BYTES = 23
|
|
22
|
-
const POS_I16_MAX = 32767 / Q1 // 327.67
|
|
23
|
-
const SCALE_U16_MAX = 65535 / Q1 // 655.35
|
|
24
|
-
|
|
25
|
-
function clampI16(v) { return Math.max(-32767, Math.min(32767, Math.round((v || 0) * Q1))) }
|
|
26
|
-
function clampU16Scale(v) { return Math.max(0, Math.min(65535, Math.round((v ?? 1) * Q1))) }
|
|
27
|
-
|
|
28
|
-
// Packs the fixed numeric fields of one entity/player record into a fresh 23-byte Uint8Array.
|
|
29
|
-
// flags: caller-supplied bitfield (onGround/sleeping/etc for players/entities respectively).
|
|
30
|
-
function packBinRecord(px, py, pz, qrot, vx, vy, vz, sx, sy, sz, flags) {
|
|
31
|
-
const buf = new Uint8Array(BIN_RECORD_BYTES)
|
|
32
|
-
const dv = new DataView(buf.buffer)
|
|
33
|
-
dv.setInt16(0, clampI16(px), true); dv.setInt16(2, clampI16(py), true); dv.setInt16(4, clampI16(pz), true)
|
|
34
|
-
dv.setInt16(6, clampI16(vx), true); dv.setInt16(8, clampI16(vy), true); dv.setInt16(10, clampI16(vz), true)
|
|
35
|
-
dv.setUint32(12, qrot >>> 0, true)
|
|
36
|
-
dv.setUint16(16, clampU16Scale(sx), true); dv.setUint16(18, clampU16Scale(sy), true); dv.setUint16(20, clampU16Scale(sz), true)
|
|
37
|
-
dv.setUint8(22, flags & 0xFF)
|
|
38
|
-
return buf
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function unpackBinRecord(buf, out) {
|
|
42
|
-
const dv = buf instanceof DataView ? buf : new DataView(buf.buffer, buf.byteOffset, buf.byteLength)
|
|
43
|
-
out.px = dv.getInt16(0, true) / Q1; out.py = dv.getInt16(2, true) / Q1; out.pz = dv.getInt16(4, true) / Q1
|
|
44
|
-
out.vx = dv.getInt16(6, true) / Q1; out.vy = dv.getInt16(8, true) / Q1; out.vz = dv.getInt16(10, true) / Q1
|
|
45
|
-
out.qrot = dv.getUint32(12, true)
|
|
46
|
-
out.sx = dv.getUint16(16, true) / Q1; out.sy = dv.getUint16(18, true) / Q1; out.sz = dv.getUint16(20, true) / Q1
|
|
47
|
-
out.flags = dv.getUint8(22)
|
|
48
|
-
return out
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function packQuat(rx, ry, rz, rw) {
|
|
52
|
-
const arx = Math.abs(rx), ary = Math.abs(ry), arz = Math.abs(rz), arw = Math.abs(rw)
|
|
53
|
-
let maxIdx = 0, maxAbs = arx
|
|
54
|
-
if (ary > maxAbs) { maxIdx = 1; maxAbs = ary }
|
|
55
|
-
if (arz > maxAbs) { maxIdx = 2; maxAbs = arz }
|
|
56
|
-
if (arw > maxAbs) { maxIdx = 3; maxAbs = arw }
|
|
57
|
-
const mval = maxIdx === 0 ? rx : maxIdx === 1 ? ry : maxIdx === 2 ? rz : rw
|
|
58
|
-
const sign = mval < 0 ? -1 : 1
|
|
59
|
-
let packed = maxIdx
|
|
60
|
-
if (maxIdx !== 0) packed = (packed << 10) | Math.max(0, Math.min(1022, Math.round((rx * sign + Math.SQRT1_2) * QSCALE)))
|
|
61
|
-
if (maxIdx !== 1) packed = (packed << 10) | Math.max(0, Math.min(1022, Math.round((ry * sign + Math.SQRT1_2) * QSCALE)))
|
|
62
|
-
if (maxIdx !== 2) packed = (packed << 10) | Math.max(0, Math.min(1022, Math.round((rz * sign + Math.SQRT1_2) * QSCALE)))
|
|
63
|
-
if (maxIdx !== 3) packed = (packed << 10) | Math.max(0, Math.min(1022, Math.round((rw * sign + Math.SQRT1_2) * QSCALE)))
|
|
64
|
-
return packed >>> 0
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Unrolled per maxIdx branch (was a QUAT_IDX[] lookup + generic loop over `indices`) -- this runs
|
|
68
|
-
// once per entity/player per snapshot, client-side, the hottest per-frame decode path, so the extra
|
|
69
|
-
// array indirection through QUAT_IDX plus a 3-iteration loop with a data-dependent out-index write
|
|
70
|
-
// is worth trading for 4 flat, branch-predictable unpacks. Each branch reads the same three 10-bit
|
|
71
|
-
// fields off `packed` in the same bit order (most-significant first, j=2..0) as the original loop,
|
|
72
|
-
// just with the literal QUAT_IDX[maxIdx] destination slots inlined instead of indexed.
|
|
73
|
-
export function unpackQuat(packed, out) {
|
|
74
|
-
const maxIdx = (packed >>> 30) & 0x3
|
|
75
|
-
const c2 = (packed & 0x3FF) / QSCALE - Math.SQRT1_2; packed = packed >>> 10
|
|
76
|
-
const c1 = (packed & 0x3FF) / QSCALE - Math.SQRT1_2; packed = packed >>> 10
|
|
77
|
-
const c0 = (packed & 0x3FF) / QSCALE - Math.SQRT1_2
|
|
78
|
-
const sumSq = c0 * c0 + c1 * c1 + c2 * c2
|
|
79
|
-
const m = Math.sqrt(Math.max(0, 1 - sumSq))
|
|
80
|
-
switch (maxIdx) {
|
|
81
|
-
case 0: out[1] = c0; out[2] = c1; out[3] = c2; out[0] = m; break
|
|
82
|
-
case 1: out[0] = c0; out[2] = c1; out[3] = c2; out[1] = m; break
|
|
83
|
-
case 2: out[0] = c0; out[1] = c1; out[3] = c2; out[2] = m; break
|
|
84
|
-
default: out[0] = c0; out[1] = c1; out[2] = c2; out[3] = m; break
|
|
85
|
-
}
|
|
86
|
-
return out
|
|
87
|
-
}
|
|
88
14
|
|
|
89
15
|
// p[12]: 8 bits pitch (range [-pi/2,pi/2]) + 8 bits yaw (range [0,2pi)) packed into one uint16
|
|
90
16
|
// enc layout (unchanged JS-value slots kept native; numeric fixed fields moved into enc[1] as a
|