@ifc-lite/viewer 1.27.0 → 1.28.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.
- package/.turbo/turbo-build.log +35 -42
- package/CHANGELOG.md +74 -0
- package/dist/assets/{basketViewActivator-B3CdrLsb.js → basketViewActivator-Ce38DhXd.js} +8 -8
- package/dist/assets/{bcf-QeHK_Aud.js → bcf-Cv_O3JfD.js} +56 -56
- package/dist/assets/{decode-worker-CgM1iNSK.js → decode-worker-Cjign7Zh.js} +1 -1
- package/dist/assets/{deflate-B-d0SYQM.js → deflate-HbyMq59o.js} +1 -1
- package/dist/assets/drawing-2d-DW98umlt.js +257 -0
- package/dist/assets/e57-source-2wI9jkCA.js +1 -0
- package/dist/assets/{exporters-B4LbZFeT.js → exporters-BuD3XRzB.js} +1309 -1153
- package/dist/assets/geometry.worker-TH3fCCoY.js +1 -0
- package/dist/assets/{geotiff-CrVtDRFq.js → geotiff-B2HA8Bwm.js} +10 -10
- package/dist/assets/{ids-DjsGFN10.js → ids-DYUFMd5f.js} +952 -945
- package/dist/assets/{ifc-lite_bg-DsYUIHm3.wasm → ifc-lite_bg-BEA5DLmg.wasm} +0 -0
- package/dist/assets/index-E9wB0zWt.css +1 -0
- package/dist/assets/{index-COYokSKc.js → index-n5O1QJMM.js} +37877 -38126
- package/dist/assets/{index.es-CY202jA3.js → index.es-BKVIpZgL.js} +9 -9
- package/dist/assets/{jpeg-D4wOkf5h.js → jpeg-C7hjKjPX.js} +1 -1
- package/dist/assets/{jspdf.es.min-DIGb9BHN.js → jspdf.es.min-oWlFc42Y.js} +4 -4
- package/dist/assets/lens-C4p1kQ0p.js +1 -0
- package/dist/assets/{lerc-DmW0_tgf.js → lerc-BfIOGhQz.js} +1 -1
- package/dist/assets/{lzw-oWetY-d6.js → lzw-B0jRuuW5.js} +1 -1
- package/dist/assets/{native-bridge-BX8_tHXE.js → native-bridge-DpB-dtEn.js} +6 -3
- package/dist/assets/{packbits-F8Nkp4NY.js → packbits-DVvBTC39.js} +1 -1
- package/dist/assets/parser.worker-BDsWQ6rc.js +182 -0
- package/dist/assets/{pdf-Dsh3HPZB.js → pdf-dVIqI5ac.js} +10 -10
- package/dist/assets/raw-C0ZJYGmN.js +1 -0
- package/dist/assets/{sandbox-BAC3a-eN.js → sandbox-qpJlrNN0.js} +2962 -2554
- package/dist/assets/server-client-DVZ2huNS.js +719 -0
- package/dist/assets/{webimage-BLV1dgmd.js → webimage-B394g0Tw.js} +1 -1
- package/dist/assets/{xlsx-Bc2HTrjC.js → xlsx-D-oHO76J.js} +8 -8
- package/dist/assets/{zstd-C_1HxVrA.js → zstd-Bf38MwV2.js} +1 -1
- package/dist/index.html +9 -9
- package/package.json +24 -23
- package/src/App.tsx +1 -3
- package/src/components/mcp/playground-dispatcher.ts +3 -0
- package/src/components/mcp/playground-files.ts +33 -1
- package/src/components/viewer/BCFPanel.tsx +1 -16
- package/src/components/viewer/ChatPanel.tsx +11 -46
- package/src/components/viewer/CommandPalette.tsx +6 -1
- package/src/components/viewer/ComparePanel.tsx +420 -0
- package/src/components/viewer/HierarchyPanel.tsx +48 -183
- package/src/components/viewer/IDSPanel.tsx +1 -26
- package/src/components/viewer/MainToolbar.tsx +94 -187
- package/src/components/viewer/MobileToolbar.tsx +1 -9
- package/src/components/viewer/PropertiesPanel.tsx +98 -127
- package/src/components/viewer/ScriptPanel.tsx +8 -34
- package/src/components/viewer/Section2DPanel.tsx +32 -1
- package/src/components/viewer/ViewerLayout.tsx +5 -2
- package/src/components/viewer/Viewport.tsx +3 -0
- package/src/components/viewer/ViewportContainer.tsx +24 -42
- package/src/components/viewer/ViewportOverlays.tsx +1 -4
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +3 -3
- package/src/components/viewer/hierarchy/ifc-icons.ts +9 -0
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +87 -0
- package/src/components/viewer/hierarchy/types.ts +1 -0
- package/src/components/viewer/hierarchy/useHierarchyTree.ts +6 -2
- package/src/components/viewer/properties/MaterialTotalsPanel.tsx +283 -0
- package/src/components/viewer/useGeometryStreaming.ts +0 -2
- package/src/hooks/federationLoadGate.test.ts +12 -2
- package/src/hooks/federationLoadGate.ts +9 -2
- package/src/hooks/ingest/federationAlign.ts +488 -0
- package/src/hooks/ingest/viewerModelIngest.ts +3 -212
- package/src/hooks/useCompare.ts +0 -0
- package/src/hooks/useCompareOverlay.ts +119 -0
- package/src/hooks/useDrawingGeneration.ts +234 -14
- package/src/hooks/useIfc.ts +1 -1
- package/src/hooks/useIfcCache.ts +100 -24
- package/src/hooks/useIfcFederation.ts +42 -811
- package/src/hooks/useIfcLoader.ts +349 -1517
- package/src/hooks/useIfcServer.ts +3 -0
- package/src/hooks/useLens.ts +5 -1
- package/src/hooks/useSymbolicAnnotations.ts +70 -38
- package/src/lib/compare/buildFingerprints.ts +173 -0
- package/src/lib/compare/describeChange.ts +0 -0
- package/src/lib/compare/geometricData.test.ts +54 -0
- package/src/lib/compare/geometricData.ts +37 -0
- package/src/lib/compare/overlay.test.ts +99 -0
- package/src/lib/compare/overlay.ts +91 -0
- package/src/lib/geo/cesium-placement.ts +1 -1
- package/src/lib/geo/reproject.ts +4 -1
- package/src/lib/llm/script-edit-ops.ts +23 -0
- package/src/lib/llm/stream-client.ts +8 -1
- package/src/lib/search/result-export.ts +7 -1
- package/src/sdk/adapters/export-adapter.ts +6 -1
- package/src/services/cacheService.ts +9 -25
- package/src/services/desktop-export.ts +2 -59
- package/src/services/file-dialog.ts +8 -142
- package/src/store/constants.ts +23 -0
- package/src/store/globalId.ts +15 -13
- package/src/store/index.ts +19 -6
- package/src/store/slices/cesiumSlice.ts +8 -1
- package/src/store/slices/compareSlice.ts +96 -0
- package/src/store/slices/drawing2DSlice.ts +8 -0
- package/src/store/slices/lensSlice.ts +8 -0
- package/src/store/slices/visibilitySlice.ts +22 -1
- package/src/store/types.ts +1 -71
- package/src/utils/acquireFileBuffer.test.ts +12 -4
- package/src/utils/ifcConfig.ts +0 -12
- package/src/utils/loadingUtils.ts +32 -0
- package/src/utils/spatialHierarchy.test.ts +53 -1
- package/src/utils/spatialHierarchy.ts +42 -2
- package/src/vite-env.d.ts +2 -0
- package/vite.config.ts +6 -3
- package/DESKTOP_CONTRACT_VERSION +0 -1
- package/dist/assets/drawing-2d-C71b8Ugx.js +0 -257
- package/dist/assets/e57-source-CQHxE8n3.js +0 -1
- package/dist/assets/event-B0kAzHa-.js +0 -1
- package/dist/assets/geometry.worker-BdH-E6NB.js +0 -1
- package/dist/assets/index-ajK6D32J.css +0 -1
- package/dist/assets/lens-PYsLu_MA.js +0 -1
- package/dist/assets/parser.worker-D591Zu_-.js +0 -182
- package/dist/assets/raw-D9iw0tmc.js +0 -1
- package/dist/assets/server-client-Cjwnm7il.js +0 -706
- package/dist/assets/tauri-core-stub-D8Fa-u43.js +0 -1
- package/dist/assets/tauri-dialog-stub-r7Wksg7o.js +0 -1
- package/dist/assets/tauri-fs-stub-BdeRC7aK.js +0 -1
- package/src/components/viewer/DesktopEntitlementBanner.tsx +0 -74
- package/src/components/viewer/SettingsPage.tsx +0 -581
- package/src/hooks/ingest/resolveDataStoreOrAbort.test.ts +0 -61
- package/src/hooks/ingest/resolveDataStoreOrAbort.ts +0 -28
- package/src/hooks/ingest/watchedGeometryStream.test.ts +0 -78
- package/src/hooks/ingest/watchedGeometryStream.ts +0 -76
- package/src/lib/desktop/desktopEntitlementEvents.ts +0 -39
- package/src/lib/desktop-entitlement.ts +0 -43
- package/src/lib/desktop-product.ts +0 -130
- package/src/lib/platform.ts +0 -23
- package/src/services/desktop-cache.ts +0 -186
- package/src/services/desktop-harness.ts +0 -196
- package/src/services/desktop-logger.ts +0 -20
- package/src/services/desktop-native-metadata.ts +0 -230
- package/src/services/desktop-panel-actions.ts +0 -43
- package/src/services/desktop-preferences.ts +0 -44
- package/src/services/fs-cache.ts +0 -212
- package/src/services/tauri-core-stub.ts +0 -7
- package/src/services/tauri-dialog-stub.ts +0 -7
- package/src/services/tauri-fs-stub.ts +0 -7
- package/src/services/tauri-modules.d.ts +0 -50
- package/src/store/slices/desktopEntitlementSlice.ts +0 -86
- package/src/utils/desktopModelSnapshot.ts +0 -358
- package/src/utils/nativeSpatialDataStore.ts +0 -277
- package/src-tauri/Cargo.toml +0 -29
- package/src-tauri/build.rs +0 -7
- package/src-tauri/capabilities/default.json +0 -18
- package/src-tauri/icons/128x128.png +0 -0
- package/src-tauri/icons/128x128@2x.png +0 -0
- package/src-tauri/icons/32x32.png +0 -0
- package/src-tauri/icons/Square107x107Logo.png +0 -0
- package/src-tauri/icons/Square142x142Logo.png +0 -0
- package/src-tauri/icons/Square150x150Logo.png +0 -0
- package/src-tauri/icons/Square284x284Logo.png +0 -0
- package/src-tauri/icons/Square30x30Logo.png +0 -0
- package/src-tauri/icons/Square310x310Logo.png +0 -0
- package/src-tauri/icons/Square44x44Logo.png +0 -0
- package/src-tauri/icons/Square71x71Logo.png +0 -0
- package/src-tauri/icons/Square89x89Logo.png +0 -0
- package/src-tauri/icons/StoreLogo.png +0 -0
- package/src-tauri/icons/icon.icns +0 -0
- package/src-tauri/icons/icon.ico +0 -0
- package/src-tauri/icons/icon.png +0 -0
- package/src-tauri/src/lib.rs +0 -21
- package/src-tauri/src/main.rs +0 -10
- package/src-tauri/tauri.conf.json +0 -39
|
@@ -1,61 +0,0 @@
|
|
|
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
|
-
import { describe, it } from 'node:test';
|
|
6
|
-
import assert from 'node:assert';
|
|
7
|
-
|
|
8
|
-
import { resolveDataStoreOrAbort } from './resolveDataStoreOrAbort.js';
|
|
9
|
-
|
|
10
|
-
const isAbortError = (err: unknown): boolean =>
|
|
11
|
-
err instanceof DOMException && err.name === 'AbortError';
|
|
12
|
-
|
|
13
|
-
describe('resolveDataStoreOrAbort', () => {
|
|
14
|
-
it('returns the parse result when not aborted', async () => {
|
|
15
|
-
const store = { id: 'store' };
|
|
16
|
-
const result = await resolveDataStoreOrAbort(Promise.resolve(store), { aborted: false });
|
|
17
|
-
assert.equal(result, store);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('throws AbortError and terminates without awaiting a blocked parse', async () => {
|
|
21
|
-
let terminated = false;
|
|
22
|
-
// A promise that never settles — mirrors a worker parse blocked on
|
|
23
|
-
// waitForEntityIndex after the geometry loop was cancelled. The previous
|
|
24
|
-
// code awaited this directly and hung forever.
|
|
25
|
-
const neverSettles = new Promise<unknown>(() => {});
|
|
26
|
-
|
|
27
|
-
await assert.rejects(
|
|
28
|
-
resolveDataStoreOrAbort(neverSettles, {
|
|
29
|
-
aborted: true,
|
|
30
|
-
terminate: () => {
|
|
31
|
-
terminated = true;
|
|
32
|
-
},
|
|
33
|
-
}),
|
|
34
|
-
isAbortError,
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
assert.equal(terminated, true, 'the worker parser should be terminated on abort');
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('swallows the abandoned parse rejection on abort', async () => {
|
|
41
|
-
// A parse that rejects after we bail must not surface as an unhandled
|
|
42
|
-
// rejection (this test would fail the process if the .catch guard were
|
|
43
|
-
// removed from resolveDataStoreOrAbort).
|
|
44
|
-
const rejecting = Promise.reject(new Error('worker died after abort'));
|
|
45
|
-
|
|
46
|
-
await assert.rejects(
|
|
47
|
-
resolveDataStoreOrAbort(rejecting, { aborted: true }),
|
|
48
|
-
isAbortError,
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
// Give the swallowed rejection a tick to settle.
|
|
52
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('works without a terminate callback', async () => {
|
|
56
|
-
await assert.rejects(
|
|
57
|
-
resolveDataStoreOrAbort(new Promise<unknown>(() => {}), { aborted: true }),
|
|
58
|
-
isAbortError,
|
|
59
|
-
);
|
|
60
|
-
});
|
|
61
|
-
});
|
|
@@ -1,28 +0,0 @@
|
|
|
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
|
-
/**
|
|
6
|
-
* Resolve a parse promise, unless the load was cancelled.
|
|
7
|
-
*
|
|
8
|
-
* A worker parse started with `waitForEntityIndex` blocks until the streaming
|
|
9
|
-
* geometry pre-pass hands over the entity index. If the geometry loop is
|
|
10
|
-
* cancelled before that handoff, the index never arrives and the parse promise
|
|
11
|
-
* never settles — awaiting it would hang the whole ingest. On abort we instead
|
|
12
|
-
* terminate the worker, abandon (and swallow) the parse promise, and throw an
|
|
13
|
-
* `AbortError` so callers treat it as a clean cancellation (matching the
|
|
14
|
-
* federated loader's `err.name === 'AbortError'` convention).
|
|
15
|
-
*/
|
|
16
|
-
export async function resolveDataStoreOrAbort<T>(
|
|
17
|
-
parsePromise: Promise<T>,
|
|
18
|
-
opts: { aborted: boolean; terminate?: () => void },
|
|
19
|
-
): Promise<T> {
|
|
20
|
-
if (opts.aborted) {
|
|
21
|
-
opts.terminate?.();
|
|
22
|
-
// Swallow the abandoned parse's eventual rejection so it doesn't surface
|
|
23
|
-
// as an unhandled rejection after we've already bailed out.
|
|
24
|
-
void parsePromise.catch(() => {});
|
|
25
|
-
throw new DOMException('Model load aborted', 'AbortError');
|
|
26
|
-
}
|
|
27
|
-
return parsePromise;
|
|
28
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
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
|
-
import { describe, it } from 'node:test';
|
|
6
|
-
import assert from 'node:assert';
|
|
7
|
-
|
|
8
|
-
import { watchedGeometryStream } from './watchedGeometryStream.js';
|
|
9
|
-
|
|
10
|
-
/** Build a controllable async source that records when return() is called. */
|
|
11
|
-
function makeSource<T>(values: T[]): { source: AsyncIterable<T>; returned: () => boolean } {
|
|
12
|
-
let didReturn = false;
|
|
13
|
-
const source: AsyncIterable<T> = {
|
|
14
|
-
[Symbol.asyncIterator]() {
|
|
15
|
-
let i = 0;
|
|
16
|
-
return {
|
|
17
|
-
next: async () => (i < values.length
|
|
18
|
-
? { done: false, value: values[i++] }
|
|
19
|
-
: { done: true, value: undefined as unknown as T }),
|
|
20
|
-
return: async () => {
|
|
21
|
-
didReturn = true;
|
|
22
|
-
return { done: true, value: undefined as unknown as T };
|
|
23
|
-
},
|
|
24
|
-
};
|
|
25
|
-
},
|
|
26
|
-
};
|
|
27
|
-
return { source, returned: () => didReturn };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const baseOpts = {
|
|
31
|
-
fileName: 'test.ifc',
|
|
32
|
-
fileSizeMB: 1,
|
|
33
|
-
getBatchCount: () => 0,
|
|
34
|
-
getLastTotalMeshes: () => 0,
|
|
35
|
-
cleanupMs: 50,
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
describe('watchedGeometryStream', () => {
|
|
39
|
-
it('re-yields every event in order then completes', async () => {
|
|
40
|
-
const { source } = makeSource([1, 2, 3]);
|
|
41
|
-
const seen: number[] = [];
|
|
42
|
-
for await (const v of watchedGeometryStream(source, baseOpts)) seen.push(v);
|
|
43
|
-
assert.deepStrictEqual(seen, [1, 2, 3]);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('stops early when shouldAbort() turns true', async () => {
|
|
47
|
-
const { source } = makeSource([1, 2, 3, 4]);
|
|
48
|
-
const seen: number[] = [];
|
|
49
|
-
let calls = 0;
|
|
50
|
-
for await (const v of watchedGeometryStream(source, {
|
|
51
|
-
...baseOpts,
|
|
52
|
-
// Abort after the second event has been consumed.
|
|
53
|
-
shouldAbort: () => (++calls > 2),
|
|
54
|
-
})) {
|
|
55
|
-
seen.push(v);
|
|
56
|
-
}
|
|
57
|
-
assert.deepStrictEqual(seen, [1, 2]);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('tears down the underlying iterator on normal completion', async () => {
|
|
61
|
-
const { source, returned } = makeSource([1]);
|
|
62
|
-
let count = 0;
|
|
63
|
-
for await (const v of watchedGeometryStream(source, baseOpts)) count += v;
|
|
64
|
-
assert.strictEqual(count, 1);
|
|
65
|
-
assert.strictEqual(returned(), true);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('tears down the underlying iterator when the consumer breaks early', async () => {
|
|
69
|
-
const { source, returned } = makeSource([1, 2, 3]);
|
|
70
|
-
const seen: number[] = [];
|
|
71
|
-
for await (const v of watchedGeometryStream(source, baseOpts)) {
|
|
72
|
-
seen.push(v);
|
|
73
|
-
break;
|
|
74
|
-
}
|
|
75
|
-
assert.deepStrictEqual(seen, [1]);
|
|
76
|
-
assert.strictEqual(returned(), true);
|
|
77
|
-
});
|
|
78
|
-
});
|
|
@@ -1,76 +0,0 @@
|
|
|
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
|
-
import { getGeometryStreamWatchdogMs } from '@ifc-lite/geometry';
|
|
6
|
-
import { boundedIteratorReturn } from './streamCleanup.js';
|
|
7
|
-
|
|
8
|
-
export interface WatchedGeometryStreamOptions {
|
|
9
|
-
/** File name, for the stall error message. */
|
|
10
|
-
fileName: string;
|
|
11
|
-
/** File size in MB, feeds the size-aware watchdog deadline. */
|
|
12
|
-
fileSizeMB: number;
|
|
13
|
-
/** Abort the stream cooperatively (e.g. user cancelled the load). */
|
|
14
|
-
shouldAbort?: () => boolean;
|
|
15
|
-
/** Current batch index — feeds the size-aware watchdog deadline. */
|
|
16
|
-
getBatchCount: () => number;
|
|
17
|
-
/** Meshes rendered so far, for the stall error message. */
|
|
18
|
-
getLastTotalMeshes: () => number;
|
|
19
|
-
/** Override the abandon-cleanup deadline (mostly for tests). */
|
|
20
|
-
cleanupMs?: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Drive a geometry stream under a size-aware watchdog, re-yielding every event.
|
|
25
|
-
*
|
|
26
|
-
* The parallel pipeline only ends once EVERY spawned geometry worker reports
|
|
27
|
-
* `complete`; if the browser fails to instantiate a worker (the "Attempting to
|
|
28
|
-
* create a Worker from an empty source" warning) that worker never reports
|
|
29
|
-
* `ready`/`complete` and never fires `onerror`, so the underlying generator can
|
|
30
|
-
* wedge forever, stranding the load on "Processing geometry (N meshes)". Racing
|
|
31
|
-
* each `next()` against a deadline converts that silent wedge into a thrown,
|
|
32
|
-
* recoverable error. On ANY exit — normal completion, abort, consumer `break`,
|
|
33
|
-
* or a watchdog throw — the `finally` bounds the underlying iterator's shutdown
|
|
34
|
-
* so cleanup (the generator's own `finally`: freeing WASM handles, tearing down
|
|
35
|
-
* workers) runs without re-blocking on the very stall the watchdog just escaped.
|
|
36
|
-
*
|
|
37
|
-
* Generic over the event type so the consumer keeps full type-narrowing in its
|
|
38
|
-
* own `switch`.
|
|
39
|
-
*/
|
|
40
|
-
export async function* watchedGeometryStream<T>(
|
|
41
|
-
source: AsyncIterable<T>,
|
|
42
|
-
options: WatchedGeometryStreamOptions,
|
|
43
|
-
): AsyncGenerator<T> {
|
|
44
|
-
const iterator = source[Symbol.asyncIterator]();
|
|
45
|
-
try {
|
|
46
|
-
while (true) {
|
|
47
|
-
const watchdogMs = getGeometryStreamWatchdogMs({
|
|
48
|
-
desktopStableWasm: false,
|
|
49
|
-
batchCount: options.getBatchCount(),
|
|
50
|
-
fileSizeMB: options.fileSizeMB,
|
|
51
|
-
});
|
|
52
|
-
let watchdogId: ReturnType<typeof setTimeout> | null = null;
|
|
53
|
-
let result: IteratorResult<T>;
|
|
54
|
-
try {
|
|
55
|
-
result = await Promise.race([
|
|
56
|
-
iterator.next(),
|
|
57
|
-
new Promise<never>((_, reject) => {
|
|
58
|
-
watchdogId = setTimeout(() => {
|
|
59
|
-
reject(new Error(
|
|
60
|
-
`Geometry stream stalled after ${watchdogMs}ms while loading ${options.fileName}. `
|
|
61
|
-
+ `Last rendered meshes: ${options.getLastTotalMeshes()}. A geometry worker likely failed to start.`,
|
|
62
|
-
));
|
|
63
|
-
}, watchdogMs);
|
|
64
|
-
}),
|
|
65
|
-
]);
|
|
66
|
-
} finally {
|
|
67
|
-
if (watchdogId !== null) clearTimeout(watchdogId);
|
|
68
|
-
}
|
|
69
|
-
if (result.done) return;
|
|
70
|
-
if (options.shouldAbort?.()) return;
|
|
71
|
-
yield result.value;
|
|
72
|
-
}
|
|
73
|
-
} finally {
|
|
74
|
-
await boundedIteratorReturn(iterator, options.cleanupMs);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
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
|
-
export const DESKTOP_ENTITLEMENT_REFRESH_EVENT = 'ifc-lite:desktop-entitlement-refresh';
|
|
6
|
-
|
|
7
|
-
interface DesktopEntitlementRefreshDetail {
|
|
8
|
-
resolve: () => void;
|
|
9
|
-
reject: (error?: unknown) => void;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const ENTITLEMENT_REFRESH_TIMEOUT_MS = 10_000;
|
|
13
|
-
|
|
14
|
-
export function requestDesktopEntitlementRefresh(): Promise<void> {
|
|
15
|
-
return new Promise((resolve, reject) => {
|
|
16
|
-
const timeoutId = globalThis.setTimeout(() => {
|
|
17
|
-
reject(new Error(
|
|
18
|
-
`Desktop entitlement refresh timed out after ${ENTITLEMENT_REFRESH_TIMEOUT_MS}ms — ` +
|
|
19
|
-
'no listener responded to the refresh event.',
|
|
20
|
-
));
|
|
21
|
-
}, ENTITLEMENT_REFRESH_TIMEOUT_MS);
|
|
22
|
-
|
|
23
|
-
window.dispatchEvent(new CustomEvent<DesktopEntitlementRefreshDetail>(
|
|
24
|
-
DESKTOP_ENTITLEMENT_REFRESH_EVENT,
|
|
25
|
-
{
|
|
26
|
-
detail: {
|
|
27
|
-
resolve: () => {
|
|
28
|
-
globalThis.clearTimeout(timeoutId);
|
|
29
|
-
resolve();
|
|
30
|
-
},
|
|
31
|
-
reject: (error?: unknown) => {
|
|
32
|
-
globalThis.clearTimeout(timeoutId);
|
|
33
|
-
reject(error);
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
));
|
|
38
|
-
});
|
|
39
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
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
|
-
import {
|
|
6
|
-
getDefaultDesktopEntitlement,
|
|
7
|
-
hasDesktopFeatureAccess,
|
|
8
|
-
type DesktopEntitlement,
|
|
9
|
-
type DesktopFeature,
|
|
10
|
-
} from './desktop-product';
|
|
11
|
-
|
|
12
|
-
type HasFn = ((params: { plan?: string; feature?: string }) => boolean) | undefined;
|
|
13
|
-
|
|
14
|
-
interface ResolveDesktopEntitlementOptions {
|
|
15
|
-
userId: string | null;
|
|
16
|
-
token: string | null;
|
|
17
|
-
has: HasFn;
|
|
18
|
-
publicMetadata: Record<string, unknown> | null | undefined;
|
|
19
|
-
now?: number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface ResolvedDesktopEntitlement {
|
|
23
|
-
entitlement: DesktopEntitlement;
|
|
24
|
-
aiAssistantEnabled: boolean;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function resolveDesktopEntitlement(options: ResolveDesktopEntitlementOptions): ResolvedDesktopEntitlement {
|
|
28
|
-
// NOTE: `token`, `has`, `publicMetadata`, and `now` are accepted but
|
|
29
|
-
// intentionally unused in this implementation. They are kept for future
|
|
30
|
-
// extensibility if a desktop-specific entitlement provider is added.
|
|
31
|
-
const { userId } = options;
|
|
32
|
-
const entitlement: DesktopEntitlement = {
|
|
33
|
-
...getDefaultDesktopEntitlement(),
|
|
34
|
-
userId,
|
|
35
|
-
};
|
|
36
|
-
const aiAssistantEnabled = hasDesktopFeatureAccess(entitlement, 'ai_assistant');
|
|
37
|
-
return { entitlement, aiAssistantEnabled };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function canUseDesktopFeatureOffline(entitlement: DesktopEntitlement, feature: DesktopFeature, now = Date.now()): boolean {
|
|
41
|
-
void now;
|
|
42
|
-
return hasDesktopFeatureAccess(entitlement, feature);
|
|
43
|
-
}
|
|
@@ -1,130 +0,0 @@
|
|
|
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
|
-
export type DesktopPlanTier = 'free' | 'pro';
|
|
6
|
-
export type DesktopEntitlementStatus = 'anonymous' | 'signed_out' | 'active' | 'trial' | 'expired' | 'grace_offline';
|
|
7
|
-
export type DesktopEntitlementSource = 'anonymous' | 'clerk_claims' | 'cached';
|
|
8
|
-
|
|
9
|
-
export type DesktopFeature =
|
|
10
|
-
| 'viewer_basic'
|
|
11
|
-
| 'workspace_restore'
|
|
12
|
-
| 'exports'
|
|
13
|
-
| 'ids_validation'
|
|
14
|
-
| 'bcf_issue_management'
|
|
15
|
-
| 'ai_assistant'
|
|
16
|
-
| 'extensions';
|
|
17
|
-
|
|
18
|
-
export interface DesktopEntitlement {
|
|
19
|
-
tier: DesktopPlanTier;
|
|
20
|
-
status: DesktopEntitlementStatus;
|
|
21
|
-
source: DesktopEntitlementSource;
|
|
22
|
-
userId: string | null;
|
|
23
|
-
validatedAt: number | null;
|
|
24
|
-
graceUntil: number | null;
|
|
25
|
-
trialEndsAt: number | null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface DesktopUsageSummary {
|
|
29
|
-
type: 'credits' | 'requests';
|
|
30
|
-
used: number;
|
|
31
|
-
limit: number;
|
|
32
|
-
pct?: number;
|
|
33
|
-
resetAt?: number;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface DesktopFeatureDefinition {
|
|
37
|
-
label: string;
|
|
38
|
-
description: string;
|
|
39
|
-
free: boolean;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const DESKTOP_FEATURES: Record<DesktopFeature, DesktopFeatureDefinition> = {
|
|
43
|
-
viewer_basic: {
|
|
44
|
-
label: 'Viewer features',
|
|
45
|
-
description: 'Core model viewing and inspection capabilities.',
|
|
46
|
-
free: true,
|
|
47
|
-
},
|
|
48
|
-
workspace_restore: {
|
|
49
|
-
label: 'Workspace restore',
|
|
50
|
-
description: 'Host-provided workspace persistence features.',
|
|
51
|
-
free: true,
|
|
52
|
-
},
|
|
53
|
-
exports: {
|
|
54
|
-
label: 'Exports',
|
|
55
|
-
description: 'Host-provided export integrations.',
|
|
56
|
-
free: true,
|
|
57
|
-
},
|
|
58
|
-
ids_validation: {
|
|
59
|
-
label: 'IDS validation',
|
|
60
|
-
description: 'Host-provided IDS workflows.',
|
|
61
|
-
free: true,
|
|
62
|
-
},
|
|
63
|
-
bcf_issue_management: {
|
|
64
|
-
label: 'BCF issue management',
|
|
65
|
-
description: 'Host-provided BCF workflows.',
|
|
66
|
-
free: true,
|
|
67
|
-
},
|
|
68
|
-
ai_assistant: {
|
|
69
|
-
label: 'Host AI assistant',
|
|
70
|
-
description: 'Optional host-provided AI integrations.',
|
|
71
|
-
free: true,
|
|
72
|
-
},
|
|
73
|
-
extensions: {
|
|
74
|
-
label: 'Extensions & flavors',
|
|
75
|
-
description: 'Install user-authored extensions, manage flavors, and access the authoring loop.',
|
|
76
|
-
free: true,
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
export function isDesktopBillingEnforced(): boolean {
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function getDesktopPlanTier(entitlementOrHasPro: DesktopEntitlement | boolean): DesktopPlanTier {
|
|
85
|
-
return typeof entitlementOrHasPro === 'boolean'
|
|
86
|
-
? (entitlementOrHasPro ? 'pro' : 'free')
|
|
87
|
-
: entitlementOrHasPro.tier;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function hasDesktopPro(entitlementOrHasPro: DesktopEntitlement | boolean): boolean {
|
|
91
|
-
return getDesktopPlanTier(entitlementOrHasPro) === 'pro';
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function hasDesktopFeatureAccess(entitlementOrHasPro: DesktopEntitlement | boolean, feature: DesktopFeature): boolean {
|
|
95
|
-
return hasDesktopPro(entitlementOrHasPro) || DESKTOP_FEATURES[feature].free;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function getDesktopFeatureCatalog(entitlementOrHasPro: DesktopEntitlement | boolean) {
|
|
99
|
-
return (Object.entries(DESKTOP_FEATURES) as Array<[DesktopFeature, DesktopFeatureDefinition]>).map(([key, value]) => ({
|
|
100
|
-
key,
|
|
101
|
-
...value,
|
|
102
|
-
enabled: hasDesktopFeatureAccess(entitlementOrHasPro, key),
|
|
103
|
-
}));
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function buildDesktopUpgradeUrl(returnTo?: string): string {
|
|
107
|
-
const fallbackReturnTo = typeof window !== 'undefined'
|
|
108
|
-
? `${window.location.pathname}${window.location.search}`
|
|
109
|
-
: '/';
|
|
110
|
-
const nextReturnTo = returnTo ?? fallbackReturnTo;
|
|
111
|
-
return `/upgrade?returnTo=${encodeURIComponent(nextReturnTo)}`;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function getDefaultDesktopEntitlement(): DesktopEntitlement {
|
|
115
|
-
return {
|
|
116
|
-
tier: 'free',
|
|
117
|
-
status: 'anonymous',
|
|
118
|
-
source: 'anonymous',
|
|
119
|
-
userId: null,
|
|
120
|
-
validatedAt: null,
|
|
121
|
-
graceUntil: null,
|
|
122
|
-
trialEndsAt: null,
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export function getDesktopPlanSummary(entitlementOrHasPro: DesktopEntitlement | boolean, usage: DesktopUsageSummary | null): string {
|
|
127
|
-
void entitlementOrHasPro;
|
|
128
|
-
void usage;
|
|
129
|
-
return 'Desktop entitlements are host-defined and disabled in the open-source web viewer build.';
|
|
130
|
-
}
|
package/src/lib/platform.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
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
|
-
/**
|
|
6
|
-
* Platform detection utilities for runtime bridge pattern
|
|
7
|
-
* Routes to WASM (browser) or native (Tauri desktop) implementations
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Detects if running in Tauri desktop environment
|
|
12
|
-
*/
|
|
13
|
-
export function isTauri(): boolean {
|
|
14
|
-
return typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Platform-specific cache implementation
|
|
19
|
-
* Returns 'indexeddb' for browser, 'filesystem' for desktop
|
|
20
|
-
*/
|
|
21
|
-
export function getCacheType(): 'indexeddb' | 'filesystem' {
|
|
22
|
-
return isTauri() ? 'filesystem' : 'indexeddb';
|
|
23
|
-
}
|
|
@@ -1,186 +0,0 @@
|
|
|
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
|
-
/**
|
|
6
|
-
* Desktop Cache Service
|
|
7
|
-
*
|
|
8
|
-
* File system-based caching for Tauri desktop apps.
|
|
9
|
-
* Replaces IndexedDB caching used in the web version.
|
|
10
|
-
*
|
|
11
|
-
* This module uses dynamic imports to avoid bundler issues in web builds.
|
|
12
|
-
* The @tauri-apps/api package is only loaded at runtime in Tauri environments.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
// Tauri API types - dynamically imported to avoid issues in web builds
|
|
16
|
-
type InvokeFn = <T>(cmd: string, args?: Record<string, unknown>) => Promise<T>;
|
|
17
|
-
|
|
18
|
-
let invoke: InvokeFn | null = null;
|
|
19
|
-
|
|
20
|
-
async function getInvoke(): Promise<InvokeFn> {
|
|
21
|
-
if (invoke) return invoke;
|
|
22
|
-
// Use globalThis.__TAURI_INTERNALS__ which is set by Tauri runtime
|
|
23
|
-
// This avoids bundler trying to resolve @tauri-apps/api in web builds
|
|
24
|
-
const win = globalThis as unknown as { __TAURI_INTERNALS__?: { invoke: InvokeFn } };
|
|
25
|
-
if (win.__TAURI_INTERNALS__?.invoke) {
|
|
26
|
-
invoke = win.__TAURI_INTERNALS__.invoke;
|
|
27
|
-
return invoke;
|
|
28
|
-
}
|
|
29
|
-
throw new Error('Tauri API not available - this module should only be used in Tauri apps');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface CacheEntry {
|
|
33
|
-
key: string;
|
|
34
|
-
fileName: string;
|
|
35
|
-
fileSize: number;
|
|
36
|
-
cacheSize: number;
|
|
37
|
-
createdAt: number;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface CacheStats {
|
|
41
|
-
entries: CacheEntry[];
|
|
42
|
-
totalSize: number;
|
|
43
|
-
entryCount: number;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface CacheResult {
|
|
47
|
-
buffer: ArrayBuffer;
|
|
48
|
-
sourceBuffer?: ArrayBuffer;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Get cached data by key
|
|
53
|
-
* @param key Cache key (typically file hash)
|
|
54
|
-
* @returns Cached data as CacheResult, or null if not found
|
|
55
|
-
*/
|
|
56
|
-
export async function getCached(key: string): Promise<CacheResult | null> {
|
|
57
|
-
try {
|
|
58
|
-
const inv = await getInvoke();
|
|
59
|
-
const data = await inv<number[] | null>('get_cached', { cacheKey: key });
|
|
60
|
-
if (data) {
|
|
61
|
-
console.log(`[DesktopCache] Cache HIT for key: ${key} (${data.length} bytes)`);
|
|
62
|
-
return { buffer: new Uint8Array(data).buffer };
|
|
63
|
-
}
|
|
64
|
-
console.log(`[DesktopCache] Cache MISS for key: ${key}`);
|
|
65
|
-
return null;
|
|
66
|
-
} catch (error) {
|
|
67
|
-
console.warn('[DesktopCache] Failed to get cached data:', error);
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Save data to cache
|
|
74
|
-
* @param key Cache key (typically file hash)
|
|
75
|
-
* @param buffer Data to cache
|
|
76
|
-
* @param fileName Original file name (for metadata)
|
|
77
|
-
* @param fileSize Original file size (for metadata)
|
|
78
|
-
* @param _sourceBuffer Source buffer (not used in desktop cache, but kept for API compatibility)
|
|
79
|
-
*/
|
|
80
|
-
export async function setCached(
|
|
81
|
-
key: string,
|
|
82
|
-
buffer: ArrayBuffer,
|
|
83
|
-
fileName: string,
|
|
84
|
-
fileSize: number,
|
|
85
|
-
_sourceBuffer?: ArrayBuffer
|
|
86
|
-
): Promise<void> {
|
|
87
|
-
try {
|
|
88
|
-
const inv = await getInvoke();
|
|
89
|
-
const data = Array.from(new Uint8Array(buffer));
|
|
90
|
-
console.log(`[DesktopCache] Caching ${fileName} (${data.length} bytes) with key: ${key}`);
|
|
91
|
-
await inv('set_cached', { cacheKey: key, data });
|
|
92
|
-
console.log(`[DesktopCache] Successfully cached ${fileName}`);
|
|
93
|
-
} catch (error) {
|
|
94
|
-
console.warn('[DesktopCache] Failed to save to cache:', error);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Clear all cached data
|
|
100
|
-
*/
|
|
101
|
-
export async function clearCache(): Promise<void> {
|
|
102
|
-
try {
|
|
103
|
-
const inv = await getInvoke();
|
|
104
|
-
await inv('clear_cache');
|
|
105
|
-
console.log('[DesktopCache] Cache cleared');
|
|
106
|
-
} catch (error) {
|
|
107
|
-
console.warn('[DesktopCache] Failed to clear cache:', error);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Get cache statistics
|
|
113
|
-
*/
|
|
114
|
-
export async function getCacheStats(): Promise<CacheStats> {
|
|
115
|
-
try {
|
|
116
|
-
const inv = await getInvoke();
|
|
117
|
-
return await inv<CacheStats>('get_cache_stats');
|
|
118
|
-
} catch (error) {
|
|
119
|
-
console.warn('[DesktopCache] Failed to get cache stats:', error);
|
|
120
|
-
return { entries: [], totalSize: 0, entryCount: 0 };
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Delete a cache entry
|
|
126
|
-
*/
|
|
127
|
-
export async function deleteCached(key: string): Promise<void> {
|
|
128
|
-
try {
|
|
129
|
-
const inv = await getInvoke();
|
|
130
|
-
await inv('delete_cache_entry', { cacheKey: key });
|
|
131
|
-
console.log('[DesktopCache] Cache entry deleted:', key);
|
|
132
|
-
} catch (error) {
|
|
133
|
-
console.warn('[DesktopCache] Failed to delete cache entry:', error);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Check if a key exists in cache
|
|
139
|
-
*/
|
|
140
|
-
export async function hasCached(key: string): Promise<boolean> {
|
|
141
|
-
const data = await getCached(key);
|
|
142
|
-
return data !== null;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export async function hasNativeGeometryCache(key: string): Promise<boolean> {
|
|
146
|
-
try {
|
|
147
|
-
const inv = await getInvoke();
|
|
148
|
-
return await inv<boolean>('has_native_geometry_cache', { cacheKey: key });
|
|
149
|
-
} catch (error) {
|
|
150
|
-
console.warn('[DesktopCache] Failed to check native geometry cache:', error);
|
|
151
|
-
return false;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export async function getNativeModelSnapshot(key: string): Promise<ArrayBuffer | null> {
|
|
156
|
-
try {
|
|
157
|
-
const inv = await getInvoke();
|
|
158
|
-
const data = await inv<number[] | null>('get_native_model_snapshot', { cacheKey: key });
|
|
159
|
-
return data ? new Uint8Array(data).buffer : null;
|
|
160
|
-
} catch (error) {
|
|
161
|
-
console.warn('[DesktopCache] Failed to read native model snapshot:', error);
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export async function setNativeModelSnapshot(key: string, buffer: ArrayBuffer): Promise<void> {
|
|
167
|
-
try {
|
|
168
|
-
const inv = await getInvoke();
|
|
169
|
-
await inv('set_native_model_snapshot', {
|
|
170
|
-
cacheKey: key,
|
|
171
|
-
data: Array.from(new Uint8Array(buffer)),
|
|
172
|
-
});
|
|
173
|
-
} catch (error) {
|
|
174
|
-
console.warn('[DesktopCache] Failed to write native model snapshot:', error);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export async function hasNativeModelSnapshot(key: string): Promise<boolean> {
|
|
179
|
-
try {
|
|
180
|
-
const inv = await getInvoke();
|
|
181
|
-
return await inv<boolean>('has_native_model_snapshot', { cacheKey: key });
|
|
182
|
-
} catch (error) {
|
|
183
|
-
console.warn('[DesktopCache] Failed to check native model snapshot:', error);
|
|
184
|
-
return false;
|
|
185
|
-
}
|
|
186
|
-
}
|