@thatopen/services 0.3.5 → 0.3.7

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.
@@ -70,5 +70,9 @@
70
70
  {
71
71
  "path": "src/built-in/UIManager/src/app/examples/Sidebar/example.ts",
72
72
  "description": "Sidebar navigation — Product teams building BIM apps often need multiple distinct views — a 3D model viewer, a data browser, a settings screen — but wiring a custom nav bar and managing view transitions from scratch adds complexity before any real feature work begins."
73
+ },
74
+ {
75
+ "path": "src/built-in/UIManager/src/models-list/example.ts",
76
+ "description": "top-models-list — top-models-list is the project files panel: it lists the project's files, uploads new ones, converts IFC to fragments, and loads `.frag` models into the 3D scene. Its loaders are PLUGGABLE — the panel owns the UI and the orchestration (progress, refresh, association), but the HOST decides how each file format is loaded, so you can teach it new formats without forking the built-in."
73
77
  }
74
78
  ]
package/package.json CHANGED
@@ -15,7 +15,7 @@
15
15
  "access": "public"
16
16
  },
17
17
  "private": false,
18
- "version": "0.3.5",
18
+ "version": "0.3.7",
19
19
  "main": "dist/index.cjs.js",
20
20
  "module": "dist/index.es.js",
21
21
  "types": "dist/index.d.ts",
@@ -39,11 +39,6 @@
39
39
  "build:cli": "vite build --config vite.config.cli.mts",
40
40
  "preview": "vite preview",
41
41
  "test:ui": "vite --config test/vite.config.mts",
42
- "test:cli-build-app": "npm run build:cli && node test/setup-test-app.mjs",
43
- "test:cli-serve-app": "cd temp/test-app && thatopen serve",
44
- "test:cli-build-component": "npm run build:cli && node test/setup-test-component.mjs",
45
- "test:cli-run-component": "cd temp/test-component && thatopen run --skip-build",
46
- "test:cli-serve-component": "cd temp/test-component && thatopen local-server",
47
42
  "template:app": "cd src/cli/templates/app && npm install && cd ../../../.. && node scripts/generate-template-tsconfig.mjs",
48
43
  "template:app:dev": "cd src/cli/templates/app && npm run dev",
49
44
  "changeset": "changeset",
