las-laz-js 0.5.0 → 0.6.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.2 (2026-10-22)
4
+
5
+ - `dedupe` and `dedupeSelection` handle more than 16,777,216 occupied cells. They used a
6
+ JavaScript Map, which stops at 2^24 entries, so a 20-30 M-point cloud at a small tolerance threw
7
+ `RangeError: Map maximum size exceeded`. A typed-array hash table (5 bytes a slot) replaces
8
+ it, with the same results and about 1.5 to 2 times the speed (2 M points: 0.9 s in 2D, 2.2 s in 3D).
9
+
10
+ ## 0.6.1 (2026-10-22)
11
+
12
+ - Demo: a read bigger than the draw cap (20 M) is drawn as an even spread over the whole cloud,
13
+ not its first 20 M points; selections and edits still act on every point read, and the
14
+ selection line says how many selected points are on screen. `?drawCap=N` lowers the cap.
15
+
16
+ ## 0.6.0 (2026-10-22)
17
+
18
+ Voxel neighbour index and the sparse-point (dust) filter.
19
+
20
+ - `selectSparse(cols, { radius, minNeighbours = 1, xy })` selects the points with fewer than
21
+ `minNeighbours` other points within `radius` (inclusive), for `removePoints`. `minNeighbours: 1`
22
+ keeps thin sparse features such as scanned blastholes. Checked point for point against PDAL's
23
+ `filters.outlier` (`method=radius`; PDAL's `min_k` is `minNeighbours`, and PDAL does not count a
24
+ point exactly `radius` away).
25
+ - `buildIndex(cols, { cellSize })`: a sorted voxel grid on flat typed arrays (6 to 8 bytes a point),
26
+ over x / y / z or a k = 3 `positions` column, with exact queries: `nearestPoint` (3D, or `xy` in
27
+ plan; `maxDistance`), `pointsInRadius` (a selection) and `pointsNearRay` (a cylinder or a cone,
28
+ with each point's `t` along the ray and `d` from its axis, for a caller's snap). Rebuild it after
29
+ any edit of the columns.
30
+ - `dedupeSelection(cols, tolerance, { z })` returns `dedupe`'s kept points as a selection and a
31
+ `map` from every input point to the kept point it merged into (its nearest within the tolerance).
32
+ - Speed (Node): a 34-million-point LAS (Float32 after a local origin) indexes in about 1.1 s at
33
+ about 8 bytes a point, and `selectSparse` at 2 m takes about 2.1 s.
34
+ - Demo: a Sparse points selection on the Edit tab.
35
+
3
36
  ## 0.5.0 (2026-10-22)
4
37
 
5
38
  Point operations.
package/README.md CHANGED
@@ -11,7 +11,8 @@ runs in the browser and in Node — and built for files far larger than memory.
11
11
  thinning, a polygon crop, a coordinate transform and the original point index; `colourize`
12
12
  colours points; `writeLas` and `createLasWriter` write LAS 1.0–1.4.
13
13
  - Point operations on columns: select by box, polygon, class or return; keep, remove, split and
14
- merge; transform coordinates; interleave positions for a renderer. See Point operations.
14
+ merge; transform coordinates; interleave positions for a renderer; find sparse points (dust);
15
+ a voxel neighbour index with nearest, radius and near-ray queries. See Point operations.
15
16
  - LAZ: point formats 0–3 and 6–8, read exactly like LAS — the same calls, the same options,
16
17
  decoded chunk by chunk in plain JavaScript (no WebAssembly) — and written with
