las-laz-js 0.5.0 → 0.6.1
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 +26 -0
- package/README.md +53 -1
- package/package.json +1 -1
- package/src/filter/dedupe.js +46 -5
- package/src/index/grid.js +198 -0
- package/src/index/query.js +329 -0
- package/src/index.js +5 -2
- package/src/ops/sparse.js +97 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.1 (2026-10-22)
|
|
4
|
+
|
|
5
|
+
- Demo: a read bigger than the draw cap (20 M) is drawn as an even spread over the whole cloud,
|
|
6
|
+
not its first 20 M points; selections and edits still act on every point read, and the
|
|
7
|
+
selection line says how many selected points are on screen. `?drawCap=N` lowers the cap.
|
|
8
|
+
|
|
9
|
+
## 0.6.0 (2026-10-22)
|
|
10
|
+
|
|
11
|
+
Voxel neighbour index and the sparse-point (dust) filter.
|
|
12
|
+
|
|
13
|
+
- `selectSparse(cols, { radius, minNeighbours = 1, xy })` selects the points with fewer than
|
|
14
|
+
`minNeighbours` other points within `radius` (inclusive), for `removePoints`. `minNeighbours: 1`
|
|
15
|
+
keeps thin sparse features such as scanned blastholes. Checked point for point against PDAL's
|
|
16
|
+
`filters.outlier` (`method=radius`; PDAL's `min_k` is `minNeighbours`, and PDAL does not count a
|
|
17
|
+
point exactly `radius` away).
|
|
18
|
+
- `buildIndex(cols, { cellSize })`: a sorted voxel grid on flat typed arrays (6 to 8 bytes a point),
|
|
19
|
+
over x / y / z or a k = 3 `positions` column, with exact queries: `nearestPoint` (3D, or `xy` in
|
|
20
|
+
plan; `maxDistance`), `pointsInRadius` (a selection) and `pointsNearRay` (a cylinder or a cone,
|
|
21
|
+
with each point's `t` along the ray and `d` from its axis, for a caller's snap). Rebuild it after
|
|
22
|
+
any edit of the columns.
|
|
23
|
+
- `dedupeSelection(cols, tolerance, { z })` returns `dedupe`'s kept points as a selection and a
|
|
24
|
+
`map` from every input point to the kept point it merged into (its nearest within the tolerance).
|
|
25
|
+
- Speed (Node): a 34-million-point LAS (Float32 after a local origin) indexes in about 1.1 s at
|
|
26
|
+
about 8 bytes a point, and `selectSparse` at 2 m takes about 2.1 s.
|
|
27
|
+
- Demo: a Sparse points selection on the Edit tab.
|
|
28
|
+
|
|
3
29
|
## 0.5.0 (2026-10-22)
|
|
4
30
|
|
|
5
31
|
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
|
|
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.
|
|
3
|
+
"version": "0.6.1",
|
|
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": {
|
package/src/filter/dedupe.js
CHANGED
|
@@ -6,18 +6,24 @@
|
|
|
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";
|
|
13
15
|
|
|
14
16
|
/**
|
|
17
|
+
* The points dedupe keeps, and where every input point went.
|
|
18
|
+
* map[i] is the output number (position in keep) of the kept point that point i
|
|
19
|
+
* merged into: for a dropped point, the nearest kept point within tolerance
|
|
20
|
+
* (lowest number on a tie); a kept point maps to itself.
|
|
15
21
|
* @param {object} cols - PointColumns ({ count, x, y, z?, ... }).
|
|
16
22
|
* @param {number} tolerance - distance below which points merge (> 0).
|
|
17
|
-
* @param {{z?: boolean}} [opts]
|
|
18
|
-
* @returns {
|
|
23
|
+
* @param {{z?: boolean}} [opts] - z: compare in 3D.
|
|
24
|
+
* @returns {{keep: Uint32Array, map: Uint32Array}}
|
|
19
25
|
*/
|
|
20
|
-
export function
|
|
26
|
+
export function dedupeSelection(cols, tolerance, opts) {
|
|
21
27
|
if (!(tolerance > 0)) throw new LasError(ERROR_CODES.INVALID_ARGUMENT, "dedupe tolerance must be > 0");
|
|
22
28
|
var use3d = !!(opts && opts.z && cols.z);
|
|
23
29
|
var tol2 = tolerance * tolerance;
|
|
@@ -44,6 +50,8 @@ export function dedupe(cols, tolerance, opts) {
|
|
|
44
50
|
var head = new Map();
|
|
45
51
|
var next = new Int32Array(n);
|
|
46
52
|
var kept = new Uint32Array(n);
|
|
53
|
+
var map = new Uint32Array(n);
|
|
54
|
+
var dropped = [];
|
|
47
55
|
var nKept = 0;
|
|
48
56
|
for (var i = 0; i < n; i++) {
|
|
49
57
|
var cx = Math.floor(x[i] / tolerance) - lo[0] + 1, cy = Math.floor(y[i] / tolerance) - lo[1] + 1;
|
|
@@ -62,16 +70,49 @@ export function dedupe(cols, tolerance, opts) {
|
|
|
62
70
|
}
|
|
63
71
|
}
|
|
64
72
|
}
|
|
65
|
-
if (dup) continue;
|
|
73
|
+
if (dup) { dropped.push(i); continue; }
|
|
66
74
|
var own = key(cx, cy, cz);
|
|
67
75
|
var prev = head.get(own);
|
|
68
76
|
next[i] = prev === undefined ? -1 : prev;
|
|
69
77
|
head.set(own, i);
|
|
78
|
+
map[i] = nKept;
|
|
70
79
|
kept[nKept++] = i;
|
|
71
80
|
}
|
|
72
81
|
|
|
82
|
+
// Each dropped point maps to its nearest kept point (the cell lists hold kept points only)
|
|
83
|
+
dropped.forEach(function (i) {
|
|
84
|
+
var cx = Math.floor(x[i] / tolerance) - lo[0] + 1, cy = Math.floor(y[i] / tolerance) - lo[1] + 1;
|
|
85
|
+
var cz = use3d ? Math.floor(z[i] / tolerance) - lo[2] + 1 : 1;
|
|
86
|
+
var best = -1, bd = Infinity;
|
|
87
|
+
for (var dx = -1; dx <= 1; dx++) {
|
|
88
|
+
for (var dy = -1; dy <= 1; dy++) {
|
|
89
|
+
for (var dz = -dzRange; dz <= dzRange; dz++) {
|
|
90
|
+
var k = head.get(key(cx + dx, cy + dy, cz + dz));
|
|
91
|
+
while (k !== undefined && k >= 0) {
|
|
92
|
+
var ex = x[k] - x[i], ey = y[k] - y[i];
|
|
93
|
+
var ez = use3d ? z[k] - z[i] : 0;
|
|
94
|
+
var d = ex * ex + ey * ey + ez * ez;
|
|
95
|
+
if (d <= tol2 && (d < bd || (d === bd && k < best))) { bd = d; best = k; }
|
|
96
|
+
k = next[k];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
map[i] = map[best];
|
|
102
|
+
});
|
|
103
|
+
return { keep: kept.slice(0, nKept), map: map };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {object} cols - PointColumns ({ count, x, y, z?, ... }).
|
|
108
|
+
* @param {number} tolerance - distance below which points merge (> 0).
|
|
109
|
+
* @param {{z?: boolean}} [opts]
|
|
110
|
+
* @returns {object} PointColumns with only the kept points
|
|
111
|
+
*/
|
|
112
|
+
export function dedupe(cols, tolerance, opts) {
|
|
113
|
+
var sel = dedupeSelection(cols, tolerance, opts).keep, nKept = sel.length;
|
|
114
|
+
|
|
73
115
|
// Every column (k values per point) follows the kept points; anything else passes through
|
|
74
|
-
var sel = kept.subarray(0, nKept);
|
|
75
116
|
var ks = Object.create(null); // a column may be named like an Object.prototype member
|
|
76
117
|
columnsOf(cols).forEach(function (c) { ks[c.name] = c.k; });
|
|
77
118
|
var out = { count: nKept };
|
|
@@ -0,0 +1,198 @@
|
|
|
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
|
+
* The home slot of a cell key (an integer below 2^53) in a table of mask + 1 slots.
|
|
61
|
+
* @param {number} key
|
|
62
|
+
* @param {number} mask
|
|
63
|
+
* @returns {number}
|
|
64
|
+
*/
|
|
65
|
+
export function hashKey(key, mask) {
|
|
66
|
+
var lo = key % 4294967296, hi = (key - lo) / 4294967296;
|
|
67
|
+
return fmix32(lo ^ fmix32(hi)) & mask;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function nextPow2(n) {
|
|
71
|
+
var p = 16;
|
|
72
|
+
while (p < n) p *= 2;
|
|
73
|
+
return p;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Build the grid over coordinates.
|
|
78
|
+
* @param {{x, y, z, stride}} coords - from coordsOf.
|
|
79
|
+
* @param {number} count - points.
|
|
80
|
+
* @param {number} [cellSize] - cell edge; omitted: about 16 points per cell.
|
|
81
|
+
* @param {boolean} [xy] - ignore Z (nz = 1).
|
|
82
|
+
* @returns {object} PointIndex
|
|
83
|
+
*/
|
|
84
|
+
export function buildGrid(coords, count, cellSize, xy) {
|
|
85
|
+
if (cellSize !== undefined && !(typeof cellSize === "number" && cellSize > 0 && isFinite(cellSize))) throw bad("cellSize must be a positive finite number, not " + String(cellSize));
|
|
86
|
+
var X = coords.x, Y = coords.y, Z = coords.z, st = coords.stride;
|
|
87
|
+
|
|
88
|
+
// 1. Bounds
|
|
89
|
+
var lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
|
|
90
|
+
for (var i = 0, o = 0; i < count; i++, o += st) {
|
|
91
|
+
var a = X[o], b = Y[o], c = xy ? 0 : Z[o];
|
|
92
|
+
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");
|
|
93
|
+
if (a < lo[0]) lo[0] = a; if (a > hi[0]) hi[0] = a;
|
|
94
|
+
if (b < lo[1]) lo[1] = b; if (b > hi[1]) hi[1] = b;
|
|
95
|
+
if (c < lo[2]) lo[2] = c; if (c > hi[2]) hi[2] = c;
|
|
96
|
+
}
|
|
97
|
+
if (count === 0) { lo = [0, 0, 0]; hi = [0, 0, 0]; }
|
|
98
|
+
var ext = [hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]];
|
|
99
|
+
if (cellSize === undefined) {
|
|
100
|
+
var prod = 1, d = 0;
|
|
101
|
+
ext.forEach(function (e) { if (e > 0) { prod *= e; d++; } });
|
|
102
|
+
cellSize = d === 0 ? 1 : Math.pow(16 * prod / count, 1 / d);
|
|
103
|
+
}
|
|
104
|
+
var nx = Math.floor(ext[0] / cellSize) + 1, ny = Math.floor(ext[1] / cellSize) + 1, nz = Math.floor(ext[2] / cellSize) + 1;
|
|
105
|
+
if (nx * ny * nz > MAX_CELLS) throw bad("cellSize too small for this extent: " + nx + " × " + ny + " × " + nz + " cells is more than 2^53");
|
|
106
|
+
var inv = 1 / cellSize, x0 = lo[0], y0 = lo[1], z0 = lo[2];
|
|
107
|
+
function keyOf(o) {
|
|
108
|
+
var cx = Math.floor((X[o] - x0) * inv); if (cx >= nx) cx = nx - 1;
|
|
109
|
+
var cy = Math.floor((Y[o] - y0) * inv); if (cy >= ny) cy = ny - 1;
|
|
110
|
+
var cz = 0;
|
|
111
|
+
if (!xy) { cz = Math.floor((Z[o] - z0) * inv); if (cz >= nz) cz = nz - 1; }
|
|
112
|
+
return (cx * ny + cy) * nz + cz;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 2. Count points per cell into the hash table
|
|
116
|
+
var cap = nextPow2(2 * Math.min(nx * ny * nz, Math.ceil(count / 16)));
|
|
117
|
+
var mask = cap - 1, used = 0;
|
|
118
|
+
var keys = new Float64Array(cap).fill(EMPTY), length = new Uint32Array(cap);
|
|
119
|
+
function grow() {
|
|
120
|
+
var oldK = keys, oldL = length;
|
|
121
|
+
cap *= 2; mask = cap - 1;
|
|
122
|
+
keys = new Float64Array(cap).fill(EMPTY); length = new Uint32Array(cap);
|
|
123
|
+
for (var s = 0; s < oldK.length; s++) {
|
|
124
|
+
if (oldK[s] === EMPTY) continue;
|
|
125
|
+
var h = hashKey(oldK[s], mask);
|
|
126
|
+
while (keys[h] !== EMPTY) h = (h + 1) & mask;
|
|
127
|
+
keys[h] = oldK[s]; length[h] = oldL[s];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (i = 0, o = 0; i < count; i++, o += st) {
|
|
131
|
+
var key = keyOf(o), h = hashKey(key, mask);
|
|
132
|
+
while (keys[h] !== EMPTY && keys[h] !== key) h = (h + 1) & mask;
|
|
133
|
+
if (keys[h] === EMPTY) {
|
|
134
|
+
if (2 * (used + 1) > cap) {
|
|
135
|
+
grow();
|
|
136
|
+
h = hashKey(key, mask);
|
|
137
|
+
while (keys[h] !== EMPTY) h = (h + 1) & mask;
|
|
138
|
+
}
|
|
139
|
+
keys[h] = key; used++;
|
|
140
|
+
}
|
|
141
|
+
length[h]++;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 3. Prefix sums give each cell's start; place point numbers (stable)
|
|
145
|
+
var start = new Uint32Array(cap), run = 0;
|
|
146
|
+
for (var s = 0; s < cap; s++) { start[s] = run; run += length[s]; }
|
|
147
|
+
var order = new Uint32Array(count);
|
|
148
|
+
for (i = 0, o = 0; i < count; i++, o += st) {
|
|
149
|
+
var k2 = keyOf(o), h2 = hashKey(k2, mask);
|
|
150
|
+
while (keys[h2] !== k2) h2 = (h2 + 1) & mask;
|
|
151
|
+
order[start[h2]++] = i;
|
|
152
|
+
}
|
|
153
|
+
for (s = 0; s < cap; s++) start[s] -= length[s];
|
|
154
|
+
|
|
155
|
+
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 };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Build a neighbour index over point columns.
|
|
160
|
+
* @param {object} cols - point columns with x, y, z, or a k = 3 positions column.
|
|
161
|
+
* @param {{cellSize?: number}} [opts] - cellSize omitted: about 16 points per cell.
|
|
162
|
+
* @returns {object} PointIndex { count, cellSize, min, nx, ny, nz, order, keys, start, length, mask, coords }
|
|
163
|
+
*/
|
|
164
|
+
export function buildIndex(cols, opts) {
|
|
165
|
+
var coords = coordsOf(cols, "buildIndex");
|
|
166
|
+
checkPointCount(cols.count);
|
|
167
|
+
return buildGrid(coords, cols.count, opts ? opts.cellSize : undefined, false);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The hash-table slot of cell (ix, iy, iz), or -1 when the cell is empty or outside the grid.
|
|
172
|
+
* @param {object} index - PointIndex.
|
|
173
|
+
* @returns {number}
|
|
174
|
+
*/
|
|
175
|
+
export function lookup(index, ix, iy, iz) {
|
|
176
|
+
if (ix < 0 || iy < 0 || iz < 0 || ix >= index.nx || iy >= index.ny || iz >= index.nz) return -1;
|
|
177
|
+
var key = (ix * index.ny + iy) * index.nz + iz, keys = index.keys, mask = index.mask;
|
|
178
|
+
var h = hashKey(key, mask);
|
|
179
|
+
while (keys[h] !== EMPTY) {
|
|
180
|
+
if (keys[h] === key) return h;
|
|
181
|
+
h = (h + 1) & mask;
|
|
182
|
+
}
|
|
183
|
+
return -1;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Refuse an index that is not self-consistent.
|
|
188
|
+
* @param {object} index - PointIndex.
|
|
189
|
+
* @param {string} fn - the calling function's name, for the message.
|
|
190
|
+
*/
|
|
191
|
+
export function checkIndex(index, fn) {
|
|
192
|
+
var ok = index !== null && typeof index === "object" && index.order instanceof Uint32Array && index.order.length === index.count && index.coords;
|
|
193
|
+
if (ok && index.count > 0) {
|
|
194
|
+
var need = (index.count - 1) * index.coords.stride + 1;
|
|
195
|
+
ok = index.coords.x.length >= need && index.coords.y.length >= need && index.coords.z.length >= need;
|
|
196
|
+
}
|
|
197
|
+
if (!ok) throw bad(fn + ": index is not self-consistent (rebuild it with buildIndex)");
|
|
198
|
+
}
|
|
@@ -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.
|
|
9
|
+
export var VERSION = "0.6.1";
|
|
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
|
+
}
|