@thatopen/services 0.18.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.
- package/README.md +4 -0
- package/dist/built-in/index.d.ts +58 -0
- package/dist/cli/commands/mcp.d.ts +3 -0
- package/dist/cli/lib/cli-client.d.ts +13 -0
- package/dist/cli/lib/config.d.ts +2 -0
- package/dist/cli/lib/zip.test.d.ts +1 -0
- package/dist/cli.js +21439 -424
- package/dist/cli.js.map +1 -0
- package/dist/core/client.d.ts +24 -6
- package/dist/core/examples/history.d.ts +4 -0
- package/dist/core/history.d.ts +204 -0
- package/dist/core/platform-client.d.ts +17 -1
- package/dist/core/recipes.d.ts +98 -0
- package/dist/index.cjs.js +3 -2
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.es.js +1671 -1217
- package/dist/index.es.js.map +1 -0
- package/dist/types/base.d.ts +9 -1
- package/dist/types/notifications.d.ts +4 -2
- package/docs/ai-quickstart.md +62 -7
- package/docs/cli/mcp.md +41 -0
- package/docs/cli/paths.json +4 -0
- package/docs/client/paths.json +4 -0
- package/docs/platform-token.md +1 -1
- package/package.json +4 -2
- package/src/built-in/GISManager/example.ts +29 -0
- package/src/built-in/UIManager/src/settings-panel/example.ts +4 -1
- package/src/built-in/index.ts +58 -0
- package/src/cli/templates/app/README.md +1 -1
- package/src/core/examples/history.ts +103 -0
package/dist/types/base.d.ts
CHANGED
|
@@ -2,7 +2,15 @@ export interface Base {
|
|
|
2
2
|
_id: ObjectId;
|
|
3
3
|
createdAt: Date;
|
|
4
4
|
updatedAt?: Date;
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* The human user who performed the action (audit trail). This used to be
|
|
7
|
+
* declared as a required `creatingUser`, a field the API has never
|
|
8
|
+
* returned — the server sends `createdBy`, optionally. Matching the wire
|
|
9
|
+
* is what a type is for.
|
|
10
|
+
*/
|
|
11
|
+
createdBy?: ObjectId;
|
|
12
|
+
/** The human user who last updated the entity (audit trail). */
|
|
13
|
+
lastUpdatedBy?: ObjectId;
|
|
6
14
|
creatingToken?: ObjectId;
|
|
7
15
|
archived?: boolean;
|
|
8
16
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
export type NotificationCategoryDto = 'automation' | 'invitation';
|
|
2
|
-
export type NotificationTypeDto = 'automation.run.started' | 'automation.run.finished' | 'invitation.added' | 'invitation.accepted' | 'project.role_changed'
|
|
1
|
+
export type NotificationCategoryDto = 'automation' | 'invitation' | 'file';
|
|
2
|
+
export type NotificationTypeDto = 'automation.run.started' | 'automation.run.finished' | 'invitation.added' | 'invitation.accepted' | 'project.role_changed' | 'project.transfer.proposed'
|
|
3
|
+
/** A file version landed in a project the recipient belongs to. */
|
|
4
|
+
| 'file.version_added';
|
|
3
5
|
/**
|
|
4
6
|
* One notification as the API returns it.
|
|
5
7
|
*
|
package/docs/ai-quickstart.md
CHANGED
|
@@ -51,8 +51,8 @@ Steps 0–2 are identical for both; they diverge only at `create` (step 3) and h
|
|
|
51
51
|
|
|
52
52
|
- **Node.js ≥ 18** and **npm** — verify with `node -v`.
|
|
53
53
|
- A **That Open Platform API token**. The user creates it themselves:
|
|
54
|
-
→ **https://platform.thatopen.com/dashboard →
|
|
55
|
-
(
|
|
54
|
+
→ **https://platform.thatopen.com/dashboard/data → API Tokens → Add → copy.**
|
|
55
|
+
(Account Settings — avatar menu → Account Settings → API Tokens — shows the same card.)
|
|
56
56
|
Ask the user to paste it. **Never print it back, never write it into a file, never commit it.**
|
|
57
57
|
|
|
58
58
|
## 1. Install the CLI
|
|
@@ -64,7 +64,7 @@ npm install -g @thatopen/services@latest
|
|
|
64
64
|
## 2. Log in (do this BEFORE creating the project)
|
|
65
65
|
|
|
66
66
|
Get the token from the platform dashboard — **https://platform.thatopen.com/dashboard**
|
|
67
|
-
→ **
|
|
67
|
+
→ **Data → API Tokens** → Add → copy (Account Settings shows the same card). Ask the
|
|
68
68
|
user to paste it, then run:
|
|
69
69
|
|
|
70
70
|
```bash
|
|
@@ -81,6 +81,22 @@ first: the next step's install pulls the private `@thatopen-platform/*-beta` pac
|
|
|
81
81
|
`create` uses your logged-in token to write an authenticated `.npmrc` so that install can
|
|
82
82
|
resolve them. No npm account or manual npm token is involved.
|
|
83
83
|
|
|
84
|
+
### The two credentials, side by side (misusing them is the classic first failure)
|
|
85
|
+
|
|
86
|
+
| | **Platform API token** | **User session JWT** |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| Where it comes from | Dashboard → Data → API Tokens | The platform session (Auth0); inside an app, `PlatformClient.fromPlatformContext()` |
|
|
89
|
+
| Lifetime | Long-lived until revoked | Hours |
|
|
90
|
+
| Client | `EngineServicesClient` | `PlatformClient` (extends the other, so it can do both) |
|
|
91
|
+
| Can | Files, folders, versions, components, executions — anything with an explicit `projectId` | All of that **plus** the JWT-only routes: `listProjects`, `getProject`, `getProjectData`, `checkPermission`, notifications |
|
|
92
|
+
| Cannot | The JWT-only routes above — they identify a *person*, a token identifies a *token* | Run unattended: it expires with the session |
|
|
93
|
+
| How it travels | `Authorization: Bearer` (the CLI does this for you); the `?accessToken=` query form is legacy | `Authorization: Bearer` |
|
|
94
|
+
|
|
95
|
+
The failure mode this table prevents: building a service on an API token and discovering
|
|
96
|
+
that `checkPermission` or `listProjects` answers 401 — not because of a bug, but because
|
|
97
|
+
those questions are only answerable about a person. If a feature needs them unattended,
|
|
98
|
+
that is a platform gap to report, not a header to keep fiddling with.
|
|
99
|
+
|
|
84
100
|
## 3. Scaffold a beta project
|
|
85
101
|
|
|
86
102
|
**[app]**
|
|
@@ -190,7 +206,11 @@ Copy this skeleton. It is the boot of an app that works.
|
|
|
190
206
|
async function main() {
|
|
191
207
|
const client = PlatformClient.fromPlatformContext();
|
|
192
208
|
|
|
193
|
-
// 1 ── built-ins registered, UI shell up
|
|
209
|
+
// 1 ── built-ins registered, UI shell up. This is the FULL globals set a
|
|
210
|
+
// built-in may ask for: OBC, OBF, BUI, THREE, FRAGS, MARKERJS (plus
|
|
211
|
+
// CUI only if your app still uses the deprecated @thatopen/ui-obc).
|
|
212
|
+
// Only OBC and BUI are strictly required, but a built-in that needs
|
|
213
|
+
// a missing one fails at init, so pass everything you import.
|
|
194
214
|
const { components } = await client.setup<OBC.Components>(
|
|
195
215
|
{ OBC, OBF, BUI, THREE, FRAGS, MARKERJS },
|
|
196
216
|
{ uuid: UIManager.uuid },
|
|
@@ -277,8 +297,11 @@ system: the platform relays messages between app tabs and external tools.
|
|
|
277
297
|
|
|
278
298
|
To command the app from outside (a script, an MCP server, your own tool):
|
|
279
299
|
|
|
280
|
-
1. The user creates a **platform token**
|
|
281
|
-
|
|
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.
|
|
282
305
|
2. Connect with `socket.io-client` and subscribe, then publish:
|
|
283
306
|
|
|
284
307
|
```bash
|
|
@@ -288,7 +311,13 @@ npm i socket.io-client # in the external tool's own project, not the app
|
|
|
288
311
|
```js
|
|
289
312
|
import { io } from "socket.io-client";
|
|
290
313
|
|
|
291
|
-
|
|
314
|
+
// WebSocket-only matters: the default long-polling handshake spreads several
|
|
315
|
+
// HTTP requests across the platform's load-balanced instances and can die
|
|
316
|
+
// before it upgrades. The token rides the auth payload, not the URL.
|
|
317
|
+
const socket = io("https://platform.thatopen.com", {
|
|
318
|
+
transports: ["websocket"],
|
|
319
|
+
auth: { accessToken: process.env.THATOPEN_TOKEN },
|
|
320
|
+
});
|
|
292
321
|
// While iterating with `thatopen serve`, OMIT appId (the dev channel is keyed
|
|
293
322
|
// to the account); pass the appId once the app is published.
|
|
294
323
|
socket.emit("channelSubscribe", { projectId, appId, kind: "cli" });
|
|
@@ -323,6 +352,32 @@ is the reply. The full walkthrough (typed contracts, collaboration events,
|
|
|
323
352
|
presence) is `node_modules/@thatopen/services/src/core/examples/channel.ts`,
|
|
324
353
|
indexed in the client API index you already loaded.
|
|
325
354
|
|
|
355
|
+
## 5c. The desktop MCP server: `thatopen mcp`
|
|
356
|
+
|
|
357
|
+
The raw socket above is for tools you write. For an LLM on the desktop (Claude
|
|
358
|
+
Desktop, Claude Code, anything that speaks MCP), the CLI ships the same bridge
|
|
359
|
+
as a ready-made MCP server over stdio:
|
|
360
|
+
|
|
361
|
+
```json
|
|
362
|
+
{
|
|
363
|
+
"mcpServers": {
|
|
364
|
+
"thatopen-platform": {
|
|
365
|
+
"command": "npx",
|
|
366
|
+
"args": ["-y", "@thatopen/services", "mcp"]
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Credentials come from `thatopen login` (`~/.thatopen/config.json`); to inject
|
|
373
|
+
them instead, set `THATOPEN_TOKEN` and `THATOPEN_API_URL` in the server's
|
|
374
|
+
`env`. Two tools: `platform-status` (can the platform be reached, is the
|
|
375
|
+
subscription alive) and `send-app-command` (publish a command to a running
|
|
376
|
+
app — `ping`, `get-loaded-models`, or whatever the app declares — and return
|
|
377
|
+
its reply). The project is named per call, by id or dashboard URL, so one MCP
|
|
378
|
+
session can drive whichever project the user has open; while iterating with
|
|
379
|
+
`thatopen serve`, `app_id` is omitted, same rule as the raw socket.
|
|
380
|
+
|
|
326
381
|
## 6. Publish (when ready)
|
|
327
382
|
|
|
328
383
|
```bash
|
package/docs/cli/mcp.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Start an MCP server over stdio that lets an LLM on the desktop (Claude Desktop, Claude Code, any MCP client) drive a running That Open app through the platform channel: send it commands and read the replies."
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## thatopen mcp
|
|
6
|
+
|
|
7
|
+
Runs an MCP (Model Context Protocol) server on stdio. Register it in an MCP
|
|
8
|
+
client's config and the LLM can command a running That Open app through the
|
|
9
|
+
platform channel — the same relay the app's `src/setups/channel.ts` listens
|
|
10
|
+
to, as one external channel of kind `mcp`.
|
|
11
|
+
|
|
12
|
+
**Usage (in the MCP client's config, not a terminal):**
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"mcpServers": {
|
|
17
|
+
"thatopen-platform": {
|
|
18
|
+
"command": "npx",
|
|
19
|
+
"args": ["-y", "@thatopen/services", "mcp"]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Credentials:** resolved like every other CLI command — the project's
|
|
26
|
+
`.thatopen`, then `~/.thatopen/config.json` from `thatopen login`. Or inject
|
|
27
|
+
`THATOPEN_TOKEN` and `THATOPEN_API_URL` through the MCP server's `env`.
|
|
28
|
+
|
|
29
|
+
**Tools exposed:**
|
|
30
|
+
|
|
31
|
+
- `platform-status` — which API the server points at, whether credentials were
|
|
32
|
+
found, and whether the channel subscription works. The first call to make
|
|
33
|
+
when another tool fails.
|
|
34
|
+
- `send-app-command` — publish a command (`ping`, `get-loaded-models`, or
|
|
35
|
+
whatever the app declares in `AppCommands`) to a running app and return its
|
|
36
|
+
reply. Takes the project per call (id or dashboard URL); `app_id` is omitted
|
|
37
|
+
while the app runs under `thatopen serve` and passed once it is published.
|
|
38
|
+
|
|
39
|
+
**The app must be open.** `delivered: 0` means no tab of that app has the
|
|
40
|
+
channel joined — the LLM is told to ask the user to open or reload the app
|
|
41
|
+
tab, the same rule as the raw socket in the quickstart's section 5b.
|
package/docs/cli/paths.json
CHANGED
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
"path": "docs/cli/login.md",
|
|
12
12
|
"description": "Authenticate with the ThatOpen platform by validating an access token against the API. Saves credentials globally (~/.thatopen/config.json) by default, or locally to the project's .thatopen file with --local. Local config takes priority over global."
|
|
13
13
|
},
|
|
14
|
+
{
|
|
15
|
+
"path": "docs/cli/mcp.md",
|
|
16
|
+
"description": "Start an MCP server over stdio that lets an LLM on the desktop (Claude Desktop, Claude Code, any MCP client) drive a running That Open app through the platform channel: send it commands and read the replies."
|
|
17
|
+
},
|
|
14
18
|
{
|
|
15
19
|
"path": "docs/cli/publish.md",
|
|
16
20
|
"description": "Build and upload the project to the platform. Automatically detects app vs cloud component from the local .thatopen config. On first publish, saves the assigned item ID back to .thatopen so subsequent runs update the same item instead of creating a new one."
|
package/docs/client/paths.json
CHANGED
|
@@ -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/docs/platform-token.md
CHANGED
|
@@ -8,7 +8,7 @@ here rather than in each of them.
|
|
|
8
8
|
Say this to the user, and wait:
|
|
9
9
|
|
|
10
10
|
> "To log in I need a That Open Platform **access token**. You create it yourself:
|
|
11
|
-
> **https://platform.thatopen.com/dashboard →
|
|
11
|
+
> **https://platform.thatopen.com/dashboard/data → API Tokens → Add → copy** (Account Settings shows the same card), then paste it
|
|
12
12
|
> here."
|
|
13
13
|
|
|
14
14
|
**Never print it back, never write it into a file, never commit it.** Not into a script, not into a
|
package/package.json
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"access": "public"
|
|
16
16
|
},
|
|
17
17
|
"private": false,
|
|
18
|
-
"version": "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",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@changesets/cli": "^2.27.7",
|
|
51
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
51
52
|
"@thatopen/components": "~3.4.0",
|
|
52
53
|
"@thatopen/components-front": "~3.4.0",
|
|
53
54
|
"@thatopen/fragments": "~3.4.0",
|
|
@@ -70,7 +71,8 @@
|
|
|
70
71
|
"vite": "^5.2.0",
|
|
71
72
|
"vite-plugin-dts": "^3.9.1",
|
|
72
73
|
"vite-plugin-eslint": "^1.8.1",
|
|
73
|
-
"vitest": "^4.1.4"
|
|
74
|
+
"vitest": "^4.1.4",
|
|
75
|
+
"zod": "^3.25.76"
|
|
74
76
|
},
|
|
75
77
|
"dependencies": {
|
|
76
78
|
"@types/geojson": "7946.0.16",
|
|
@@ -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.
|
package/src/built-in/index.ts
CHANGED
|
@@ -1666,6 +1666,64 @@ declare class _GISManager extends OBC.Component {
|
|
|
1666
1666
|
private _tilesOpacity;
|
|
1667
1667
|
private _fadeTarget;
|
|
1668
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);
|
|
1669
1727
|
private _passRegistered;
|
|
1670
1728
|
private _ensureOcclusionPass;
|
|
1671
1729
|
private readonly _occlusion;
|
|
@@ -40,7 +40,7 @@ Apps run inside the That Open Platform within a project, not as standalone websi
|
|
|
40
40
|
To develop locally:
|
|
41
41
|
|
|
42
42
|
1. Run `npm run dev` — watches source with esbuild and serves the bundle on port 4000.
|
|
43
|
-
2. Open your project on the platform
|
|
43
|
+
2. Open your project on the platform, click the Local App icon, then Get Started.
|
|
44
44
|
3. Live reload is enabled — save a file to rebuild automatically.
|
|
45
45
|
|
|
46
46
|
**Important**: use `npm run dev` (it runs `thatopen serve` under the hood). Do NOT run
|
|
@@ -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>;
|