las-laz-js 0.3.0 → 0.4.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/src/laz/pool.js CHANGED
@@ -1,13 +1,14 @@
1
1
  // src/laz/pool.js
2
2
  //==========================================================
3
- // A pool of workers that decode LAZ chunks in parallel (chunks are
4
- // independent, so N cores decode about N times as fast). Browser module
5
- // Workers or Node worker_threads, running src/laz/chunkWorker.js. Tasks
6
- // queue and go to the next free worker; results come back as transferred
7
- // buffers. close() terminates every worker and fails what is still
8
- // queued, so nothing outlives a read. When workers cannot start (no
9
- // Worker, a blocked script), createLazPool returns null and the reader
10
- // decodes on its own thread.
3
+ // A pool of workers that decode and compress LAZ chunks in parallel
4
+ // (chunks are independent, so N cores do about N times the work).
5
+ // Browser module Workers or Node worker_threads, running
6
+ // src/laz/chunkWorker.js. Tasks queue and go to the next free worker;
7
+ // their buffers go there, and the results come back, transferred.
8
+ // close() terminates every worker and fails what is still queued, so
9
+ // nothing outlives a read or a write. When workers cannot start (no
10
+ // Worker, a blocked or failing script), createLazPool returns null and
11
+ // the reader or writer works on its own thread.
11
12
  //
12
13
  // Uses the chunk decoder translated from laz-rs (https://github.com/laz-rs/laz-rs),
13
14
  // itself a port of LASzip by rapidlasso. (c) 2007-2015 martin isenburg,
@@ -23,7 +24,7 @@ var NODE_OS = ["node", "os"].join(":");
23
24
  var MAX_THREADS = 16;
24
25
 
25
26
  /**
26
- * How many decode workers to use by default: one fewer than the cores, at most 16.
27
+ * How many LAZ workers to use by default: one fewer than the cores, at most 16.
27
28
  * @returns {Promise<number>}
28
29
  */