17
18
  `compress: true`, byte for byte what PDAL (LAZperf) writes (bar one lossless exception, see
@@ -215,6 +216,57 @@ Float32 buffer; an axis left out of `origin` is not shifted ({ x, y } keeps worl
215
216
  defaults to true and picks the type of a new array; `into` must be a Float32Array or Float64Array
216
217
  long enough, else `INVALID_ARGUMENT`.
217
218
 
219
+ ### Sparse points (dust)
220
+
221
+ ```js
222
+ import { selectSparse, removePoints } from "las-laz-js";
223
+
224
+ var dust = selectSparse(pts, { radius: 2 }); // fewer than 1 other point within 2 m
225
+ var clean = removePoints(pts, dust);
226
+ var lone = selectSparse(pts, { radius: 0.5, minNeighbours: 4, xy: true }); // counted in 2D
227
+ ```
228
+
229
+ A point is sparse when fewer than `minNeighbours` (default 1) **other** points lie within
230
+ `radius` of it, inclusive; `radius` is required, in the columns' units. The result is a selection,
231
+ so it can be shown before anything is deleted. `minNeighbours: 1` keeps any point with a single
232
+ neighbour inside the radius, so thin sparse features — a line of points down a scanned blasthole —
233
+ survive; dust that clumps survives too. A higher `minNeighbours` removes small clumps, at the
234
+ cost of thin features. It works on columns in memory (x / y / z, or a k = 3 `positions`): 34 million
235
+ points take about 2 s in Node. With PDAL's `filters.outlier` (`method=radius`), `min_k` is the same
236
+ as `minNeighbours`, but PDAL does not count a point exactly `radius` away.
237
+
238
+ ### Neighbour index
239
+
240
+ ```js
241
+ import { buildIndex, nearestPoint, pointsInRadius, pointsNearRay } from "las-laz-js";
242
+
243
+ var index = buildIndex(pts); // or { cellSize: 0.5 }; omitted: ~16 points a cell
244
+ var i = nearestPoint(index, [x, y, z]); // point number, or -1
245
+ var j = nearestPoint(index, [x, y], { xy: true, maxDistance: 0.3 }); // nearest in plan, within 0.3
246
+ var near = pointsInRadius(index, [x, y, z], 5); // a selection
247
+ var hit = pointsNearRay(index, origin, direction, { radius: 0.2, growth: 0 });
248
+ // hit = { points, t, d }: every point within radius + growth · t of the ray, t along it, d from its axis
249
+ ```
250
+
251
+ A sorted voxel grid on flat typed arrays, 6 to 8 bytes a point (34 million points build in about
252
+ 1 s). Results are exact — every candidate is distance-checked in Float64 — "within" is inclusive,
253
+ and ties go to the lowest point number. `pointsNearRay` is a cylinder for `growth: 0` (an
254
+ orthographic camera's pick ray) and a cone for `growth > 0` (a perspective camera's, `growth` the
255
+ tangent of the half-angle); points behind `origin` are left out, and choosing a snap point from `t`
256
+ and `d` is the caller's. The index refers to the coordinate arrays, it does not copy them: rebuild
257
+ it after any keep, remove, merge or transform — after an `inPlace` edit an old index would read
258
+ overwritten memory, and queries cannot tell.
259
+
260
+ ### Dedupe with an index map
261
+
262
+ ```js
263
+ import { dedupeSelection } from "las-laz-js";
264
+
265
+ var { keep, map } = dedupeSelection(pts, 0.01); // { z: true } for 3D
266
+ var unique = keepPoints(pts, keep); // what dedupe(pts, 0.01) returns
267
+ // map[i]: where input point i went in unique — a dropped point maps to its nearest kept point
268
+ ```
269
+
218
270
  ## LAZ
219
271
 
220
272
  A LAZ file is opened and read with the same calls as LAS. Each LASzip chunk (50,000 points in
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "las-laz-js",
3
- "version": "0.5.0",
3
+ "version": "0.6.2",
4
4
  "description": "Zero-dependency JavaScript reader and writer for ASPRS LAS point clouds, and reader and writer for LAZ (LASzip), for the browser and Node, built for files larger than memory.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -6,18 +6,25 @@
6
6
  // objects to columns. XY by default (one Z per XY location, as a
7
7
  // 2.5D surface wants); { z: true } compares in 3D.
8
8
  // Every column follows the kept points (k values per point).
9
+ // dedupeSelection gives the same keep as a selection, plus a map from
10
+ // every input point to the output point it merged into.
9
11
  //==========================================================
10
12
 
11
13
  import { LasError, ERROR_CODES } from "../errors.js";
12
14
  import { columnsOf, gather, sameType } from "../ops/columns.js";
15
+ import { hash32 } from "../index/grid.js";
13
16
 
14
17
  /**
18
+ * The points dedupe keeps, and where every input point went.
19
+ * map[i] is the output number (position in keep) of the kept point that point i
20
+ * merged into: for a dropped point, the nearest kept point within tolerance
21
+ * (lowest number on a tie); a kept point maps to itself.
15
22
  * @param {object} cols - PointColumns ({ count, x, y, z?, ... }).
16
23
  * @param {number} tolerance - distance below which points merge (> 0).
17
- * @param {{z?: boolean}} [opts]
18
- * @returns {object} PointColumns with only the kept points
24
+ * @param {{z?: boolean}} [opts] - z: compare in 3D.
25
+ * @returns {{keep: Uint32Array, map: Uint32Array}}
19
26
  */
20
- export function dedupe(cols, tolerance, opts) {
27
+ export function dedupeSelection(cols, tolerance, opts) {
21
28
  if (!(tolerance > 0)) throw new LasError(ERROR_CODES.INVALID_ARGUMENT, "dedupe tolerance must be > 0");
22
29
  var use3d = !!(opts && opts.z && cols.z);
23
30
  var tol2 = tolerance * tolerance;
@@ -40,10 +47,40 @@ export function dedupe(cols, tolerance, opts) {
40
47
  var dense = (hi[0] - lo[0] + 3) * SY * SZ <= 9007199254740992;
41
48
  function key(i, j, k) { return dense ? (i * SY + j) * SZ + k : (i * 73856093 + j * 19349663 + k * 83492791) % 9007199254740881; }
42
49
 
43
- // Each cell is a linked list through next[] (Map: key -> most recent point)
44
- var head = new Map();
50
+ function cellKey(p) {
51
+ return key(Math.floor(x[p] / tolerance) - lo[0] + 1, Math.floor(y[p] / tolerance) - lo[1] + 1, use3d ? Math.floor(z[p] / tolerance) - lo[2] + 1 : 1);
52
+ }
53
+
54
+ // Each cell is a linked list of kept points through next[]. Its newest point sits in
55
+ // an open-addressing table (linear probing, power-of-two capacity, at most half
56
+ // full). A slot holds the point number and an 8-bit tag of its key's hash; the key
57
+ // itself is recomputed from the point's cell, only when the tag matches. 5 bytes a
58
+ // slot. (A Map stops at 2^24 cells.)
59
+ var cells = (hi[0] - lo[0] + 1) * (hi[1] - lo[1] + 1) * (hi[2] - lo[2] + 1);
60
+ var cap = 16;
61
+ while (cap < Math.min(n, cells)) cap *= 2;
62
+ var mask = cap - 1, used = 0, table = new Int32Array(cap).fill(-1), tags = new Uint8Array(cap);
63
+ var tag = 0; // the tag of the key find() last looked up
64
+ // The slot of cell key c: its newest kept point, or -1 (the free slot c would take)
65
+ function find(c) {
66
+ var g = hash32(c), h = g & mask;
67
+ tag = Math.imul(g, 0x9E3779B1) >>> 24; // top bits of a multiply mix every bit of g
68
+ while (table[h] >= 0 && (tags[h] !== tag || cellKey(table[h]) !== c)) h = (h + 1) & mask;
69
+ return h;
70
+ }
71
+ function grow() {
72
+ var old = table, oldTags = tags;
73
+ cap *= 2; mask = cap - 1; table = new Int32Array(cap).fill(-1); tags = new Uint8Array(cap);
74
+ for (var s = 0; s < old.length; s++) {
75
+ if (old[s] < 0) continue;
76
+ var h = hash32(cellKey(old[s])) & mask;
77
+ while (table[h] >= 0) h = (h + 1) & mask;
78
+ table[h] = old[s]; tags[h] = oldTags[s];
79
+ }
80
+ }
45
81
  var next = new Int32Array(n);
46
82
  var kept = new Uint32Array(n);
83
+ var map = new Uint32Array(n);
47
84
  var nKept = 0;
48
85
  for (var i = 0; i < n; i++) {
49
86
  var cx = Math.floor(x[i] / tolerance) - lo[0] + 1, cy = Math.floor(y[i] / tolerance) - lo[1] + 1;
@@ -52,8 +89,8 @@ export function dedupe(cols, tolerance, opts) {
52
89
  for (var dx = -1; dx <= 1 && !dup; dx++) {
53
90
  for (var dy = -1; dy <= 1 && !dup; dy++) {
54
91
  for (var dz = -dzRange; dz <= dzRange && !dup; dz++) {
55
- var k = head.get(key(cx + dx, cy + dy, cz + dz));
56
- while (k !== undefined && k >= 0) {
92
+ var k = table[find(key(cx + dx, cy + dy, cz + dz))];
93
+ while (k >= 0) {
57
94
  var ex = x[k] - x[i], ey = y[k] - y[i];
58
95
  var ez = use3d ? z[k] - z[i] : 0;
59
96
  if (ex * ex + ey * ey + ez * ez <= tol2) { dup = true; break; }
@@ -63,15 +100,54 @@ export function dedupe(cols, tolerance, opts) {
63
100
  }
64
101
  }
65
102
  if (dup) continue;
66
- var own = key(cx, cy, cz);
67
- var prev = head.get(own);
68
- next[i] = prev === undefined ? -1 : prev;
69
- head.set(own, i);
103
+ var own = key(cx, cy, cz), h = find(own);
104
+ if (table[h] < 0) {
105
+ if (2 * (used + 1) > cap) { grow(); h = find(own); }
106
+ used++;
107
+ tags[h] = tag;
108
+ }
109
+ next[i] = table[h];
110
+ table[h] = i;
111
+ map[i] = nKept;
70
112
  kept[nKept++] = i;
71
113
  }
72
114
 
115
+ // Each dropped point (every point not in kept) maps to its nearest kept point;
116
+ // the cell lists hold kept points only
117
+ for (i = 0, p = 0; i < n; i++) {
118
+ if (p < nKept && kept[p] === i) { p++; continue; }
119
+ var cx = Math.floor(x[i] / tolerance) - lo[0] + 1, cy = Math.floor(y[i] / tolerance) - lo[1] + 1;
120
+ var cz = use3d ? Math.floor(z[i] / tolerance) - lo[2] + 1 : 1;
121
+ var best = -1, bd = Infinity;
122
+ for (var dx = -1; dx <= 1; dx++) {
123
+ for (var dy = -1; dy <= 1; dy++) {
124
+ for (var dz = -dzRange; dz <= dzRange; dz++) {
125
+ var k = table[find(key(cx + dx, cy + dy, cz + dz))];
126
+ while (k >= 0) {
127
+ var ex = x[k] - x[i], ey = y[k] - y[i];
128
+ var ez = use3d ? z[k] - z[i] : 0;
129
+ var d = ex * ex + ey * ey + ez * ez;
130
+ if (d <= tol2 && (d < bd || (d === bd && k < best))) { bd = d; best = k; }
131
+ k = next[k];
132
+ }
133
+ }
134
+ }
135
+ }
136
+ map[i] = map[best];
137
+ }
138
+ return { keep: kept.slice(0, nKept), map: map };
139
+ }
140
+
141
+ /**
142
+ * @param {object} cols - PointColumns ({ count, x, y, z?, ... }).
143
+ * @param {number} tolerance - distance below which points merge (> 0).
144
+ * @param {{z?: boolean}} [opts]
145
+ * @returns {object} PointColumns with only the kept points
146
+ */
147
+ export function dedupe(cols, tolerance, opts) {
148
+ var sel = dedupeSelection(cols, tolerance, opts).keep, nKept = sel.length;
149
+
73
150
  // Every column (k values per point) follows the kept points; anything else passes through
74
- var sel = kept.subarray(0, nKept);
75
151
  var ks = Object.create(null); // a column may be named like an Object.prototype member
76
152
  columnsOf(cols).forEach(function (c) { ks[c.name] = c.k; });
77
153
  var out = { count: nKept };
@@ -0,0 +1,207 @@
1
+ // src/index/grid.js
2
+ //==========================================================
3
+ // The voxel neighbour index: a sorted voxel grid on flat typed
4
+ // arrays. Point numbers are counting-sorted by cell into `order`
5
+ // (stable, so a cell's points stay ascending); an open-addressing
6
+ // hash table (linear probing, power-of-two capacity) maps each
7
+ // occupied cell's key to its `start` and `length` in `order`.
8
+ // Coordinates are referenced, not copied: x / y / z columns, or a
9
+ // k = 3 positions column (Kirra's kadCloud layout) read with a
10
+ // stride of 3. About 6 bytes per point at 16 points per cell.
11
+ // The grid only prunes: queries (query.js) distance-check every
12
+ // candidate in Float64, so their results are exact.
13
+ //==========================================================
14
+
15
+ import { LasError, ERROR_CODES } from "../errors.js";
16
+ import { columnsOf, countOf } from "../ops/columns.js";
17
+
18
+ var MAX_POINTS = 4294967295;
19
+ var MAX_CELLS = 9007199254740992; // 2^53: keys stay exact integers
20
+ var EMPTY = -1;
21
+
22
+ function bad(message) { return new LasError(ERROR_CODES.INVALID_ARGUMENT, message); }
23
+
24
+ /**
25
+ * The coordinates of point columns: x / y / z (stride 1), else a k = 3
26
+ * positions column (stride 3, y and z as offset views of it).
27
+ * Point i's x is coords.x[i * coords.stride].
28
+ * @param {object} cols - point columns.
29
+ * @param {string} fn - the calling function's name, for the message.
30
+ * @returns {{x: ArrayLike<number>, y: ArrayLike<number>, z: ArrayLike<number>, stride: number}}
31
+ */
32
+ export function coordsOf(cols, fn) {
33
+ var count = countOf(cols);
34
+ var k = Object.create(null);
35
+ columnsOf(cols).forEach(function (c) { k[c.name] = c.k; });
36
+ if (k.x === 1 && k.y === 1 && k.z === 1) return { x: cols.x, y: cols.y, z: cols.z, stride: 1 };
37
+ if (k.positions === 3 || (count === 0 && k.positions === 1)) {
38
+ var p = cols.positions;
39
+ return { x: p, y: p.subarray(1), z: p.subarray(2), stride: 3 };
40
+ }
41
+ throw bad(fn + " needs x, y, z columns or a k = 3 positions column");
42
+ }
43
+
44
+ /**
45
+ * Refuse a point count the index cannot number (order is a Uint32Array).
46
+ * @param {number} n
47
+ */
48
+ export function checkPointCount(n) {
49
+ if (n > MAX_POINTS) throw new LasError(ERROR_CODES.TOO_MANY_POINTS, "the neighbour index takes at most 4,294,967,295 points, not " + n);
50
+ }
51
+
52
+ // murmur3's 32-bit finaliser: every input bit reaches the low bits the mask keeps
53
+ function fmix32(h) {
54
+ h ^= h >>> 16; h = Math.imul(h, 0x85EBCA6B);
55
+ h ^= h >>> 13; h = Math.imul(h, 0xC2B2AE35);
56
+ return (h ^ (h >>> 16)) >>> 0;
57
+ }
58
+
59
+ /**
60
+ * A 32-bit hash of a cell key (an integer below 2^53).
61
+ * @param {number} key
62
+ * @returns {number}
63
+ */
64
+ export function hash32(key) {
65
+ var lo = key % 4294967296, hi = (key - lo) / 4294967296;
66
+ return fmix32(lo ^ fmix32(hi));
67
+ }
68
+
69
+ /**
70
+ * The home slot of a cell key (an integer below 2^53) in a table of mask + 1 slots.
71
+ * @param {number} key
72
+ * @param {number} mask
73
+ * @returns {number}
74
+ */
75
+ export function hashKey(key, mask) {
76
+ return hash32(key) & mask;
77
+ }
78
+
79
+ function nextPow2(n) {
80
+ var p = 16;
81
+ while (p < n) p *= 2;
82
+ return p;
83
+ }
84
+
85
+ /**
86
+ * Build the grid over coordinates.
87
+ * @param {{x, y, z, stride}} coords - from coordsOf.
88
+ * @param {number} count - points.
89
+ * @param {number} [cellSize] - cell edge; omitted: about 16 points per cell.
90
+ * @param {boolean} [xy] - ignore Z (nz = 1).
91
+ * @returns {object} PointIndex
92
+ */
93
+ export function buildGrid(coords, count, cellSize, xy) {
94
+ if (cellSize !== undefined && !(typeof cellSize === "number" && cellSize > 0 && isFinite(cellSize))) throw bad("cellSize must be a positive finite number, not " + String(cellSize));
95
+ var X = coords.x, Y = coords.y, Z = coords.z, st = coords.stride;
96
+
97
+ // 1. Bounds
98
+ var lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
99
+ for (var i = 0, o = 0; i < count; i++, o += st) {
100
+ var a = X[o], b = Y[o], c = xy ? 0 : Z[o];
101
+ if (a - a !== 0 || b - b !== 0 || c - c !== 0) throw bad("point " + i + " has a non-finite " + (a - a !== 0 ? "x" : b - b !== 0 ? "y" : "z") + " coordinate");
102
+ if (a < lo[0]) lo[0] = a; if (a > hi[0]) hi[0] = a;
103
+ if (b < lo[1]) lo[1] = b; if (b > hi[1]) hi[1] = b;
104
+ if (c < lo[2]) lo[2] = c; if (c > hi[2]) hi[2] = c;
105
+ }
106
+ if (count === 0) { lo = [0, 0, 0]; hi = [0, 0, 0]; }
107
+ var ext = [hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]];
108
+ if (cellSize === undefined) {
109
+ var prod = 1, d = 0;
110
+ ext.forEach(function (e) { if (e > 0) { prod *= e; d++; } });
111
+ cellSize = d === 0 ? 1 : Math.pow(16 * prod / count, 1 / d);
112
+ }
113
+ var nx = Math.floor(ext[0] / cellSize) + 1, ny = Math.floor(ext[1] / cellSize) + 1, nz = Math.floor(ext[2] / cellSize) + 1;
114
+ if (nx * ny * nz > MAX_CELLS) throw bad("cellSize too small for this extent: " + nx + " × " + ny + " × " + nz + " cells is more than 2^53");
115
+ var inv = 1 / cellSize, x0 = lo[0], y0 = lo[1], z0 = lo[2];
116
+ function keyOf(o) {
117
+ var cx = Math.floor((X[o] - x0) * inv); if (cx >= nx) cx = nx - 1;
118
+ var cy = Math.floor((Y[o] - y0) * inv); if (cy >= ny) cy = ny - 1;
119
+ var cz = 0;
120
+ if (!xy) { cz = Math.floor((Z[o] - z0) * inv); if (cz >= nz) cz = nz - 1; }
121
+ return (cx * ny + cy) * nz + cz;
122
+ }
123
+
124
+ // 2. Count points per cell into the hash table
125
+ var cap = nextPow2(2 * Math.min(nx * ny * nz, Math.ceil(count / 16)));
126
+ var mask = cap - 1, used = 0;
127
+ var keys = new Float64Array(cap).fill(EMPTY), length = new Uint32Array(cap);
128
+ function grow() {
129
+ var oldK = keys, oldL = length;
130
+ cap *= 2; mask = cap - 1;
131
+ keys = new Float64Array(cap).fill(EMPTY); length = new Uint32Array(cap);
132
+ for (var s = 0; s < oldK.length; s++) {
133
+ if (oldK[s] === EMPTY) continue;
134
+ var h = hashKey(oldK[s], mask);
135
+ while (keys[h] !== EMPTY) h = (h + 1) & mask;
136
+ keys[h] = oldK[s]; length[h] = oldL[s];
137
+ }
138
+ }
139
+ for (i = 0, o = 0; i < count; i++, o += st) {
140
+ var key = keyOf(o), h = hashKey(key, mask);
141
+ while (keys[h] !== EMPTY && keys[h] !== key) h = (h + 1) & mask;
142
+ if (keys[h] === EMPTY) {
143
+ if (2 * (used + 1) > cap) {
144
+ grow();
145
+ h = hashKey(key, mask);
146
+ while (keys[h] !== EMPTY) h = (h + 1) & mask;
147
+ }
148
+ keys[h] = key; used++;
149
+ }
150
+ length[h]++;
151
+ }
152
+
153
+ // 3. Prefix sums give each cell's start; place point numbers (stable)
154
+ var start = new Uint32Array(cap), run = 0;
155
+ for (var s = 0; s < cap; s++) { start[s] = run; run += length[s]; }
156
+ var order = new Uint32Array(count);
157
+ for (i = 0, o = 0; i < count; i++, o += st) {
158
+ var k2 = keyOf(o), h2 = hashKey(k2, mask);
159
+ while (keys[h2] !== k2) h2 = (h2 + 1) & mask;
160
+ order[start[h2]++] = i;
161
+ }
162
+ for (s = 0; s < cap; s++) start[s] -= length[s];
163
+
164
+ return { count: count, cellSize: cellSize, min: [x0, y0, z0], nx: nx, ny: ny, nz: nz, order: order, keys: keys, start: start, length: length, mask: mask, coords: coords };
165
+ }
166
+
167
+ /**
168
+ * Build a neighbour index over point columns.
169
+ * @param {object} cols - point columns with x, y, z, or a k = 3 positions column.
170
+ * @param {{cellSize?: number}} [opts] - cellSize omitted: about 16 points per cell.
171
+ * @returns {object} PointIndex { count, cellSize, min, nx, ny, nz, order, keys, start, length, mask, coords }
172
+ */
173
+ export function buildIndex(cols, opts) {
174
+ var coords = coordsOf(cols, "buildIndex");
175
+ checkPointCount(cols.count);
176
+ return buildGrid(coords, cols.count, opts ? opts.cellSize : undefined, false);
177
+ }
178
+
179
+ /**
180
+ * The hash-table slot of cell (ix, iy, iz), or -1 when the cell is empty or outside the grid.
181
+ * @param {object} index - PointIndex.
182
+ * @returns {number}
183
+ */
184
+ export function lookup(index, ix, iy, iz) {
185
+ if (ix < 0 || iy < 0 || iz < 0 || ix >= index.nx || iy >= index.ny || iz >= index.nz) return -1;
186
+ var key = (ix * index.ny + iy) * index.nz + iz, keys = index.keys, mask = index.mask;
187
+ var h = hashKey(key, mask);
188
+ while (keys[h] !== EMPTY) {
189
+ if (keys[h] === key) return h;
190
+ h = (h + 1) & mask;
191
+ }
192
+ return -1;
193
+ }
194
+
195
+ /**
196
+ * Refuse an index that is not self-consistent.
197
+ * @param {object} index - PointIndex.
198
+ * @param {string} fn - the calling function's name, for the message.
199
+ */
200
+ export function checkIndex(index, fn) {
201
+ var ok = index !== null && typeof index === "object" && index.order instanceof Uint32Array && index.order.length === index.count && index.coords;
202
+ if (ok && index.count > 0) {
203
+ var need = (index.count - 1) * index.coords.stride + 1;
204
+ ok = index.coords.x.length >= need && index.coords.y.length >= need && index.coords.z.length >= need;
205
+ }
206
+ if (!ok) throw bad(fn + ": index is not self-consistent (rebuild it with buildIndex)");
207
+ }
@@ -0,0 +1,329 @@
1
+ // src/index/query.js
2
+ //==========================================================
3
+ // Queries on a PointIndex (grid.js). The grid only prunes: every
4
+ // candidate is distance-checked in Float64 against the stored
5
+ // coordinates, "within" is inclusive (<=) and ties go to the lowest
6
+ // point number, so each answer equals a plain scan of the points.
7
+ // Cell ranges and cell distances are widened by a millionth of a
8
+ // cell so float rounding can never skip a cell holding an answer.
9
+ // When a query would visit more cells than the hash table has
10
+ // slots (a cell size far below the query's reach, or a query far
11
+ // outside the cloud), it walks the occupied slots instead.
12
+ //==========================================================
13
+
14
+ import { LasError, ERROR_CODES } from "../errors.js";
15
+ import { checkIndex, lookup as lookupCell } from "./grid.js";
16
+
17
+ var EPS = 1e-6; // in cells
18
+
19
+ function bad(message) { return new LasError(ERROR_CODES.INVALID_ARGUMENT, message); }
20
+
21
+ function fin(v) { return typeof v === "number" && isFinite(v); }
22
+
23
+ // [x, y, z] as Numbers; with xy, [x, y] or [x, y, anything]
24
+ function checkPoint(p, xy, name) {
25
+ var ok = Array.isArray(p) && (p.length === 3 || (xy && p.length === 2)) && fin(p[0]) && fin(p[1]) && (xy || fin(p[2]));
26
+ if (!ok) throw bad(name + " must be [x, y, z]" + (xy ? " or [x, y]" : "") + " of finite numbers");
27
+ return [p[0], p[1], xy ? 0 : p[2]];
28
+ }
29
+
30
+ function checkPositive(v, name, optional) {
31
+ if (optional && v === undefined) return;
32
+ if (!(fin(v) && v > 0)) throw bad(name + " must be a positive finite number, not " + String(v));
33
+ }
34
+
35
+ function dims(index) { return [index.nx, index.ny, index.nz]; }
36
+
37
+ // Inclusive cell range on one axis covering [a, b], clamped; lo > hi when empty
38
+ function cellRange(index, axis, a, b) {
39
+ var n = dims(index)[axis], inv = 1 / index.cellSize, m = index.min[axis];
40
+ var lo = Math.floor((a - m) * inv - EPS), hi = Math.floor((b - m) * inv + EPS);
41
+ return [Math.max(0, lo), Math.min(n - 1, hi)];
42
+ }
43
+
44
+ // Squared distance from q to the box of cell (ix, iy, iz), the box widened by EPS cells
45
+ function cellDist2(index, ix, iy, iz, q, xy) {
46
+ var cs = index.cellSize, g = cs * EPS, m = index.min, c = [ix, iy, iz], s = 0;
47
+ for (var a = 0; a < (xy ? 2 : 3); a++) {
48
+ var lo = m[a] + c[a] * cs - g, hi = m[a] + (c[a] + 1) * cs + g;
49
+ var d = lo - q[a] > 0 ? lo - q[a] : (q[a] - hi > 0 ? q[a] - hi : 0);
50
+ s += d * d;
51
+ }
52
+ return s;
53
+ }
54
+
55
+ function decode(index, key) {
56
+ var iz = key % index.nz, t = (key - iz) / index.nz, iy = t % index.ny;
57
+ return [(t - iy) / index.ny, iy, iz];
58
+ }
59
+
60
+ /**
61
+ * Call fn(slot, ix, iy, iz) for every occupied cell in an inclusive cell box.
62
+ * Walks the table instead when the box has more cells than the table has slots.
63
+ * @param {object} index - PointIndex.
64
+ * @param {number[]} lo - [ix, iy, iz] low corner.
65
+ * @param {number[]} hi - [ix, iy, iz] high corner.
66
+ * @param {function} fn
67
+ */
68
+ export function visitBox(index, lo, hi, fn) {
69
+ if (lo[0] > hi[0] || lo[1] > hi[1] || lo[2] > hi[2]) return;
70
+ var cells = (hi[0] - lo[0] + 1) * (hi[1] - lo[1] + 1) * (hi[2] - lo[2] + 1), keys = index.keys;
71
+ if (cells > keys.length) {
72
+ for (var s = 0; s < keys.length; s++) {
73
+ if (keys[s] < 0) continue;
74
+ var c = decode(index, keys[s]);
75
+ if (c[0] >= lo[0] && c[0] <= hi[0] && c[1] >= lo[1] && c[1] <= hi[1] && c[2] >= lo[2] && c[2] <= hi[2]) fn(s, c[0], c[1], c[2]);
76
+ }
77
+ return;
78
+ }
79
+ for (var ix = lo[0]; ix <= hi[0]; ix++) {
80
+ for (var iy = lo[1]; iy <= hi[1]; iy++) {
81
+ for (var iz = lo[2]; iz <= hi[2]; iz++) {
82
+ var slot = lookupCell(index, ix, iy, iz);
83
+ if (slot >= 0) fn(slot, ix, iy, iz);
84
+ }
85
+ }
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Every point within `radius` of a point (inclusive), as a selection.
91
+ * @param {object} index - PointIndex from buildIndex.
92
+ * @param {number[]} point - [x, y, z]; with xy, [x, y] is enough.
93
+ * @param {number} radius - > 0.
94
+ * @param {{xy?: boolean}} [opts] - xy: measure in XY only.
95
+ * @returns {Uint32Array} ascending point numbers
96
+ */
97
+ export function pointsInRadius(index, point, radius, opts) {
98
+ checkIndex(index, "pointsInRadius");
99
+ var xy = !!(opts && opts.xy);
100
+ var q = checkPoint(point, xy, "point");
101
+ checkPositive(radius, "radius");
102
+ if (index.count === 0) return new Uint32Array(0);
103
+ var rx = cellRange(index, 0, q[0] - radius, q[0] + radius), ry = cellRange(index, 1, q[1] - radius, q[1] + radius);
104
+ var rz = xy ? [0, index.nz - 1] : cellRange(index, 2, q[2] - radius, q[2] + radius);
105
+ var r2 = radius * radius, co = index.coords, X = co.x, Y = co.y, Z = co.z, st = co.stride, order = index.order;
106
+ var hits = [];
107
+ visitBox(index, [rx[0], ry[0], rz[0]], [rx[1], ry[1], rz[1]], function (s, ix, iy, iz) {
108
+ if (cellDist2(index, ix, iy, iz, q, xy) > r2) return;
109
+ for (var j = index.start[s], e = j + index.length[s]; j < e; j++) {
110
+ var p = order[j], o = p * st;
111
+ var dx = X[o] - q[0], dy = Y[o] - q[1], dz = xy ? 0 : Z[o] - q[2];
112
+ if (dx * dx + dy * dy + dz * dz <= r2) hits.push(p);
113
+ }
114
+ });
115
+ return Uint32Array.from(hits).sort();
116
+ }
117
+
118
+ /**
119
+ * The nearest point (lowest number on a tie), or -1 when none is within maxDistance.
120
+ * @param {object} index - PointIndex from buildIndex.
121
+ * @param {number[]} point - [x, y, z]; with xy, [x, y] is enough.
122
+ * @param {{maxDistance?: number, xy?: boolean}} [opts] - maxDistance omitted: unlimited.
123
+ * @returns {number}
124
+ */
125
+ export function nearestPoint(index, point, opts) {
126
+ checkIndex(index, "nearestPoint");
127
+ var xy = !!(opts && opts.xy);
128
+ var q = checkPoint(point, xy, "point");
129
+ var maxD = opts ? opts.maxDistance : undefined;
130
+ checkPositive(maxD, "maxDistance", true);
131
+ if (index.count === 0) return -1;
132
+ var maxD2 = maxD === undefined ? Infinity : maxD * maxD;
133
+ var n = dims(index), cs = index.cellSize, inv = 1 / cs, m = index.min, axes = xy ? 2 : 3;
134
+ var c = [0, 0, 0];
135
+ for (var a = 0; a < axes; a++) c[a] = Math.min(n[a] - 1, Math.max(0, Math.floor((q[a] - m[a]) * inv)));
136
+ var co = index.coords, X = co.x, Y = co.y, Z = co.z, st = co.stride, order = index.order;
137
+ var best = -1, bd = Infinity, visited = 0, budget = index.keys.length;
138
+ // How far the query lies outside the grid on each axis (0 inside)
139
+ var out = [0, 0, 0];
140
+ for (a = 0; a < axes; a++) {
141
+ var gl = m[a], gh = m[a] + n[a] * cs;
142
+ out[a] = Math.max(0, (q[a] < gl ? gl - q[a] : q[a] > gh ? q[a] - gh : 0) - cs * EPS);
143
+ }
144
+
145
+ function cell(ix, iy, iz) {
146
+ visited++;
147
+ var s = lookupCell(index, ix, iy, iz);
148
+ if (s < 0 || cellDist2(index, ix, iy, iz, q, xy) > Math.min(bd, maxD2)) return;
149
+ for (var j = index.start[s], e = j + index.length[s]; j < e; j++) {
150
+ var p = order[j], o = p * st;
151
+ var dx = X[o] - q[0], dy = Y[o] - q[1], dz = xy ? 0 : Z[o] - q[2];
152
+ var d = dx * dx + dy * dy + dz * dz;
153
+ if (d <= maxD2 && (d < bd || (d === bd && p < best))) { bd = d; best = p; }
154
+ }
155
+ }
156
+ // Every grid cell at Chebyshev distance r (in the axes used) from c
157
+ function ring(r) {
158
+ var x0 = Math.max(0, c[0] - r), x1 = Math.min(n[0] - 1, c[0] + r);
159
+ var y0 = Math.max(0, c[1] - r), y1 = Math.min(n[1] - 1, c[1] + r);
160
+ var z0 = xy ? 0 : Math.max(0, c[2] - r), z1 = xy ? n[2] - 1 : Math.min(n[2] - 1, c[2] + r);
161
+ function column(ix, iy) { for (var iz = z0; iz <= z1; iz++) cell(ix, iy, iz); }
162
+ for (var ix = x0; ix <= x1; ix++) {
163
+ if (ix === c[0] - r || ix === c[0] + r) {
164
+ for (var iy = y0; iy <= y1; iy++) column(ix, iy);
165
+ continue;
166
+ }
167
+ if (c[1] - r >= 0) column(ix, c[1] - r);
168
+ if (c[1] + r < n[1]) column(ix, c[1] + r);
169
+ if (xy) continue;
170
+ // interior columns: only the two z faces
171
+ for (iy = Math.max(y0, c[1] - r + 1); iy <= Math.min(y1, c[1] + r - 1); iy++) {
172
+ if (c[2] - r >= 0) cell(ix, iy, c[2] - r);
173
+ if (c[2] + r < n[2]) cell(ix, iy, c[2] + r);
174
+ }
175
+ }
176
+ }
177
+
178
+ function gap(g) { g = Math.max(0, g - cs * EPS); return g * g; }
179
+
180
+ for (var r = 0; ; r++) {
181
+ ring(r);
182
+ if (visited > budget) return nearestByScan(index, q, xy, maxD2, best, bd);
183
+ // Unvisited points lie beyond a face of the cells c - r .. c + r on some axis a, and
184
+ // inside the grid on the others: a lower bound on their squared distance
185
+ var bound2 = Infinity;
186
+ for (a = 0; a < axes; a++) {
187
+ var rest = 0;
188
+ for (var b = 0; b < axes; b++) if (b !== a) rest += out[b] * out[b];
189
+ if (c[a] - r > 0) bound2 = Math.min(bound2, gap(q[a] - (m[a] + (c[a] - r) * cs)) + rest);
190
+ if (c[a] + r < n[a] - 1) bound2 = Math.min(bound2, gap(m[a] + (c[a] + r + 1) * cs - q[a]) + rest);
191
+ }
192
+ if (bound2 === Infinity) break;
193
+ if (best >= 0 && bd < bound2) break;
194
+ if (bound2 > maxD2) break;
195
+ }
196
+ return best;
197
+ }
198
+
199
+ // The occupied cells in order of distance, for queries a ring search would make slow
200
+ function nearestByScan(index, q, xy, maxD2, best, bd) {
201
+ var keys = index.keys, cand = [];
202
+ for (var s = 0; s < keys.length; s++) {
203
+ if (keys[s] < 0) continue;
204
+ var c = decode(index, keys[s]), cd = cellDist2(index, c[0], c[1], c[2], q, xy);
205
+ if (cd <= maxD2 && cd <= bd) cand.push([cd, s]);
206
+ }
207
+ cand.sort(function (u, v) { return u[0] - v[0]; });
208
+ var co = index.coords, X = co.x, Y = co.y, Z = co.z, st = co.stride, order = index.order;
209
+ for (var i = 0; i < cand.length; i++) {
210
+ if (cand[i][0] > bd) break;
211
+ s = cand[i][1];
212
+ for (var j = index.start[s], e = j + index.length[s]; j < e; j++) {
213
+ var p = order[j], o = p * st;
214
+ var dx = X[o] - q[0], dy = Y[o] - q[1], dz = xy ? 0 : Z[o] - q[2];
215
+ var d = dx * dx + dy * dy + dz * dz;
216
+ if (d <= maxD2 && (d < bd || (d === bd && p < best))) { bd = d; best = p; }
217
+ }
218
+ }
219
+ return best;
220
+ }
221
+
222
+ /**
223
+ * Every point near a ray: within radius + growth · t of its axis, where t >= 0 is the
224
+ * point's distance along it (growth 0: a cylinder, as an orthographic view's pick ray;
225
+ * growth > 0: a cone, as a perspective camera's, growth = tan of the half-angle).
226
+ * The snap rules (front-most, priorities) are the caller's: pick from t and d.
227
+ * @param {object} index - PointIndex from buildIndex.
228
+ * @param {number[]} origin - [x, y, z].
229
+ * @param {number[]} direction - [x, y, z], any length > 0.
230
+ * @param {{radius: number, growth?: number, maxT?: number}} opts - maxT omitted: unlimited.
231
+ * @returns {{points: Uint32Array, t: Float64Array, d: Float64Array}} points ascending; t, d parallel
232
+ */
233
+ export function pointsNearRay(index, origin, direction, opts) {
234
+ checkIndex(index, "pointsNearRay");
235
+ var o = checkPoint(origin, false, "origin");
236
+ var dir = checkPoint(direction, false, "direction");
237
+ var len = Math.sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]);
238
+ if (!(len > 0 && isFinite(len))) throw bad("direction must not be zero");
239
+ var u = [dir[0] / len, dir[1] / len, dir[2] / len];
240
+ opts = opts || {};
241
+ checkPositive(opts.radius, "radius");
242
+ var growth = opts.growth === undefined ? 0 : opts.growth;
243
+ if (!(fin(growth) && growth >= 0)) throw bad("growth must be a finite number >= 0, not " + String(growth));
244
+ checkPositive(opts.maxT, "maxT", true);
245
+ var radius = opts.radius, maxT = opts.maxT;
246
+ var points = [], ts = [], ds = [];
247
+ var co = index.coords, X = co.x, Y = co.y, Z = co.z, st = co.stride, order = index.order;
248
+
249
+ function check(s) {
250
+ for (var j = index.start[s], e = j + index.length[s]; j < e; j++) {
251
+ var p = order[j], q = p * st;
252
+ var vx = X[q] - o[0], vy = Y[q] - o[1], vz = Z[q] - o[2];
253
+ var t = vx * u[0] + vy * u[1] + vz * u[2];
254
+ if (t < 0 || (maxT !== undefined && t > maxT)) continue;
255
+ var cx = vy * u[2] - vz * u[1], cy = vz * u[0] - vx * u[2], cz = vx * u[1] - vy * u[0];
256
+ var d = Math.sqrt(cx * cx + cy * cy + cz * cz);
257
+ if (d <= radius + growth * t) { points.push(p); ts.push(t); ds.push(d); }
258
+ }
259
+ }
260
+
261
+ if (index.count > 0) walk();
262
+ var idx = points.map(function (_, i) { return i; }).sort(function (a, b) { return points[a] - points[b]; });
263
+ return {
264
+ points: Uint32Array.from(idx, function (i) { return points[i]; }),
265
+ t: Float64Array.from(idx, function (i) { return ts[i]; }),
266
+ d: Float64Array.from(idx, function (i) { return ds[i]; })
267
+ };
268
+
269
+ function walk() {
270
+ var cs = index.cellSize, m = index.min, n = dims(index);
271
+ var lo = [m[0], m[1], m[2]], hi = [m[0] + n[0] * cs, m[1] + n[1] * cs, m[2] + n[2] * cs];
272
+ // No point has a t beyond the far side of the grid's bounding sphere
273
+ var half = 0, dc = 0;
274
+ for (var a = 0; a < 3; a++) { var e = (hi[a] - lo[a]) / 2, w = (lo[a] + hi[a]) / 2 - o[a]; half += e * e; dc += w * w; }
275
+ var T = Math.sqrt(dc) + Math.sqrt(half);
276
+ if (maxT !== undefined && maxT < T) T = maxT;
277
+ var R = radius + growth * T + cs * EPS;
278
+ // Clip [0, T] to where the ray is within R of the grid's box (slab method)
279
+ var t0 = 0, t1 = T;
280
+ for (a = 0; a < 3; a++) {
281
+ if (u[a] === 0) {
282
+ if (o[a] < lo[a] - R || o[a] > hi[a] + R) return;
283
+ continue;
284
+ }
285
+ var ta = (lo[a] - R - o[a]) / u[a], tb = (hi[a] + R - o[a]) / u[a];
286
+ if (ta > tb) { var sw = ta; ta = tb; tb = sw; }
287
+ if (ta > t0) t0 = ta;
288
+ if (tb < t1) t1 = tb;
289
+ }
290
+ if (t0 > t1) return;
291
+ // Many more cells along the way than occupied cells: test each occupied cell once
292
+ var across = 2 * R / cs + 1;
293
+ if (((t1 - t0) / cs + 1) * across * across > index.keys.length) return scanCells();
294
+ var seen = new Set();
295
+ for (var t = t0; t <= t1; t += cs) {
296
+ var t2 = Math.min(t + cs, t1), rr = radius + growth * t2;
297
+ var bl = [0, 0, 0], bh = [0, 0, 0];
298
+ for (a = 0; a < 3; a++) {
299
+ var pa = o[a] + t * u[a], pb = o[a] + t2 * u[a];
300
+ var r = cellRange(index, a, Math.min(pa, pb) - rr, Math.max(pa, pb) + rr);
301
+ bl[a] = r[0]; bh[a] = r[1];
302
+ }
303
+ visitBox(index, bl, bh, function (s) {
304
+ if (seen.has(s)) return;
305
+ seen.add(s);
306
+ check(s);
307
+ });
308
+ if (t2 >= t1) break;
309
+ }
310
+ }
311
+
312
+ // A cell can hold an answer only if its centre is near enough: every point is
313
+ // within hd of the centre, so its t and d differ from the centre's by at most hd
314
+ function scanCells() {
315
+ var cs = index.cellSize, m = index.min, keys = index.keys, hd = cs * Math.sqrt(3) / 2 + cs * EPS;
316
+ for (var s = 0; s < keys.length; s++) {
317
+ if (keys[s] < 0) continue;
318
+ var c = decode(index, keys[s]);
319
+ var vx = m[0] + (c[0] + 0.5) * cs - o[0], vy = m[1] + (c[1] + 0.5) * cs - o[1], vz = m[2] + (c[2] + 0.5) * cs - o[2];
320
+ var tc = vx * u[0] + vy * u[1] + vz * u[2];
321
+ if (tc + hd < 0 || (maxT !== undefined && tc - hd > maxT)) continue;
322
+ var cx = vy * u[2] - vz * u[1], cy = vz * u[0] - vx * u[2], cz = vx * u[1] - vy * u[0];
323
+ if (Math.sqrt(cx * cx + cy * cy + cz * cz) - hd > radius + growth * Math.max(0, tc + hd)) continue;
324
+ check(s);
325
+ }
326
+ }
327
+ }
328
+
329
+ export { checkPoint, checkPositive, cellRange, cellDist2, decode };
package/src/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  //==========================================================
7
7
 
8
8
  /** Library version, matches package.json. */
9
- export var VERSION = "0.5.0";
9
+ export var VERSION = "0.6.2";
10
10
 
11
11
  export { LasError, ERROR_CODES } from "./errors.js";
12
12
  export { openLas, LasReader } from "./LasReader.js";
@@ -16,11 +16,14 @@ export { parseCrs } from "./format/LasCrs.js";
16
16
  export { POINT_FORMATS, formatHasRgb } from "./format/PointFormats.js";
17
17
  export { decodeRecords, DEFAULT_FIELDS, FIELD_TYPES } from "./read/decodeRecords.js";
18
18
  export { makeKeep, countKept } from "./filter/filters.js";
19
- export { dedupe } from "./filter/dedupe.js";
19
+ export { dedupe, dedupeSelection } from "./filter/dedupe.js";
20
+ export { buildIndex } from "./index/grid.js";
21
+ export { pointsInRadius, nearestPoint, pointsNearRay } from "./index/query.js";
20
22
  export { selectBox, selectPolygon, selectClasses, selectReturns, thinSelection, unionSelections, intersectSelections, subtractSelections } from "./ops/select.js";
21
23
  export { keepPoints, removePoints, splitPoints, mergePoints } from "./ops/apply.js";
22
24
  export { makeTransform, transformPoints } from "./ops/transform.js";
23
25
  export { interleavePositions } from "./ops/interleave.js";
26
+ export { selectSparse } from "./ops/sparse.js";
24
27
  export { MemorySource } from "./io/MemorySource.js";
25
28
  export { BlobSource } from "./io/BlobSource.js";
26
29
  export { NodeFileSource } from "./io/NodeFileSource.js";
@@ -0,0 +1,97 @@
1
+ // src/ops/sparse.js
2
+ //==========================================================
3
+ // The sparse-point (dust) filter: select the points that have fewer
4
+ // than minNeighbours other points within radius (inclusive), for
5
+ // removePoints. A private grid with cells one radius wide is built
6
+ // and dropped. Each cell looks up its 27 neighbour cells once (9 in
7
+ // 2D); a point near enough to a cell face that rounding could reach
8
+ // two cells away gets its own lookup instead, so the count is exact.
9
+ // Counting stops at minNeighbours: a point on a dense surface
10
+ // usually gets there inside its own cell.
11
+ //==========================================================
12
+
13
+ import { LasError, ERROR_CODES } from "../errors.js";
14
+ import { coordsOf, buildGrid, lookup, checkPointCount } from "../index/grid.js";
15
+ import { cellRange, visitBox, decode } from "../index/query.js";
16
+
17
+ var FACE = 2e-6; // in cells: nearer a face than this, a point gets its own cell lookup
18
+
19
+ function bad(message) { return new LasError(ERROR_CODES.INVALID_ARGUMENT, message); }
20
+
21
+ /**
22
+ * The points with fewer than minNeighbours other points within radius.
23
+ * minNeighbours 1 keeps any point with one neighbour inside the radius, so thin
24
+ * sparse features (a line of points down a scanned blasthole) survive; clumped
25
+ * dust survives too, and a higher minNeighbours removes small clumps at the
26
+ * cost of thin features.
27
+ * @param {object} cols - point columns with x, y, z, or a k = 3 positions column.
28
+ * @param {{radius: number, minNeighbours?: number, xy?: boolean}} opts - radius in the columns' units (required); minNeighbours default 1; xy: count in 2D.
29
+ * @returns {Uint32Array} selection of the sparse points
30
+ */
31
+ export function selectSparse(cols, opts) {
32
+ var radius = opts ? opts.radius : undefined;
33
+ if (!(typeof radius === "number" && radius > 0 && isFinite(radius))) throw bad("selectSparse radius must be a positive finite number, not " + String(radius));
34
+ var minN = opts.minNeighbours === undefined ? 1 : opts.minNeighbours;
35
+ if (!(Number.isInteger(minN) && minN >= 1)) throw bad("selectSparse minNeighbours must be a whole number >= 1, not " + String(minN));
36
+ var xy = !!opts.xy;
37
+ var coords = coordsOf(cols, "selectSparse"), n = cols.count;
38
+ checkPointCount(n);
39
+ if (n === 0) return new Uint32Array(0);
40
+
41
+ var g = buildGrid(coords, n, radius, xy);
42
+ var X = coords.x, Y = coords.y, Z = coords.z, st = coords.stride, order = g.order, start = g.start, length = g.length;
43
+ var r2 = radius * radius, sparse = new Uint8Array(n), nSparse = 0;
44
+ var dz = xy ? 0 : 1;
45
+
46
+ // Count p's other points within radius over a list of slots, stopping at minN
47
+ function enough(p, slots) {
48
+ var o = p * st, px = X[o], py = Y[o], pz = xy ? 0 : Z[o], k = 0;
49
+ for (var a = 0; a < slots.length; a++) {
50
+ var s = slots[a];
51
+ for (var j = start[s], e = j + length[s]; j < e; j++) {
52
+ var q = order[j];
53
+ if (q === p) continue;
54
+ var w = q * st, ex = X[w] - px, ey = Y[w] - py, ez = xy ? 0 : Z[w] - pz;
55
+ if (ex * ex + ey * ey + ez * ez <= r2 && ++k >= minN) return true;
56
+ }
57
+ }
58
+ return false;
59
+ }
60
+
61
+ // A point on a cell face: the cells its own radius box covers, its own cell first
62
+ function slotsAround(o, own) {
63
+ var rx = cellRange(g, 0, X[o] - radius, X[o] + radius), ry = cellRange(g, 1, Y[o] - radius, Y[o] + radius);
64
+ var rz = xy ? [0, 0] : cellRange(g, 2, Z[o] - radius, Z[o] + radius), out = [own];
65
+ visitBox(g, [rx[0], ry[0], rz[0]], [rx[1], ry[1], rz[1]], function (u) { if (u !== own) out.push(u); });
66
+ return out;
67
+ }
68
+
69
+ var keys = g.keys, inv = 1 / g.cellSize, x0 = g.min[0], y0 = g.min[1], z0 = g.min[2];
70
+ for (var s = 0; s < keys.length; s++) {
71
+ if (keys[s] < 0) continue;
72
+ var c = decode(g, keys[s]);
73
+ var near = [s]; // own cell first
74
+ for (var ix = c[0] - 1; ix <= c[0] + 1; ix++) {
75
+ for (var iy = c[1] - 1; iy <= c[1] + 1; iy++) {
76
+ for (var iz = c[2] - dz; iz <= c[2] + dz; iz++) {
77
+ if (ix === c[0] && iy === c[1] && iz === c[2]) continue;
78
+ var t = lookup(g, ix, iy, iz);
79
+ if (t >= 0) near.push(t);
80
+ }
81
+ }
82
+ }
83
+ for (var j = start[s], e = j + length[s]; j < e; j++) {
84
+ var p = order[j], o = p * st;
85
+ // Where p sits in its cell (0 to 1 per axis): away from the faces, every point
86
+ // within one radius (= one cell) is in the 27 cells around
87
+ var fx = (X[o] - x0) * inv - c[0], fy = (Y[o] - y0) * inv - c[1], fz = xy ? 0.5 : (Z[o] - z0) * inv - c[2];
88
+ var slots = near;
89
+ if (fx < FACE || fx > 1 - FACE || fy < FACE || fy > 1 - FACE || fz < FACE || fz > 1 - FACE) slots = slotsAround(o, s);
90
+ if (!enough(p, slots)) { sparse[p] = 1; nSparse++; }
91
+ }
92
+ }
93
+
94
+ var out = new Uint32Array(nSparse);
95
+ for (var i = 0, w = 0; i < n; i++) if (sparse[i]) out[w++] = i;
96
+ return out;
97
+ }