@thatopen/services 0.19.0 → 0.20.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.
@@ -297,8 +297,11 @@ system: the platform relays messages between app tabs and external tools.
297
297
 
298
298
  To command the app from outside (a script, an MCP server, your own tool):
299
299
 
300
- 1. The user creates a **platform token** with the **API** permission
301
- (dashboard → Data → API Tokens). Same secrecy rules as always.
300
+ 1. The user creates a **platform token** (dashboard → Data → API Tokens). Same
301
+ secrecy rules as always. The token itself needs no special permission: what
302
+ the channel checks is the token owner's role in the project, which must allow
303
+ **API calls**. Project Admin and Member have it by default; a custom role
304
+ needs it ticked.
302
305
  2. Connect with `socket.io-client` and subscribe, then publish:
303
306
 
304
307
  ```bash
@@ -35,6 +35,10 @@
35
35
  "path": "src/core/examples/hidden-files.ts",
36
36
  "description": "Hidden file lifecycle — create, get, list by parent, download, and delete."
37
37
  },
38
+ {
39
+ "path": "src/core/examples/history.ts",
40
+ "description": "History and recipes — a project's connected documents (Revit, AutoCAD…), their version timelines, change requests (suggestions), and recipes: scripts that write suggestions in an app's native language."
41
+ },
38
42
  {
39
43
  "path": "src/core/examples/icons.ts",
40
44
  "description": "Item icon management — upload, download, and remove icons on any item (file, app, or component)."
package/package.json CHANGED
@@ -15,7 +15,7 @@
15
15
  "access": "public"
16
16
  },
17
17
  "private": false,
18
- "version": "0.19.0",
18
+ "version": "0.20.0",
19
19
  "main": "dist/index.cjs.js",
20
20
  "module": "dist/index.es.js",
21
21
  "types": "dist/index.d.ts",
@@ -85,8 +85,37 @@ await gis.enableTiles();
85
85
  makes the see-through effect possible. On other hosts they are added to the
86
86
  scene directly and stay opaque. You do not need to do anything for this: the
87
87
  component detects it.
88
+
89
+ On those deferred hosts the tiles also dissolve gradually near the camera's far
90
+ plane instead of ending in a hard edge. `horizonFade` is the fraction of the far
91
+ distance over which this happens: with the default `0.8` and a far plane at
92
+ 1000, the tiles are fully visible up to 200 and fully transparent at 1000. It
93
+ combines with the see-through opacity, and `0` turns it off. While the map is
94
+ loaded the fade is a fraction of `mapFar` (see below), not of the camera's own
95
+ far plane.
96
+
97
+ While the tiles are loaded, the empty background behind them is also painted
98
+ as a sky: a gradient from `skyHorizonColor` (default `#d7e3ee`, a light hazy
99
+ blue) at the horizon to `skyZenithColor` (default `#6a9bd1`) straight up, so
100
+ the tiles dissolve into the horizon colour. Set `sky = false` to keep the
101
+ background as it is. Like the fade, it only exists on deferred hosts.
102
+
103
+ To let the map reach the horizon, `GISManager` takes over the far plane of the
104
+ world's cameras while the tiles are loaded: `mapFar`, `10000` by default. With
105
+ the default `horizonFade` of `0.8` the map is fully visible up to 2 km and
106
+ fades out until 10 km. A far plane the application or the user set is only
107
+ remembered while the map is loaded and comes back when `disableTiles()` runs
108
+ (a larger far set by code that grows it, for example when it loads a large
109
+ point cloud, is respected, and kept). Setting `mapFar` while the map is loaded
110
+ applies immediately. This works on every host, deferred or not.
88
111
  */
89
112
 
113
+ gis.horizonFade = 0.4;
114
+ gis.mapFar = 15000;
115
+ gis.skyHorizonColor = "#e8d9c5";
116
+ gis.skyZenithColor = new THREE.Color(0x4f86c6);
117
+ gis.sky = true;
118
+
90
119
  gis.onStateChanged.add((state) => console.log("tiles visible:", state.tilesVisible));
91
120
  gis.setTilesOpacity(0.6);
92
121
 
@@ -79,7 +79,10 @@ components.get(UIManager).init();
79
79
  ### 🤖 What the default panel manages automatically
80
80
 
81
81
  **Graphics** — point/line/edge styling, background, shadows, ambient
82
- occlusion — whatever `top-viewer` supports, persisted across reloads.
82
+ occlusion — whatever `top-viewer` supports, persisted across reloads. When the
83
+ app uses `GISManager`, a "GIS Map" group also appears, with the sky on/off, the
84
+ horizon fade (how far before the camera's far plane the map dissolves, 0 = hard
85
+ cut) and the sky's horizon and zenith colours.
83
86
 
