@openbim/ifcx 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +207 -2
- package/bundler/fetch-imports.d.ts +53 -0
- package/bundler/fetch-imports.js +137 -0
- package/bundler/openbim_ifcx_wasm.d.ts +139 -0
- package/bundler/openbim_ifcx_wasm.js +10 -0
- package/bundler/openbim_ifcx_wasm_bg.js +481 -0
- package/bundler/openbim_ifcx_wasm_bg.wasm +0 -0
- package/bundler/openbim_ifcx_wasm_bg.wasm.d.ts +18 -0
- package/bundler/package.json +3 -0
- package/fetch-imports.d.ts +53 -0
- package/fetch-imports.js +139 -0
- package/openbim_ifcx_wasm.d.ts +139 -0
- package/openbim_ifcx_wasm.js +490 -0
- package/openbim_ifcx_wasm_bg.wasm +0 -0
- package/openbim_ifcx_wasm_bg.wasm.d.ts +18 -0
- package/package.json +50 -4
- package/web/fetch-imports.d.ts +53 -0
- package/web/fetch-imports.js +137 -0
- package/web/openbim_ifcx_wasm.d.ts +182 -0
- package/web/openbim_ifcx_wasm.js +585 -0
- package/web/openbim_ifcx_wasm_bg.wasm +0 -0
- package/web/openbim_ifcx_wasm_bg.wasm.d.ts +18 -0
- package/web/package.json +3 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 point-grey
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,208 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @openbim/ifcx
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Read, write, validate and compose **IFC5 / IFCX** files, and export them as
|
|
4
|
+
GLB, from JavaScript and TypeScript. A WebAssembly build of the
|
|
5
|
+
[`openbim-ifcx`](https://crates.io/crates/openbim-ifcx) Rust crate, from the
|
|
6
|
+
`openbim-ifcx-wasm` crate in [openbimrs/ifcx](https://github.com/openbimrs/ifcx).
|
|
7
|
+
|
|
8
|
+
Published to npm as `@openbim/ifcx`: one package for Node 18 and later,
|
|
9
|
+
bundlers (webpack, Rollup) and plain browser pages, with TypeScript
|
|
10
|
+
declarations for each. Every build is tested from the packed tarball,
|
|
11
|
+
the browser builds in headless Chrome.
|
|
12
|
+
|
|
13
|
+
Targets the `ifcx_alpha` draft of buildingSMART's IFC5. IFCX is still a
|
|
14
|
+
moving draft; see the
|
|
15
|
+
[capabilities](https://github.com/openbimrs/ifcx/blob/main/docs/capabilities.md).
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npm install @openbim/ifcx
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Entry points
|
|
24
|
+
|
|
25
|
+
| Import | Build | Loads the wasm module |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| `@openbim/ifcx` in Node (`require` or `import`) | CommonJS (`wasm-bindgen --target nodejs`) | synchronously, on load |
|
|
28
|
+
| `@openbim/ifcx` in a bundler | ES module (`--target bundler`) | through the bundler's WebAssembly support, e.g. webpack 5 `experiments.asyncWebAssembly` |
|
|
29
|
+
| `@openbim/ifcx/web` | ES module (`--target web`) | when you `await init()` |
|
|
30
|
+
|
|
31
|
+
`package.json` `exports` picks the build: the `node` condition gets the
|
|
32
|
+
CommonJS build, everything else the bundler build. Use `@openbim/ifcx/web`
|
|
33
|
+
for a page without a bundler, and for bundlers without WebAssembly ES
|
|
34
|
+
module integration, such as Vite (or add `vite-plugin-wasm`). All three
|
|
35
|
+
export the same API; the `web` build adds the default `init` export (and
|
|
36
|
+
`initSync`).
|
|
37
|
+
|
|
38
|
+
## Node
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
const { readFileSync, writeFileSync } = require("node:fs");
|
|
42
|
+
const { IfcxFile, compose, exportGlb } = require("@openbim/ifcx"); // or import
|
|
43
|
+
|
|
44
|
+
const model = readFileSync("model.ifcx"); // a Buffer is a Uint8Array
|
|
45
|
+
const file = IfcxFile.parse(model); // or a string
|
|
46
|
+
console.log(file.header.id, file.nodeCount);
|
|
47
|
+
|
|
48
|
+
const report = file.validate();
|
|
49
|
+
for (const f of report.failures) {
|
|
50
|
+
console.log(f.node, f.attribute, f.pointer, f.kind, f.message);
|
|
51
|
+
}
|
|
52
|
+
const text = file.write(); // JSON text, unchanged from the input
|
|
53
|
+
|
|
54
|
+
// Layers weakest first: the last layer's opinions win.
|
|
55
|
+
const tree = compose([model, readFileSync("overlay.ifcx")]);
|
|
56
|
+
console.log(Object.keys(tree.children)); // the root nodes
|
|
57
|
+
|
|
58
|
+
// Any glTF viewer opens the result.
|
|
59
|
+
writeFileSync("model.glb", exportGlb([model, readFileSync("overlay.ifcx")]));
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Browser
|
|
63
|
+
|
|
64
|
+
With a bundler, import the package as usual; the bundler loads the wasm
|
|
65
|
+
module:
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
import { compose, exportGlb, fetchImports } from "@openbim/ifcx";
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Without one, serve the package's `web/` directory, map the name in an
|
|
72
|
+
import map (or import the file by URL), and call `init()` once before
|
|
73
|
+
anything else. `init()` fetches `openbim_ifcx_wasm_bg.wasm` from next to
|
|
74
|
+
the module; pass `init({ module_or_path: url })` to load it from elsewhere.
|
|
75
|
+
|
|
76
|
+
```html
|
|
77
|
+
<script type="importmap">
|
|
78
|
+
{ "imports": { "@openbim/ifcx/web": "/node_modules/@openbim/ifcx/web/openbim_ifcx_wasm.js" } }
|
|
79
|
+
</script>
|
|
80
|
+
<script type="module">
|
|
81
|
+
import init, { IfcxFile, exportGlb, fetchImports, validate } from "@openbim/ifcx/web";
|
|
82
|
+
|
|
83
|
+
await init();
|
|
84
|
+
const url = new URL("models/house.ifcx", location.href);
|
|
85
|
+
const model = new Uint8Array(await (await fetch(url)).arrayBuffer());
|
|
86
|
+
|
|
87
|
+
// Fetch what the model imports, relative to its own URL, then use it.
|
|
88
|
+
const imports = await fetchImports(model, { baseUrl: url });
|
|
89
|
+
const report = validate(model, { imports });
|
|
90
|
+
const glb = exportGlb(model, { imports }); // e.g. for three.js's GLTFLoader.parse
|
|
91
|
+
console.log(IfcxFile.parse(model).header.id, report.valid, glb.length);
|
|
92
|
+
</script>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## API
|
|
96
|
+
|
|
97
|
+
| Call | Returns |
|
|
98
|
+
| --- | --- |
|
|
99
|
+
| `IfcxFile.parse(input)` | an `IfcxFile`; `input` is JSON text or UTF-8 bytes |
|
|
100
|
+
| `file.write(pretty?)` | the file as JSON text, lossless |
|
|
101
|
+
| `file.toJSON()` | the file as a plain object (`JSON.stringify(file)` works) |
|
|
102
|
+
| `file.header` | the `header` object |
|
|
103
|
+
| `file.nodeCount` | number of entries in `data` |
|
|
104
|
+
| `file.validate()` | a `ValidationReport` against the file's own `schemas` |
|
|
105
|
+
| `compose(layers, options?)` | the composed tree as a `ComposedNode` |
|
|
106
|
+
| `validate(layers, options?)` | a `ValidationReport` over all layers and resolved imports |
|
|
107
|
+
| `exportGlb(layers, options?)` | the composed model as binary glTF 2.0, a `Uint8Array` |
|
|
108
|
+
| `fetchImports(layers, options?)` | a promise of a `Map` from import `uri` to file, for `options.imports` |
|
|
109
|
+
| `init(input?)` | `@openbim/ifcx/web` only: loads the wasm module; await it once first |
|
|
110
|
+
|
|
111
|
+
`layers` is one input or an array of them, weakest first. TypeScript
|
|
112
|
+
declarations ship with the package.
|
|
113
|
+
|
|
114
|
+
**Lossless round trip.** `write()` keeps key order, fields the draft does
|
|
115
|
+
not define, `null` deletions and every number exactly as read. A plain
|
|
116
|
+
object from `toJSON()` cannot promise that: JavaScript lists integer-like
|
|
117
|
+
keys first and rounds integers beyond 2^53. Keep the `IfcxFile` and call
|
|
118
|
+
`write()` to save a file.
|
|
119
|
+
|
|
120
|
+
**Composed tree.** `compose` returns the artificial root (path `""`) over
|
|
121
|
+
every root node. Each node is `{ path, attributes, children }`, the shape of
|
|
122
|
+
upstream's `PostCompositionNode`, with `children` keyed by child name. Nodes
|
|
123
|
+
shared through inheritance appear once per place they are used.
|
|
124
|
+
|
|
125
|
+
**Imports.** Without `options.imports`, imports are not resolved: the layers'
|
|
126
|
+
schemas and data are concatenated in order, as upstream's `Federate` does.
|
|
127
|
+
With `options.imports` (an object or a `Map` from the exact import `uri` to
|
|
128
|
+
a file), the layers become the imports of a synthetic main layer, as
|
|
129
|
+
upstream's `ifcx compose` command builds it, and every import must be
|
|
130
|
+
supplied. `integrity` values are checked against the supplied bytes. In
|
|
131
|
+
upstream order an import overrides the layer that imports it, and a later
|
|
132
|
+
layer overrides both. The WebAssembly module never touches the network or
|
|
133
|
+
the filesystem.
|
|
134
|
+
|
|
135
|
+
**Fetching imports.** `fetchImports(layers, options?)` collects them for
|
|
136
|
+
you, in JavaScript: it reads each layer's `imports`, fetches every `uri`
|
|
137
|
+
(recursively, each once, concurrently) and resolves to a `Map` keyed by
|
|
138
|
+
the exact import `uri`, ready for `options.imports`.
|
|
139
|
+
|
|
140
|
+
- `baseUrl`: what relative URIs of the given layers resolve against;
|
|
141
|
+
`location.href` by default in a browser. A fetched file's own imports
|
|
142
|
+
resolve against its URL.
|
|
143
|
+
- `fetch`: the function that loads a URL, the global `fetch` by default.
|
|
144
|
+
Pass your own for credentials, a cache, a mirror, or Node files:
|
|
145
|
+
`{ fetch: async (url) => readFile(new URL(url)) }`. It may return a
|
|
146
|
+
`Response`, a `Uint8Array`, an `ArrayBuffer` or a string. Without a
|
|
147
|
+
`baseUrl` it receives a relative `uri` unchanged.
|
|
148
|
+
- `imports`: files already at hand, never fetched again.
|
|
149
|
+
- `signal`: an `AbortSignal` passed to every fetch.
|
|
150
|
+
|
|
151
|
+
The in-memory map keys a file by its exact `uri`, as upstream's
|
|
152
|
+
`InMemoryLayerProvider` does, so two different files imported under the
|
|
153
|
+
same relative `uri` from different directories cannot both be supplied;
|
|
154
|
+
the first one fetched is used. Anything that cannot be fetched rejects with
|
|
155
|
+
an `IfcxError` with code `fetch`; `integrity` and import cycles are checked
|
|
156
|
+
by `compose`, `validate` and `exportGlb`.
|
|
157
|
+
|
|
158
|
+
**GLB export.** `exportGlb` writes the render scene of the composed tree:
|
|
159
|
+
transformed, instanced meshes, lines and points with their materials, one
|
|
160
|
+
glTF node per instance named by its IFCX path. The root node turns IFCX's
|
|
161
|
+
Z-up into glTF's Y-up (`yUp: false` keeps Z-up) and carries the scene
|
|
162
|
+
origin, so georeferenced models keep their coordinates
|
|
163
|
+
(`originOnRoot: false` centres the model on the glTF origin instead).
|
|
164
|
+
Geometry values that do not decode are left out. `options.imports` works
|
|
165
|
+
as for `compose`.
|
|
166
|
+
|
|
167
|
+
**Validation report.** `{ valid, failures }`, each failure
|
|
168
|
+
`{ node, attribute, pointer, kind, message }`: the node path, the attribute
|
|
169
|
+
id, an RFC 6901 pointer into the value (`""` for the value itself), a stable
|
|
170
|
+
`kind` such as `missing-schema`, `type-mismatch`, `not-an-integer`,
|
|
171
|
+
`not-an-option`, `missing-key`, `too-few-elements` or `too-many-elements`,
|
|
172
|
+
and a human message.
|
|
173
|
+
|
|
174
|
+
## Errors
|
|
175
|
+
|
|
176
|
+
Every failure throws an `Error` with `name === "IfcxError"` and a stable
|
|
177
|
+
`code`:
|
|
178
|
+
|
|
179
|
+
| `code` | Meaning |
|
|
180
|
+
| --- | --- |
|
|
181
|
+
| `read` | not an IFCX file: invalid JSON or the wrong shape; the message gives line and column |
|
|
182
|
+
| `write` | the file could not be written |
|
|
183
|
+
| `layer` | imports could not be resolved: a missing file, an import cycle, an `integrity` mismatch |
|
|
184
|
+
| `compose` | a reference cycle, or a reference to a node no layer defines |
|
|
185
|
+
| `invalid-argument` | an argument of the wrong type, or no layers |
|
|
186
|
+
| `glb` | the scene could not be written as GLB (over 4 GiB) |
|
|
187
|
+
| `fetch` | `fetchImports` could not fetch an import, or got no file back |
|
|
188
|
+
|
|
189
|
+
A code is never renamed or reused.
|
|
190
|
+
|
|
191
|
+
## Building
|
|
192
|
+
|
|
193
|
+
```sh
|
|
194
|
+
cargo install wasm-bindgen-cli --version 0.2.128 --locked
|
|
195
|
+
crates/openbim-ifcx-wasm/scripts/build-npm-pkg.sh # builds pkg/, runs every check
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
The script binds one release build three times (`pkg/`, `pkg/bundler/`,
|
|
199
|
+
`pkg/web/`), runs the Node suite, then `npm pack`s the package and checks
|
|
200
|
+
each entry point from the tarball: Node `require` and `import`, a webpack
|
|
201
|
+
bundle, and the `web` and bundler builds in headless Chrome, which parse,
|
|
202
|
+
validate, compose, fetch imports and export GLB from the repository's
|
|
203
|
+
fixtures. It finds Chrome through `CHROME_BIN`, `PATH` or a Playwright
|
|
204
|
+
download; `IFCX_SKIP_BROWSER=1` skips the browser part with a warning.
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
MIT, like the rest of `openbimrs/ifcx`.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { IfcxInput } from "./openbim_ifcx_wasm.js";
|
|
2
|
+
|
|
3
|
+
/** The part of a `fetch` `Response` that `fetchImports` reads. */
|
|
4
|
+
export interface FetchedResponse {
|
|
5
|
+
ok?: boolean;
|
|
6
|
+
status?: number;
|
|
7
|
+
statusText?: string;
|
|
8
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Loads one imported file. `fetch` itself fits; a custom function may also
|
|
13
|
+
* return the file directly. `url` is the import `uri` resolved against the
|
|
14
|
+
* importing file's URL (or `baseUrl`); when a relative `uri` has no base to
|
|
15
|
+
* resolve against, a custom function receives the `uri` unchanged.
|
|
16
|
+
*/
|
|
17
|
+
export type ImportFetcher = (
|
|
18
|
+
url: string,
|
|
19
|
+
init?: { signal?: AbortSignal },
|
|
20
|
+
) => Promise<FetchedResponse | IfcxInput | ArrayBuffer>;
|
|
21
|
+
|
|
22
|
+
/** Options for `fetchImports`. */
|
|
23
|
+
export interface FetchImportsOptions {
|
|
24
|
+
/**
|
|
25
|
+
* URL that relative import URIs of the given layers resolve against.
|
|
26
|
+
* Defaults to `location.href` in a browser. Imports of a fetched file
|
|
27
|
+
* resolve against that file's URL.
|
|
28
|
+
*/
|
|
29
|
+
baseUrl?: string | URL;
|
|
30
|
+
/** Loads each file. Defaults to the global `fetch`. */
|
|
31
|
+
fetch?: ImportFetcher;
|
|
32
|
+
/** Files already at hand, keyed by import `uri`; never fetched again. */
|
|
33
|
+
imports?: Record<string, IfcxInput> | Map<string, IfcxInput>;
|
|
34
|
+
/** Passed to every fetch call. */
|
|
35
|
+
signal?: AbortSignal;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Fetch every file the layers import, recursively and each `uri` once, and
|
|
40
|
+
* return them keyed by the exact import `uri`: the `imports` option of
|
|
41
|
+
* `compose`, `validate` and `exportGlb`. Network access happens here, in
|
|
42
|
+
* JavaScript; the WebAssembly module never fetches anything. A file that
|
|
43
|
+
* cannot be fetched rejects with an `IfcxError` whose `code` is `"fetch"`.
|
|
44
|
+
*
|
|
45
|
+
* ```js
|
|
46
|
+
* const imports = await fetchImports(layers, { baseUrl: "https://example.org/model/" });
|
|
47
|
+
* const tree = compose(layers, { imports });
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export function fetchImports(
|
|
51
|
+
layers: IfcxInput | IfcxInput[],
|
|
52
|
+
options?: FetchImportsOptions,
|
|
53
|
+
): Promise<Map<string, IfcxInput>>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Import resolution over `fetch`, for the `imports` option of `compose`,
|
|
2
|
+
// `validate` and `exportGlb`.
|
|
3
|
+
//
|
|
4
|
+
// Plain JavaScript on purpose: the Rust crates perform no network access
|
|
5
|
+
// (ADR 0002), so the binding resolves imports in memory only and this
|
|
6
|
+
// module does the fetching on the JavaScript side. It reads each layer's
|
|
7
|
+
// `imports` list, fetches every named file (recursively, each URI once) and
|
|
8
|
+
// returns the files keyed by their exact import `uri`, which is what the
|
|
9
|
+
// in-memory `imports` option expects. Integrity values are checked later,
|
|
10
|
+
// by the binding, against the fetched bytes.
|
|
11
|
+
//
|
|
12
|
+
// This file is the ES module source. scripts/build-npm-pkg.sh copies it into
|
|
13
|
+
// the `web` and `bundler` builds and derives the CommonJS copy for the Node
|
|
14
|
+
// build from it, so it must stay self-contained: no imports, and every
|
|
15
|
+
// export a top-level `export async function` or `export function`.
|
|
16
|
+
|
|
17
|
+
/** The imports of an IFCX file, or none when it is not readable JSON. */
|
|
18
|
+
function importUris(input) {
|
|
19
|
+
let text = input;
|
|
20
|
+
if (typeof input !== "string") {
|
|
21
|
+
text = new TextDecoder().decode(input);
|
|
22
|
+
}
|
|
23
|
+
let file;
|
|
24
|
+
try {
|
|
25
|
+
file = JSON.parse(text);
|
|
26
|
+
} catch {
|
|
27
|
+
// Not JSON: the binding call that receives it reports where.
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
const imports = file !== null && typeof file === "object" ? file.imports : undefined;
|
|
31
|
+
if (!Array.isArray(imports)) return [];
|
|
32
|
+
return imports
|
|
33
|
+
.map((entry) => (entry !== null && typeof entry === "object" ? entry.uri : undefined))
|
|
34
|
+
.filter((uri) => typeof uri === "string");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function fetchError(message, cause) {
|
|
38
|
+
const error = new Error(message, cause === undefined ? undefined : { cause });
|
|
39
|
+
error.name = "IfcxError";
|
|
40
|
+
error.code = "fetch";
|
|
41
|
+
return error;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isInput(value) {
|
|
45
|
+
return typeof value === "string" || value instanceof Uint8Array;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The URL `uri` names, relative to `base`; `undefined` if it cannot be resolved. */
|
|
49
|
+
function resolveUrl(uri, base) {
|
|
50
|
+
try {
|
|
51
|
+
return base === undefined ? new URL(uri).href : new URL(uri, base).href;
|
|
52
|
+
} catch {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** What a fetch function returned, as an IFCX input. */
|
|
58
|
+
async function readResponse(response, uri, url) {
|
|
59
|
+
if (isInput(response)) return response;
|
|
60
|
+
if (response instanceof ArrayBuffer) return new Uint8Array(response);
|
|
61
|
+
if (response !== null && typeof response === "object" && typeof response.arrayBuffer === "function") {
|
|
62
|
+
if (response.ok === false) {
|
|
63
|
+
const status = [response.status, response.statusText].filter(Boolean).join(" ");
|
|
64
|
+
throw fetchError(`could not fetch import ${JSON.stringify(uri)} from ${url}: HTTP ${status}`);
|
|
65
|
+
}
|
|
66
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
67
|
+
}
|
|
68
|
+
throw fetchError(
|
|
69
|
+
`the fetch function returned neither a Response, a Uint8Array, an ArrayBuffer nor a string for ${JSON.stringify(uri)}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Fetch every file the layers import, recursively, keyed by the exact
|
|
75
|
+
* import `uri`, for the `imports` option of `compose`, `validate` and
|
|
76
|
+
* `exportGlb`.
|
|
77
|
+
*/
|
|
78
|
+
export async function fetchImports(layers, options = {}) {
|
|
79
|
+
const list = Array.isArray(layers) ? layers : [layers];
|
|
80
|
+
for (const layer of list) {
|
|
81
|
+
if (!isInput(layer)) {
|
|
82
|
+
throw fetchError("every layer must be a string or a Uint8Array");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const fetcher = options.fetch ?? globalThis.fetch;
|
|
86
|
+
if (typeof fetcher !== "function") {
|
|
87
|
+
throw fetchError("no fetch function: pass options.fetch");
|
|
88
|
+
}
|
|
89
|
+
const custom = options.fetch !== undefined;
|
|
90
|
+
const base =
|
|
91
|
+
options.baseUrl !== undefined ? String(options.baseUrl) : globalThis.location?.href;
|
|
92
|
+
const init = options.signal === undefined ? undefined : { signal: options.signal };
|
|
93
|
+
|
|
94
|
+
const found = new Map();
|
|
95
|
+
if (options.imports !== undefined) {
|
|
96
|
+
const known = options.imports instanceof Map ? options.imports : Object.entries(options.imports);
|
|
97
|
+
for (const [uri, file] of known) found.set(uri, file);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Breadth first; each level's fetches run concurrently. `url` is where a
|
|
101
|
+
// layer came from, so its relative imports resolve against it.
|
|
102
|
+
let level = list.map((file) => ({ file, url: base }));
|
|
103
|
+
for (const [, file] of found) level.push({ file, url: base });
|
|
104
|
+
while (level.length > 0) {
|
|
105
|
+
const pending = [];
|
|
106
|
+
for (const { file, url: importer } of level) {
|
|
107
|
+
for (const uri of importUris(file)) {
|
|
108
|
+
if (found.has(uri)) continue;
|
|
109
|
+
found.set(uri, undefined); // claimed: fetch each URI once
|
|
110
|
+
const url = resolveUrl(uri, importer);
|
|
111
|
+
if (url === undefined && !custom) {
|
|
112
|
+
throw fetchError(
|
|
113
|
+
`cannot resolve import ${JSON.stringify(uri)} to a URL: pass options.baseUrl`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
pending.push(
|
|
117
|
+
(async () => {
|
|
118
|
+
let response;
|
|
119
|
+
try {
|
|
120
|
+
response = await fetcher(url ?? uri, init);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
throw fetchError(
|
|
123
|
+
`could not fetch import ${JSON.stringify(uri)} from ${url ?? uri}: ${error?.message ?? error}`,
|
|
124
|
+
error,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
const fetched = await readResponse(response, uri, url ?? uri);
|
|
128
|
+
found.set(uri, fetched);
|
|
129
|
+
return { file: fetched, url: url ?? importer };
|
|
130
|
+
})(),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
level = await Promise.all(pending);
|
|
135
|
+
}
|
|
136
|
+
return found;
|
|
137
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
|
|
4
|
+
/** An IFCX file as JSON text or as its UTF-8 bytes. */
|
|
5
|
+
export type IfcxInput = string | Uint8Array;
|
|
6
|
+
|
|
7
|
+
/** Options for `compose` and `validate`. */
|
|
8
|
+
export interface LayerOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Files that imports name, keyed by the exact import `uri`. When given
|
|
11
|
+
* (even empty), imports are resolved as upstream does and every import
|
|
12
|
+
* must be here; without it, imports are not resolved.
|
|
13
|
+
*/
|
|
14
|
+
imports?: Record<string, IfcxInput> | Map<string, IfcxInput>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** One node of a composed tree. The root has path `""`. */
|
|
18
|
+
export interface ComposedNode {
|
|
19
|
+
path: string;
|
|
20
|
+
attributes: Record<string, unknown>;
|
|
21
|
+
children: Record<string, ComposedNode>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The `kind` of a validation failure. */
|
|
25
|
+
export type ValidationFailureKind =
|
|
26
|
+
| "missing-schema"
|
|
27
|
+
| "unknown-inherited-schema"
|
|
28
|
+
| "inheritance-cycle"
|
|
29
|
+
| "type-mismatch"
|
|
30
|
+
| "not-an-integer"
|
|
31
|
+
| "not-an-option"
|
|
32
|
+
| "missing-key"
|
|
33
|
+
| "too-few-elements"
|
|
34
|
+
| "too-many-elements"
|
|
35
|
+
| "missing-restrictions"
|
|
36
|
+
| "unknown-data-type"
|
|
37
|
+
| "other";
|
|
38
|
+
|
|
39
|
+
/** One attribute value that does not match its schema. */
|
|
40
|
+
export interface ValidationFailure {
|
|
41
|
+
/** Path of the node carrying the attribute. */
|
|
42
|
+
node: string;
|
|
43
|
+
/** Attribute id, which is also the schema id. */
|
|
44
|
+
attribute: string;
|
|
45
|
+
/** RFC 6901 JSON pointer into the value; `""` for the value itself. */
|
|
46
|
+
pointer: string;
|
|
47
|
+
kind: ValidationFailureKind;
|
|
48
|
+
message: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ValidationReport {
|
|
52
|
+
valid: boolean;
|
|
53
|
+
failures: ValidationFailure[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Compose layers, weakest first, into a tree of plain objects: the
|
|
58
|
+
* artificial root (path `""`) over every root node.
|
|
59
|
+
*/
|
|
60
|
+
export function compose(layers: IfcxInput | IfcxInput[], options?: LayerOptions): ComposedNode;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check the attributes of the layers, weakest first, against the schemas of
|
|
64
|
+
* every layer (and of resolved imports), after merging nodes that share a
|
|
65
|
+
* path.
|
|
66
|
+
*/
|
|
67
|
+
export function validate(layers: IfcxInput | IfcxInput[], options?: LayerOptions): ValidationReport;
|
|
68
|
+
|
|
69
|
+
/** Options for `exportGlb`. */
|
|
70
|
+
export interface GlbOptions extends LayerOptions {
|
|
71
|
+
/** Rotate IFCX's Z-up axes to glTF's Y-up. Default `true`. */
|
|
72
|
+
yUp?: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Keep the model's coordinates by putting the scene origin on the root
|
|
75
|
+
* node. With `false` the model sits around the glTF origin, which is then
|
|
76
|
+
* only recorded in the root's `extras.ifcxOrigin`. Default `true`.
|
|
77
|
+
*/
|
|
78
|
+
originOnRoot?: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Compose layers, weakest first, and write their render scene as a binary
|
|
83
|
+
* glTF 2.0 (`.glb`) file: transformed, instanced meshes, lines and points
|
|
84
|
+
* with their materials, one glTF node per instance named by its IFCX path.
|
|
85
|
+
*/
|
|
86
|
+
export function exportGlb(layers: IfcxInput | IfcxInput[], options?: GlbOptions): Uint8Array;
|
|
87
|
+
|
|
88
|
+
/** The `code` of an `IfcxError`. */
|
|
89
|
+
export type IfcxErrorCode =
|
|
90
|
+
| "read"
|
|
91
|
+
| "write"
|
|
92
|
+
| "layer"
|
|
93
|
+
| "compose"
|
|
94
|
+
| "invalid-argument"
|
|
95
|
+
| "glb"
|
|
96
|
+
| "fetch";
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* One IFCX file. Writing it back is lossless: key order, unknown fields,
|
|
102
|
+
* `null` deletions and every number stay as read.
|
|
103
|
+
*
|
|
104
|
+
* A newtype because `wasm_bindgen` can only export a type defined in this
|
|
105
|
+
* crate; all state and behaviour are the core's.
|
|
106
|
+
*/
|
|
107
|
+
export class IfcxFile {
|
|
108
|
+
private constructor();
|
|
109
|
+
free(): void;
|
|
110
|
+
[Symbol.dispose](): void;
|
|
111
|
+
/**
|
|
112
|
+
* Read an IFCX file from its JSON text or UTF-8 bytes.
|
|
113
|
+
*/
|
|
114
|
+
static parse(input: IfcxInput): IfcxFile;
|
|
115
|
+
/**
|
|
116
|
+
* The file as a plain object, so `JSON.stringify(file)` works. Unlike
|
|
117
|
+
* `write`, a plain object may reorder integer-like keys and round
|
|
118
|
+
* integers beyond 2^53.
|
|
119
|
+
*/
|
|
120
|
+
toJSON(): Record<string, unknown>;
|
|
121
|
+
/**
|
|
122
|
+
* Check every attribute against this file's own `schemas`. Imports are
|
|
123
|
+
* not resolved; use the `validate` function to include them.
|
|
124
|
+
*/
|
|
125
|
+
validate(): ValidationReport;
|
|
126
|
+
/**
|
|
127
|
+
* The file as JSON text; `pretty` indents it by two spaces.
|
|
128
|
+
*/
|
|
129
|
+
write(pretty?: boolean | null): string;
|
|
130
|
+
/**
|
|
131
|
+
* The `header` object.
|
|
132
|
+
*/
|
|
133
|
+
readonly header: Record<string, unknown>;
|
|
134
|
+
/**
|
|
135
|
+
* Number of entries in `data`; several may share a path.
|
|
136
|
+
*/
|
|
137
|
+
readonly nodeCount: number;
|
|
138
|
+
}
|
|
139
|
+
export * from "./fetch-imports.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/* @ts-self-types="./openbim_ifcx_wasm.d.ts" */
|
|
2
|
+
import * as wasm from "./openbim_ifcx_wasm_bg.wasm";
|
|
3
|
+
import { __wbg_set_wasm } from "./openbim_ifcx_wasm_bg.js";
|
|
4
|
+
|
|
5
|
+
__wbg_set_wasm(wasm);
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
IfcxFile, compose, exportGlb, validate
|
|
9
|
+
} from "./openbim_ifcx_wasm_bg.js";
|
|
10
|
+
export { fetchImports } from "./fetch-imports.js";
|