@@ -0,0 +1,120 @@
1
+ /* MD
2
+ ## top-models-list
3
+ ---
4
+ top-models-list is the project files panel: it lists the project's files, uploads new ones, converts IFC to fragments, and loads `.frag` models into the 3D scene. Its loaders are PLUGGABLE — the panel owns the UI and the orchestration (progress, refresh, association), but the HOST decides how each file format is loaded, so you can teach it new formats without forking the built-in.
5
+
6
+ This tutorial covers the two extension points. First, registering a custom loader for a file extension — either imperatively with `registerLoader(ext, fn)` or up front via the `loaders` property; a loader receives `(fileId, ctx)` where `ctx` carries the engine (`components`), the platform `client`, and per-file alignment persistence. A format with no registered loader simply hides its load action. Second, overriding the IFC to fragments `converter` — omit it to use the built-in default (which drives the project's IfcFragmenter cloud component), or provide one to run conversion elsewhere.
7
+
8
+ By the end you'll have a files panel that knows how to load a custom format and how to run a custom IFC conversion, with the panel's UI untouched.
9
+ */
10
+
11
+ import { html } from "lit";
12
+ import * as THREE from "three";
13
+ import * as OBC from "@thatopen/components";
14
+ import * as OBF from "@thatopen/components-front";
15
+ import * as FRAGS from "@thatopen/fragments";
16
+ import * as BUI from "@thatopen/ui";
17
+ import { PlatformClient } from "thatopen-services";
18
+ import uiManagerDef from "../../index";
19
+ import type { App } from "../app/index";
20
+ import type { ModelsList } from "./index";
21
+
22
+ const UIManager = uiManagerDef.componentDefinition;
23
+
24
+ const client = PlatformClient.fromPlatformContext();
25
+
26
+ // UIManager registers every platform web component (top-app, top-viewer,
27
+ // top-models-list, …) before the DOM renders.
28
+ const { components } = (await client.setup(
29
+ { OBC, OBF, BUI, THREE, FRAGS },
30
+ { uuid: UIManager.uuid },
31
+ )) as { components: OBC.Components };
32
+
33
+ components.get(UIManager).init();
34
+
35
+ const app = document.createElement("top-app") as unknown as App;
36
+
37
+ app.setup = (waitUntil) => {
38
+ waitUntil(
39
+ (async () => {
40
+ const fragments = components.get(OBC.FragmentsManager);
41
+ const workerUrl = await FRAGS.FragmentsModels.getWorker();
42
+ fragments.init(await FRAGS.toClassicWorker(workerUrl), {
43
+ classicWorker: true,
44
+ });
45
+ })(),
46
+ "Fragments Core",
47
+ );
48
+ return { components, client };
49
+ };
50
+
51
+ // One stable <top-models-list>, kept by reference so re-rendering top-app reuses
52
+ // it. The loaders are read LIVE, so registration can happen at any time.
53
+ const modelsList = document.createElement("top-models-list") as ModelsList;
54
+
55
+ // ── Extension point 1: a custom loader, keyed by file extension ──────────────
56
+ // A ModelLoader is `(fileId, ctx) => void | Promise<void>`. The panel calls it
57
+ // when the user loads a file whose extension matches. `ctx` gives you everything
58
+ // you need: { name, ext, components, client, getAlignment, setAlignment }. Do
59
+ // whatever it takes to put the file in the scene — fetch it via the client,
60
+ // parse it, and add geometry to a world from `components`.
61
+ modelsList.registerLoader("xyz", async (fileId, ctx) => {
62
+ // const buffer = await ctx.client
63
+ // .downloadFile(fileId)
64
+ // .then((r: Response) => r.arrayBuffer());
65
+ // ...parse `buffer` and add it to the world here...
66
+ // Persist/restore a placement matrix across reloads via the panel's app-data:
67
+ // const saved = ctx.getAlignment(fileId);
68
+ // ctx.setAlignment(fileId, matrix.toArray());
69
+ console.log(`[xyz loader] would load "${ctx.name}" (${fileId})`, ctx.components);
70
+ });
71
+
72
+ // Equivalent declarative form — hand the whole registry over at once:
73
+ // modelsList.loaders = { xyz: async (fileId, ctx) => { ... } };
74
+ //
75
+ // Registering a loader for an extension is also what makes that format's files
76
+ // APPEAR in the list (recognition follows the loaders registry). For a
77
+ // CONVERT-style action — e.g. a PLY that runs a PLY→3D-tiles cloud component
78
+ // then views the result — register a RICH entry so the button reads correctly:
79
+ // modelsList.registerLoader("ply", {
80
+ // label: "Generate 3D tiles",
81
+ // icon: "mdi:cube-scan",
82
+ // fn: async (fileId, ctx) => {
83
+ // // const { executionId } = await ctx.client.executeComponent(...);
84
+ // // ...poll until done, then open the produced tileset...
85
+ // },
86
+ // });
87
+
88
+ // ── Extension point 2: override the IFC → fragments converter ────────────────
89
+ // OMIT this to keep the built-in default (drives the project's IfcFragmenter
90
+ // cloud component via the platform client). Provide it to run conversion
91
+ // elsewhere / differently — the panel keeps the progress bar, reload-survival,
92
+ // association and refresh; your converter just `start`s and is `poll`ed.
93
+ modelsList.converter = {
94
+ async start(fileId) {
95
+ // kick off your conversion; return an execution id to poll
96
+ return `exec-${fileId}`;
97
+ },
98
+ async poll(executionId) {
99
+ // report progress; when done, hand back the produced .frag id
100
+ return { done: true, ok: true, fragId: "demo-frag", message: "Converted" };
101
+ },
102
+ };
103
+
104
+ // Mount the panel into a one-area layout. (In a real app it sits beside a
105
+ // top-viewer so loaded models have a world to render into.)
106
+ app.elements = {
107
+ files: () => html`${modelsList}`,
108
+ };
109
+ app.layouts = {
110
+ main: {
111
+ label: "Files",
112
+ icon: "mdi:folder-multiple-outline",
113
+ template: `"files" 1fr / 1fr`,
114
+ },
115
+ };
116
+ app.layout = "main";
117
+
118
+ const container = document.getElementById("that-open-app") ?? document.body;
119
+ container.appendChild(app);
120
+ document.body.style.margin = "0";
@@ -17,10 +17,12 @@
17
17
  "@thatopen/fragments": "~3.4.0",
18
18
  "@thatopen/services": "file:../../../..",
19
19
  "@thatopen/ui": "~3.4.4",
20
- "three": ">=0.182.0",
20
+ "three": "0.185.0",
21
21
  "@sparkjsdev/spark": "^0.1.10",
22
22
  "3d-tiles-renderer": "^0.4.28",
23
23
  "camera-controls": "^3.1.2",
24
+ "opentype.js": "1.3.4",
25
+ "potree-core": "^2.0.15",
24
26
  "web-ifc": "^0.0.77"
25
27
  },
26
28
  "devDependencies": {
@@ -23,9 +23,21 @@ function getBetaAliases() {
23
23
  }
24
24
  }
25
25
 
26
+ // Some three.js example loaders (e.g. TTFLoader) import their deps from a
27
+ // jsdelivr `/+esm` CDN URL. A bundler can't put a URL import in an IIFE — it
28
+ // degrades to a runtime `require("https://…")` that throws — so rewrite any such
29
+ // URL back to the bare package name and let it resolve from node_modules.
30
+ const CDN_ESM = /^https:\/\/cdn\.jsdelivr\.net\/npm\/((?:@[^/]+\/)?[^@/]+)(?:@[^/]+)?\/\+esm$/;
31
+
26
32
  export default defineConfig({
27
33
  resolve: {
28
- alias: getBetaAliases(),
34
+ alias: [
35
+ { find: CDN_ESM, replacement: '$1' },
36
+ ...Object.entries(getBetaAliases()).map(([find, replacement]) => ({
37
+ find,
38
+ replacement,
39
+ })),
40
+ ],
29
41
  },
30
42
  build: {
31
43
  lib: {