29
30
  export async function defaultThreads() {
@@ -38,33 +39,57 @@ export async function defaultThreads() {
38
39
  return Math.max(1, Math.min(MAX_THREADS, cores - 1));
39
40
  }
40
41
 
41
- // One worker, browser or Node, behind the same small interface
42
- async function spawn(onMessage, onFail) {
42
+ // Node worker options: the parent's flags are inherited, except --input-type
43
+ // (node --input-type=module -e passes it on, and a worker has no string input
44
+ // for it). Only then is execArgv given: an explicit one is checked flag by
45
+ // flag, and some parents (the test runner) hold flags a worker may not take.
46
+ function nodeWorkerOptions(argv) {
47
+ var isInputType = function (a, i) { return /^--input-type(=|$)/.test(a) || argv[i - 1] === "--input-type"; };
48
+ if (!argv.some(isInputType)) return undefined;
49
+ return { execArgv: argv.filter(function (a, i) { return !isInputType(a, i); }) };
50
+ }
51
+
52
+ // One worker, browser or Node, behind the same small interface. Resolves once
53
+ // the worker says it is ready; rejects if it fails or exits first (a blocked
54
+ // or broken script), so a pool that cannot start is known before any task.
55
+ async function spawn(onMessage, onFail, nodeUrl) {
56
+ var started = false, ready, failed;
57
+ var up = new Promise(function (res, rej) { ready = res; failed = rej; });
58
+ function message(m) { if (m && m.ready) { started = true; ready(); } else onMessage(m); }
59
+ function error(e) { if (started) onFail(e); else failed(e); }
60
+ var handle;
43
61
  if (typeof Worker === "function") {
44
62
  // written inline so bundlers (Vite, webpack 5) see the worker and emit it
45
63
  var w = new Worker(new URL("./chunkWorker.js", import.meta.url), { type: "module" });
46
- w.onmessage = function (e) { onMessage(e.data); };
47
- w.onerror = function (e) { if (e && e.preventDefault) e.preventDefault(); onFail(new Error(e && e.message ? e.message : "LAZ worker failed")); };
48
- return { post: function (m, t) { w.postMessage(m, t); }, close: function () { w.terminate(); } };
64
+ w.onmessage = function (e) { message(e.data); };
65
+ w.onerror = function (e) { if (e && e.preventDefault) e.preventDefault(); error(new Error(e && e.message ? e.message : "LAZ worker failed")); };
66
+ handle = { post: function (m, t) { w.postMessage(m, t); }, close: function () { w.terminate(); } };
67
+ } else {
68
+ var wt = await import(/* webpackIgnore: true */ /* @vite-ignore */ NODE_WORKER_THREADS);
69
+ var nw = new wt.Worker(nodeUrl || new URL("./chunkWorker.js", import.meta.url), nodeWorkerOptions(process.execArgv));
70
+ nw.on("message", message);
71
+ nw.on("error", error);
72
+ nw.on("exit", function (code) { if (!started) failed(new Error("LAZ worker exited (" + code + ") before it started")); });
73
+ // Node runs while a worker has a task and not while it is idle, so a forgotten
74
+ // pool never keeps it running (unref after the "message" listener, which refs again)
75
+ handle = { post: function (m, t) { nw.postMessage(m, t); }, close: function () { return nw.terminate(); }, hold: function () { nw.ref(); }, release: function () { nw.unref(); } };
49
76
  }
50
- var wt = await import(/* webpackIgnore: true */ /* @vite-ignore */ NODE_WORKER_THREADS);
51
- var nw = new wt.Worker(new URL("./chunkWorker.js", import.meta.url));
52
- nw.unref(); // a forgotten pool never keeps Node running
53
- nw.on("message", onMessage);
54
- nw.on("error", onFail);
55
- return { post: function (m, t) { nw.postMessage(m, t); }, close: function () { return nw.terminate(); } };
77
+ try { await up; } catch (e) { handle.close(); throw e; }
78
+ if (handle.release) handle.release();
79
+ return handle;
56
80
  }
57
81
 
58
82
  /**
59
83
  * @param {number} n - workers to start (2 or more)
60
- * @returns {Promise<{size:number, decode:function(object): Promise<Uint8Array>, close:function(): void}|null>}
84
+ * @param {URL} [nodeUrl] - a Node worker entry in place of chunkWorker.js (tests)
85
+ * @returns {Promise<{size:number, decode:function(object): Promise<Uint8Array>, compress:function(object): Promise<{bytes:Uint8Array, sizes:number[]}>, close:function(): void}|null>}
61
86
  */
62
- export async function createLazPool(n) {
87
+ export async function createLazPool(n, nodeUrl) {
63
88
  var workers = [], idle = [], queue = [], waiting = new Map(), nextId = 1, closed = false;
64
89
 
65
90
  function fail(err) {
66
91
  // a worker that dies takes the pool with it: every waiting task fails
67
- var e = err instanceof LasError ? err : new LasError(ERROR_CODES.UNSUPPORTED, "a LAZ decode worker failed: " + (err && err.message ? err.message : String(err)));
92
+ var e = err instanceof LasError ? err : new LasError(ERROR_CODES.UNSUPPORTED, "a LAZ worker failed: " + (err && err.message ? err.message : String(err)));
68
93
  waiting.forEach(function (job) { job.reject(e); });
69
94
  waiting.clear();
70
95
  queue.forEach(function (job) { job.reject(e); });
@@ -76,7 +101,8 @@ export async function createLazPool(n) {
76
101
  var w = idle.pop(), job = queue.shift();
77
102
  job.worker = w;
78
103
  waiting.set(job.msg.id, job);
79
- w.post(job.msg, [job.msg.packed]);
104
+ if (w.hold) w.hold();
105
+ w.post(job.msg, job.transfer);
80
106
  }
81
107
  }
82
108
 
@@ -84,20 +110,32 @@ export async function createLazPool(n) {
84
110
  var job = waiting.get(m.id);
85
111
  if (!job) return;
86
112
  waiting.delete(m.id);
113
+ if (job.worker.release) job.worker.release();
87
114
  idle.push(job.worker);
88
115
  if (m.error) job.reject(new LasError(m.error.code || ERROR_CODES.UNSUPPORTED, m.error.message, m.error.offset));
89
- else job.resolve(new Uint8Array(m.records));
116
+ else job.resolve(job.result(m));
90
117
  pump();
91
118
  }
92
119
 
93
- try {
94
- for (var i = 0; i < n; i++) workers.push(await spawn(onMessage, fail));
95
- } catch (e) {
120
+ var starts = [];
121
+ for (var i = 0; i < n; i++) starts.push(spawn(onMessage, fail, nodeUrl));
122
+ var settled = await Promise.allSettled(starts);
123
+ workers = settled.filter(function (s) { return s.status === "fulfilled"; }).map(function (s) { return s.value; });
124
+ if (workers.length < n) {
125
+ // any worker that cannot start means the reader decodes on its own thread
96
126
  workers.forEach(function (w) { w.close(); });
97
127
  return null;
98
128
  }
99
129
  idle = workers.slice();
100
130
 
131
+ function run(msg, transfer, result) {
132
+ if (closed) return Promise.reject(new LasError(ERROR_CODES.ABORTED, "the LAZ pool is closed"));
133
+ return new Promise(function (resolve, reject) {
134
+ queue.push({ msg: Object.assign({ id: nextId++ }, msg), transfer: transfer, result: result, resolve: resolve, reject: reject });
135
+ pump();
136
+ });
137
+ }
138
+
101
139
  return {
102
140
  size: n,
103
141
  /**
@@ -106,16 +144,21 @@ export async function createLazPool(n) {
106
144
  * @returns {Promise<Uint8Array>} the chunks' raw LAS records, in order
107
145
  */
108
146
  decode: function (task) {
109
- if (closed) return Promise.reject(new LasError(ERROR_CODES.ABORTED, "the LAZ decode pool is closed"));
110
- return new Promise(function (resolve, reject) {
111
- queue.push({ msg: Object.assign({ id: nextId++ }, task), resolve: resolve, reject: reject });
112
- pump();
113
- });
147
+ return run(task, [task.packed], function (m) { return new Uint8Array(m.records); });
148
+ },
149
+ /**
150
+ * Compress whole chunks from their own copy of the raw records (transferred).
151
+ * @param {{vlr:object, recordLength:number, records:ArrayBuffer, counts:number[]}} task
152
+ * counts: each chunk's points, the chunks back to back from the start of records
153
+ * @returns {Promise<{bytes:Uint8Array, sizes:number[]}>} the chunks, back to back, and each one's length
154
+ */
155
+ compress: function (task) {
156
+ return run(Object.assign({ op: "compress" }, task), [task.records], function (m) { return { bytes: new Uint8Array(m.chunks), sizes: m.sizes }; });
114
157
  },
115
158
  close: function () {
116
159
  if (closed) return;
117
160
  closed = true;
118
- fail(new LasError(ERROR_CODES.ABORTED, "the read stopped"));
161
+ fail(new LasError(ERROR_CODES.ABORTED, "the LAZ pool was closed"));
119
162
  workers.forEach(function (w) { w.close(); });
120
163
  }
121
164
  };
@@ -8,19 +8,23 @@
8
8
  // itself a port of LASzip by rapidlasso. (c) 2007-2014 martin isenburg,
9
9
  // rapidlasso; (c) 2014 Uday Verma, Hobu, Inc.; (c) 2019 Thomas Montaigu.
10
10
  // Licensed under the Apache License, Version 2.0 (see LICENSE-APACHE and
11
- // NOTICE). Changes: translated from Rust to JavaScript, Brent Buffham 2026.
11
+ // NOTICE). Changes: translated from Rust to JavaScript, Brent Buffham 2026;
12
+ // the compressor (ByteCompressor) added beside the decompressor.
12
13
  //==========================================================
13
14
 
14
15
  import { ArithmeticModel } from "../models.js";
15
16
 
16
- /** Byte v2 decompressor for `count` extra bytes: one per chunk. */
17
- export function ByteDecompressor(count) {
18
- this.size = count;
19
- this.last = new Uint8Array(count);
20
- this.models = [];
21
- for (var i = 0; i < count; i++) this.models.push(new ArithmeticModel(256));
17
+ // The state and models both halves start a chunk with: the last bytes and one model per byte
18
+ function initByte(self, count) {
19
+ self.size = count;
20
+ self.last = new Uint8Array(count);
21
+ self.models = [];
22
+ for (var i = 0; i < count; i++) self.models.push(new ArithmeticModel(256));
22
23
  }
23
24
 
25
+ /** Byte v2 decompressor for `count` extra bytes: one per chunk. */
26
+ export function ByteDecompressor(count) { initByte(this, count); }
27
+
24
28
  ByteDecompressor.prototype.first = function (src, s, view, o) {
25
29
  for (var i = 0; i < this.size; i++) { this.last[i] = src[s + i]; view.setUint8(o + i, src[s + i]); }
26
30
  };
@@ -31,3 +35,15 @@ ByteDecompressor.prototype.next = function (dec, view, o) {
31
35
  view.setUint8(o + i, this.last[i]);
32
36
  }
33
37
  };
38
+
39
+ /** Byte v2 compressor for `count` extra bytes: one per chunk, the mirror of ByteDecompressor. */
40
+ export function ByteCompressor(count) { initByte(this, count); }
41
+
42
+ ByteCompressor.prototype.first = function (src, s) { this.last.set(src.subarray(s, s + this.size)); };
43
+
44
+ ByteCompressor.prototype.next = function (enc, src, s) {
45
+ for (var i = 0; i < this.size; i++) {
46
+ enc.encodeSymbol(this.models[i], (src[s + i] - this.last[i]) & 255); // u8 wrapping_sub
47
+ this.last[i] = src[s + i];
48
+ }
49
+ };
@@ -12,11 +12,14 @@
12
12
  // rapidlasso; (c) 2014 Uday Verma, Hobu, Inc.; (c) 2019 Thomas Montaigu.
13
13
  // Licensed under the Apache License, Version 2.0 (see LICENSE-APACHE and
14
14
  // NOTICE). Changes: translated from Rust to JavaScript, i64 as two u32
15
- // halves, Brent Buffham 2026.
15
+ // halves, Brent Buffham 2026; the compressor (GpsTime11Compressor) added
16
+ // beside the decompressor, rounding its multiplier as LAZperf does; its
17
+ // multiplier and 64-bit difference shared with the Point14 compressor.
16
18
  //==========================================================
17
19
 
18
20
  import { ArithmeticModel } from "../models.js";
19
21
  import { IntegerDecompressor } from "../IntegerDecompressor.js";
22
+ import { IntegerCompressor } from "../IntegerCompressor.js";
20
23
 
21
24
  var LE = true;
22
25
  var MULTI = 500, MULTI_MINUS = -10;
@@ -24,18 +27,39 @@ var MULTI_UNCHANGED = MULTI - MULTI_MINUS + 1; // 511
24
27
  var MULTI_CODE_FULL = MULTI - MULTI_MINUS + 2; // 512
25
28
  var MULTI_TOTAL = MULTI - MULTI_MINUS + 6; // 516
26
29
 
30
+ // The state and models both halves start a chunk with; IC is the integer coder
31
+ // (IntegerCompressor or IntegerDecompressor)
32
+ function initGps(self, IC) {
33
+ self.size = 8;
34
+ self.multiModel = new ArithmeticModel(MULTI_TOTAL);
35
+ self.zeroDiffModel = new ArithmeticModel(6);
36
+ self.ic = new IC(32, 9);
37
+ self.last = 0;
38
+ self.nextSeq = 0;
39
+ self.hi = new Int32Array(4);
40
+ self.lo = new Float64Array(4); // u32 values, held exactly
41
+ self.diffs = new Int32Array(4);
42
+ self.extremes = new Int32Array(4);
43
+ }
44
+
45
+ // A full time (hi, lo) starts the next of the four sequences, with no difference yet
46
+ function startSequence(self, hi, lo) {
47
+ self.nextSeq = (self.nextSeq + 1) & 3;
48
+ self.hi[self.nextSeq] = hi;
49
+ self.lo[self.nextSeq] = lo;
50
+ self.last = self.nextSeq;
51
+ self.diffs[self.last] = 0;
52
+ self.extremes[self.last] = 0;
53
+ }
54
+
55
+ // After three extreme differences in a row, the latest becomes the new reference
56
+ function extreme(self, d) {
57
+ if (++self.extremes[self.last] > 3) { self.diffs[self.last] = d; self.extremes[self.last] = 0; }
58
+ }
59
+
27
60
  /** GpsTime11 v2 decompressor: one per chunk. */
28
61
  export function GpsTime11Decompressor() {
29
- this.size = 8;
30
- this.multiModel = new ArithmeticModel(MULTI_TOTAL);
31
- this.zeroDiffModel = new ArithmeticModel(6);
32
- this.ic = new IntegerDecompressor(32, 9);
33
- this.last = 0;
34
- this.nextSeq = 0;
35
- this.hi = new Int32Array(4);
36
- this.lo = new Float64Array(4); // u32 values, held exactly
37
- this.diffs = new Int32Array(4);
38
- this.extremes = new Int32Array(4);
62
+ initGps(this, IntegerDecompressor);
39
63
  }
40
64
 
41
65
  GpsTime11Decompressor.prototype._add = function (i, d) {
@@ -46,17 +70,8 @@ GpsTime11Decompressor.prototype._add = function (i, d) {
46
70
  };
47
71
 
48
72
  GpsTime11Decompressor.prototype._full = function (dec) {
49
- this.nextSeq = (this.nextSeq + 1) & 3;
50
- this.hi[this.nextSeq] = this.ic.decompress(dec, this.hi[this.last], 8);
51
- this.lo[this.nextSeq] = dec.readInt();
52
- this.last = this.nextSeq;
53
- this.diffs[this.last] = 0;
54
- this.extremes[this.last] = 0;
55
- };
56
-
57
- // After three extreme differences in a row, the latest becomes the new reference
58
- GpsTime11Decompressor.prototype._extreme = function (d) {
59
- if (++this.extremes[this.last] > 3) { this.diffs[this.last] = d; this.extremes[this.last] = 0; }
73
+ var hi = this.ic.decompress(dec, this.hi[this.last], 8);
74
+ startSequence(this, hi, dec.readInt());
60
75
  };
61
76
 
62
77
  GpsTime11Decompressor.prototype.first = function (src, s, view, o) {
@@ -88,19 +103,19 @@ GpsTime11Decompressor.prototype.next = function (dec, view, o) {
88
103
  } else if (multi < MULTI_UNCHANGED) {
89
104
  if (multi === 0) {
90
105
  d = this.ic.decompress(dec, 0, 7);
91
- this._extreme(d);
106
+ extreme(this, d);
92
107
  } else if (multi < MULTI) {
93
108
  d = this.ic.decompress(dec, Math.imul(multi, this.diffs[L]), multi < 10 ? 2 : 3);
94
109
  } else if (multi === MULTI) {
95
110
  d = this.ic.decompress(dec, Math.imul(MULTI, this.diffs[L]), 4);
96
- this._extreme(d);
111
+ extreme(this, d);
97
112
  } else {
98
113
  multi = MULTI - multi;
99
114
  if (multi > MULTI_MINUS) {
100
115
  d = this.ic.decompress(dec, Math.imul(multi, this.diffs[L]), 5);
101
116
  } else {
102
117
  d = this.ic.decompress(dec, Math.imul(MULTI_MINUS, this.diffs[L]), 6);
103
- this._extreme(d);
118
+ extreme(this, d);
104
119
  }
105
120
  }
106
121
  this._add(L, d);
@@ -116,3 +131,128 @@ GpsTime11Decompressor.prototype.next = function (dec, view, o) {
116
131
  view.setUint32(o, this.lo[this.last], LE);
117
132
  view.setInt32(o + 4, this.hi[this.last], LE);
118
133
  };
134
+
135
+ // The multiplier between this and the last difference, as LAZperf rounds it:
136
+ // std::round of the f32 quotient (half away from zero), then (int). LASzip and
137
+ // laz-rs add 0.5f in f32 instead, which differs only within an f32 step of a
138
+ // half; the fixtures' oracle is LAZperf. The one quotient past i32 (2^31, a last
139
+ // difference of +-1) converts as x86 does, to -2^31. Either choice decodes the same.
140
+ export function quantizeMulti(curr, last) {
141
+ var q = Math.fround(Math.fround(curr) / Math.fround(last));
142
+ var m = q >= 0 ? Math.floor(q + 0.5) : -Math.floor(0.5 - q);
143
+ return m > 2147483647 ? -2147483648 : m;
144
+ }
145
+
146
+ // A DataView over the caller's records, made once per buffer
147
+ function viewOf(item, src) {
148
+ if (item._src !== src) { item._src = src; item._view = new DataView(src.buffer, src.byteOffset, src.byteLength); }
149
+ return item._view;
150
+ }
151
+
152
+ /** GpsTime11 v2 compressor: one per chunk, the mirror of GpsTime11Decompressor. */
153
+ export function GpsTime11Compressor() {
154
+ initGps(this, IntegerCompressor);
155
+ this._src = null;
156
+ this._view = null;
157
+ }
158
+
159
+
160
+ // (hi, lo) - (hi0, lo0) as an i32, or null when the 64-bit difference does not fit
161
+ // (hi signed, lo unsigned; shared with the Point14 compressor's GPS time)
162
+ export function diff32(hi, lo, hi0, lo0) {
163
+ var dl = lo - lo0, dh = hi - hi0;
164
+ if (dl < 0) { dl += 4294967296; dh -= 1; }
165
+ dh = dh | 0; // the high word of the i64 wrapping_sub
166
+ if (dh === 0 && dl < 2147483648) return dl;
167
+ if (dh === -1 && dl >= 2147483648) return dl - 4294967296;
168
+ return null;
169
+ }
170
+
171
+ // (hi, lo) minus sequence i as an i32, or null when the 64-bit difference does not fit
172
+ GpsTime11Compressor.prototype._diff = function (i, hi, lo) { return diff32(hi, lo, this.hi[i], this.lo[i]); };
173
+
174
+ // The first other sequence (1-3 on from the last) this time is within i32 of, or 0
175
+ GpsTime11Compressor.prototype._other = function (hi, lo) {
176
+ for (var i = 1; i < 4; i++) if (this._diff((this.last + i) & 3, hi, lo) !== null) return i;
177
+ return 0;
178
+ };
179
+
180
+ GpsTime11Compressor.prototype._full = function (enc, hi, lo) {
181
+ this.ic.compress(enc, this.hi[this.last], hi, 8);
182
+ enc.writeInt(lo);
183
+ startSequence(this, hi, lo);
184
+ };
185
+
186
+ GpsTime11Compressor.prototype.first = function (src, s) {
187
+ var v = viewOf(this, src);
188
+ this.lo[0] = v.getUint32(s, LE);
189
+ this.hi[0] = v.getInt32(s + 4, LE);
190
+ };
191
+
192
+ GpsTime11Compressor.prototype.next = function (enc, src, s) {
193
+ var v = viewOf(this, src);
194
+ var lo = v.getUint32(s, LE), hi = v.getInt32(s + 4, LE);
195
+ for (;;) {
196
+ var L = this.last, d, j, multi;
197
+ if (hi === this.hi[L] && lo === this.lo[L]) {
198
+ if (this.diffs[L] === 0) enc.encodeSymbol(this.zeroDiffModel, 0);
199
+ else enc.encodeSymbol(this.multiModel, MULTI_UNCHANGED);
200
+ return;
201
+ }
202
+ d = this._diff(L, hi, lo);
203
+ if (this.diffs[L] === 0) {
204
+ if (d !== null) {
205
+ enc.encodeSymbol(this.zeroDiffModel, 1);
206
+ this.ic.compress(enc, 0, d, 0);
207
+ this.diffs[L] = d;
208
+ this.extremes[L] = 0;
209
+ } else if ((j = this._other(hi, lo))) {
210
+ enc.encodeSymbol(this.zeroDiffModel, j + 2);
211
+ this.last = (L + j) & 3;
212
+ continue; // laz-rs recurses: code again against the new sequence
213
+ } else {
214
+ enc.encodeSymbol(this.zeroDiffModel, 2);
215
+ this._full(enc, hi, lo);
216
+ }
217
+ } else if (d !== null) {
218
+ multi = quantizeMulti(d, this.diffs[L]);
219
+ if (multi === 1) {
220
+ enc.encodeSymbol(this.multiModel, 1);
221
+ this.ic.compress(enc, this.diffs[L], d, 1);
222
+ this.extremes[L] = 0;
223
+ } else if (multi > 0) {
224
+ if (multi < MULTI) {
225
+ enc.encodeSymbol(this.multiModel, multi);
226
+ this.ic.compress(enc, Math.imul(multi, this.diffs[L]), d, multi < 10 ? 2 : 3);
227
+ } else {
228
+ enc.encodeSymbol(this.multiModel, MULTI);
229
+ this.ic.compress(enc, Math.imul(MULTI, this.diffs[L]), d, 4);
230
+ extreme(this, d);
231
+ }
232
+ } else if (multi < 0) {
233
+ if (multi > MULTI_MINUS) {
234
+ enc.encodeSymbol(this.multiModel, MULTI - multi);
235
+ this.ic.compress(enc, Math.imul(multi, this.diffs[L]), d, 5);
236
+ } else {
237
+ enc.encodeSymbol(this.multiModel, MULTI - MULTI_MINUS);
238
+ this.ic.compress(enc, Math.imul(MULTI_MINUS, this.diffs[L]), d, 6);
239
+ extreme(this, d);
240
+ }
241
+ } else {
242
+ enc.encodeSymbol(this.multiModel, 0);
243
+ this.ic.compress(enc, 0, d, 7);
244
+ extreme(this, d);
245
+ }
246
+ } else if ((j = this._other(hi, lo))) {
247
+ enc.encodeSymbol(this.multiModel, MULTI_CODE_FULL + j);
248
+ this.last = (L + j) & 3;
249
+ continue;
250
+ } else {
251
+ enc.encodeSymbol(this.multiModel, MULTI_CODE_FULL);
252
+ this._full(enc, hi, lo);
253
+ }
254
+ this.hi[this.last] = hi;
255
+ this.lo[this.last] = lo;
256
+ return;
257
+ }
258
+ };
@@ -4,8 +4,9 @@
4
4
  // (formats 0-5) starts with. Each point codes which attributes changed
5
5
  // since the last one; X and Y are corrections to a streaming median
6
6
  // of the recent differences, Z to the last height at the same return
7
- // level, all through integer decompressors whose contexts follow the
8
- // return structure.
7
+ // level, all through integer coders whose contexts follow the return
8
+ // structure. The compressor and decompressor below make the same model
9
+ // choices from the same state, so they stay in step point for point.
9
10
  //
10
11
  // Ported from laz-rs src/las/point0.rs, module v2, and the median and
11
12
  // return tables of src/las/utils.rs (https://github.com/laz-rs/laz-rs),
@@ -13,11 +14,13 @@
13
14
  // rapidlasso; (c) 2014 Uday Verma, Hobu, Inc.; (c) 2019 Thomas Montaigu.
14
15
  // Licensed under the Apache License, Version 2.0 (see LICENSE-APACHE and
15
16
  // NOTICE). Changes: translated from Rust to JavaScript, working on raw
16
- // record bytes, Brent Buffham 2026.
17
+ // record bytes, Brent Buffham 2026; the compressor (Point10Compressor)
18
+ // added beside the decompressor, sharing its models, median and tables.
17
19
  //==========================================================
18
20
 
19
21
  import { ArithmeticModel } from "../models.js";
20
22
  import { IntegerDecompressor } from "../IntegerDecompressor.js";
23
+ import { IntegerCompressor } from "../IntegerCompressor.js";
21
24
 
22
25
  var LE = true;
23
26
 
@@ -66,23 +69,34 @@ function models(n, symbols) { var out = []; for (var i = 0; i < n; i++) out.push
66
69
  function lazyModel(table, i) { return table[i] || (table[i] = new ArithmeticModel(256)); }
67
70
  function medians() { var out = []; for (var i = 0; i < 16; i++) out.push(new StreamingMedian()); return out; }
68
71
 
72
+ // The Y and Z coder contexts, from the bit lengths of the X (and Y) corrections just coded
73
+ // (Point14 uses them too)
74
+ export function yContext(single, icDx) { var k = icDx.k; return single + (k < 20 ? k & 0xFFFFFFFE : 20); }
75
+ export function zContext(single, icDx, icDy) { var k = Math.floor((icDx.k + icDy.k) / 2); return single + (k < 18 ? k & 0xFFFFFFFE : 18); }
76
+
77
+ // The state and models both halves start a chunk with; IC is the integer coder
78
+ // (IntegerCompressor or IntegerDecompressor), so each side gets the same widths and contexts
79
+ function initPoint10(self, IC) {
80
+ self.size = 20;
81
+ self.lastIntensity = new Uint16Array(16);
82
+ self.xMedian = medians();
83
+ self.yMedian = medians();
84
+ self.lastHeight = new Int32Array(8);
85
+ self.changedValues = new ArithmeticModel(64);
86
+ self.scanAngleRank = models(2, 256);
87
+ self.bitByte = new Array(256);
88
+ self.classification = new Array(256);
89
+ self.userData = new Array(256);
90
+ self.icIntensity = new IC(16, 4);
91
+ self.icPointSourceId = new IC(16, 1);
92
+ self.icDx = new IC(32, 2);
93
+ self.icDy = new IC(32, 22);
94
+ self.icZ = new IC(32, 20);
95
+ }
96
+
69
97
  /** Point10 v2 decompressor: one per chunk. */
70
98
  export function Point10Decompressor() {
71
- this.size = 20;
72
- this.lastIntensity = new Uint16Array(16);
73
- this.xMedian = medians();
74
- this.yMedian = medians();
75
- this.lastHeight = new Int32Array(8);
76
- this.changedValues = new ArithmeticModel(64);
77
- this.scanAngleRank = models(2, 256);
78
- this.bitByte = new Array(256);
79
- this.classification = new Array(256);
80
- this.userData = new Array(256);
81
- this.icIntensity = new IntegerDecompressor(16, 4);
82
- this.icPointSourceId = new IntegerDecompressor(16, 1);
83
- this.icDx = new IntegerDecompressor(32, 2);
84
- this.icDy = new IntegerDecompressor(32, 22);
85
- this.icZ = new IntegerDecompressor(32, 20);
99
+ initPoint10(this, IntegerDecompressor);
86
100
  }
87
101
 
88
102
  /** The first point of a chunk is stored raw. */
@@ -125,12 +139,10 @@ Point10Decompressor.prototype.next = function (dec, view, o) {
125
139
  var diff = this.icDx.decompress(dec, this.xMedian[m].get(), single);
126
140
  this.x = (this.x + diff) | 0;
127
141
  this.xMedian[m].add(diff);
128
- var kx = this.icDx.k;
129
- diff = this.icDy.decompress(dec, this.yMedian[m].get(), single + (kx < 20 ? kx & 0xFFFFFFFE : 20));
142
+ diff = this.icDy.decompress(dec, this.yMedian[m].get(), yContext(single, this.icDx));
130
143
  this.y = (this.y + diff) | 0;
131
144
  this.yMedian[m].add(diff);
132
- var kz = Math.floor((this.icDx.k + this.icDy.k) / 2);
133
- this.z = this.icZ.decompress(dec, this.lastHeight[l], single + (kz < 18 ? kz & 0xFFFFFFFE : 18));
145
+ this.z = this.icZ.decompress(dec, this.lastHeight[l], zContext(single, this.icDx, this.icDy));
134
146
  this.lastHeight[l] = this.z;
135
147
  view.setInt32(o, this.x, LE); view.setInt32(o + 4, this.y, LE); view.setInt32(o + 8, this.z, LE);
136
148
  view.setUint16(o + 12, this.intensity, LE);
@@ -140,3 +152,60 @@ Point10Decompressor.prototype.next = function (dec, view, o) {
140
152
  view.setUint8(o + 17, this.user);
141
153
  view.setUint16(o + 18, this.psid, LE);
142
154
  };
155
+
156
+ // A DataView over the caller's records, made once per buffer
157
+ function viewOf(item, src) {
158
+ if (item._src !== src) { item._src = src; item._view = new DataView(src.buffer, src.byteOffset, src.byteLength); }
159
+ return item._view;
160
+ }
161
+
162
+ /** Point10 v2 compressor: one per chunk, the mirror of Point10Decompressor. */
163
+ export function Point10Compressor() {
164
+ initPoint10(this, IntegerCompressor);
165
+ this._src = null;
166
+ this._view = null;
167
+ }
168
+
169
+ /** The first point of a chunk is stored raw; it only sets the state. */
170
+ Point10Compressor.prototype.first = function (src, s) {
171
+ var v = viewOf(this, src);
172
+ this.x = v.getInt32(s, LE); this.y = v.getInt32(s + 4, LE); this.z = v.getInt32(s + 8, LE);
173
+ this.bits = v.getUint8(s + 14);
174
+ this.cls = v.getUint8(s + 15);
175
+ this.angle = v.getInt8(s + 16);
176
+ this.user = v.getUint8(s + 17);
177
+ this.psid = v.getUint16(s + 18, LE);
178
+ };
179
+
180
+ Point10Compressor.prototype.next = function (enc, src, s) {
181
+ var v = viewOf(this, src);
182
+ var x = v.getInt32(s, LE), y = v.getInt32(s + 4, LE), z = v.getInt32(s + 8, LE);
183
+ var intensity = v.getUint16(s + 12, LE), bits = v.getUint8(s + 14), cls = v.getUint8(s + 15);
184
+ var angle = v.getInt8(s + 16), user = v.getUint8(s + 17), psid = v.getUint16(s + 18, LE);
185
+ var r = bits & 7, n = (bits >>> 3) & 7;
186
+ var m = NUMBER_RETURN_MAP[n][r], l = NUMBER_RETURN_LEVEL[n][r];
187
+ // return number, number of returns, scan direction and edge fill the bit byte, so "any changed" is the byte changed
188
+ var changed = (bits !== this.bits ? 32 : 0) | (intensity !== this.lastIntensity[m] ? 16 : 0) | (cls !== this.cls ? 8 : 0) |
189
+ (angle !== this.angle ? 4 : 0) | (user !== this.user ? 2 : 0) | (psid !== this.psid ? 1 : 0);
190
+ enc.encodeSymbol(this.changedValues, changed);
191
+ if (changed & 32) enc.encodeSymbol(lazyModel(this.bitByte, this.bits), bits);
192
+ if (changed & 16) {
193
+ this.icIntensity.compress(enc, this.lastIntensity[m], intensity, m < 3 ? m : 3);
194
+ this.lastIntensity[m] = intensity;
195
+ }
196
+ if (changed & 8) enc.encodeSymbol(lazyModel(this.classification, this.cls), cls);
197
+ if (changed & 4) enc.encodeSymbol(this.scanAngleRank[(bits >>> 6) & 1], (angle - this.angle) & 255); // u8 wrapping_sub
198
+ if (changed & 2) enc.encodeSymbol(lazyModel(this.userData, this.user), user);
199
+ if (changed & 1) this.icPointSourceId.compress(enc, this.psid, psid, 0);
200
+ var single = n === 1 ? 1 : 0;
201
+ var diff = (x - this.x) | 0; // i32 wrapping_sub
202
+ this.icDx.compress(enc, this.xMedian[m].get(), diff, single);
203
+ this.xMedian[m].add(diff);
204
+ diff = (y - this.y) | 0;
205
+ this.icDy.compress(enc, this.yMedian[m].get(), diff, yContext(single, this.icDx));
206
+ this.yMedian[m].add(diff);
207
+ this.icZ.compress(enc, this.lastHeight[l], z, zContext(single, this.icDx, this.icDy));
208
+ this.lastHeight[l] = z;
209
+ this.x = x; this.y = y; this.z = z;
210
+ this.bits = bits; this.cls = cls; this.angle = angle; this.user = user; this.psid = psid;
211
+ };