@ifc-lite/pointcloud 0.11.0 → 0.13.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/README.md +96 -0
- package/dist/copc/copc-hierarchy.d.ts +102 -0
- package/dist/copc/copc-hierarchy.d.ts.map +1 -0
- package/dist/copc/copc-hierarchy.js +210 -0
- package/dist/copc/copc-hierarchy.js.map +1 -0
- package/dist/copc/copc-info.d.ts +52 -0
- package/dist/copc/copc-info.d.ts.map +1 -0
- package/dist/copc/copc-info.js +147 -0
- package/dist/copc/copc-info.js.map +1 -0
- package/dist/copc/copc-reader.d.ts +60 -0
- package/dist/copc/copc-reader.d.ts.map +1 -0
- package/dist/copc/copc-reader.js +138 -0
- package/dist/copc/copc-reader.js.map +1 -0
- package/dist/copc/copc-worker-client.d.ts +37 -0
- package/dist/copc/copc-worker-client.d.ts.map +1 -0
- package/dist/copc/copc-worker-client.js +154 -0
- package/dist/copc/copc-worker-client.js.map +1 -0
- package/dist/copc/copc-worker-host.d.ts +11 -0
- package/dist/copc/copc-worker-host.d.ts.map +1 -0
- package/dist/copc/copc-worker-host.js +73 -0
- package/dist/copc/copc-worker-host.js.map +1 -0
- package/dist/formats/ifcx-points.d.ts +3 -0
- package/dist/formats/ifcx-points.d.ts.map +1 -1
- package/dist/formats/ifcx-points.js +23 -7
- package/dist/formats/ifcx-points.js.map +1 -1
- package/dist/formats/las.d.ts +1 -0
- package/dist/formats/las.d.ts.map +1 -1
- package/dist/formats/las.js +1 -1
- package/dist/formats/las.js.map +1 -1
- package/dist/formats/pcd-header.d.ts +19 -0
- package/dist/formats/pcd-header.d.ts.map +1 -0
- package/dist/formats/pcd-header.js +89 -0
- package/dist/formats/pcd-header.js.map +1 -0
- package/dist/formats/pcd.d.ts +3 -0
- package/dist/formats/pcd.d.ts.map +1 -1
- package/dist/formats/pcd.js +41 -119
- package/dist/formats/pcd.js.map +1 -1
- package/dist/from-ifcx-attributes.d.ts +3 -0
- package/dist/from-ifcx-attributes.d.ts.map +1 -1
- package/dist/from-ifcx-attributes.js +12 -5
- package/dist/from-ifcx-attributes.js.map +1 -1
- package/dist/index.d.ts +13 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -2
- package/dist/index.js.map +1 -1
- package/dist/lod/copc-lod-tree.d.ts +15 -0
- package/dist/lod/copc-lod-tree.d.ts.map +1 -0
- package/dist/lod/copc-lod-tree.js +67 -0
- package/dist/lod/copc-lod-tree.js.map +1 -0
- package/dist/lod/frustum.d.ts +13 -0
- package/dist/lod/frustum.d.ts.map +1 -0
- package/dist/lod/frustum.js +38 -0
- package/dist/lod/frustum.js.map +1 -0
- package/dist/lod/pacer.d.ts +62 -0
- package/dist/lod/pacer.d.ts.map +1 -0
- package/dist/lod/pacer.js +56 -0
- package/dist/lod/pacer.js.map +1 -0
- package/dist/lod/select.d.ts +80 -0
- package/dist/lod/select.d.ts.map +1 -0
- package/dist/lod/select.js +155 -0
- package/dist/lod/select.js.map +1 -0
- package/dist/point-source.d.ts +11 -0
- package/dist/point-source.d.ts.map +1 -0
- package/dist/point-source.js +5 -0
- package/dist/point-source.js.map +1 -0
- package/dist/streaming/blob-source.d.ts +3 -2
- package/dist/streaming/blob-source.d.ts.map +1 -1
- package/dist/streaming/blob-source.js +2 -9
- package/dist/streaming/blob-source.js.map +1 -1
- package/dist/streaming/decode-worker.js +22 -0
- package/dist/streaming/decode-worker.js.map +1 -1
- package/dist/streaming/http-range-source.d.ts +47 -0
- package/dist/streaming/http-range-source.d.ts.map +1 -0
- package/dist/streaming/http-range-source.js +102 -0
- package/dist/streaming/http-range-source.js.map +1 -0
- package/dist/streaming/inline-worker.js +1 -1
- package/dist/streaming/laz-perf-loader.d.ts +58 -0
- package/dist/streaming/laz-perf-loader.d.ts.map +1 -0
- package/dist/streaming/laz-perf-loader.js +107 -0
- package/dist/streaming/laz-perf-loader.js.map +1 -0
- package/dist/streaming/laz-source.d.ts +0 -40
- package/dist/streaming/laz-source.d.ts.map +1 -1
- package/dist/streaming/laz-source.js +1 -94
- package/dist/streaming/laz-source.js.map +1 -1
- package/dist/streaming/protocol.d.ts +45 -0
- package/dist/streaming/protocol.d.ts.map +1 -1
- package/dist/streaming/protocol.js.map +1 -1
- package/dist/streaming/types.d.ts +14 -0
- package/dist/streaming/types.d.ts.map +1 -1
- package/dist/streaming/worker-client.d.ts +25 -0
- package/dist/streaming/worker-client.d.ts.map +1 -1
- package/dist/streaming/worker-client.js +12 -5
- package/dist/streaming/worker-client.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -29,6 +29,30 @@ if (chunk) {
|
|
|
29
29
|
}
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
For native import or coordinate processing, `decodeIfcxPointSource(attributes)`
|
|
33
|
+
returns authored numeric channels before float32 staging: owned `Float64Array`
|
|
34
|
+
`positions` and optional normalized RGB `colors`, plus `pointCount`,
|
|
35
|
+
`sourcePrecision` and `sourceEncoding`. It uses the same PCD > inline array >
|
|
36
|
+
base64 precedence and validation as `decodeIfcxPointAttribute`, returning `null`
|
|
37
|
+
when no supported attribute is present. It applies no offsets, transforms,
|
|
38
|
+
normals or bounds. ASCII PCD and inline numbers retain JavaScript number
|
|
39
|
+
precision; binary and compressed PCD retain decoded float64 values. The
|
|
40
|
+
`sourcePrecision` is `f32` only when all encoded coordinates are float32
|
|
41
|
+
(including `points::base64`); otherwise it is `f64` (also for integer PCD
|
|
42
|
+
coordinates decoded as numbers). `sourceEncoding` is `pcd-ascii`, `pcd-binary`,
|
|
43
|
+
`pcd-binary_compressed`, `points-array` or `points-base64`. Widening an f32
|
|
44
|
+
source does not recover precision already lost by its author.
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { decodeIfcxPointSource } from '@ifc-lite/pointcloud';
|
|
48
|
+
|
|
49
|
+
declare const attributes: ReadonlyMap<string, unknown>;
|
|
50
|
+
const source = decodeIfcxPointSource(attributes);
|
|
51
|
+
if (source) {
|
|
52
|
+
console.log(source.positions, source.sourcePrecision, source.sourceEncoding);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
32
56
|
`DecodedPointChunk.normals` carries source-supplied oriented normals as
|
|
33
57
|
row-aligned `nx, ny, nz` float triples when a decoder provides them. PLY
|
|
34
58
|
supports complete `nx`/`ny`/`nz` properties in ASCII and binary files and
|
|
@@ -81,6 +105,78 @@ Notes:
|
|
|
81
105
|
- Strict CSPs need `script-src 'self' blob:` to allow the `Blob`-URL
|
|
82
106
|
worker.
|
|
83
107
|
|
|
108
|
+
## COPC: range-read octree point clouds
|
|
109
|
+
|
|
110
|
+
A [COPC](https://copc.io) file is a LAZ 1.4 file organised as an octree, so a
|
|
111
|
+
viewer can read only the nodes it needs, from a local `File` or over HTTP
|
|
112
|
+
Range requests. `openCopcWorkerReader` opens one in the decode worker, loads
|
|
113
|
+
the root hierarchy page, and decodes nodes on demand. Detection is by the
|
|
114
|
+
`copc`/`info` VLR (`isCopcHeader` on the first `COPC_PROBE_BYTES` bytes), not
|
|
115
|
+
by the `.copc.laz` file name.
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { openCopcWorkerReader } from '@ifc-lite/pointcloud';
|
|
119
|
+
|
|
120
|
+
declare const file: File;
|
|
121
|
+
const reader = await openCopcWorkerReader({ source: { kind: 'blob', blob: file } });
|
|
122
|
+
const root = reader.hierarchy.nodes.get('0-0-0-0');
|
|
123
|
+
if (root) {
|
|
124
|
+
const chunk = await reader.readNode(root, { stride: 1 });
|
|
125
|
+
console.log(`${chunk.pointCount} points in the root node`);
|
|
126
|
+
}
|
|
127
|
+
// Deeper hierarchy pages load on demand: reader.loadPage(ref) for any
|
|
128
|
+
// ref in reader.hierarchy.pendingPages.
|
|
129
|
+
reader.close();
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
For a remote file, pass `{ kind: 'http', url }`. The server must answer
|
|
133
|
+
Range requests with `206 Partial Content`; a server that ignores `Range` is
|
|
134
|
+
refused rather than downloading the whole file. When CORS hides
|
|
135
|
+
`Content-Range`, the size comes from a `HEAD` request and each response is
|
|
136
|
+
checked by its exact length. Hierarchy pages come from the file, so the
|
|
137
|
+
page walk is iterative with a visited set and page, node and byte budgets;
|
|
138
|
+
exceeding a budget is an error, never a silently truncated octree.
|
|
139
|
+
|
|
140
|
+
## View-dependent level of detail
|
|
141
|
+
|
|
142
|
+
`selectLod(root, camera, { pointBudget })` picks which octree nodes to show.
|
|
143
|
+
It is pure (no renderer, no I/O) and works on any additive octree through the
|
|
144
|
+
small `LodNode` interface; `createCopcLodTree` adapts a COPC hierarchy.
|
|
145
|
+
|
|
146
|
+
- Nodes outside the view frustum are never selected.
|
|
147
|
+
- The selected node with the largest projected screen span refines first,
|
|
148
|
+
while its span exceeds `pixelThreshold` (default 96 px) and the selection
|
|
149
|
+
stays within `maxNodes` (default `clamp(pointBudget / 128, 8, 1024)`).
|
|
150
|
+
Parents always precede their children.
|
|
151
|
+
- The point budget is water-filled across the selection: each node gets an
|
|
152
|
+
equal share capped by its own point count, and every node carries a
|
|
153
|
+
`stride` hint whose decoded count never exceeds its share.
|
|
154
|
+
- Nodes that should refine but whose hierarchy page is not loaded yet are
|
|
155
|
+
listed in `needsChildren`.
|
|
156
|
+
|
|
157
|
+
`LodPacer` sizes a fast first pass to a time budget (default 200 ms) from a
|
|
158
|
+
learned read rate, an asymmetric moving average that drops fast and rises
|
|
159
|
+
slowly, with outlier clipping. `shouldReplacePass` swaps what is on screen
|
|
160
|
+
only for something better: more points for the same view, or a complete
|
|
161
|
+
pass for a newer view.
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import { LodPacer, createCopcLodTree, selectLod, type CopcWorkerReader, type LodCamera } from '@ifc-lite/pointcloud';
|
|
165
|
+
|
|
166
|
+
declare const reader: CopcWorkerReader;
|
|
167
|
+
declare const camera: LodCamera;
|
|
168
|
+
// Bounds in the decoded frame; move the camera into it (view-projection x placement).
|
|
169
|
+
const tree = createCopcLodTree(reader.hierarchy, reader.file.info, reader.originOffset);
|
|
170
|
+
const pacer = new LodPacer();
|
|
171
|
+
const [firstPass] = pacer.passBudgets(10_000_000);
|
|
172
|
+
if (tree.root) {
|
|
173
|
+
const selection = selectLod(tree.root, camera, { pointBudget: firstPass });
|
|
174
|
+
for (const node of selection.needsChildren) {
|
|
175
|
+
for (const ref of tree.pendingPagesUnder(node)) void reader.loadPage(ref);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
84
180
|
## API
|
|
85
181
|
|
|
86
182
|
See the [docs site](https://ifclite.dev/docs/) for guides and the full API reference.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* COPC hierarchy pages (#6869).
|
|
3
|
+
*
|
|
4
|
+
* A page is a packed array of 32-byte entries:
|
|
5
|
+
* VoxelKey (int32 level, x, y, z) | uint64 offset | int32 byteSize | int32 pointCount
|
|
6
|
+
* `pointCount >= 0` describes a node whose LAZ chunk sits at `offset`;
|
|
7
|
+
* `pointCount === -1` points at a CHILD PAGE of `byteSize` bytes at `offset`.
|
|
8
|
+
*
|
|
9
|
+
* The page tree comes from the file, so its shape is attacker-controlled.
|
|
10
|
+
* Following AGENTS.md "Bounding walks over file-supplied references", the
|
|
11
|
+
* walk is ITERATIVE (no stack to overflow), keeps a global visited set of
|
|
12
|
+
* page offsets (a page is a pure function of its offset, so memoising is
|
|
13
|
+
* safe and kills cycles and fan-out), and carries work budgets for pages,
|
|
14
|
+
* nodes and page bytes. Exceeding a budget THROWS: a truncated hierarchy
|
|
15
|
+
* would render as silently missing geometry.
|
|
16
|
+
*/
|
|
17
|
+
import { type VoxelKey } from './copc-info.js';
|
|
18
|
+
/** Deepest level accepted: keeps `2^d` cell indices inside int32. */
|
|
19
|
+
export declare const MAX_COPC_LEVEL = 30;
|
|
20
|
+
/** Largest single node accepted (points). Real writers stay far below. */
|
|
21
|
+
export declare const MAX_COPC_NODE_POINTS: number;
|
|
22
|
+
export interface CopcNodeEntry {
|
|
23
|
+
key: VoxelKey;
|
|
24
|
+
/** Absolute file offset of the node's LAZ chunk. */
|
|
25
|
+
offset: number;
|
|
26
|
+
byteSize: number;
|
|
27
|
+
pointCount: number;
|
|
28
|
+
}
|
|
29
|
+
export interface CopcPageRef {
|
|
30
|
+
/** Root of the subtree the page describes. */
|
|
31
|
+
key: VoxelKey;
|
|
32
|
+
offset: number;
|
|
33
|
+
byteSize: number;
|
|
34
|
+
}
|
|
35
|
+
export interface CopcHierarchyPage {
|
|
36
|
+
nodes: CopcNodeEntry[];
|
|
37
|
+
pages: CopcPageRef[];
|
|
38
|
+
}
|
|
39
|
+
export interface CopcHierarchyLimits {
|
|
40
|
+
maxPages: number;
|
|
41
|
+
maxNodes: number;
|
|
42
|
+
maxPageBytes: number;
|
|
43
|
+
}
|
|
44
|
+
export declare const DEFAULT_COPC_HIERARCHY_LIMITS: CopcHierarchyLimits;
|
|
45
|
+
/** Parse and validate one page. Every entry is checked against the file size. */
|
|
46
|
+
export declare function parseCopcHierarchyPage(bytes: Uint8Array, fileSize: number): CopcHierarchyPage;
|
|
47
|
+
export type CopcChildState = 'node' | 'page' | 'absent';
|
|
48
|
+
/** The eight children of `key`. */
|
|
49
|
+
export declare function copcChildKeys(key: VoxelKey): VoxelKey[];
|
|
50
|
+
/**
|
|
51
|
+
* The hierarchy known so far: nodes from loaded pages, plus pointers to
|
|
52
|
+
* pages not yet loaded. Pages load on demand, so a huge file's hierarchy is
|
|
53
|
+
* only read where the camera looks.
|
|
54
|
+
*/
|
|
55
|
+
export declare class CopcHierarchy {
|
|
56
|
+
readonly limits: CopcHierarchyLimits;
|
|
57
|
+
readonly nodes: Map<string, CopcNodeEntry>;
|
|
58
|
+
/** Unloaded child pages, keyed by the id of their subtree root. */
|
|
59
|
+
readonly pendingPages: Map<string, CopcPageRef>;
|
|
60
|
+
private readonly seenPageOffsets;
|
|
61
|
+
private pageBytes;
|
|
62
|
+
private pageCount;
|
|
63
|
+
constructor(limits?: CopcHierarchyLimits);
|
|
64
|
+
get loadedPageCount(): number;
|
|
65
|
+
/** Page bytes still allowed by `limits.maxPageBytes`. */
|
|
66
|
+
get remainingPageBytes(): number;
|
|
67
|
+
/**
|
|
68
|
+
* Throw unless `ref` fits the page and byte budgets. Call BEFORE reading a
|
|
69
|
+
* page: a hostile pointer can name a page as large as the file, and a
|
|
70
|
+
* budget checked only after parsing has already paid for it.
|
|
71
|
+
*/
|
|
72
|
+
assertCanLoad(ref: {
|
|
73
|
+
byteSize: number;
|
|
74
|
+
}): void;
|
|
75
|
+
/**
|
|
76
|
+
* Admit the page read from `ref`: the root page first, then only pages
|
|
77
|
+
* that are pending. Throws when a budget is exceeded or a node key
|
|
78
|
+
* repeats; pointers to already-seen pages are dropped.
|
|
79
|
+
*/
|
|
80
|
+
addPage(ref: {
|
|
81
|
+
offset: number;
|
|
82
|
+
byteSize: number;
|
|
83
|
+
key?: VoxelKey;
|
|
84
|
+
}, page: CopcHierarchyPage): void;
|
|
85
|
+
private isPending;
|
|
86
|
+
/** Whether `key` is a known node, the root of an unloaded page, or neither. */
|
|
87
|
+
stateOf(key: VoxelKey): CopcChildState;
|
|
88
|
+
}
|
|
89
|
+
/** Reads one page's bytes and parses it; the reader owns the I/O. */
|
|
90
|
+
export type CopcPageFetcher = (ref: CopcPageRef, signal?: AbortSignal) => Promise<CopcHierarchyPage>;
|
|
91
|
+
/**
|
|
92
|
+
* Load every pending page, iteratively (breadth-first, `concurrency` reads
|
|
93
|
+
* in flight), optionally only those whose subtree root is at most
|
|
94
|
+
* `maxLevel` deep. The visited set and the hierarchy's budgets bound the
|
|
95
|
+
* work; there is no recursion.
|
|
96
|
+
*/
|
|
97
|
+
export declare function loadPendingCopcPages(hierarchy: CopcHierarchy, fetchPage: CopcPageFetcher, options?: {
|
|
98
|
+
maxLevel?: number;
|
|
99
|
+
signal?: AbortSignal;
|
|
100
|
+
concurrency?: number;
|
|
101
|
+
}): Promise<void>;
|
|
102
|
+
//# sourceMappingURL=copc-hierarchy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"copc-hierarchy.d.ts","sourceRoot":"","sources":["../../src/copc/copc-hierarchy.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAc,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAG3D,qEAAqE;AACrE,eAAO,MAAM,cAAc,KAAK,CAAC;AACjC,0EAA0E;AAC1E,eAAO,MAAM,oBAAoB,QAAU,CAAC;AAE5C,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,QAAQ,CAAC;IACd,oDAAoD;IACpD,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,8CAA8C;IAC9C,GAAG,EAAE,QAAQ,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,KAAK,EAAE,WAAW,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,6BAA6B,EAAE,mBAI3C,CAAC;AAQF,iFAAiF;AACjF,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,GAAG,iBAAiB,CAyC7F;AAED,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAC;AAExD,mCAAmC;AACnC,wBAAgB,aAAa,CAAC,GAAG,EAAE,QAAQ,GAAG,QAAQ,EAAE,CAMvD;AAED;;;;GAIG;AACH,qBAAa,aAAa;IAQZ,QAAQ,CAAC,MAAM,EAAE,mBAAmB;IAPhD,QAAQ,CAAC,KAAK,6BAAoC;IAClD,mEAAmE;IACnE,QAAQ,CAAC,YAAY,2BAAkC;IACvD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAqB;IACrD,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,SAAS,CAAK;gBAED,MAAM,GAAE,mBAAmD;IAEhF,IAAI,eAAe,IAAI,MAAM,CAE5B;IAED,yDAAyD;IACzD,IAAI,kBAAkB,IAAI,MAAM,CAE/B;IAED;;;;OAIG;IACH,aAAa,CAAC,GAAG,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAS9C;;;;OAIG;IACH,OAAO,CAAC,GAAG,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,QAAQ,CAAA;KAAE,EAAE,IAAI,EAAE,iBAAiB,GAAG,IAAI;IAmCjG,OAAO,CAAC,SAAS;IAKjB,+EAA+E;IAC/E,OAAO,CAAC,GAAG,EAAE,QAAQ,GAAG,cAAc;CAMvC;AAED,qEAAqE;AACrE,MAAM,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAErG;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,aAAa,EACxB,SAAS,EAAE,eAAe,EAC1B,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAO,GAC9E,OAAO,CAAC,IAAI,CAAC,CAef"}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
/**
|
|
5
|
+
* COPC hierarchy pages (#6869).
|
|
6
|
+
*
|
|
7
|
+
* A page is a packed array of 32-byte entries:
|
|
8
|
+
* VoxelKey (int32 level, x, y, z) | uint64 offset | int32 byteSize | int32 pointCount
|
|
9
|
+
* `pointCount >= 0` describes a node whose LAZ chunk sits at `offset`;
|
|
10
|
+
* `pointCount === -1` points at a CHILD PAGE of `byteSize` bytes at `offset`.
|
|
11
|
+
*
|
|
12
|
+
* The page tree comes from the file, so its shape is attacker-controlled.
|
|
13
|
+
* Following AGENTS.md "Bounding walks over file-supplied references", the
|
|
14
|
+
* walk is ITERATIVE (no stack to overflow), keeps a global visited set of
|
|
15
|
+
* page offsets (a page is a pure function of its offset, so memoising is
|
|
16
|
+
* safe and kills cycles and fan-out), and carries work budgets for pages,
|
|
17
|
+
* nodes and page bytes. Exceeding a budget THROWS: a truncated hierarchy
|
|
18
|
+
* would render as silently missing geometry.
|
|
19
|
+
*/
|
|
20
|
+
import { voxelKeyId } from './copc-info.js';
|
|
21
|
+
const ENTRY_SIZE = 32;
|
|
22
|
+
/** Deepest level accepted: keeps `2^d` cell indices inside int32. */
|
|
23
|
+
export const MAX_COPC_LEVEL = 30;
|
|
24
|
+
/** Largest single node accepted (points). Real writers stay far below. */
|
|
25
|
+
export const MAX_COPC_NODE_POINTS = 1 << 24;
|
|
26
|
+
export const DEFAULT_COPC_HIERARCHY_LIMITS = {
|
|
27
|
+
maxPages: 1 << 16,
|
|
28
|
+
maxNodes: 1 << 21,
|
|
29
|
+
maxPageBytes: (1 << 21) * ENTRY_SIZE,
|
|
30
|
+
};
|
|
31
|
+
function checkSpan(what, offset, size, fileSize) {
|
|
32
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || offset + size > fileSize) {
|
|
33
|
+
throw new Error(`COPC: ${what} [${offset}, +${size}) lies outside the ${fileSize}-byte file`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Parse and validate one page. Every entry is checked against the file size. */
|
|
37
|
+
export function parseCopcHierarchyPage(bytes, fileSize) {
|
|
38
|
+
if (bytes.length % ENTRY_SIZE !== 0) {
|
|
39
|
+
throw new Error(`COPC: hierarchy page of ${bytes.length} bytes is not a multiple of ${ENTRY_SIZE}`);
|
|
40
|
+
}
|
|
41
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
42
|
+
const page = { nodes: [], pages: [] };
|
|
43
|
+
for (let at = 0; at < bytes.length; at += ENTRY_SIZE) {
|
|
44
|
+
const key = {
|
|
45
|
+
d: view.getInt32(at, true),
|
|
46
|
+
x: view.getInt32(at + 4, true),
|
|
47
|
+
y: view.getInt32(at + 8, true),
|
|
48
|
+
z: view.getInt32(at + 12, true),
|
|
49
|
+
};
|
|
50
|
+
const offset = view.getUint32(at + 16, true) + view.getUint32(at + 20, true) * 0x100000000;
|
|
51
|
+
const byteSize = view.getInt32(at + 24, true);
|
|
52
|
+
const pointCount = view.getInt32(at + 28, true);
|
|
53
|
+
if (key.d < 0 || key.d > MAX_COPC_LEVEL)
|
|
54
|
+
throw new Error(`COPC: hierarchy key level ${key.d} out of range`);
|
|
55
|
+
const cells = 2 ** key.d;
|
|
56
|
+
for (const c of [key.x, key.y, key.z]) {
|
|
57
|
+
if (c < 0 || c >= cells)
|
|
58
|
+
throw new Error(`COPC: hierarchy key ${voxelKeyId(key)} has a cell outside [0, ${cells})`);
|
|
59
|
+
}
|
|
60
|
+
if (pointCount === -1) {
|
|
61
|
+
if (byteSize <= 0 || byteSize % ENTRY_SIZE !== 0) {
|
|
62
|
+
throw new Error(`COPC: child page ${voxelKeyId(key)} has invalid size ${byteSize}`);
|
|
63
|
+
}
|
|
64
|
+
checkSpan(`child page ${voxelKeyId(key)}`, offset, byteSize, fileSize);
|
|
65
|
+
page.pages.push({ key, offset, byteSize });
|
|
66
|
+
}
|
|
67
|
+
else if (pointCount === 0) {
|
|
68
|
+
page.nodes.push({ key, offset: 0, byteSize: 0, pointCount: 0 });
|
|
69
|
+
}
|
|
70
|
+
else if (pointCount > 0) {
|
|
71
|
+
if (pointCount > MAX_COPC_NODE_POINTS) {
|
|
72
|
+
throw new Error(`COPC: node ${voxelKeyId(key)} declares ${pointCount} points (limit ${MAX_COPC_NODE_POINTS})`);
|
|
73
|
+
}
|
|
74
|
+
if (byteSize <= 0)
|
|
75
|
+
throw new Error(`COPC: node ${voxelKeyId(key)} has ${pointCount} points but ${byteSize} bytes`);
|
|
76
|
+
checkSpan(`node ${voxelKeyId(key)}`, offset, byteSize, fileSize);
|
|
77
|
+
page.nodes.push({ key, offset, byteSize, pointCount });
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
throw new Error(`COPC: hierarchy entry ${voxelKeyId(key)} has invalid point count ${pointCount}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return page;
|
|
84
|
+
}
|
|
85
|
+
/** The eight children of `key`. */
|
|
86
|
+
export function copcChildKeys(key) {
|
|
87
|
+
const out = [];
|
|
88
|
+
for (let i = 0; i < 8; i++) {
|
|
89
|
+
out.push({ d: key.d + 1, x: key.x * 2 + (i & 1), y: key.y * 2 + ((i >> 1) & 1), z: key.z * 2 + ((i >> 2) & 1) });
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The hierarchy known so far: nodes from loaded pages, plus pointers to
|
|
95
|
+
* pages not yet loaded. Pages load on demand, so a huge file's hierarchy is
|
|
96
|
+
* only read where the camera looks.
|
|
97
|
+
*/
|
|
98
|
+
export class CopcHierarchy {
|
|
99
|
+
limits;
|
|
100
|
+
nodes = new Map();
|
|
101
|
+
/** Unloaded child pages, keyed by the id of their subtree root. */
|
|
102
|
+
pendingPages = new Map();
|
|
103
|
+
seenPageOffsets = new Set();
|
|
104
|
+
pageBytes = 0;
|
|
105
|
+
pageCount = 0;
|
|
106
|
+
constructor(limits = DEFAULT_COPC_HIERARCHY_LIMITS) {
|
|
107
|
+
this.limits = limits;
|
|
108
|
+
}
|
|
109
|
+
get loadedPageCount() {
|
|
110
|
+
return this.pageCount;
|
|
111
|
+
}
|
|
112
|
+
/** Page bytes still allowed by `limits.maxPageBytes`. */
|
|
113
|
+
get remainingPageBytes() {
|
|
114
|
+
return Math.max(0, this.limits.maxPageBytes - this.pageBytes);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Throw unless `ref` fits the page and byte budgets. Call BEFORE reading a
|
|
118
|
+
* page: a hostile pointer can name a page as large as the file, and a
|
|
119
|
+
* budget checked only after parsing has already paid for it.
|
|
120
|
+
*/
|
|
121
|
+
assertCanLoad(ref) {
|
|
122
|
+
if (this.pageCount + 1 > this.limits.maxPages) {
|
|
123
|
+
throw new Error(`COPC: hierarchy exceeds ${this.limits.maxPages} pages`);
|
|
124
|
+
}
|
|
125
|
+
if (ref.byteSize > this.remainingPageBytes) {
|
|
126
|
+
throw new Error(`COPC: hierarchy exceeds ${this.limits.maxPageBytes} page bytes`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Admit the page read from `ref`: the root page first, then only pages
|
|
131
|
+
* that are pending. Throws when a budget is exceeded or a node key
|
|
132
|
+
* repeats; pointers to already-seen pages are dropped.
|
|
133
|
+
*/
|
|
134
|
+
addPage(ref, page) {
|
|
135
|
+
if (this.pageCount > 0 && !this.isPending(ref)) {
|
|
136
|
+
throw new Error(`COPC: page at ${ref.offset} is not a pending hierarchy page`);
|
|
137
|
+
}
|
|
138
|
+
this.pageCount++;
|
|
139
|
+
this.pageBytes += ref.byteSize;
|
|
140
|
+
if (this.pageCount > this.limits.maxPages) {
|
|
141
|
+
throw new Error(`COPC: hierarchy exceeds ${this.limits.maxPages} pages`);
|
|
142
|
+
}
|
|
143
|
+
if (this.pageBytes > this.limits.maxPageBytes) {
|
|
144
|
+
throw new Error(`COPC: hierarchy exceeds ${this.limits.maxPageBytes} page bytes`);
|
|
145
|
+
}
|
|
146
|
+
this.seenPageOffsets.add(ref.offset);
|
|
147
|
+
if (ref.key)
|
|
148
|
+
this.pendingPages.delete(voxelKeyId(ref.key));
|
|
149
|
+
if (this.nodes.size + page.nodes.length > this.limits.maxNodes) {
|
|
150
|
+
throw new Error(`COPC: hierarchy exceeds ${this.limits.maxNodes} nodes`);
|
|
151
|
+
}
|
|
152
|
+
for (const node of page.nodes) {
|
|
153
|
+
const id = voxelKeyId(node.key);
|
|
154
|
+
if (this.nodes.has(id))
|
|
155
|
+
throw new Error(`COPC: hierarchy lists node ${id} twice`);
|
|
156
|
+
this.nodes.set(id, node);
|
|
157
|
+
}
|
|
158
|
+
for (const child of page.pages) {
|
|
159
|
+
// The visited set is the cycle guard: a pointer back to any page we
|
|
160
|
+
// have read or queued is dropped, never followed.
|
|
161
|
+
if (this.seenPageOffsets.has(child.offset))
|
|
162
|
+
continue;
|
|
163
|
+
const id = voxelKeyId(child.key);
|
|
164
|
+
// Two pointers for one subtree root: overwriting would silently drop
|
|
165
|
+
// the first page's subtree, so treat it like a repeated node.
|
|
166
|
+
if (this.pendingPages.has(id))
|
|
167
|
+
throw new Error(`COPC: hierarchy lists child page ${id} twice`);
|
|
168
|
+
this.seenPageOffsets.add(child.offset);
|
|
169
|
+
this.pendingPages.set(id, child);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
isPending(ref) {
|
|
173
|
+
if (!ref.key)
|
|
174
|
+
return false;
|
|
175
|
+
return this.pendingPages.get(voxelKeyId(ref.key))?.offset === ref.offset;
|
|
176
|
+
}
|
|
177
|
+
/** Whether `key` is a known node, the root of an unloaded page, or neither. */
|
|
178
|
+
stateOf(key) {
|
|
179
|
+
const id = voxelKeyId(key);
|
|
180
|
+
if (this.nodes.has(id))
|
|
181
|
+
return 'node';
|
|
182
|
+
if (this.pendingPages.has(id))
|
|
183
|
+
return 'page';
|
|
184
|
+
return 'absent';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Load every pending page, iteratively (breadth-first, `concurrency` reads
|
|
189
|
+
* in flight), optionally only those whose subtree root is at most
|
|
190
|
+
* `maxLevel` deep. The visited set and the hierarchy's budgets bound the
|
|
191
|
+
* work; there is no recursion.
|
|
192
|
+
*/
|
|
193
|
+
export async function loadPendingCopcPages(hierarchy, fetchPage, options = {}) {
|
|
194
|
+
const maxLevel = options.maxLevel ?? Infinity;
|
|
195
|
+
const concurrency = Math.max(1, Math.floor(options.concurrency ?? 6));
|
|
196
|
+
while (true) {
|
|
197
|
+
options.signal?.throwIfAborted();
|
|
198
|
+
const batch = [...hierarchy.pendingPages.values()].filter((ref) => ref.key.d <= maxLevel).slice(0, concurrency);
|
|
199
|
+
if (batch.length === 0)
|
|
200
|
+
return;
|
|
201
|
+
// Budget the whole batch before any read starts.
|
|
202
|
+
batch.reduce((bytes, ref) => {
|
|
203
|
+
hierarchy.assertCanLoad({ byteSize: bytes + ref.byteSize });
|
|
204
|
+
return bytes + ref.byteSize;
|
|
205
|
+
}, 0);
|
|
206
|
+
const pages = await Promise.all(batch.map((ref) => fetchPage(ref, options.signal)));
|
|
207
|
+
batch.forEach((ref, i) => hierarchy.addPage(ref, pages[i]));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=copc-hierarchy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"copc-hierarchy.js","sourceRoot":"","sources":["../../src/copc/copc-hierarchy.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAE/D;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,UAAU,EAAiB,MAAM,gBAAgB,CAAC;AAE3D,MAAM,UAAU,GAAG,EAAE,CAAC;AACtB,qEAAqE;AACrE,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,CAAC;AACjC,0EAA0E;AAC1E,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,IAAI,EAAE,CAAC;AA4B5C,MAAM,CAAC,MAAM,6BAA6B,GAAwB;IAChE,QAAQ,EAAE,CAAC,IAAI,EAAE;IACjB,QAAQ,EAAE,CAAC,IAAI,EAAE;IACjB,YAAY,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,GAAG,UAAU;CACrC,CAAC;AAEF,SAAS,SAAS,CAAC,IAAY,EAAE,MAAc,EAAE,IAAY,EAAE,QAAgB;IAC7E,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,MAAM,GAAG,CAAC,IAAI,MAAM,GAAG,IAAI,GAAG,QAAQ,EAAE,CAAC;QAC5E,MAAM,IAAI,KAAK,CAAC,SAAS,IAAI,KAAK,MAAM,MAAM,IAAI,sBAAsB,QAAQ,YAAY,CAAC,CAAC;IAChG,CAAC;AACH,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,sBAAsB,CAAC,KAAiB,EAAE,QAAgB;IACxE,IAAI,KAAK,CAAC,MAAM,GAAG,UAAU,KAAK,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CAAC,2BAA2B,KAAK,CAAC,MAAM,+BAA+B,UAAU,EAAE,CAAC,CAAC;IACtG,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;IAC5E,MAAM,IAAI,GAAsB,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;IACzD,KAAK,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,KAAK,CAAC,MAAM,EAAE,EAAE,IAAI,UAAU,EAAE,CAAC;QACrD,MAAM,GAAG,GAAa;YACpB,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;YAC1B,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAC,EAAE,IAAI,CAAC;YAC9B,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAC,EAAE,IAAI,CAAC;YAC9B,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,EAAE,IAAI,CAAC;SAChC,CAAC;QACF,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,EAAE,IAAI,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,EAAE,IAAI,CAAC,GAAG,WAAW,CAAC;QAC3F,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,EAAE,IAAI,CAAC,CAAC;QAC9C,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,EAAE,IAAI,CAAC,CAAC;QAChD,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,cAAc;YAAE,MAAM,IAAI,KAAK,CAAC,6BAA6B,GAAG,CAAC,CAAC,eAAe,CAAC,CAAC;QAC5G,MAAM,KAAK,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;QACzB,KAAK,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YACtC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,KAAK;gBAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,UAAU,CAAC,GAAG,CAAC,2BAA2B,KAAK,GAAG,CAAC,CAAC;QACtH,CAAC;QACD,IAAI,UAAU,KAAK,CAAC,CAAC,EAAE,CAAC;YACtB,IAAI,QAAQ,IAAI,CAAC,IAAI,QAAQ,GAAG,UAAU,KAAK,CAAC,EAAE,CAAC;gBACjD,MAAM,IAAI,KAAK,CAAC,oBAAoB,UAAU,CAAC,GAAG,CAAC,qBAAqB,QAAQ,EAAE,CAAC,CAAC;YACtF,CAAC;YACD,SAAS,CAAC,cAAc,UAAU,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;YACvE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC7C,CAAC;aAAM,IAAI,UAAU,KAAK,CAAC,EAAE,CAAC;YAC5B,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC;QAClE,CAAC;aAAM,IAAI,UAAU,GAAG,CAAC,EAAE,CAAC;YAC1B,IAAI,UAAU,GAAG,oBAAoB,EAAE,CAAC;gBACtC,MAAM,IAAI,KAAK,CAAC,cAAc,UAAU,CAAC,GAAG,CAAC,aAAa,UAAU,kBAAkB,oBAAoB,GAAG,CAAC,CAAC;YACjH,CAAC;YACD,IAAI,QAAQ,IAAI,CAAC;gBAAE,MAAM,IAAI,KAAK,CAAC,cAAc,UAAU,CAAC,GAAG,CAAC,QAAQ,UAAU,eAAe,QAAQ,QAAQ,CAAC,CAAC;YACnH,SAAS,CAAC,QAAQ,UAAU,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;YACjE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC,CAAC;QACzD,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,KAAK,CAAC,yBAAyB,UAAU,CAAC,GAAG,CAAC,4BAA4B,UAAU,EAAE,CAAC,CAAC;QACpG,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAID,mCAAmC;AACnC,MAAM,UAAU,aAAa,CAAC,GAAa;IACzC,MAAM,GAAG,GAAe,EAAE,CAAC;IAC3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3B,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC;IACnH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;GAIG;AACH,MAAM,OAAO,aAAa;IAQH;IAPZ,KAAK,GAAG,IAAI,GAAG,EAAyB,CAAC;IAClD,mEAAmE;IAC1D,YAAY,GAAG,IAAI,GAAG,EAAuB,CAAC;IACtC,eAAe,GAAG,IAAI,GAAG,EAAU,CAAC;IAC7C,SAAS,GAAG,CAAC,CAAC;IACd,SAAS,GAAG,CAAC,CAAC;IAEtB,YAAqB,SAA8B,6BAA6B;QAA3D,WAAM,GAAN,MAAM,CAAqD;IAAG,CAAC;IAEpF,IAAI,eAAe;QACjB,OAAO,IAAI,CAAC,SAAS,CAAC;IACxB,CAAC;IAED,yDAAyD;IACzD,IAAI,kBAAkB;QACpB,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC;IAChE,CAAC;IAED;;;;OAIG;IACH,aAAa,CAAC,GAAyB;QACrC,IAAI,IAAI,CAAC,SAAS,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;YAC9C,MAAM,IAAI,KAAK,CAAC,2BAA2B,IAAI,CAAC,MAAM,CAAC,QAAQ,QAAQ,CAAC,CAAC;QAC3E,CAAC;QACD,IAAI,GAAG,CAAC,QAAQ,GAAG,IAAI,CAAC,kBAAkB,EAAE,CAAC;YAC3C,MAAM,IAAI,KAAK,CAAC,2BAA2B,IAAI,CAAC,MAAM,CAAC,YAAY,aAAa,CAAC,CAAC;QACpF,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,OAAO,CAAC,GAAyD,EAAE,IAAuB;QACxF,IAAI,IAAI,CAAC,SAAS,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC;YAC/C,MAAM,IAAI,KAAK,CAAC,iBAAiB,GAAG,CAAC,MAAM,kCAAkC,CAAC,CAAC;QACjF,CAAC;QACD,IAAI,CAAC,SAAS,EAAE,CAAC;QACjB,IAAI,CAAC,SAAS,IAAI,GAAG,CAAC,QAAQ,CAAC;QAC/B,IAAI,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;YAC1C,MAAM,IAAI,KAAK,CAAC,2BAA2B,IAAI,CAAC,MAAM,CAAC,QAAQ,QAAQ,CAAC,CAAC;QAC3E,CAAC;QACD,IAAI,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC;YAC9C,MAAM,IAAI,KAAK,CAAC,2BAA2B,IAAI,CAAC,MAAM,CAAC,YAAY,aAAa,CAAC,CAAC;QACpF,CAAC;QACD,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACrC,IAAI,GAAG,CAAC,GAAG;YAAE,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;QAC3D,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;YAC/D,MAAM,IAAI,KAAK,CAAC,2BAA2B,IAAI,CAAC,MAAM,CAAC,QAAQ,QAAQ,CAAC,CAAC;QAC3E,CAAC;QACD,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YAC9B,MAAM,EAAE,GAAG,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAChC,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;gBAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,EAAE,QAAQ,CAAC,CAAC;YAClF,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;QAC3B,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YAC/B,oEAAoE;YACpE,kDAAkD;YAClD,IAAI,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC;gBAAE,SAAS;YACrD,MAAM,EAAE,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACjC,qEAAqE;YACrE,8DAA8D;YAC9D,IAAI,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC;gBAAE,MAAM,IAAI,KAAK,CAAC,oCAAoC,EAAE,QAAQ,CAAC,CAAC;YAC/F,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YACvC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;QACnC,CAAC;IACH,CAAC;IAEO,SAAS,CAAC,GAAuC;QACvD,IAAI,CAAC,GAAG,CAAC,GAAG;YAAE,OAAO,KAAK,CAAC;QAC3B,OAAO,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,KAAK,GAAG,CAAC,MAAM,CAAC;IAC3E,CAAC;IAED,+EAA+E;IAC/E,OAAO,CAAC,GAAa;QACnB,MAAM,EAAE,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;QAC3B,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,OAAO,MAAM,CAAC;QACtC,IAAI,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,OAAO,MAAM,CAAC;QAC7C,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF;AAKD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,SAAwB,EACxB,SAA0B,EAC1B,UAA6E,EAAE;IAE/E,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,QAAQ,CAAC;IAC9C,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,WAAW,IAAI,CAAC,CAAC,CAAC,CAAC;IACtE,OAAO,IAAI,EAAE,CAAC;QACZ,OAAO,CAAC,MAAM,EAAE,cAAc,EAAE,CAAC;QACjC,MAAM,KAAK,GAAG,CAAC,GAAG,SAAS,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,QAAQ,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC;QAChH,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAC/B,iDAAiD;QACjD,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;YAC1B,SAAS,CAAC,aAAa,CAAC,EAAE,QAAQ,EAAE,KAAK,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC;YAC5D,OAAO,KAAK,GAAG,GAAG,CAAC,QAAQ,CAAC;QAC9B,CAAC,EAAE,CAAC,CAAC,CAAC;QACN,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACpF,KAAK,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9D,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type LasHeader } from '../formats/las.js';
|
|
2
|
+
import type { PointCloudBBox } from '../types.js';
|
|
3
|
+
import type { PointSourceSpatialMetadata, RangeByteSource } from '../streaming/types.js';
|
|
4
|
+
/** Byte size of the LAS 1.4 public header block. */
|
|
5
|
+
export declare const LAS14_HEADER_SIZE = 375;
|
|
6
|
+
/** Header + the `copc` VLR header + its payload: everything detection needs. */
|
|
7
|
+
export declare const COPC_PROBE_BYTES: number;
|
|
8
|
+
/** VLRs are control-plane metadata; a larger block is refused before allocation. */
|
|
9
|
+
export declare const MAX_COPC_VLR_BYTES: number;
|
|
10
|
+
/** The `copc`/`info` VLR payload. */
|
|
11
|
+
export interface CopcInfo {
|
|
12
|
+
/** Centre of the root octree cube, in native (unshifted) coordinates. */
|
|
13
|
+
center: [number, number, number];
|
|
14
|
+
/** Half the edge length of the root cube. */
|
|
15
|
+
halfsize: number;
|
|
16
|
+
/** Point spacing at the root level; halves with every level. */
|
|
17
|
+
spacing: number;
|
|
18
|
+
/** Absolute file offset of the root hierarchy page. */
|
|
19
|
+
rootHierOffset: number;
|
|
20
|
+
/** Byte size of the root hierarchy page (a multiple of 32). */
|
|
21
|
+
rootHierSize: number;
|
|
22
|
+
gpsTimeMin: number;
|
|
23
|
+
gpsTimeMax: number;
|
|
24
|
+
}
|
|
25
|
+
/** Everything `openCopc` learns before touching point data. */
|
|
26
|
+
export interface CopcFileInfo {
|
|
27
|
+
header: LasHeader;
|
|
28
|
+
info: CopcInfo;
|
|
29
|
+
fileSize: number;
|
|
30
|
+
spatialMetadata?: PointSourceSpatialMetadata;
|
|
31
|
+
}
|
|
32
|
+
/** True when `bytes` (at least `COPC_PROBE_BYTES` of a file's start) is a COPC header. */
|
|
33
|
+
export declare function isCopcHeader(bytes: Uint8Array): boolean;
|
|
34
|
+
/** Parse the 160-byte `copc`/`info` payload. Throws on a non-finite or impossible cube. */
|
|
35
|
+
export declare function parseCopcInfo(payload: Uint8Array, fileSize: number): CopcInfo;
|
|
36
|
+
/**
|
|
37
|
+
* Read the header and VLR block through `source`, validating that this is a
|
|
38
|
+
* COPC file this reader can decode. Point data is not touched.
|
|
39
|
+
*/
|
|
40
|
+
export declare function readCopcFileInfo(source: RangeByteSource, signal?: AbortSignal): Promise<CopcFileInfo>;
|
|
41
|
+
/** An octree address: level `d` and integer cell `x, y, z` in `[0, 2^d)`. */
|
|
42
|
+
export interface VoxelKey {
|
|
43
|
+
d: number;
|
|
44
|
+
x: number;
|
|
45
|
+
y: number;
|
|
46
|
+
z: number;
|
|
47
|
+
}
|
|
48
|
+
/** Stable string id for a key (`"d-x-y-z"`), the form COPC tools print. */
|
|
49
|
+
export declare function voxelKeyId(key: VoxelKey): string;
|
|
50
|
+
/** The cube a node covers, in native coordinates: the root cube split `2^d` ways per axis. */
|
|
51
|
+
export declare function copcNodeBounds(info: CopcInfo, key: VoxelKey): PointCloudBBox;
|
|
52
|
+
//# sourceMappingURL=copc-info.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"copc-info.d.ts","sourceRoot":"","sources":["../../src/copc/copc-info.ts"],"names":[],"mappings":"AAgBA,OAAO,EAA6B,KAAK,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAE,0BAA0B,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAEzF,oDAAoD;AACpD,eAAO,MAAM,iBAAiB,MAAM,CAAC;AAGrC,gFAAgF;AAChF,eAAO,MAAM,gBAAgB,QAAuD,CAAC;AACrF,oFAAoF;AACpF,eAAO,MAAM,kBAAkB,QAAW,CAAC;AAK3C,qCAAqC;AACrC,MAAM,WAAW,QAAQ;IACvB,yEAAyE;IACzE,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,6CAA6C;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC;IAChB,uDAAuD;IACvD,cAAc,EAAE,MAAM,CAAC;IACvB,+DAA+D;IAC/D,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,+DAA+D;AAC/D,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,SAAS,CAAC;IAClB,IAAI,EAAE,QAAQ,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,0BAA0B,CAAC;CAC9C;AAYD,0FAA0F;AAC1F,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAQvD;AAED,2FAA2F;AAC3F,wBAAgB,aAAa,CAAC,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAoB7E;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,eAAe,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CA0B3G;AA2BD,6EAA6E;AAC7E,MAAM,WAAW,QAAQ;IACvB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,2EAA2E;AAC3E,wBAAgB,UAAU,CAAC,GAAG,EAAE,QAAQ,GAAG,MAAM,CAEhD;AAED,8FAA8F;AAC9F,wBAAgB,cAAc,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,QAAQ,GAAG,cAAc,CAQ5E"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
/**
|
|
5
|
+
* COPC 1.0 header and `copc`/`info` VLR (#6869), from the specification at
|
|
6
|
+
* https://copc.io.
|
|
7
|
+
*
|
|
8
|
+
* A COPC file is a LAS 1.4 file (point formats 6, 7 or 8, LAZ-compressed)
|
|
9
|
+
* whose FIRST VLR is `copc`/1, a 160-byte record describing the octree cube
|
|
10
|
+
* and where the root hierarchy page lives. Detection is by that VLR, not by
|
|
11
|
+
* the `.copc.laz` file name: a renamed COPC file is still COPC and a plain
|
|
12
|
+
* LAZ named `.copc.laz` is not.
|
|
13
|
+
*/
|
|
14
|
+
import { extractWktCrsIdentifiers } from '../spatial-wkt.js';
|
|
15
|
+
import { parseLasHeader, readU64LE } from '../formats/las.js';
|
|
16
|
+
/** Byte size of the LAS 1.4 public header block. */
|
|
17
|
+
export const LAS14_HEADER_SIZE = 375;
|
|
18
|
+
const VLR_HEADER_SIZE = 54;
|
|
19
|
+
const COPC_INFO_SIZE = 160;
|
|
20
|
+
/** Header + the `copc` VLR header + its payload: everything detection needs. */
|
|
21
|
+
export const COPC_PROBE_BYTES = LAS14_HEADER_SIZE + VLR_HEADER_SIZE + COPC_INFO_SIZE;
|
|
22
|
+
/** VLRs are control-plane metadata; a larger block is refused before allocation. */
|
|
23
|
+
export const MAX_COPC_VLR_BYTES = 16 << 20;
|
|
24
|
+
const COPC_POINT_FORMATS = new Set([6, 7, 8]);
|
|
25
|
+
const LASF_PROJECTION = 'LASF_Projection';
|
|
26
|
+
const WKT_RECORD_ID = 2112;
|
|
27
|
+
function readAscii(bytes, start, length) {
|
|
28
|
+
let out = '';
|
|
29
|
+
for (let i = start; i < start + length; i++) {
|
|
30
|
+
const c = bytes[i];
|
|
31
|
+
if (c === 0)
|
|
32
|
+
break;
|
|
33
|
+
out += String.fromCharCode(c);
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
/** True when `bytes` (at least `COPC_PROBE_BYTES` of a file's start) is a COPC header. */
|
|
38
|
+
export function isCopcHeader(bytes) {
|
|
39
|
+
if (bytes.length < COPC_PROBE_BYTES)
|
|
40
|
+
return false;
|
|
41
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
42
|
+
if (view.getUint32(0, true) !== 0x4653414c)
|
|
43
|
+
return false; // "LASF"
|
|
44
|
+
if (view.getUint8(24) !== 1 || view.getUint8(25) !== 4)
|
|
45
|
+
return false;
|
|
46
|
+
const headerSize = view.getUint16(94, true);
|
|
47
|
+
if (headerSize !== LAS14_HEADER_SIZE)
|
|
48
|
+
return false;
|
|
49
|
+
return readAscii(bytes, headerSize + 2, 16) === 'copc' && view.getUint16(headerSize + 18, true) === 1;
|
|
50
|
+
}
|
|
51
|
+
/** Parse the 160-byte `copc`/`info` payload. Throws on a non-finite or impossible cube. */
|
|
52
|
+
export function parseCopcInfo(payload, fileSize) {
|
|
53
|
+
if (payload.length < COPC_INFO_SIZE)
|
|
54
|
+
throw new Error('COPC: info VLR truncated');
|
|
55
|
+
const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
|
|
56
|
+
const center = [view.getFloat64(0, true), view.getFloat64(8, true), view.getFloat64(16, true)];
|
|
57
|
+
const halfsize = view.getFloat64(24, true);
|
|
58
|
+
const spacing = view.getFloat64(32, true);
|
|
59
|
+
const rootHierOffset = readU64LE(view, 40);
|
|
60
|
+
const rootHierSize = readU64LE(view, 48);
|
|
61
|
+
const gpsTimeMin = view.getFloat64(56, true);
|
|
62
|
+
const gpsTimeMax = view.getFloat64(64, true);
|
|
63
|
+
if (!center.every(Number.isFinite))
|
|
64
|
+
throw new Error('COPC: info VLR has a non-finite octree centre');
|
|
65
|
+
if (!(Number.isFinite(halfsize) && halfsize > 0))
|
|
66
|
+
throw new Error(`COPC: invalid octree halfsize ${halfsize}`);
|
|
67
|
+
if (!(Number.isFinite(spacing) && spacing > 0))
|
|
68
|
+
throw new Error(`COPC: invalid root spacing ${spacing}`);
|
|
69
|
+
if (rootHierSize <= 0 || rootHierSize % 32 !== 0) {
|
|
70
|
+
throw new Error(`COPC: root hierarchy page size ${rootHierSize} is not a positive multiple of 32`);
|
|
71
|
+
}
|
|
72
|
+
if (rootHierOffset < LAS14_HEADER_SIZE || rootHierOffset + rootHierSize > fileSize) {
|
|
73
|
+
throw new Error(`COPC: root hierarchy page [${rootHierOffset}, +${rootHierSize}) lies outside the ${fileSize}-byte file`);
|
|
74
|
+
}
|
|
75
|
+
return { center, halfsize, spacing, rootHierOffset, rootHierSize, gpsTimeMin, gpsTimeMax };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Read the header and VLR block through `source`, validating that this is a
|
|
79
|
+
* COPC file this reader can decode. Point data is not touched.
|
|
80
|
+
*/
|
|
81
|
+
export async function readCopcFileInfo(source, signal) {
|
|
82
|
+
const head = await source.read(0, COPC_PROBE_BYTES, signal);
|
|
83
|
+
if (!isCopcHeader(head)) {
|
|
84
|
+
throw new Error('COPC: not a COPC file (LAS 1.4 header whose first VLR is copc/info expected)');
|
|
85
|
+
}
|
|
86
|
+
const header = parseLasHeader(head);
|
|
87
|
+
if (!COPC_POINT_FORMATS.has(header.pointDataFormatId)) {
|
|
88
|
+
throw new Error(`COPC: point data format ${header.pointDataFormatId} is not one of 6, 7, 8`);
|
|
89
|
+
}
|
|
90
|
+
const vlrEnd = header.pointDataOffset;
|
|
91
|
+
if (vlrEnd < COPC_PROBE_BYTES || vlrEnd > source.size) {
|
|
92
|
+
throw new Error(`COPC: point data offset ${vlrEnd} lies outside the file`);
|
|
93
|
+
}
|
|
94
|
+
if (vlrEnd - LAS14_HEADER_SIZE > MAX_COPC_VLR_BYTES) {
|
|
95
|
+
throw new Error(`COPC: VLR block of ${vlrEnd - LAS14_HEADER_SIZE} bytes exceeds the ${MAX_COPC_VLR_BYTES}-byte limit`);
|
|
96
|
+
}
|
|
97
|
+
const vlrs = vlrEnd > COPC_PROBE_BYTES
|
|
98
|
+
? concat(head, await source.read(COPC_PROBE_BYTES, vlrEnd, signal))
|
|
99
|
+
: head;
|
|
100
|
+
if (vlrs.length < vlrEnd)
|
|
101
|
+
throw new Error('COPC: VLR block truncated');
|
|
102
|
+
const info = parseCopcInfo(vlrs.subarray(LAS14_HEADER_SIZE + VLR_HEADER_SIZE, COPC_PROBE_BYTES), source.size);
|
|
103
|
+
const wkt = findWkt(vlrs, header.numberOfVlrs, vlrEnd);
|
|
104
|
+
const spatialMetadata = wkt
|
|
105
|
+
? { ...extractWktCrsIdentifiers(wkt), wkt, provenance: 'LAS OGC WKT VLR' }
|
|
106
|
+
: undefined;
|
|
107
|
+
return { header, info, fileSize: source.size, ...(spatialMetadata ? { spatialMetadata } : {}) };
|
|
108
|
+
}
|
|
109
|
+
/** Walk the VLR list (bounded by both the declared count and the byte block). */
|
|
110
|
+
function findWkt(bytes, count, end) {
|
|
111
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
112
|
+
let at = LAS14_HEADER_SIZE;
|
|
113
|
+
for (let i = 0; i < count && at + VLR_HEADER_SIZE <= end; i++) {
|
|
114
|
+
const userId = readAscii(bytes, at + 2, 16);
|
|
115
|
+
const recordId = view.getUint16(at + 18, true);
|
|
116
|
+
const length = view.getUint16(at + 20, true);
|
|
117
|
+
const dataStart = at + VLR_HEADER_SIZE;
|
|
118
|
+
if (dataStart + length > end)
|
|
119
|
+
break;
|
|
120
|
+
if (userId === LASF_PROJECTION && recordId === WKT_RECORD_ID) {
|
|
121
|
+
return new TextDecoder().decode(bytes.subarray(dataStart, dataStart + length)).replace(/\0+$/, '');
|
|
122
|
+
}
|
|
123
|
+
at = dataStart + length;
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
function concat(a, b) {
|
|
128
|
+
const out = new Uint8Array(a.length + b.length);
|
|
129
|
+
out.set(a, 0);
|
|
130
|
+
out.set(b, a.length);
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
/** Stable string id for a key (`"d-x-y-z"`), the form COPC tools print. */
|
|
134
|
+
export function voxelKeyId(key) {
|
|
135
|
+
return `${key.d}-${key.x}-${key.y}-${key.z}`;
|
|
136
|
+
}
|
|
137
|
+
/** The cube a node covers, in native coordinates: the root cube split `2^d` ways per axis. */
|
|
138
|
+
export function copcNodeBounds(info, key) {
|
|
139
|
+
const side = (2 * info.halfsize) / 2 ** key.d;
|
|
140
|
+
const min = [
|
|
141
|
+
info.center[0] - info.halfsize + key.x * side,
|
|
142
|
+
info.center[1] - info.halfsize + key.y * side,
|
|
143
|
+
info.center[2] - info.halfsize + key.z * side,
|
|
144
|
+
];
|
|
145
|
+
return { min, max: [min[0] + side, min[1] + side, min[2] + side] };
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=copc-info.js.map
|