las-laz-js 0.3.1 → 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 +71 -0
- package/NOTICE +1 -1
- package/README.md +200 -19
- package/package.json +2 -2
- package/src/LasReader.js +6 -4
- package/src/filter/dedupe.js +14 -5
- package/src/filter/filters.js +31 -10
- package/src/index.js +5 -1
- package/src/laz/ArithmeticEncoder.js +123 -0
- package/src/laz/IntegerCompressor.js +76 -0
- package/src/laz/LazVlr.js +63 -1
- package/src/laz/chunkTable.js +139 -109
- package/src/laz/chunkWorker.js +49 -18
- package/src/laz/compressChunk.js +107 -0
- package/src/laz/decompressChunk.js +40 -10
- package/src/laz/pool.js +32 -18
- package/src/laz/v2/byte.js +23 -7
- package/src/laz/v2/gpstime11.js +165 -25
- package/src/laz/v2/point10.js +91 -22
- package/src/laz/v2/rgb12.js +62 -7
- package/src/laz/v3/layered.js +138 -40
- package/src/laz/v3/point14.js +231 -33
- package/src/ops/apply.js +144 -0
- package/src/ops/columns.js +209 -0
- package/src/ops/interleave.js +81 -0
- package/src/ops/polygon.js +83 -0
- package/src/ops/select.js +245 -0
- package/src/ops/transform.js +189 -0
- package/src/read/decodeRecords.js +41 -13
- package/src/workers/workerProtocol.js +8 -2
- package/src/write/encodeRecords.js +3 -2
- package/src/write/writeLas.js +259 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,76 @@
|
|
|
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
|
+
|
|
40
|
+
## 0.4.0 (2026-10-22)
|
|
41
|
+
|
|
42
|
+
Writes LAZ.
|
|
43
|
+
|
|
44
|
+
- `writeLas(points, { compress: true })` and `createLasWriter(sink, { compress: true })` write LAZ:
|
|
45
|
+
point formats 0–3 (point-wise) and 6–8 (layered), with a LASzip VLR, 50,000-point chunks and a
|
|
46
|
+
chunk table, as PDAL (LAZperf) lays it out. Plain JavaScript, translated from laz-rs
|
|
47
|
+
(Apache-2.0), like the decoders. Formats 4, 5, 9, 10 cannot be compressed (`UNSUPPORTED`).
|
|
48
|
+
- `chunkSize`: points per chunk, a whole number 1 to 4294967294 (default 50,000); ignored without
|
|
49
|
+
`compress`.
|
|
50
|
+
- `threads`: chunks compress on the worker pool the LAZ reader uses, written in chunk order, so the
|
|
51
|
+
bytes are the same for any thread count. Automatic by default: from 4 chunks up, with at most 4
|
|
52
|
+
workers, as gains flatten after about 4 (explicit `threads` are honoured); `1` is the calling
|
|
53
|
+
thread. A 10 M-point LAZ (format 3) rewritten: `writeLas` 7166 ms on one thread, 1708 ms
|
|
54
|
+
automatic (4 workers, 4.2×), 1624 ms with 16; `createLasWriter` 7109 ms, 2033–2246 ms, 1657 ms.
|
|
55
|
+
- With workers, up to about 2 tasks of 4 chunks per worker of records are in flight: about 55 MB
|
|
56
|
+
at the automatic 4 workers with 50,000-point format 3 chunks. That holds across awaited
|
|
57
|
+
`writePoints` calls; unawaited calls, or one huge batch, submit at once. An abandoned writer
|
|
58
|
+
keeps its workers until `close()`.
|
|
59
|
+
- A streaming LAZ writer stops at its first error (a chunk, a sink write, `close()`):
|
|
60
|
+
`writePoints` and `close` keep rejecting. Start a new writer. After `close()`, both reject
|
|
61
|
+
(`ABORTED`).
|
|
62
|
+
- The compressed bytes are PDAL's (LAZperf's) with one lossless exception: in formats 6–8 a GPS
|
|
63
|
+
time of −0 following +0 is coded as a change (LAZperf finds the doubles equal).
|
|
64
|
+
- Extra bytes are not written yet (as for LAS); they arrive with 0.7.0.
|
|
65
|
+
- Checked against PDAL 2.10: the committed fixtures' chunks, chunk tables and LASzip VLRs are
|
|
66
|
+
PDAL's bytes; PDAL reads what we write (`pdal info`, `pdal translate`) and finds the same points
|
|
67
|
+
field by field. Real files compress and read back to the same points: a 1.1 GB, 34-million-point
|
|
68
|
+
LAS streams to a 155 MB LAZ (0.14 of its size) in 6.4 s; a 10-million-point LAZ recompresses to
|
|
69
|
+
the same size in 2.7 s. That file, written by PDAL 1.6 in 2019, comes back as LAS 1.2 format 3 with
|
|
70
|
+
the original's point data byte for byte, and opens in CloudCompare as LAS 1.2 format 3 and as
|
|
71
|
+
LAS 1.4 format 7.
|
|
72
|
+
- Docs: README "Writing LAZ"; the demo says LAZ is read and written.
|
|
73
|
+
|
|
3
74
|
## 0.3.1 (2026-10-21)
|
|
4
75
|
|
|
5
76
|
Worker fixes.
|
package/NOTICE
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
las-laz-js
|
|
2
2
|
Copyright (c) 2026 Brent Buffham (https://kirra-design.com). MIT licence (see LICENSE).
|
|
3
3
|
|
|
4
|
-
The LAZ decoder in src/laz/
|
|
4
|
+
The LAZ decoder and encoder in src/laz/ are a JavaScript translation of laz-rs
|
|
5
5
|
(https://github.com/laz-rs/laz-rs), which is itself a Rust port of LASzip
|
|
6
6
|
(https://laszip.org) by rapidlasso. Those files are licensed under the
|
|
7
7
|
Apache License, Version 2.0 (see LICENSE-APACHE), not the MIT licence:
|
package/README.md
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
# las-laz-js
|
|
2
2
|
|
|
3
|
-
A JavaScript reader and writer for ASPRS **LAS** point clouds
|
|
3
|
+
A JavaScript reader and writer for ASPRS **LAS** point clouds and **LAZ**
|
|
4
4
|
(LASzip-compressed LAS), published on npm as `las-laz-js`. Plain ES modules, no dependencies,
|
|
5
5
|
runs in the browser and in Node — and built for files far larger than memory.
|
|
6
6
|
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
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.
|
|
15
|
+
- LAZ: point formats 0–3 and 6–8, read exactly like LAS — the same calls, the same options,
|
|
16
|
+
decoded chunk by chunk in plain JavaScript (no WebAssembly) — and written with
|
|
17
|
+
`compress: true`, byte for byte what PDAL (LAZperf) writes (bar one lossless exception, see
|
|
18
|
+
Writing LAZ), compressed in parallel.
|
|
14
19
|
- Big files: a 1.1 GB, 34-million-point LAS reads in under a second in Node, and a 10-million-point
|
|
15
20
|
LAZ decodes on every core (a 10-million-point file in about 0.6 s); neither is held in memory
|
|
16
21
|
whole (see LAZ below for the one exception).
|
|
17
22
|
- Also: a ready-made module Worker (`las-laz-js/worker`) and a
|
|
18
23
|
[Three.js demo](https://brentbuffham.github.io/las-laz-js/).
|
|
19
|
-
- Not yet: writing LAZ (0.3.0).
|
|
20
24
|
|
|
21
25
|
## Install
|
|
22
26
|
|
|
@@ -34,8 +38,9 @@ Drop a LAS or LAZ file onto the page. It is read locally (never uploaded), decod
|
|
|
34
38
|
library's Worker (LAZ on every core) behind a progress bar, then shown once in a Three.js viewer;
|
|
35
39
|
colour changes recolour the points already read, without reading the file again. The side panel shows the header, VLRs, coordinate
|
|
36
40
|
system and LAZ compression; reads with class, return, bounds, thinning and dedupe filters;
|
|
37
|
-
colours by RGB, intensity, elevation or class; lists point statistics;
|
|
38
|
-
|
|
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/.
|
|
39
44
|
|
|
40
45
|
## Reading
|
|
41
46
|
|
|
@@ -55,7 +60,11 @@ var pts = await las.readPoints({
|
|
|
55
60
|
fields: ["x", "y", "z", "classification", "red", "green", "blue"],
|
|
56
61
|
classes: [2], // keep only these classification codes
|
|
57
62
|
returns: [1], // keep only these return numbers
|
|
58
|
-
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)
|
|
59
68
|
maxPoints: 5000000, // even thinning; 0 keeps every point
|
|
60
69
|
dedupe: 0.01, // drop points within 1 cm (XY; dedupeZ: true for 3D)
|
|
61
70
|
localOrigin: { x: 478000, y: 6772000 }, float32: true, // small Float32 coordinates for the GPU
|
|
@@ -63,7 +72,7 @@ var pts = await las.readPoints({
|
|
|
63
72
|
signal: abortController.signal
|
|
64
73
|
});
|
|
65
74
|
pts.count; pts.x; pts.y; pts.z; // one typed array per field
|
|
66
|
-
pts.bounds; pts.stats; // world bounds, per-class counts
|
|
75
|
+
pts.bounds; pts.stats; // world bounds (after any transform), per-class counts
|
|
67
76
|
```
|
|
68
77
|
|
|
69
78
|
Fields: `x y z intensity returnNumber numberOfReturns scanDirection edgeOfFlightLine
|
|
@@ -71,10 +80,141 @@ classification classificationFlags scannerChannel scanAngle userData pointSource
|
|
|
71
80
|
red green blue nir`. Fields a point format does not hold are left out. `scanAngle` is in
|
|
72
81
|
degrees for every format.
|
|
73
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
|
+
|
|
74
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.
|
|
75
95
|
`maxPoints` keeps exactly that many of the points that pass the filters, spread evenly over
|
|
76
96
|
the whole file; with filters set it first counts the passing points, which reads the file twice.
|
|
77
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
|
+
|
|
78
218
|
## LAZ
|
|
79
219
|
|
|
80
220
|
A LAZ file is opened and read with the same calls as LAS. Each LASzip chunk (50,000 points in
|
|
@@ -135,6 +275,7 @@ import { writeLas, createLasWriter } from "las-laz-js";
|
|
|
135
275
|
|
|
136
276
|
var bytes = await writeLas(pts, { version: "1.2", pointFormat: 3, crs: { epsg: 28350 } });
|
|
137
277
|
var v14 = await writeLas(pts, { version: "1.4", pointFormat: 7, crs: { wkt: las.crs.wkt } });
|
|
278
|
+
var laz = await writeLas(pts, { compress: true, pointFormat: 3 }); // LAZ
|
|
138
279
|
|
|
139
280
|
var w = await createLasWriter(sink, { version: "1.2", pointFormat: 3, scale, offset });
|
|
140
281
|
// sink: an OPFS writer from openScratchSyncWriter, a Node FileHandle, or any { write(bytes, position) }
|
|
@@ -146,7 +287,44 @@ A version and point format that do not go together (format 6 in LAS 1.2) are ref
|
|
|
146
287
|
silently changed. Scale and offset are chosen from the bounds when not given. `crs.epsg`
|
|
147
288
|
writes a GeoKey record; `crs.wkt` a WKT record. Point formats 6–10 must carry WKT (LAS 1.4), so
|
|
148
289
|
an EPSG code alone is refused for them — the library has no projection database to turn a code
|
|
149
|
-
into WKT.
|
|
290
|
+
into WKT.
|
|
291
|
+
|
|
292
|
+
| Option | Default | Meaning |
|
|
293
|
+
|---|---|---|
|
|
294
|
+
| `compress` | `false` | write LAZ (LASzip) instead of LAS: point formats 0–3 and 6–8; formats 4, 5, 9, 10 are refused (`UNSUPPORTED`) |
|
|
295
|
+
| `chunkSize` | `50000` | points per LAZ chunk, a whole number from 1 to 4294967294; ignored without `compress` |
|
|
296
|
+
| `threads` | automatic | workers that compress chunks: automatic by default (from 4 chunks up, at most 4 workers), `1` compresses on the calling thread, `n` asks for n workers; only with `compress` |
|
|
297
|
+
|
|
298
|
+
### Writing LAZ
|
|
299
|
+
|
|
300
|
+
`writeLas(pts, { compress: true })` and `createLasWriter(sink, { compress: true, ... })` write the
|
|
301
|
+
same records as the LAS writers, compressed `chunkSize` points at a time (LASzip's 50,000 by
|
|
302
|
+
default) behind a LASzip VLR, with the chunk table at the end of the point data. The layout and
|
|
303
|
+
the compressed bytes are those PDAL (LAZperf) writes: for the same points, version, format, scale
|
|
304
|
+
and offset, the point data is identical to PDAL's — with one exception: in formats 6–8 a GPS time
|
|
305
|
+
of −0 following +0 is coded as a change, so it reads back as −0 (lossless), where LAZperf compares
|
|
306
|
+
the doubles, finds them equal and writes no change. Point formats 0–3 are written point-wise
|
|
307
|
+
(LASzip item version 2), 6–8 layered (item version 3).
|
|
308
|
+
|
|
309
|
+
- **Speed.** Chunks are independent, so they compress on the same pool of workers that decode
|
|
310
|
+
LAZ, and are written strictly in chunk order: the bytes are the same for any `threads`. Gains
|
|
311
|
+
flatten after about four workers, so with `threads` automatic the writers use at most four (one
|
|
312
|
+
fewer than the cores, if that is less): a 10-million-point format 3 file writes in about 1.7 s
|
|
313
|
+
that way on a 24-core machine, 1.6 s with `threads: 16`, about 7 s on one thread. A write of
|
|
314
|
+
under four chunks (200,000 points) stays on one thread, where workers would cost more than they
|
|
315
|
+
save. If workers cannot start, it falls back to one thread.
|
|
316
|
+
- **Memory.** With workers, up to about 2 tasks of 4 chunks per worker are in flight at once —
|
|
317
|
+
about 55 MB of records at the automatic four workers with 50,000-point format 3 chunks (about
|
|
318
|
+
220 MB at `threads: 16`). That bound holds across awaited `writePoints` calls; calls made
|
|
319
|
+
without waiting, or one huge batch, submit all at once. Pass a smaller `threads` or `chunkSize`
|
|
320
|
+
to bound it further. A writer that is abandoned without `close()` keeps its workers in a
|
|
321
|
+
browser until it is closed.
|
|
322
|
+
- **Errors.** A streaming LAZ writer stops for good at its first error — in a chunk, in a sink
|
|
323
|
+
write or in `close()` — and `writePoints` and `close` keep rejecting with it. Start a new
|
|
324
|
+
writer. After a successful `close()`, both reject (`ABORTED`, "the LAZ writer is closed").
|
|
325
|
+
- **Extra bytes** are not written (the writers have never written them): a file read with
|
|
326
|
+
extra bytes is written without them. They arrive with 0.7.0.
|
|
327
|
+
- The streaming writer needs `scale` and `offset` up front, as for LAS.
|
|
150
328
|
|
|
151
329
|
## In a Worker
|
|
152
330
|
|
|
@@ -160,7 +338,7 @@ worker.postMessage({ id: 2, type: "chunks", payload: { fields: ["x", "y", "z"],
|
|
|
160
338
|
The protocol is documented in `src/workers/workerProtocol.js` and can be driven directly with
|
|
161
339
|
`createWorkerHandler(post)`.
|
|
162
340
|
|
|
163
|
-
**Bundlers.** The worker entry and the LAZ decode workers are found through
|
|
341
|
+
**Bundlers.** The worker entry and the LAZ decode and encode workers are found through
|
|
164
342
|
`new Worker(new URL(..., import.meta.url))`, which Vite and webpack 5 follow. With Vite, keep the
|
|
165
343
|
package out of dependency pre-bundling (`optimizeDeps: { exclude: ["las-laz-js"] }`), so the
|
|
166
344
|
worker files stay next to the module that loads them.
|
|
@@ -182,11 +360,12 @@ COPC, EPT and E57 are not read.
|
|
|
182
360
|
## Provenance
|
|
183
361
|
|
|
184
362
|
The LAS reader, writer and big-file handling come from [Kirra](https://kirra-design.com),
|
|
185
|
-
Brent Buffham's blast-design app, rewritten as a standalone library. The LAZ decoder in
|
|
186
|
-
`src/laz/`
|
|
363
|
+
Brent Buffham's blast-design app, rewritten as a standalone library. The LAZ decoder and encoder in
|
|
364
|
+
`src/laz/` are a JavaScript translation of [laz-rs](https://github.com/laz-rs/laz-rs) by Thomas
|
|
187
365
|
Montaigu, itself a Rust port of [LASzip](https://laszip.org) by Martin Isenburg (rapidlasso).
|
|
188
|
-
It is checked against an independent
|
|
189
|
-
[PDAL](https://pdal.io),
|
|
366
|
+
It is checked against an independent implementation: the test fixtures were compressed by
|
|
367
|
+
[PDAL](https://pdal.io), every record must equal PDAL's own decompression, and what this
|
|
368
|
+
library compresses must equal PDAL's bytes and be read by PDAL.
|
|
190
369
|
Formats are written to the ASPRS LAS 1.4 R15 specification; see `docs/LAS_FORMAT_NOTES.md`.
|
|
191
370
|
|
|
192
371
|
## Development
|
|
@@ -200,12 +379,14 @@ node _dev/makeLazFixtures.mjs # remake test/fixtures/laz with PDAL (needs PDAL
|
|
|
200
379
|
```
|
|
201
380
|
|
|
202
381
|
Private sample files go in the gitignored `samples/` folder, or are listed by path in
|
|
203
|
-
`samples/paths.txt`; the sample tests compare each file with its own header,
|
|
204
|
-
LAS twin (same count and bounds) field by field.
|
|
382
|
+
`samples/paths.txt`; the sample tests compare each file with its own header, each LAZ with a
|
|
383
|
+
LAS twin (same count and bounds) field by field, and compress each file to LAZ and read it back.
|
|
384
|
+
Set `LAS_PDAL=<path to pdal>` to also have PDAL read what this library writes
|
|
385
|
+
(`test/laz/pdalWrite.test.js` and the sample tests).
|
|
205
386
|
|
|
206
387
|
## Licence
|
|
207
388
|
|
|
208
|
-
`MIT AND Apache-2.0`. The library is MIT © Brent Buffham (`LICENSE`). The LAZ decoder in
|
|
389
|
+
`MIT AND Apache-2.0`. The library is MIT © Brent Buffham (`LICENSE`). The LAZ decoder and encoder in
|
|
209
390
|
`src/laz/`, translated from laz-rs / LASzip, is under the Apache License 2.0 (`LICENSE-APACHE`,
|
|
210
391
|
`NOTICE`). Both are permissive: use, change and ship it in open or closed software, keeping the
|
|
211
392
|
notices.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "las-laz-js",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Zero-dependency JavaScript reader and writer for ASPRS LAS point clouds, and reader for LAZ (LASzip), for the browser and Node, built for files larger than memory.",
|
|
3
|
+
"version": "0.5.0",
|
|
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": {
|
|
7
7
|
".": "./src/index.js",
|
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) {
|
package/src/filter/dedupe.js
CHANGED
|
@@ -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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
}
|
package/src/filter/filters.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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) *
|
|
45
|
-
if (x <
|
|
46
|
-
var y = v.getInt32(at + 4, LE) *
|
|
47
|
-
if (y <
|
|
48
|
-
|
|
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.
|
|
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";
|