@openbim/ifcx 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -92,7 +92,8 @@ export type IfcxErrorCode =
92
92
  | "layer"
93
93
  | "compose"
94
94
  | "invalid-argument"
95
- | "glb";
95
+ | "glb"
96
+ | "fetch";
96
97
 
97
98
 
98
99
 
@@ -135,3 +136,4 @@ export class IfcxFile {
135
136
  */
136
137
  readonly nodeCount: number;
137
138
  }
139
+ export * from "./fetch-imports.js";
@@ -487,3 +487,4 @@ const wasmBytes = require('fs').readFileSync(wasmPath);
487
487
  const wasmModule = new WebAssembly.Module(wasmBytes);
488
488
  let wasmInstance = new WebAssembly.Instance(wasmModule, __wbg_get_imports());
489
489
  let wasm = wasmInstance.exports;
490
+ exports.fetchImports = require("./fetch-imports.js").fetchImports;
Binary file
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@openbim/ifcx",
3
- "description": "Read, write, validate and compose IFC5 / IFCX files, and export GLB, from JavaScript (WebAssembly build of openbim-ifcx).",
4
- "version": "0.1.0",
3
+ "description": "Read, write, validate and compose IFC5 / IFCX files, and export GLB, from JavaScript in Node and the browser (WebAssembly build of openbim-ifcx).",
4
+ "version": "0.2.1",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -11,16 +11,42 @@
11
11
  "type": "commonjs",
12
12
  "main": "openbim_ifcx_wasm.js",
13
13
  "types": "openbim_ifcx_wasm.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "node": {
17
+ "types": "./openbim_ifcx_wasm.d.ts",
18
+ "default": "./openbim_ifcx_wasm.js"
19
+ },
20
+ "default": {
21
+ "types": "./bundler/openbim_ifcx_wasm.d.ts",
22
+ "default": "./bundler/openbim_ifcx_wasm.js"
23
+ }
24
+ },
25
+ "./bundler": {
26
+ "types": "./bundler/openbim_ifcx_wasm.d.ts",
27
+ "default": "./bundler/openbim_ifcx_wasm.js"
28
+ },
29
+ "./web": {
30
+ "types": "./web/openbim_ifcx_wasm.d.ts",
31
+ "default": "./web/openbim_ifcx_wasm.js"
32
+ },
33
+ "./package.json": "./package.json",
34
+ "./*": "./*"
35
+ },
14
36
  "files": [
15
37
  "openbim_ifcx_wasm.js",
16
38
  "openbim_ifcx_wasm.d.ts",
17
39
  "openbim_ifcx_wasm_bg.wasm",
18
40
  "openbim_ifcx_wasm_bg.wasm.d.ts",
41
+ "fetch-imports.js",
42
+ "fetch-imports.d.ts",
43
+ "bundler/",
44
+ "web/",
19
45
  "README.md",
20
46
  "LICENSE"
21
47
  ],
22
48
  "engines": {
23
49
  "node": ">=18"
24
50
  },
25
- "keywords": ["ifcx", "ifc5", "ifc", "bim", "openbim", "gltf", "wasm"]
51
+ "keywords": ["ifcx", "ifc5", "ifc", "bim", "openbim", "gltf", "wasm", "browser"]
26
52
  }
@@ -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,182 @@
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
+
140
+ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
141
+
142
+ export interface InitOutput {
143
+ readonly memory: WebAssembly.Memory;
144
+ readonly __wbg_ifcxfile_free: (a: number, b: number) => void;
145
+ readonly compose: (a: number, b: number, c: number) => void;
146
+ readonly exportGlb: (a: number, b: number, c: number) => void;
147
+ readonly ifcxfile_header: (a: number, b: number) => void;
148
+ readonly ifcxfile_nodeCount: (a: number) => number;
149
+ readonly ifcxfile_parse: (a: number, b: number) => void;
150
+ readonly ifcxfile_toJSON: (a: number, b: number) => void;
151
+ readonly ifcxfile_validate: (a: number, b: number) => void;
152
+ readonly ifcxfile_write: (a: number, b: number, c: number) => void;
153
+ readonly validate: (a: number, b: number, c: number) => void;
154
+ readonly __wbindgen_export: (a: number, b: number) => number;
155
+ readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
156
+ readonly __wbindgen_export3: (a: number) => void;
157
+ readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
158
+ readonly __wbindgen_export4: (a: number, b: number, c: number) => void;
159
+ }
160
+
161
+ export type SyncInitInput = BufferSource | WebAssembly.Module;
162
+
163
+ /**
164
+ * Instantiates the given `module`, which can either be bytes or
165
+ * a precompiled `WebAssembly.Module`.
166
+ *
167
+ * @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
168
+ *
169
+ * @returns {InitOutput}
170
+ */
171
+ export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
172
+
173
+ /**
174
+ * If `module_or_path` is {RequestInfo} or {URL}, makes a request and
175
+ * for everything else, calls `WebAssembly.instantiate` directly.
176
+ *
177
+ * @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
178
+ *
179
+ * @returns {Promise<InitOutput>}
180
+ */
181
+ export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
182
+ export * from "./fetch-imports.js";