84
87
  **Clip styling** — fill/edge color, opacity, width for the clip tool,
85
88
  shared live with `top-viewer-toolbar`'s clip button.
@@ -30,9 +30,7 @@ export interface CDENamingSchema {
30
30
  separator: string;
31
31
  segments: CDENamingSegment[];
32
32
  }
33
- // `Base` now declares `createdBy` itself (the field the API actually
34
- // returns); this narrows it to the string the CDE panels render.
35
- export interface CDEFile extends Omit<Item, 'createdBy'> {
33
+ export interface CDEFile extends Omit<Item, 'creatingUser'> {
36
34
  createdBy: string;
37
35
  metadata: Record<string, CDEMetadataValue>;
38
36
  versions?: ItemVersion[];
@@ -1668,6 +1666,64 @@ declare class _GISManager extends OBC.Component {
1668
1666
  private _tilesOpacity;
1669
1667
  private _fadeTarget;
1670
1668
  private _fadeQuad;
1669
+ private _horizonTarget;
1670
+ private _horizonQuad;
1671
+ /**
1672
+ * Fraction (0–1) of the camera's far distance over which the 3D tiles dissolve
1673
+ * into what is behind them, instead of ending in a hard edge at the far plane:
1674
+ * the tiles are fully visible up to `far * (1 - horizonFade)` and fully
1675
+ * transparent at `far`. Default 0.8. `0` turns it off (opaque tiles are drawn
1676
+ * straight into the frame at opacity 1, as before). Combines with
1677
+ * `setTilesOpacity`: final alpha = opacity × fade. Deferred hosts only — where
1678
+ * the tiles are added to the scene directly there is no fade. Vector layers
1679
+ * are not faded.
1680
+ */
1681
+ horizonFade: number;
1682
+ private readonly _far;
1683
+ private _farCameras;
1684
+ /**
1685
+ * Far plane (distance) the world's cameras get while the 3D tiles are loaded, so
1686
+ * the map reaches the horizon. Default `10000`. Changing it while the map is
1687
+ * loaded takes effect immediately. `horizonFade` is a fraction of this far while
1688
+ * the map is loaded (with `0.8`, the map fades from 2 km to 10 km).
1689
+ *
1690
+ * The camera's own far plane (e.g. the Settings panel's "Far plane") is
1691
+ * overridden while the map is loaded: it is remembered, not applied, and comes
1692
+ * back when the map is unloaded. Code that grows the far plane itself while the
1693
+ * map is loaded still works: a larger far than `mapFar` is left alone, and is
1694
+ * kept on unload.
1695
+ */
1696
+ get mapFar(): number;
1697
+ set mapFar(far: number);
1698
+ /** True while the map owns the cameras' far plane (the 3D tiles are loaded). */
1699
+ get farOverridden(): boolean;
1700
+ /** The far plane the user asked for, remembered while `farOverridden` (otherwise the cameras hold it). */
1701
+ get userCameraFar(): number;
1702
+ /** Records the far plane the user wants; not applied until the map is unloaded. Only meaningful while `farOverridden`. */
1703
+ setUserCameraFar(far: number): void;
1704
+ private _resolveFarCameras;
1705
+ /**
1706
+ * Shows a sky gradient behind everything while the 3D tiles are loaded: the
1707
+ * empty background (pixels nothing has drawn to) goes from `skyHorizonColor` at
1708
+ * the horizon to `skyZenithColor` straight up, so the tiles, dissolving with
1709
+ * `horizonFade`, melt into the horizon colour. Below the horizon it is the
1710
+ * horizon colour; an orthographic camera sees one flat colour. Default `true`;
1711
+ * `false` leaves the background untouched. Deferred hosts only — elsewhere
1712
+ * there is no sky. Drawn behind the vector layers and the tiles.
1713
+ */
1714
+ sky: boolean;
1715
+ private readonly _skyHorizon;
1716
+ private readonly _skyZenith;
1717
+ private _skyHorizonColor;
1718
+ private _skyZenithColor;
1719
+ private _skyPass;
1720
+ private static _setSkyRgb;
1721
+ /** Sky colour at the horizon (any CSS colour string, hex number or `THREE.Color`). Default `"#d7e3ee"`, a light hazy blue. */
1722
+ get skyHorizonColor(): THREE.ColorRepresentation;
1723
+ set skyHorizonColor(color: THREE.ColorRepresentation);
1724
+ /** Sky colour straight up. Default `"#6a9bd1"`, a deeper blue. */
1725
+ get skyZenithColor(): THREE.ColorRepresentation;
1726
+ set skyZenithColor(color: THREE.ColorRepresentation);
1671
1727
  private _passRegistered;
1672
1728
  private _ensureOcclusionPass;
1673
1729
  private readonly _occlusion;
@@ -0,0 +1,103 @@
1
+ // description: "History and recipes — a project's connected documents (Revit, AutoCAD…), their version timelines, change requests (suggestions), and recipes: scripts that write suggestions in an app's native language."
2
+ import { EngineServicesClient } from '../client';
3
+ import type { Recipe } from '../recipes';
4
+
5
+ // The mental model, in three sentences:
6
+ // 1. Every connected document (a Revit model, an AutoCAD drawing) has its
7
+ // version history: who synced, when, what changed.
8
+ // 2. Nobody writes in somebody else's document. Changes arrive as SUGGESTIONS
9
+ // (create / modify / delete native data) that the owner accepts or
10
+ // dismisses in the native app. That is why running any of this is safe.
11
+ // 3. A RECIPE is a script that writes suggestions in an application's own
12
+ // language. Anything that wants to change a Revit document uses a Revit
13
+ // recipe to say it in Revit's words.
14
+
15
+ export async function readTheProjectHistory(
16
+ client: EngineServicesClient,
17
+ projectId: string,
18
+ ) {
19
+ const history = client.history(projectId);
20
+
21
+ // Which documents are connected, and from which application.
22
+ const docs = await history.docs();
23
+ for (const { source, docId } of docs) {
24
+ const doc = history.doc(source, docId);
25
+
26
+ // The timeline is ordered by the version graph, oldest first, with
27
+ // `ordinal` filled in. The last entry is the head.
28
+ const timeline = await doc.timeline();
29
+ const head = timeline[timeline.length - 1];
30
+ console.log(
31
+ `${source}-${docId}: ${timeline.length} versions, head by ${head?.author}`,
32
+ );
33
+
34
+ // A version's geometry: full exports ARE the model file; ordinary
35
+ // versions carry a hidden delta.
36
+ if (head?.deltaId) await doc.delta(head);
37
+ else if (head?.full) await doc.model();
38
+
39
+ // The change requests waiting for the document's owner.
40
+ const pending = await doc.pendingSuggestions();
41
+ console.log(`${pending.length} suggestions pending`);
42
+ }
43
+ }
44
+
45
+ // A recipe is its manifest plus one function. The settingsSchema is what apps
46
+ // render as a form — anything naming an EXISTING thing must be an enum the
47
+ // app fills, so people pick instead of typing names.
48
+ const pointsToBlocks: Recipe<{ layer: string; block: string }> = {
49
+ name: 'points-to-autocad-blocks',
50
+ description: 'Places an AutoCAD block on every input point.',
51
+ to: 'autocad',
52
+ inputs: ['points'],
53
+ runtime: 'browser',
54
+ settingsSchema: {
55
+ type: 'object',
56
+ properties: {
57
+ layer: { type: 'string' },
58
+ block: { type: 'string' },
59
+ },
60
+ required: ['layer', 'block'],
61
+ },
62
+ rules: [{ reads: 'input points', proposes: 'one block per point' }],
63
+ async run({ input, settings }) {
64
+ const points = input as { x: number; y: number; z: number }[];
65
+ // Build the suggestion payload (a .frag with envelope revitflow/op@2,
66
+ // one item per change, attributes in the TARGET's own vocabulary).
67
+ // The payload builder lives in the fragments toolkit; omitted here.
68
+ const payload = await buildOpFrag(points, settings);
69
+ return {
70
+ title: `Place ${points.length} ${settings.block} blocks`,
71
+ verb: 'add',
72
+ targets: points.length,
73
+ payload,
74
+ ruleTallies: [{ read: points.length, proposed: points.length }],
75
+ };
76
+ },
77
+ };
78
+
79
+ export async function runARecipe(
80
+ client: EngineServicesClient,
81
+ projectId: string,
82
+ ) {
83
+ client.recipes.register(pointsToBlocks);
84
+
85
+ // Catalogs filter by target application: "who can speak autocad?".
86
+ client.recipes.list({ to: 'autocad' });
87
+
88
+ // Running is always safe: it only queues a suggestion on the target
89
+ // document. Nothing changes in AutoCAD until its owner applies it there.
90
+ const { suggestion } = await client.recipes.run('points-to-autocad-blocks', {
91
+ projectId,
92
+ target: { source: 'autocad', docId: 'site-plan' },
93
+ input: [{ x: 0, y: 0, z: 0 }],
94
+ settings: { layer: 'DETECTORS', block: 'SIEMENS-FDO181' },
95
+ author: 'recipe:points-to-autocad-blocks',
96
+ });
97
+ console.log(`queued suggestion ${suggestion.opId}`);
98
+ }
99
+
100
+ declare function buildOpFrag(
101
+ points: { x: number; y: number; z: number }[],
102
+ settings: { layer: string; block: string },
103
+ ): Promise<Blob>;