las-laz-js 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0 (2026-10-22)
4
+
5
+ Point operations.
6
+
7
+ - Operations on point columns (the object `readPoints` returns, or any `{ count, <name>: TypedArray }`
8
+ whose arrays are `count × k` long), with no dependencies. A selection is a `Uint32Array` of
9
+ strictly ascending point indices, checked on use (`INVALID_ARGUMENT` names the bad position).
10
+ - Select: `selectBox`, `selectPolygon` (even-odd; `boundary: "touching"` keeps points on the edge,
11
+ `"inside"` drops them; `tolerance`; `minZ` / `maxZ`), `selectClasses`, `selectReturns` (`returns`,
12
+ `first`, `last`, `single`, any one matches), `thinSelection` (the reader's `maxPoints` rule,
13
+ exported), and `unionSelections`, `intersectSelections`, `subtractSelections`.
14
+ - Apply: `keepPoints`, `removePoints`, `splitPoints`, `mergePoints`. Copies by default; `inPlace: true`
15
+ on keep and remove compacts each column inside its own buffer and returns views of it (columns that
16
+ share memory are refused). `mergePoints` takes the union of the columns, lists zero-filled ones
17
+ in `filled`, widens types (Float64 over Float32 over integers; Int32 or Uint32 with Float32 gives
18
+ Float64), and refuses a `k` mismatch, a column named `count` or `filled`, and signed with unsigned
19
+ integers unless the signed type is strictly wider.
20
+ - Transform: `makeTransform({ shift, rotateZ, rotate, scale, about })` builds a row-major `{ m, t }`;
21
+ `transformPoints(cols, T, { inPlace })` applies it in Float64 and returns a new `{ count, columns }`
22
+ object (a copy has new x, y, z and shares the rest; `inPlace` returns the input's arrays, overwritten).
23
+ - `interleavePositions(cols, { layout, into, at, origin, float32 })` packs x y (z) into a caller's
24
+ Float32 / Float64 buffer, shifting by `origin` in Float64 first.
25
+ - Reads: `index` field (each point's record number; Uint32, Float64 above 4,294,967,295 points);
26
+ `polygon` / `boundary` / `tolerance` crop while reading, so only the region is decoded and held;
27
+ `transform` applies in the fixed order record → world → T → minus `localOrigin` → Float32.
28
+ Filters apply in file coordinates; the result's bounds and `dedupe` are in output coordinates,
29
+ and the worker's elevation colour range follows the transform.
30
+ - Changed: an omitted (or `null`) `bounds` limit is now open. Before, a missing limit kept nothing.
31
+ `polygon: null` means no polygon. `selectBox` and the `minZ` / `maxZ` of `selectPolygon` read a
32
+ `null` bound as open too.
33
+ - `dedupe` moves every column (k values per point, such as an interleaved `positions` or `rgb`) with
34
+ the kept points; other properties pass through. `transformPoints` with `inPlace` refuses x, y or z
35
+ that share memory. `mergePoints` ignores a zero-point input when widening types.
36
+ - Demo: an Edit tab (polygon, box and class selections; keep, delete, split, transform, crop on
37
+ read) and LAZ export.
38
+ - Docs: README "Point operations".
39
+
3
40
  ## 0.4.0 (2026-10-22)
4
41
 
5
42
  Writes LAZ.
package/README.md CHANGED
@@ -7,8 +7,11 @@ runs in the browser and in Node — and built for files far larger than memory.
7
7
  **Status: early (0.x).** It works and is tested, but the API may still change before 1.0.
8
8
 
9
9
  - Working: `openLas` reads the header, VLRs / EVLRs and coordinate system of LAS 1.0–1.4;
10
- `readPoints` and `chunks` decode every point format (0–10) a window at a time, with filters
11
- and thinning; `colourize` colours points; `writeLas` and `createLasWriter` write LAS 1.0–1.4.
10
+ `readPoints` and `chunks` decode every point format (0–10) a window at a time, with filters,
11
+ thinning, a polygon crop, a coordinate transform and the original point index; `colourize`
12
+ colours points; `writeLas` and `createLasWriter` write LAS 1.0–1.4.
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.
12
15
  - LAZ: point formats 0–3 and 6–8, read exactly like LAS — the same calls, the same options,
13
16
  decoded chunk by chunk in plain JavaScript (no WebAssembly) — and written with
14
17
  `compress: true`, byte for byte what PDAL (LAZperf) writes (bar one lossless exception, see
@@ -35,8 +38,9 @@ Drop a LAS or LAZ file onto the page. It is read locally (never uploaded), decod
35
38
  library's Worker (LAZ on every core) behind a progress bar, then shown once in a Three.js viewer;
36
39
  colour changes recolour the points already read, without reading the file again. The side panel shows the header, VLRs, coordinate
37
40
  system and LAZ compression; reads with class, return, bounds, thinning and dedupe filters;
38
- colours by RGB, intensity, elevation or class; lists point statistics; and saves the points back
39
- out as LAS. To run it locally: `npm run demo`, then open http://localhost:8080/.
41
+ colours by RGB, intensity, elevation or class; lists point statistics; selects points by polygon,
42
+ box or class and keeps, deletes, splits, transforms or crops them (the Edit tab); and saves the
43
+ points back out as LAS or LAZ. To run it locally: `npm run demo`, then open http://localhost:8080/.
40
44
 
41
45
  ## Reading
42
46
 
@@ -56,7 +60,11 @@ var pts = await las.readPoints({
56
60
  fields: ["x", "y", "z", "classification", "red", "green", "blue"],
57
61
  classes: [2], // keep only these classification codes
58
62
  returns: [1], // keep only these return numbers
59
- bounds: { minX, maxX, minY, maxY, minZ, maxZ },
63
+ bounds: { minX, maxX, minY, maxY, minZ, maxZ }, // any limit may be left out: an omitted limit is open
64
+ polygon: [x0, y0, x1, y1, x2, y2], // crop: keep points inside it (file coordinates, XY, even-odd); null is none
65
+ boundary: "touching", // polygon edge: "touching" keeps points on it (default), "inside" drops them
66
+ tolerance: 0, // file units: within this of a polygon edge counts as on it
67
+ transform: T, // { m, t } from makeTransform, applied to world coordinates (see Point operations)
60
68
  maxPoints: 5000000, // even thinning; 0 keeps every point
61
69
  dedupe: 0.01, // drop points within 1 cm (XY; dedupeZ: true for 3D)
62
70
  localOrigin: { x: 478000, y: 6772000 }, float32: true, // small Float32 coordinates for the GPU
@@ -64,7 +72,7 @@ var pts = await las.readPoints({
64
72
  signal: abortController.signal
65
73
  });
66
74
  pts.count; pts.x; pts.y; pts.z; // one typed array per field
67
- pts.bounds; pts.stats; // world bounds, per-class counts
75
+ pts.bounds; pts.stats; // world bounds (after any transform), per-class counts
68
76
  ```
69
77
 
70
78
  Fields: `x y z intensity returnNumber numberOfReturns scanDirection edgeOfFlightLine
@@ -72,10 +80,141 @@ classification classificationFlags scannerChannel scanAngle userData pointSource
72
80
  red green blue nir`. Fields a point format does not hold are left out. `scanAngle` is in
73
81
  degrees for every format.
74
82
 
83
+ `index` is one more field, for every format: the column is present for the points the filters and
84
+ `maxPoints` keep, and each value is that record's position in the file (0-based; it is not
85
+ renumbered after filtering). It is a `Uint32Array`, or a `Float64Array` for a file of more than
86
+ 4,294,967,295 points. It is an ordinary column, so keep, remove, split and merge carry it, and it
87
+ tells you which records of the file are left.
88
+
75
89
  Filters run on the raw record before it is decoded, so a filtered-out point costs a byte read.
90
+ `polygon` is a crop on read: only the points inside it are decoded and held, so a site region of
91
+ a large file never costs the whole file in memory. It needs at least 3 vertices (a flat list of
92
+ x, y pairs, implicitly closed) and combines with the other filters and `maxPoints`. A LAZ crop
93
+ saves memory and field decoding but still decompresses every chunk (there is no spatial index), so
94
+ its time is close to a full read.
76
95
  `maxPoints` keeps exactly that many of the points that pass the filters, spread evenly over
77
96
  the whole file; with filters set it first counts the passing points, which reads the file twice.
78
97
 
98
+ In `bounds`, a limit that is left out (or `null`) is open; before 0.5.0 a missing limit kept
99
+ nothing. `polygon: null` means no polygon. `boundary` and `tolerance` belong to `polygon`: a point
100
+ within `tolerance` of an edge or vertex is on the boundary, kept by `"touching"` and dropped by
101
+ `"inside"`. They are not `dedupe`'s tolerance, which is the value of the `dedupe` option.
102
+
103
+ `transform` moves the points while they are decoded, in a fixed order: record → world (scale and
104
+ offset) → `transform` (Float64) → minus `localOrigin` → Float32 (when `float32` and `localOrigin`
105
+ are set). Raw scanner coordinates never pass through Float32 at full magnitude. The filters
106
+ (`classes`, `returns`, `bounds`, `polygon`) apply in **file** coordinates, before the transform;
107
+ the result's `bounds`, its stats and `dedupe` are in the output coordinates. To crop a scan in
108
+ site coordinates, read it transformed and use `selectPolygon`. The Worker's elevation colour
109
+ range follows the transform.
110
+
111
+ ## Point operations
112
+
113
+ Zero-dependency operations on **point columns**: the object `readPoints` returns, or any object
114
+ of the same shape, `{ count, <name>: TypedArray, ... }`. A property is a column when it is a typed
115
+ array whose length is `count × k` for a whole number `k` of 1 or more (`k` is 1 for `x` or
116
+ `classification`, 3 for an interleaved `positions` or `rgb`). Operations carry every column, `k`
117
+ values per point, and nothing else: `bounds`, `stats` and the like are not copied, so rebuild them
118
+ from the result. A **selection** is a `Uint32Array` of point indices, strictly ascending, each
119
+ below `count`. Everything that takes one checks it in one pass and throws `INVALID_ARGUMENT`
120
+ naming the first bad position; everything that makes one returns a valid one.
121
+
122
+ ### Select
123
+
124
+ ```js
125
+ import { selectBox, selectPolygon, selectClasses, selectReturns, thinSelection,
126
+ unionSelections, intersectSelections, subtractSelections } from "las-laz-js";
127
+
128
+ var inBox = selectBox(pts, { minX: 477010, maxX: 477050, minZ: 295 }); // inclusive; a limit left out is open
129
+ var inSite = selectPolygon(pts, poly, { boundary: "inside", tolerance: 0.001 }); // poly: [x0, y0, x1, y1, ...]
130
+ var ground = selectClasses(pts, [2]);
131
+ var last = selectReturns(pts, { last: true, single: true }); // returns: [..], first, last, single; any one matches
132
+ var thin = thinSelection(pts.count, 1000); // exactly min(count, 1000) indices, spread evenly
133
+ var wanted = intersectSelections(inSite, ground); // also unionSelections, subtractSelections
134
+ ```
135
+
136
+ `selectPolygon` takes a flat `[x0, y0, x1, y1, ...]` list of at least 3 vertices, closed for you,
137
+ in the columns' own coordinates; the interior is the even-odd rule, so a concave polygon works.
138
+ Points on the boundary are kept by `boundary: "touching"` (the default) and dropped by `"inside"`,
139
+ so the two differ by exactly the boundary points, and `splitPoints` by one polygon loses and
140
+ duplicates nothing. Coordinates are not exact in binary, so a point mathematically on an edge may not
141
+ count as on it at `tolerance: 0`; use a small tolerance (for example 1e-6 of the units) to catch
142
+ boundary points. `minZ` / `maxZ` add a height limit. `thinSelection` is the reader's
143
+ `maxPoints` rule: `keepPoints(all, thinSelection(n, t))` gives the points
144
+ `readPoints({ maxPoints: t })` does.
145
+
146
+ ### Keep, remove, split, merge
147
+
148
+ ```js
149
+ import { keepPoints, removePoints, splitPoints, mergePoints } from "las-laz-js";
150
+
151
+ var kept = keepPoints(pts, wanted); // new columns of the same types
152
+ var without = removePoints(pts, wanted);
153
+ var [inside, outside] = splitPoints(pts, inSite); // always copies; every point lands in one result
154
+ var both = mergePoints([inside, outside]); // concatenated in order; both.filled is []
155
+
156
+ var crop = keepPoints(big, selectPolygon(big, poly), { inPlace: true }); // no second copy
157
+ ```
158
+
159
+ `inPlace: true` (for `keepPoints` and `removePoints`) slides the kept values down inside each
160
+ column's own buffer and returns views of that memory: the input is overwritten, and a big cloud is
161
+ cropped without a second copy. Columns that share memory (the same array twice, or overlapping
162
+ views) are refused, since compacting one would corrupt the other.
163
+
164
+ `mergePoints` takes the union of the column names; a column missing from an input is zero for
165
+ that input's points, and its name is listed in `result.filled`. A column named `count` or
166
+ `filled` is refused. A column's `k` must agree across inputs. The result's type is the widest:
167
+ Float64 over Float32 over the integer types; an Int32 or Uint32 with a Float32 gives Float64
168
+ (Float32 holds integers exactly only to 2^24). Signed with unsigned integers merge only when the
169
+ signed type is strictly wider (Int16 with Uint8 is Int16; Int8 with Uint8, or Int32 with Uint32,
170
+ is `INVALID_ARGUMENT`). An input with no points is ignored when the type is chosen, so an empty
171
+ Int8 column does not block a Uint16 one; a column only an empty input has keeps its type.
172
+ Coordinates must already share one frame.
173
+
174
+ ### Transform
175
+
176
+ ```js
177
+ import { makeTransform, transformPoints } from "las-laz-js";
178
+
179
+ // p' = scale · R · (p − about) + about + shift; rotateZ is degrees anticlockwise about +Z
180
+ var T = makeTransform({ rotateZ: 90, about: [477050, 6770015, 0], shift: [10, 0, 0] });
181
+ // T = { m: [0, -1, 0, 1, 0, 0, 0, 0, 1], t: [7247075, 6292965, 0] } row-major: p' = m·p + t
182
+
183
+ var moved = transformPoints(pts, T); // { count, columns }: new x, y, z; the rest shared
184
+ var same = transformPoints(pts, T, { inPlace: true }); // the same x, y, z arrays, overwritten
185
+
186
+ var local = await las.readPoints({ // or while reading
187
+ fields: ["x", "y", "z"], transform: T,
188
+ localOrigin: { x: 477050, y: 6770015 }, float32: true
189
+ });
190
+ ```
191
+
192
+ A transform is a plain object, `{ m, t }`, with `m` nine numbers (a row-major 3×3) and `t` three.
193
+ Any affine map fits, so a fitted registration can be passed the same way; it is not Three.js's
194
+ column-major `Matrix4`. `makeTransform` takes `shift`, `about`, `scale` and either `rotateZ` or
195
+ `rotate` (a row-major 3×3; not both). `transformPoints` needs `x`, `y` and `z`, computes in
196
+ Float64 and stores in the columns' own types. It returns a new `{ count, columns }` object either
197
+ way: with `inPlace` the arrays in it are the input's, overwritten (and `x`, `y` and `z` must not
198
+ share memory, or it throws `INVALID_ARGUMENT`). On read, the order is the one
199
+ above.
200
+
201
+ ### Interleave for a renderer
202
+
203
+ ```js
204
+ import { interleavePositions } from "las-laz-js";
205
+
206
+ var positions = interleavePositions(pts, { origin: { x: 477000, y: 6770000 } }); // Float32Array, x y z per point
207
+ geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
208
+
209
+ // or into a buffer you own, starting at point 'at'; layout "xy" writes two values per point
210
+ interleavePositions(pts, { into: buffer, at: 50000, layout: "xyz", origin: { x: 477000, y: 6770000, z: 300 } });
211
+ ```
212
+
213
+ The origin is subtracted in Float64 before storing, so world coordinates keep their decimals in a
214
+ Float32 buffer; an axis left out of `origin` is not shifted ({ x, y } keeps world Z). `float32`
215
+ defaults to true and picks the type of a new array; `into` must be a Float32Array or Float64Array
216
+ long enough, else `INVALID_ARGUMENT`.
217
+
79
218
  ## LAZ
80
219
 
81
220
  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.4.0",
3
+ "version": "0.5.0",
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/LasReader.js CHANGED
@@ -25,6 +25,7 @@ import { createChunkDecoder } from "./laz/decompressChunk.js";
25
25
  import { createLazPool, defaultThreads } from "./laz/pool.js";
26
26
  import { makeKeep, countKept } from "./filter/filters.js";
27
27
  import { dedupe } from "./filter/dedupe.js";
28
+ import { checkTransform } from "./ops/transform.js";
28
29
  import { openScratchSyncWriter } from "./io/ScratchBinaryFile.js";
29
30
 
30
31
  var DEFAULT_WINDOW = 64 * 1024 * 1024;
@@ -179,7 +180,7 @@ LasReader.prototype._plan = async function () {
179
180
 
180
181
  /**
181
182
  * Iterate the selected points window by window.
182
- * @param {object} [opts] - fields, classes, returns, bounds, maxPoints, localOrigin, float32, onProgress, signal
183
+ * @param {object} [opts] - fields, classes, returns, bounds, polygon (crop; with boundary "touching" | "inside" and tolerance), maxPoints, transform ({ m, t } from makeTransform: world coordinates are transformed in Float64 before localOrigin is subtracted; filters apply in file coordinates), localOrigin, float32, onProgress, signal
183
184
  * @returns {AsyncGenerator<object>} PointColumns per window
184
185
  */
185
186
  LasReader.prototype.chunks = async function* (opts) {
@@ -192,6 +193,7 @@ LasReader.prototype.chunks = async function* (opts) {
192
193
  // makes decodeRecords write straight into readPoints' result columns.
193
194
  LasReader.prototype._scan = async function* (opts, into) {
194
195
  var h = this.header;
196
+ if (opts.transform) checkTransform(opts.transform, "transform");
195
197
  var plan = await this._plan();
196
198
  var keep = makeKeep(opts, h);
197
199
  var fields = resolveFields(h, opts.fields);
@@ -208,7 +210,7 @@ LasReader.prototype._scan = async function* (opts, into) {
208
210
  var kept = 0, w = 0;
209
211
  for await (var bytes of windowBytes(plan, pool, buf, opts.signal)) {
210
212
  var win = list[w];
211
- var cols = decodeRecords(bytes, h, { fields: fields, keep: keep, thin: thin, localOrigin: opts.localOrigin, float32: opts.float32, into: into ? into(win) : null });
213
+ var cols = decodeRecords(bytes, h, { fields: fields, keep: keep, thin: thin, localOrigin: opts.localOrigin, float32: opts.float32, transform: opts.transform, firstIndex: win.firstRecord, into: into ? into(win) : null });
212
214
  kept += cols.count;
213
215
  w++;
214
216
  if (typeof opts.onProgress === "function") opts.onProgress(w / list.length, "read " + kept + " points");
@@ -257,7 +259,7 @@ LasReader.prototype._countPassing = async function (plan, pool, buf, keep, signa
257
259
 
258
260
  /**
259
261
  * Read the selected points into one set of columns.
260
- * @param {object} [opts] - chunks() options plus dedupe (tolerance) and dedupeZ
262
+ * @param {object} [opts] - chunks() options plus dedupe (tolerance) and dedupeZ; dedupe runs on the output coordinates (after transform and localOrigin)
261
263
  * @returns {Promise<object>} PointColumns plus bounds and stats
262
264
  */
263
265
  LasReader.prototype.readPoints = async function (opts) {
@@ -273,7 +275,7 @@ LasReader.prototype.readPoints = async function (opts) {
273
275
  var cap = !filtered ? (opts.maxPoints > 0 ? Math.min(opts.maxPoints, h.pointCount) : h.pointCount)
274
276
  : (opts.maxPoints > 0 ? Math.min(opts.maxPoints, h.pointCount) : 0);
275
277
  var acc = {}, n = 0;
276
- fields.forEach(function (f) { acc[f] = new (columnType(f, opts))(cap || 1024); });
278
+ fields.forEach(function (f) { acc[f] = new (columnType(f, opts, h))(cap || 1024); });
277
279
  function into(win) {
278
280
  // A known cap is never exceeded, so it never grows; filters alone grow by doubling
279
281
  if (!cap && n + win.count > acc[fields[0]].length) {
@@ -5,10 +5,11 @@
5
5
  // tolerance-sized cells, neighbours checked), ported from point
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
- // Every column follows the kept points.
8
+ // Every column follows the kept points (k values per point).
9
9
  //==========================================================
10
10
 
11
11
  import { LasError, ERROR_CODES } from "../errors.js";
12
+ import { columnsOf, gather, sameType } from "../ops/columns.js";
12
13
 
13
14
  /**
14
15
  * @param {object} cols - PointColumns ({ count, x, y, z?, ... }).
@@ -69,13 +70,21 @@ export function dedupe(cols, tolerance, opts) {
69
70
  kept[nKept++] = i;
70
71
  }
71
72
 
73
+ // Every column (k values per point) follows the kept points; anything else passes through
74
+ var sel = kept.subarray(0, nKept);
75
+ var ks = Object.create(null); // a column may be named like an Object.prototype member
76
+ columnsOf(cols).forEach(function (c) { ks[c.name] = c.k; });
72
77
  var out = { count: nKept };
73
78
  Object.keys(cols).forEach(function (name) {
79
+ if (name === "count") return;
74
80
  var col = cols[name];
75
- if (!ArrayBuffer.isView(col) || col.length !== cols.count) { if (name !== "count") out[name] = col; return; }
76
- var c = new col.constructor(nKept);
77
- for (var q = 0; q < nKept; q++) c[q] = col[kept[q]];
78
- out[name] = c;
81
+ var v = col;
82
+ if (Object.prototype.hasOwnProperty.call(ks, name)) {
83
+ v = sameType(col, nKept * ks[name]);
84
+ gather(col, v, ks[name], sel);
85
+ }
86
+ // defineProperty so a column named "__proto__" stays a column
87
+ Object.defineProperty(out, name, { value: v, enumerable: true, writable: true, configurable: true });
79
88
  });
80
89
  return out;
81
90
  }
@@ -2,8 +2,8 @@
2
2
  //==========================================================
3
3
  // Record filters that run on the RAW record, before decoding, so
4
4
  // rejected points cost one or two byte reads (Kirra's classFilter
5
- // trick, generalised): classification codes, return numbers and a
6
- // 3D box. Filters combine with AND.
5
+ // trick, generalised): classification codes, return numbers, a
6
+ // 3D box and a polygon (crop on read). Filters combine with AND.
7
7
  // maxPoints thinning is NOT Kirra's floor-stride-and-cap rule, which
8
8
  // kept only the start of the file and, run before the filters,
9
9
  // could leave nothing: decodeRecords thins the records that PASS,
@@ -11,12 +11,16 @@
11
11
  //==========================================================
12
12
 
13
13
  import { POINT_FORMATS } from "../format/PointFormats.js";
14
+ import { makePolygonTest } from "../ops/polygon.js";
14
15
 
15
16
  var LE = true;
16
17
 
18
+ function limit(v, open) { return v === undefined || v === null ? open : v; }
19
+
17
20
  /**
18
21
  * Build a keep(view, recordOffset) test for decodeRecords.
19
- * @param {object} [filter] - { classes?: number[], returns?: number[], bounds?: {minX,maxX,minY,maxY,minZ,maxZ} }
22
+ * @param {object} [filter] - { classes?: number[], returns?: number[], bounds?: {minX,maxX,minY,maxY,minZ,maxZ} (an omitted or null limit is open),
23
+ * polygon?: number[] (null or omitted: no crop) [x0, y0, x1, y1, ...] with boundary ("touching" | "inside") and tolerance as selectPolygon, in the file's world coordinates }
20
24
  * @param {object} header - from parseHeader.
21
25
  * @returns {function(DataView, number): boolean|null} null when there is nothing to filter
22
26
  */
@@ -38,15 +42,32 @@ export function makeKeep(filter, header) {
38
42
  ? function (v, at) { return ret[v.getUint8(at + 14) & 15] === 1; } // bits-ok
39
43
  : function (v, at) { return ret[v.getUint8(at + 14) & 7] === 1; }); // bits-ok
40
44
  }
45
+ var sc = header.scale, of = header.offset;
41
46
  if (filter.bounds) {
42
- var b = filter.bounds, s = header.scale, o = header.offset;
47
+ // an omitted limit is open
48
+ var b = filter.bounds;
49
+ var minX = limit(b.minX, -Infinity), maxX = limit(b.maxX, Infinity);
50
+ var minY = limit(b.minY, -Infinity), maxY = limit(b.maxY, Infinity);
51
+ var minZ = limit(b.minZ, -Infinity), maxZ = limit(b.maxZ, Infinity);
52
+ tests.push(function (v, at) {
53
+ var x = v.getInt32(at, LE) * sc.x + of.x;
54
+ if (x < minX || x > maxX) return false;
55
+ var y = v.getInt32(at + 4, LE) * sc.y + of.y;
56
+ if (y < minY || y > maxY) return false;
57
+ var z = v.getInt32(at + 8, LE) * sc.z + of.z;
58
+ return z >= minZ && z <= maxZ;
59
+ });
60
+ }
61
+ if (filter.polygon !== undefined && filter.polygon !== null) {
62
+ // the file's world x, y of the raw record, as the bounds test computes them; boundary and tolerance as selectPolygon
63
+ var poly = makePolygonTest(filter.polygon, { boundary: filter.boundary, tolerance: filter.tolerance });
64
+ var inPoly = poly.test, pminX = poly.minX, pmaxX = poly.maxX, pminY = poly.minY, pmaxY = poly.maxY;
43
65
  tests.push(function (v, at) {
44
- var x = v.getInt32(at, LE) * s.x + o.x;
45
- if (x < b.minX || x > b.maxX) return false;
46
- var y = v.getInt32(at + 4, LE) * s.y + o.y;
47
- if (y < b.minY || y > b.maxY) return false;
48
- var z = v.getInt32(at + 8, LE) * s.z + o.z;
49
- return z >= b.minZ && z <= b.maxZ;
66
+ var x = v.getInt32(at, LE) * sc.x + of.x;
67
+ if (x < pminX || x > pmaxX) return false; // y is not decoded for a point outside the box in x
68
+ var y = v.getInt32(at + 4, LE) * sc.y + of.y;
69
+ if (y < pminY || y > pmaxY) return false;
70
+ return inPoly(x, y);
50
71
  });
51
72
  }
52
73
  if (!tests.length) return null;
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.4.0";
9
+ export var VERSION = "0.5.0";
10
10
 
11
11
  export { LasError, ERROR_CODES } from "./errors.js";
12
12
  export { openLas, LasReader } from "./LasReader.js";
@@ -17,6 +17,10 @@ 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
19
  export { dedupe } from "./filter/dedupe.js";
20
+ export { selectBox, selectPolygon, selectClasses, selectReturns, thinSelection, unionSelections, intersectSelections, subtractSelections } from "./ops/select.js";
21
+ export { keepPoints, removePoints, splitPoints, mergePoints } from "./ops/apply.js";
22
+ export { makeTransform, transformPoints } from "./ops/transform.js";
23
+ export { interleavePositions } from "./ops/interleave.js";
20
24
  export { MemorySource } from "./io/MemorySource.js";
21
25
  export { BlobSource } from "./io/BlobSource.js";
22
26
  export { NodeFileSource } from "./io/NodeFileSource.js";
@@ -0,0 +1,144 @@
1
+ // src/ops/apply.js
2
+ //==========================================================
3
+ // Apply a selection to point columns: keepPoints, removePoints,
4
+ // splitPoints, and join clouds with mergePoints. Each carries every
5
+ // column, k values per point, and nothing else. Copy by default;
6
+ // { inPlace: true } slides the kept values down inside each column's
7
+ // own buffer and returns subarray views of it.
8
+ //==========================================================
9
+
10
+ import { LasError, ERROR_CODES } from "../errors.js";
11
+ import { columnsOf, checkSelection, countOf, widestType, sameType, gather, checkNoSharedMemory } from "./columns.js";
12
+
13
+ function bad(message) { return new LasError(ERROR_CODES.INVALID_ARGUMENT, message); }
14
+
15
+ // dst gets every point of src not in sel, in order. dst may be src: the
16
+ // write position never passes the read position.
17
+ function gatherComplement(src, dst, k, count, sel) {
18
+ var p = 0;
19
+ var w = 0;
20
+ for (var i = 0; i < count; i++) {
21
+ if (p < sel.length && sel[p] === i) { p++; continue; }
22
+ var from = i * k;
23
+ for (var j = 0; j < k; j++) dst[w + j] = src[from + j];
24
+ w += k;
25
+ }
26
+ }
27
+
28
+ // A column on the output; defineProperty so a column named "__proto__" stays a column.
29
+ function setColumn(out, name, a) {
30
+ Object.defineProperty(out, name, { value: a, enumerable: true, writable: true, configurable: true });
31
+ }
32
+
33
+ // Shared body of keep / remove / split: validate once, then one pass per column.
34
+ function pick(cols, sel, keep, inPlace) {
35
+ var count = countOf(cols);
36
+ var s = checkSelection(sel, count);
37
+ var n = keep ? s.length : count - s.length;
38
+ var out = { count: n };
39
+ var list = columnsOf(cols);
40
+ if (inPlace) checkNoSharedMemory(cols, list);
41
+ list.forEach(function (c) {
42
+ var src = cols[c.name];
43
+ var dst = inPlace ? src : sameType(src, n * c.k);
44
+ if (keep) gather(src, dst, c.k, s);
45
+ else gatherComplement(src, dst, c.k, count, s);
46
+ setColumn(out, c.name, inPlace ? src.subarray(0, n * c.k) : dst);
47
+ });
48
+ return out;
49
+ }
50
+
51
+ /**
52
+ * The selected points of every column, in order.
53
+ * @param {object} cols - point columns { count, <name>: TypedArray }.
54
+ * @param {Uint32Array|number[]} sel - strictly ascending indices below cols.count.
55
+ * @param {{inPlace?: boolean}} [opts] - inPlace: slide the kept values down inside each column's own buffer and return subarray views of it (the input is overwritten; two columns sharing memory, the same array twice or overlapping views, throw INVALID_ARGUMENT, while distinct non-overlapping views of one buffer are fine). Default: new columns of the same types.
56
+ * @returns {object} { count, <every column> } and nothing else.
57
+ */
58
+ export function keepPoints(cols, sel, opts) {
59
+ return pick(cols, sel, true, !!(opts && opts.inPlace));
60
+ }
61
+
62
+ /**
63
+ * Every point except the selected ones, in order.
64
+ * @param {object} cols - point columns { count, <name>: TypedArray }.
65
+ * @param {Uint32Array|number[]} sel - strictly ascending indices below cols.count.
66
+ * @param {{inPlace?: boolean}} [opts] - inPlace as keepPoints.
67
+ * @returns {object} { count, <every column> } and nothing else.
68
+ */
69
+ export function removePoints(cols, sel, opts) {
70
+ return pick(cols, sel, false, !!(opts && opts.inPlace));
71
+ }
72
+
73
+ /**
74
+ * Split into the selected points and the rest. Always copies; the input
75
+ * is untouched and every point lands in exactly one result.
76
+ * @param {object} cols - point columns { count, <name>: TypedArray }.
77
+ * @param {Uint32Array|number[]} sel - strictly ascending indices below cols.count.
78
+ * @returns {object[]} [inside, outside], each { count, <every column> }.
79
+ */
80
+ export function splitPoints(cols, sel) {
81
+ var count = countOf(cols);
82
+ var s = checkSelection(sel, count);
83
+ return [pick(cols, s, true, false), pick(cols, s, false, false)];
84
+ }
85
+
86
+ /**
87
+ * Concatenate point columns in order. The column set is the union by name
88
+ * in first-appearance order. A column missing from an input with points is
89
+ * 0 for those points and its name is listed in `filled`. A column's k must
90
+ * agree across inputs (a zero-point input has no k to disagree with); its
91
+ * type is the widest across the inputs that have points (a zero-point input
92
+ * is ignored, unless no input with points has the column), and merging signed with unsigned
93
+ * integers that would wrap throws. Coordinates must already share a frame.
94
+ * @param {object[]} list - point-column objects.
95
+ * @returns {object} { count, <every column>, filled: string[] }.
96
+ */
97
+ export function mergePoints(list) {
98
+ if (!Array.isArray(list)) throw bad("list must be an Array of point columns");
99
+ var total = 0;
100
+ var info = Object.create(null);
101
+ var names = [];
102
+ var perInput = list.map(function (cols, li) {
103
+ var n, entries;
104
+ try {
105
+ n = countOf(cols);
106
+ entries = columnsOf(cols);
107
+ } catch (err) {
108
+ throw bad("list[" + li + "]: " + err.message);
109
+ }
110
+ var found = Object.create(null);
111
+ entries.forEach(function (c) {
112
+ if (c.name === "count" || c.name === "filled") throw bad("column \"" + c.name + "\" in list[" + li + "] is a reserved name; rename it before merging");
113
+ found[c.name] = true;
114
+ var e = info[c.name];
115
+ if (!e) { e = info[c.name] = { k: 0, types: [], emptyTypes: [] }; names.push(c.name); }
116
+ if (n > 0) {
117
+ if (e.k !== 0 && e.k !== c.k) throw bad("column \"" + c.name + "\" has k = " + e.k + " in an earlier input but k = " + c.k + " in list[" + li + "]");
118
+ e.k = c.k;
119
+ }
120
+ (n > 0 ? e.types : e.emptyTypes).push(globalThis[Object.prototype.toString.call(cols[c.name]).slice(8, -1)]);
121
+ });
122
+ total += n;
123
+ return { n: n, found: found };
124
+ });
125
+ var out = { count: total };
126
+ var filled = [];
127
+ names.forEach(function (name) {
128
+ var e = info[name];
129
+ var k = e.k || 1;
130
+ var Type = widestType(e.types.length > 0 ? e.types : e.emptyTypes, name);
131
+ var dst = new Type(total * k);
132
+ var at = 0;
133
+ perInput.forEach(function (inp, i) {
134
+ if (inp.n > 0) {
135
+ if (inp.found[name]) dst.set(list[i][name], at * k);
136
+ else if (filled.indexOf(name) < 0) filled.push(name);
137
+ }
138
+ at += inp.n;
139
+ });
140
+ setColumn(out, name, dst);
141
+ });
142
+ out.filled = names.filter(function (name) { return filled.indexOf(name) >= 0; });
143
+ return out;
144
+ }