@nebula-spatial/viewer 0.3.0 → 0.4.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/CHANGELOG.md +93 -49
- package/README.md +593 -218
- package/dist/assets/types.d.ts +234 -0
- package/dist/cad/types.d.ts +16 -26
- package/dist/index-Lsk3kHNe.js +6718 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +4 -4924
- package/dist/mujoco-runtime-s85kwjo7.js +208 -0
- package/dist/plugins.d.ts +47 -3
- package/dist/session-B_UiRctl.js +762 -0
- package/dist/session-CGaj14lM.js +93 -0
- package/dist/stage-BxROW_dP.js +421 -0
- package/dist/types.d.ts +24 -2
- package/dist/ui/types.d.ts +45 -0
- package/dist/usd-session-Dcd20Z6G.js +522 -0
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -1,220 +1,595 @@
|
|
|
1
|
-
# @nebula-spatial/viewer
|
|
2
|
-
|
|
3
|
-
浏览器侧只读 Three.js Viewer Facade。负责场景运行时、相机、输入、拾取、渲染循环和
|
|
4
|
-
资源释放;`
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
ctx.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
viewer
|
|
1
|
+
# @nebula-spatial/viewer
|
|
2
|
+
|
|
3
|
+
浏览器侧只读 Three.js 统一资产 Viewer Facade。负责场景运行时、相机、输入、拾取、渲染循环和
|
|
4
|
+
资源释放;`openAsset()` 统一加载 CAD、GLB/glTF、URDF、MJCF 与 USD,`viewer.cad` 保留与
|
|
5
|
+
`@nebula-spatial/cad-loader` 集成的兼容能力。
|
|
6
|
+
|
|
7
|
+
Viewer 不解析 DXF/DWG,也不提供编辑命令、历史记录或业务状态管理。
|
|
8
|
+
|
|
9
|
+
## 插件与 Headless 模式
|
|
10
|
+
|
|
11
|
+
Viewer 的扩展首选插件化:插件通过受限的 `ViewerPluginContext` 读取不可变快照、调用
|
|
12
|
+
commands、订阅事件并注册贡献点,不能访问内部 scene、renderer 或 camera。
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
let stop: (() => void) | undefined;
|
|
16
|
+
const annotationPlugin = {
|
|
17
|
+
id: 'annotation',
|
|
18
|
+
activate(ctx) {
|
|
19
|
+
stop = ctx.events.on('camera-change', () => updateAnnotations(ctx.cad.$read()));
|
|
20
|
+
ctx.contribute({
|
|
21
|
+
kind: 'command',
|
|
22
|
+
id: 'annotation.refresh',
|
|
23
|
+
handler: () => updateAnnotations(ctx.cad.$read()),
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
deactivate() { stop?.(); /* 清理业务资源 */ },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const viewer = createViewer({ viewport, canvas, cadInteraction: 'manual' });
|
|
30
|
+
viewer.use([annotationPlugin]);
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`viewer.use()` 会立即激活插件;`viewer.dispose()` 按注册逆序停用插件并回收插件注册的
|
|
34
|
+
事件、贡献点和 DOM。重复的插件或贡献点 id 会被拒绝。
|
|
35
|
+
|
|
36
|
+
### 插件生命周期
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
interface ViewerPlugin {
|
|
40
|
+
id: string;
|
|
41
|
+
activate(context: ViewerPluginContext): void | Promise<void>;
|
|
42
|
+
deactivate?(): void | Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- `activate`:`viewer.use([...])` 调用时立即执行;可以在里面注册事件、贡献点、布局挂载。
|
|
47
|
+
若返回 Promise,异步失败会被捕获并回滚该插件(不会波及后来用同 id 注册的替换插件)。
|
|
48
|
+
- `deactivate`:`viewer.dispose()` 或 `use(..., { replace: true })` 换掉插件时调用。Viewer
|
|
49
|
+
已自动回收它注册的事件、贡献点和 DOM,这里只清理**插件自己的**业务资源(定时器、外部
|
|
50
|
+
请求、业务状态)。async 失败只记录,不抛出。
|
|
51
|
+
- 同一个 `id` 只能注册一次;一个插件内的贡献点 `id` 也必须全局唯一(`command` 类按
|
|
52
|
+
`command.id` 在 `viewer.commands` 里注册)。
|
|
53
|
+
|
|
54
|
+
### `ViewerPluginContext` 的 API
|
|
55
|
+
|
|
56
|
+
插件在 `activate` 拿到的 `context` 是受限的宿主句柄,能力分四块,且**只能读、只能通过
|
|
57
|
+
commands 写**,不能直接改 `viewer.cad.layers.rows[0].visible` 这类可变模型。
|
|
58
|
+
|
|
59
|
+
**读状态(返回不可变快照,不是活模型)**
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
ctx.cad.$read(): CadViewerSnapshot // 深冻结 CAD 状态快照
|
|
63
|
+
ctx.asset.$read(): AssetPreviewSnapshot | null // 活动资产;无资产为 null
|
|
64
|
+
ctx.asset.$readControl(): AssetControlState // 按钮 pressed 的权威状态
|
|
65
|
+
ctx.viewer.$getState(): ViewerState // { viewMode, selectedObject, highlightedObject, isDisposed }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
快照每次调用都是新的深冻结对象,可直接用于 React `useSyncExternalStore` / Vue `ref`。
|
|
69
|
+
|
|
70
|
+
**写状态(命令,经过内核保证不变量)**
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
ctx.commands.setLayerVisible(index, visible): boolean // 确定性设置,返回是否真的改变了
|
|
74
|
+
ctx.commands.setSelection(selection: CadSelection | null): void
|
|
75
|
+
ctx.commands.fitDocument(): boolean
|
|
76
|
+
ctx.commands.setViewMode(mode): void
|
|
77
|
+
ctx.commands.play()/pause()/reset(): boolean
|
|
78
|
+
ctx.commands.fitView(): boolean
|
|
79
|
+
ctx.commands.setUpAxis('y' | 'z'): boolean
|
|
80
|
+
ctx.commands.setGroundVisible(visible): boolean
|
|
81
|
+
ctx.commands.setCollisionVisible(visible): boolean
|
|
82
|
+
ctx.commands.setTheme('dark' | 'light'): boolean
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
这些命令经过 active epoch、ready phase 与 capability 门控。插件命令失败返回 `false`;成功控制变化统一发布 `asset-control-change`,插件不要保存自己的 pressed truth。
|
|
86
|
+
|
|
87
|
+
**订阅事件**
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
ctx.events.on(type, listener): () => void // 返回退订函数;插件停用时自动退订
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`type` 是 `ViewerEventMap` 和 `CadViewerEventMap` 的并集(`camera-change`、
|
|
94
|
+
`document-change`、`layer-change`、`inspector-change`、`hud-change`、`load-progress`、
|
|
95
|
+
`error` 等,按事件源自动路由到 viewer 或 cad)。事件载荷始终带最新状态快照或最小
|
|
96
|
+
payload,业务不要假设事件里能拿到活模型。
|
|
97
|
+
|
|
98
|
+
**布局挂载**
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
ctx.layout.mount(node, options?: { position?: 'left' | 'right' | 'top' | 'bottom' | 'overlay' }): Disposable
|
|
102
|
+
ctx.layout.onInsetsChange(listener: (insets) => void): () => void
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`mount` 把插件自己的 DOM 挂进 viewer 的布局宿主,返回的 `Disposable.dispose()` 或插件停用
|
|
106
|
+
时移除该节点。`position` 是建议的默认归属,`overlay` 是默认值(不参与视口 inset 计算)。
|
|
107
|
+
`onInsetsChange` 订阅 `ui-layout-change` 的 `contentInsets`,让插件在面板开合时自己排布、
|
|
108
|
+
Viewer 会在 inset 变化时自行 resize——这是"逃逸通道",业务不需要请求内核新增一个上下面板贡献点,
|
|
109
|
+
用 `mount` + `onInsetsChange` 就能实现任意方位布局。
|
|
110
|
+
|
|
111
|
+
### 注册贡献点(`ctx.contribute`)
|
|
112
|
+
|
|
113
|
+
贡献点定义"插件能往 Viewer 里注入什么",目前共 8 类。`contribute` 返回 `Disposable`,
|
|
114
|
+
`dispose()` 或插件停用时会自动注销对应贡献点并清理其 DOM/订阅。
|
|
115
|
+
|
|
116
|
+
| kind | 作用 | 关键字段 |
|
|
117
|
+
|---|---|---|
|
|
118
|
+
| `panel` | 挂一个常驻面板 | `factory(ctx) => Node` |
|
|
119
|
+
| `toolbar` | 往工具栏加按钮 | `items: ToolbarItem[]` |
|
|
120
|
+
| `hud` | 悬浮层内容 | `factory(ctx) => Node` |
|
|
121
|
+
| `statusbar` | 状态栏条目 | `factory(ctx) => Node` |
|
|
122
|
+
| `inspector` | 选中对象的属性渲染(多分派) | `matcher(sel) => boolean` + `render(ctx, sel) => Node` |
|
|
123
|
+
| `interaction` | 拦截拾取/选择/hover 管线 | `hooks: { onPick?, onHover?, onSelect?, onClear? }` |
|
|
124
|
+
| `command` | 注册可执行命令(可绑快捷键) | `handler(ctx, args?) => void` |
|
|
125
|
+
| `fit-policy` | 定制 `fitView()` 的范围解析 | `resolve(ctx) => Box3 \| null` |
|
|
126
|
+
|
|
127
|
+
**`factory` 的定义**(panel/hud/statusbar 类贡献点):
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
factory: (ctx: ViewerPluginContext) => Node
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
- 它是一个**惰性渲染钩子**:不是注册时执行,而是 Viewer 真正把这个贡献点挂进布局 DOM 时
|
|
134
|
+
才调用一次。注册一个 panel 贡献点不会立刻渲染,`viewer.use()` 激活插件 + 该面板进入
|
|
135
|
+
布局后才会调用。
|
|
136
|
+
- **只调一次**。返回一个真实的 DOM `Node`(`Element` 或 `Fragment`),Viewer 会包进一个
|
|
137
|
+
带 `data-viewer-contribution` / `data-viewer-contribution-id` 标记的容器并加进宿主。
|
|
138
|
+
返回非 `Node` 会抛错(`factory` 必须返回 Node,不是 string / 异步 / VNode)。
|
|
139
|
+
- 它拿到和 `activate` 同一个受限 `context`,所以可以在挂载时把 `ctx.cad.$read()`、
|
|
140
|
+
`ctx.events` 绑定进返回的 Node 里,让面板内部自行响应状态变化——因为 `factory` 本身
|
|
141
|
+
**不负责每次状态变化时重建 DOM**,响应逻辑要在 Node 内部(一个 Vue 组件、一段自己订阅
|
|
142
|
+
`ctx.events` 的逻辑)完成。
|
|
143
|
+
- 贡献点 `dispose()` 或插件停用时,容器连同里面的 Node 一起被移除;`factory` 内创建的业务
|
|
144
|
+
资源由你在 `deactivate` 里清理。
|
|
145
|
+
|
|
146
|
+
**与 `inspector` 的 `render` 的区别**(这是最易混的,两者都返回 Node,但时机不同):
|
|
147
|
+
|
|
148
|
+
| 钩子 | 调用时机 | 次数 | 职责 |
|
|
149
|
+
|---|---|---|---|
|
|
150
|
+
| `factory`(panel/hud/statusbar) | 贡献点挂载进布局时 | 一次性 | 常驻面板的初始 DOM 结构 |
|
|
151
|
+
| `render`(inspector) | 每次 `inspector-change` | 每次选中变化 | 按当前选中项重建属性详情 |
|
|
152
|
+
|
|
153
|
+
`panel` 的 `factory` 建一次"固定壳子",内容由内部订阅事件自己更新;`inspector` 的 `render`
|
|
154
|
+
随 selection 变化反复调用,因为属性面板内容由"当前选中的对象"决定。
|
|
155
|
+
|
|
156
|
+
**inspector 分派**:内核按 `CadSelection.kind`(`insert`/`text`)分派,多个 `inspector`
|
|
157
|
+
贡献点用 `matcher` 竞争,注册顺序在前者优先(可用 `id` 去重或控制优先级),命中的那个
|
|
158
|
+
`render`。它是"只读 context 渲染",不改任何状态。
|
|
159
|
+
|
|
160
|
+
**interaction 钩子**(行为类,需受控):
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
hooks: {
|
|
164
|
+
onPick(hit: CadHit | null): boolean | void, // 返回 false = 消费这次点击,截断内核管线
|
|
165
|
+
onHover(hit: CadHit | null): void,
|
|
166
|
+
onSelect(selection: CadSelection | null): void,
|
|
167
|
+
onClear(): void,
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`onPick` 返回 `false` 时,Viewer 不再执行它默认的选择/拾取——这是插件"接管点击"的入口
|
|
172
|
+
(比如标注模式)。`managed` 模式下内置交互管线先走插件 hook,再落到 Viewer 默认行为;
|
|
173
|
+
`manual` 模式下 Viewer 不做默认行为,只把事件交给插件,插件自己调 `hitTest`/`select`/`overlays`。
|
|
174
|
+
`onSelect`/`onClear` 在 inspector 选择变化/清空时触发。
|
|
175
|
+
|
|
176
|
+
**fit-policy**:`viewer.fitView()` 无参调用时,先询问所有 `fit-policy` 贡献点的 `resolve`,
|
|
177
|
+
返回非 `null` 的第一个作为适配范围;都不返回则回退到混合内容范围。适合"自定义某个来源的
|
|
178
|
+
适配边界"这类需求。
|
|
179
|
+
|
|
180
|
+
### 与 `cadInteraction` 的关系
|
|
181
|
+
|
|
182
|
+
CAD 点击策略由 `cadInteraction` 统一表达:`managed`(默认)由 Viewer 完成拾取和选择,
|
|
183
|
+
`manual` 只提供 `viewer.cad.hitTest/select/clearSelection` 原语,`disabled` 关闭 CAD 点击语义
|
|
184
|
+
只保留导航。`cad-readonly` UI 仅在 `managed` 模式下挂载。
|
|
185
|
+
|
|
186
|
+
- `managed`:插件 `interaction` hook 在最前面拦截,hook 不消费时走 Viewer 默认选择。
|
|
187
|
+
- `manual`:Viewer 不做选择,插件完全接管——用 `hitTest` 自己决定点到了什么,再调
|
|
188
|
+
`select` / `clearSelection` / `overlays`。
|
|
189
|
+
- `disabled`:Viewer 不做 CAD 语义拾取,交互 hook 的 `onPick`/`onHover` 不再派发,
|
|
190
|
+
插件不该假设会收到点击。
|
|
191
|
+
|
|
192
|
+
`viewer.overlays.set/remove` 是手动模式的受控 overlay 原语;传入的 `Object3D` 默认由业务
|
|
193
|
+
拥有,只有显式传 `{ owned: true }` 时 Viewer 才在移除时负责释放。
|
|
194
|
+
|
|
195
|
+
`openAsset()` 是 CAD、3D 模型和仿真资产的统一打开入口。CAD asset 的 `ready` 在头部解析完成时
|
|
196
|
+
resolve;增量几何和 sidecar 仍通过 `load-progress`、`document-change` 与 `hud-change` 继续报告。
|
|
197
|
+
替换中的 open 会以 `E_SUPERSEDED` 结束,外部 `AbortSignal` 取消会以 `E_ABORTED` 结束。
|
|
198
|
+
旧的 `viewer.cad.open()` 仅作为兼容入口保留,已标记为 deprecated。
|
|
199
|
+
|
|
200
|
+
`viewer.cad.getSnapshot()` 返回深冻结快照(包括独立的 `CadInspectorSnapshot` / `CadHudSnapshot` 数据形状),适合状态管理;图层可用
|
|
201
|
+
`layers.setVisible(index, visible)` / `setVisibleBatch()` 做确定性更新。无参
|
|
202
|
+
`viewer.fitView()` 与 `viewer.fitAll()` 适配全部内容,`viewer.cad.fitSelection()` 只适配当前
|
|
203
|
+
CAD 选择。
|
|
204
|
+
|
|
205
|
+
高层相机状态通过 `viewer.camera.getState()`、`setState()` 和 `reset()` 访问,不暴露底层
|
|
206
|
+
Three.js Camera。应用成功后会同步发布 `camera-change` 和 `viewport-change`;跨 2D/3D 时还会
|
|
207
|
+
发布 `view-mode-change`。`camera-change.reason` 区分 `pointer`、`wheel`、`fit`、`restore`、
|
|
208
|
+
`api` 和 `view-mode`。当前 `setState()` 立即应用快照,动画切换属于后续能力。
|
|
209
|
+
|
|
210
|
+
## 统一资产入口
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
const preview = viewer.openAsset('/models/robot.glb', { format: 'auto' });
|
|
214
|
+
const stop = preview.subscribe((snapshot) => updateLoadingUi(snapshot));
|
|
215
|
+
await preview.ready;
|
|
216
|
+
await preview.close();
|
|
217
|
+
stop();
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`openAsset()` 同步返回句柄;首次 snapshot 固定为 `detecting`。Viewer 同时只保留一个活动资产,
|
|
221
|
+
旧句柄的 `close()` 只等待自己的资源清理,不会关闭后来打开的资产。`closeAsset()` 关闭调用瞬间
|
|
222
|
+
的活动资产。普通模型以 borrowed 方式挂入唯一 scene runtime,GPU 资源只由模型 session 释放。
|
|
223
|
+
|
|
224
|
+
GLB/glTF 文件没有权威的 up 轴声明(规范 Y-up,CAD 导出常为 Z-up)。预览世界固定 Z-up;
|
|
225
|
+
对轴向错误的模型传 `upAxis: 'y'` 打开(仅旋转资产根节点,不移动相机与世界),或运行期用
|
|
226
|
+
`preview.setUpAxis('y' | 'z')` / `preview.getUpAxis()` 切换(切换后自动重铺地面并重新适配
|
|
227
|
+
相机,不平移贴地)。能力标志为 `capabilities.canReorientUpAxis`。仿真资产的格式自带 up 轴,
|
|
228
|
+
不提供该能力。
|
|
229
|
+
|
|
230
|
+
地面(网格线 + 反射 + 接影地板)可通过 `preview.setGroundVisible(false)` 整体隐藏、`preview.getGroundVisible()`
|
|
231
|
+
查询,对应 `capabilities.canToggleGround`;模型与仿真预览均支持。隐藏时跳过反射 pass,接影地板与地面阴影一并隐藏。
|
|
232
|
+
|
|
233
|
+
GLB/glTF 的 Draco decoder 路径由宿主显式配置,不使用站点根路径默认值:
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
const viewer = createViewer({
|
|
237
|
+
viewport,
|
|
238
|
+
canvas,
|
|
239
|
+
assets: { dracoDecoderPath: new URL('./draco/', document.baseURI).href },
|
|
240
|
+
});
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
URDF 和 MJCF 以 MuJoCo WASM 运行,仍由 Viewer 唯一的 scene/runtime/RAF 驱动。多文件模型请传
|
|
244
|
+
`{ kind: 'files', files, entry }`;`entry` 是包内 URDF 或 MJCF XML 路径。USD 会加载视觉与可可靠映射的物理数据,远端 URL 会以根 USD 文件的完整 URL 作为相对子层、纹理和其他资源的解析基址。
|
|
245
|
+
当前明确支持动态刚体以及树形拓扑的 `PhysicsFixedJoint`、`PhysicsRevoluteJoint`、`PhysicsPrismaticJoint`,包括局部锚点、轴向、有限/无限限位、零宽锁定限位和 force drive;这些资产会启用 play/pause/reset、碰撞代理和拖拽施力。闭环/多父关节、其他关节类型或无法保持物理语义的属性不会被静默编译成错误仿真:当 OpenUSD 已经成功构建完整视觉场景、且失败属于 Viewer 明确识别的 USD 到 MuJoCo 物理语义缺口时,会显式降级为仅视觉预览,禁用播放、复位、碰撞代理和拖拽,并在场景顶部展示降级原因;网络请求、USD/视觉解析、安全上下文、单位校验、取消操作、MuJoCo 初始化及未知错误仍按加载失败处理。没有刚体的 USD 仍作为正常的静态视觉资产预览,不显示降级警告。
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
const preview = viewer.openAsset({
|
|
249
|
+
kind: 'files',
|
|
250
|
+
entry: 'robot/scene.xml',
|
|
251
|
+
files: selectedFiles.map((file) => ({ path: file.webkitRelativePath || file.name, file })),
|
|
252
|
+
}, { format: 'mjcf' });
|
|
253
|
+
await preview.ready;
|
|
254
|
+
preview.pause();
|
|
255
|
+
preview.reset();
|
|
256
|
+
preview.play();
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### 远端仿真资产与格式识别边界
|
|
260
|
+
|
|
261
|
+
省略 `format` 与 `format: 'auto'` 等价:只按明确扩展名或已注册 loader 的明确 `match()` 结果识别。
|
|
262
|
+
通用 `.xml`、无扩展名 URL、目录 URL 不会自动猜测为 MJCF、URDF 或 CAD;无法识别时显示
|
|
263
|
+
“不支持的资产格式”,调用方应指定 `format`。格式识别成功不代表文件内容或全部仿真语义一定受支持;
|
|
264
|
+
读取、解析、依赖资源或运行时初始化失败显示“资产加载失败”。
|
|
265
|
+
|
|
266
|
+
远端单文件应传实际入口文件 URL,而不是目录 URL,例如:
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
const preview = viewer.openAsset('https://cdn.example.com/robot/scene.xml', {
|
|
270
|
+
format: 'mjcf',
|
|
271
|
+
baseUrl: 'https://cdn.example.com/robot/',
|
|
272
|
+
});
|
|
273
|
+
await preview.ready;
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Viewer 不枚举 CDN 目录。远端目录型资产应传实际入口文件 URL,保持服务器上的相对目录结构;
|
|
277
|
+
需要签名 URL、私有鉴权或路径重写时,通过 `resourceResolver` 映射入口文件引用的子资源:
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
const preview = viewer.openAsset('https://cdn.example.com/robot/scene.xml', {
|
|
281
|
+
format: 'mjcf',
|
|
282
|
+
baseUrl: 'https://cdn.example.com/robot/',
|
|
283
|
+
resourceResolver: {
|
|
284
|
+
async resolve(path, { signal } = {}) {
|
|
285
|
+
return getAuthorizedAssetUrl(path, { signal });
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
await preview.ready;
|
|
30
290
|
```
|
|
31
291
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
`
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
`
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
`
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
`
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
`
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
##
|
|
187
|
-
|
|
188
|
-
Viewer
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
viewer.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
292
|
+
对于私有 OSS/S3/CDN,常见的预签名 URL 只授权一个具体 Object Key。`root.usd` 的签名通常不能
|
|
293
|
+
复用于 `asset.usdc`、`physics/physics.usda`、纹理等兄弟资源,也不能把入口 URL 的 query
|
|
294
|
+
原样拼接到其他路径。`resourceResolver.resolve(path)` 必须为每个 `path` 返回独立有效的签名 URL
|
|
295
|
+
或已经取得的 `Blob`;入口 URL 自身也必须仍在有效期内。USD loader 会先提取分层 USD 的外部
|
|
296
|
+
引用,再通过该 resolver 递归取得 layer、mesh、material 和 texture 资源;任一依赖返回 401/403
|
|
297
|
+
都会作为鉴权加载失败报告,不会进入视觉降级流程。
|
|
298
|
+
|
|
299
|
+
`{ kind: 'files', files, entry }` 只表示浏览器已经持有的本地 `File | Blob` 文件包,不能把远端
|
|
300
|
+
URL 字符串放进 `files[].file`。业务层负责入口选择、必要的格式判断、鉴权和依赖映射;不能只依赖
|
|
301
|
+
Viewer 自动识别所有资产形态,也不能把本地样本验证等同于任意远端资产都兼容。普通 glTF 的
|
|
302
|
+
外部资源仍由 `GLTFLoader` 按 URL 请求,不应假设所有格式的子资源都经过 `resourceResolver`;
|
|
303
|
+
私有 glTF 资产优先采用同源/签名目录,或交付为 GLB。
|
|
304
|
+
|
|
305
|
+
OpenUSD 使用 pthread WASM,部署必须在安全上下文中提供以下响应头:
|
|
306
|
+
|
|
307
|
+
```text
|
|
308
|
+
Cross-Origin-Opener-Policy: same-origin
|
|
309
|
+
Cross-Origin-Embedder-Policy: require-corp
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
`@mujoco/mujoco` 和 `@openusd-wasm/three-loader` 保持动态加载。Vite 浏览器构建需要将 MuJoCo
|
|
313
|
+
包中的 Node-only 裸模块 `module` 精确 alias 到一个导出 `createRequire()` 的浏览器拒绝模块;
|
|
314
|
+
仓库的 `vite.config.ts` 和独立消费者验证脚本包含该配置。可通过 `assets.mujocoBaseUrl` 与
|
|
315
|
+
`assets.openUsdBaseUrl` 指向自行托管的 runtime 文件目录。
|
|
316
|
+
|
|
317
|
+
## CAD 快速开始
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
import { createViewer } from '@nebula-spatial/viewer';
|
|
321
|
+
|
|
322
|
+
const viewer = createViewer({
|
|
323
|
+
viewport: document.querySelector<HTMLElement>('#viewport')!,
|
|
324
|
+
canvas: document.querySelector<HTMLCanvasElement>('#viewport canvas')!,
|
|
325
|
+
initialViewMode: '2d',
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
viewer.cad.on('document-change', ({ document }) => {
|
|
329
|
+
updateDocumentTitle(document?.filename ?? '');
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const asset = viewer.openAsset('/api/files/demo/result', {
|
|
333
|
+
format: 'cad',
|
|
334
|
+
filename: 'demo.dxf',
|
|
335
|
+
cad: {
|
|
336
|
+
documentKey: 'demo',
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
await asset.ready;
|
|
341
|
+
const document = viewer.cad.document;
|
|
342
|
+
if (!document) throw new Error('CAD document was not opened');
|
|
343
|
+
|
|
344
|
+
viewer.dispose();
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Viewer 默认使用 Three.js 内置 `RoomEnvironment` 提供基于图像的环境光,不会替换场景背景。
|
|
348
|
+
普通 GLTF/GLB 预览使用该环境光,保留舞台地面、反射和主光阴影,关闭仿真专用的半球光与轮廓光。
|
|
349
|
+
仿真资产继续使用自身灯光配置。传入 HDR 时,普通模型使用该 HDR 替代 RoomEnvironment。
|
|
350
|
+
外部系统也可以传入自己的等距柱状 HDR URL:
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
const viewer = createViewer({
|
|
354
|
+
viewport,
|
|
355
|
+
canvas,
|
|
356
|
+
environment: {
|
|
357
|
+
type: 'hdr',
|
|
358
|
+
url: 'https://static.example.com/environments/studio.hdr',
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
HDR 资源由浏览器直接请求,因此资源服务器需要允许当前应用域名进行 CORS 访问。Viewer
|
|
364
|
+
销毁时会释放已经加载的环境纹理;加载失败时保留原场景背景并在控制台输出警告。
|
|
365
|
+
|
|
366
|
+
如果业务需要调整 CAD 文字构建参数,应在第一次文字请求前调用全局初始化入口:
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
import { configureViewerText, createViewer } from '@nebula-spatial/viewer';
|
|
370
|
+
|
|
371
|
+
configureViewerText({
|
|
372
|
+
defaultFontURL: '/fonts/cad-fallback.ttf',
|
|
373
|
+
useWorker: true,
|
|
374
|
+
sdfGlyphSize: 64,
|
|
375
|
+
textureWidth: 2048,
|
|
376
|
+
});
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
`configureViewerText()` 配置的是 Viewer 内部 `cad-loader` 使用的 Troika 模块实例,沿用
|
|
380
|
+
Troika 全局、模块级、一次性配置语义。字体资源和 `resolveTextFont` 映射策略由业务提供;
|
|
381
|
+
如果不调用该方法,Viewer 不会注入额外默认值,Troika 的原有行为保持不变。
|
|
382
|
+
|
|
383
|
+
几何准备 Worker 默认从 Viewer npm 包内的相对资源路径加载,支持应用部署在子路径。
|
|
384
|
+
需要自行托管 Worker 或满足特定 CSP 时,可通过 `CadOpenOptions.workerUrl` 或
|
|
385
|
+
`workerFactory` 覆盖 Worker 创建方式。
|
|
386
|
+
|
|
387
|
+
`CadLoadSource` 支持 URL、`ArrayBuffer`、`File` 及对应的 `{ kind: ... }` 形式。`openAsset()`
|
|
388
|
+
会替换当前活动资产;调用 `viewer.closeAsset()` 关闭活动资产。旧的 `viewer.cad.open()` /
|
|
389
|
+
`viewer.cad.close()` 会保留兼容行为,但已标记为 deprecated;新代码不应再使用它们管理生命周期。
|
|
390
|
+
|
|
391
|
+
`viewer.cad` 暴露以下只读模型,可通过 `document-change`、`layer-change`、
|
|
392
|
+
`block-change`、`inspector-change` 和 `hud-change` 绑定到产品 UI:
|
|
393
|
+
|
|
394
|
+
- `document`:文档信息、加载阶段和 `units`(`source`;若 pack 提供则包含 `scaleToMeters`)。
|
|
395
|
+
- `layers`:图层可见性与颜色覆盖。
|
|
396
|
+
- `blocks`:INSERT 使用项和实例可见性。
|
|
397
|
+
- `inspector`:当前 CAD INSERT 或文本选择。
|
|
398
|
+
- `hud`:视图与渲染统计。
|
|
399
|
+
|
|
400
|
+
CAD 文档的外部 block sidecar 按初始视口需求并行加载,并在首批资源就绪后渐进呈现;后续批次
|
|
401
|
+
会在不打断交互的情况下追加到当前文档。
|
|
402
|
+
|
|
403
|
+
传入 `ui.root` 后,默认 `preset:'auto'`:CAD 资产显示 CAD 面板/HUD,model 与 simulation
|
|
404
|
+
显示右下角 preview 工具栏和加载/失败状态层;两组 UI 按资产 profile 互斥。`preset:'cad-readonly'`
|
|
405
|
+
只注册 CAD UI,`preset:'none'` 完全 headless。`ui.cad.parts` 配置 CAD 部件,`ui.scene.parts` 配置模型/仿真场景控件。
|
|
406
|
+
|
|
407
|
+
`ui.insetTarget` 默认是 viewport;设为 `'none'` 时 Viewer 只发布 `ui-layout-change` 而不修改元素
|
|
408
|
+
inset。内置 UI 位于 `ui.root` 的 ShadowRoot;第三方贡献点位于 light DOM zone(可通过
|
|
409
|
+
`[data-viewer-zone]` 定位),外观由插件自己负责。
|
|
410
|
+
|
|
411
|
+
内置 UI 的文案和单位格式化通过稳定配置入口覆盖,不需要依赖 Shadow DOM 内部 class:
|
|
412
|
+
|
|
413
|
+
```ts
|
|
414
|
+
const viewer = createViewer({
|
|
415
|
+
viewport,
|
|
416
|
+
canvas,
|
|
417
|
+
ui: {
|
|
418
|
+
root: document.querySelector('#viewer-ui')!,
|
|
419
|
+
preset: 'auto',
|
|
420
|
+
insetTarget: document.querySelector('#viewport')!,
|
|
421
|
+
scene: {
|
|
422
|
+
parts: { playback: true, ground: true, collision: true },
|
|
423
|
+
},
|
|
424
|
+
locale: 'en-US',
|
|
425
|
+
messages: { 'toolbar.fit': 'Frame' },
|
|
426
|
+
cad: {
|
|
427
|
+
unitFormatter: (value, { unit }) => `${value.toFixed(2)} ${unit}`,
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
消息解析优先级为 `messageResolver`、`messages`、内置 locale fallback;`unitFormatter` 会
|
|
434
|
+
覆盖属性面板和比例尺的数值标签。没有传入这些配置时,默认保持简体中文和图纸单位的现有显示。
|
|
435
|
+
|
|
436
|
+
面板状态默认使用 `nebula-viewer:<instance>:*` 命名空间持久化;可通过 `ui.cad.persistenceKey`
|
|
437
|
+
指定稳定实例标识,或通过同步的 `ui.cad.stateStore.read/write` 接入业务状态存储。设置
|
|
438
|
+
`ui.cad.persistence: false` 会完全关闭状态读写。
|
|
439
|
+
|
|
440
|
+
内置快捷键默认只绑定到 `viewport`;Viewer 会在该区域内的鼠标/触控操作后将焦点移入该元素,
|
|
441
|
+
因此不会污染页面其它区域的快捷键。不会响应 `input`、`textarea`、`select` 或
|
|
442
|
+
`contenteditable` 内的按键。`ui.cad.keyboard.target` 可指定其它作用域,
|
|
443
|
+
`ui.cad.keyboard.shortcuts.fit` 可修改适应视图按键或设为 `false` 单独关闭。内置 UI 的“适应”按钮和
|
|
444
|
+
快捷键会优先适应当前 CAD 选择;没有选择时才适应整张图纸。
|
|
445
|
+
|
|
446
|
+
## 从 0.3 迁移到 0.4
|
|
447
|
+
|
|
448
|
+
0.4.0 不只是增加若干格式,而是把 Viewer 从 CAD 为主的 facade 调整为 CAD、3D 模型和仿真资产的
|
|
449
|
+
统一预览运行时。模型与仿真共享同一个 SceneRuntime、Renderer、Camera、environment、导航和 RAF;
|
|
450
|
+
MuJoCo/OpenUSD 只负责格式解析、物理运行和 session 更新,不再拥有第二套页面渲染循环。
|
|
451
|
+
|
|
452
|
+
### 1. 统一资产生命周期
|
|
453
|
+
|
|
454
|
+
```ts
|
|
455
|
+
// 0.3:CAD 专属打开入口
|
|
456
|
+
const document = await viewer.cad.open(source, cadOptions);
|
|
457
|
+
viewer.cad.close();
|
|
458
|
+
|
|
459
|
+
// 0.4:所有格式使用同一个生命周期入口
|
|
460
|
+
const asset = viewer.openAsset(source, {
|
|
461
|
+
format: 'cad',
|
|
462
|
+
filename: 'drawing.dxf',
|
|
463
|
+
cad: {
|
|
464
|
+
documentKey: 'drawing',
|
|
465
|
+
resolveTextFont,
|
|
466
|
+
},
|
|
467
|
+
});
|
|
468
|
+
await asset.ready;
|
|
469
|
+
const document = viewer.cad.document;
|
|
470
|
+
await viewer.closeAsset();
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
`viewer.cad` 没有被删除,它仍是 CAD 文档、图层、选择、HUD 和 CAD 操作的专属能力入口;只有
|
|
474
|
+
open/close 生命周期被提升到 Viewer。`viewer.cad.open()` / `viewer.cad.close()` 暂时兼容并已
|
|
475
|
+
标记 deprecated。GLB/glTF、URDF、MJCF 和 USD 同样通过 `openAsset()` 返回 `AssetPreview`,调用方
|
|
476
|
+
可订阅 snapshot,或使用句柄的 play/pause/reset、地面、碰撞和 up-axis 能力。
|
|
477
|
+
|
|
478
|
+
### 2. 模型与仿真接入
|
|
479
|
+
|
|
480
|
+
- 不再由 Embed/业务代码自行创建 GLTFLoader、挂载模型、管理 loading/failed overlay 或维护独立 RAF。
|
|
481
|
+
- GLB/glTF 的 Draco decoder 必须通过 `assets.dracoDecoderPath`(或单次打开选项)由宿主显式配置。
|
|
482
|
+
- URDF/MJCF 需要 MuJoCo runtime,USD 需要 OpenUSD runtime;可用 `assets.mujocoBaseUrl` 与
|
|
483
|
+
`assets.openUsdBaseUrl` 指向自托管目录。
|
|
484
|
+
- 远端目录型资产传入口文件 URL,并保持相对依赖目录;`{ kind:'files' }` 只用于本地 `File | Blob`
|
|
485
|
+
文件包。跨域部署还要配置 CORS、MIME 与鉴权;USD 页面必须满足 HTTPS 和 COOP/COEP。
|
|
486
|
+
- `format:'auto'` 只识别明确扩展名/loader match。通用 `.xml`、无扩展名和目录 URL 应显式传 format,
|
|
487
|
+
不再通过 `unknownFormatPolicy:'cad'` 猜测。
|
|
488
|
+
|
|
489
|
+
### 3. UI 与主题迁移
|
|
490
|
+
|
|
491
|
+
- `ui.preset` 默认值改为 `'auto'`,按资产自动激活 CAD 或 scene UI。需要 0.3 CAD-only 行为时显式
|
|
492
|
+
设置 `preset:'cad-readonly'`;完全自建 UI 使用 `'none'`。
|
|
493
|
+
- CAD 配置迁移到 `ui.cad`,模型/仿真控件配置到 `ui.scene`,详细字段映射见下方表格。
|
|
494
|
+
- 删除 Embed/业务侧重复的 preview toolbar、loading/failed overlay、抓取 pointer 监听和 viewport
|
|
495
|
+
inset 同步,改由内置插件、LayoutEngine 与 Viewer 内核承担。
|
|
496
|
+
- `ui.theme` 是唯一初始主题来源。内置主题按钮在 Viewer 内部原地切换,不需要 URL 参数、宿主回调
|
|
497
|
+
或 Embed 调用 `setTheme()`;公开的 `viewer.setTheme()` 只用于可选的外部编程控制。
|
|
498
|
+
|
|
499
|
+
| 旧配置 | 0.4 配置 |
|
|
500
|
+
| --- | --- |
|
|
501
|
+
| `ui.parts` | `ui.cad.parts` |
|
|
502
|
+
| `ui.persistence` / `ui.persistenceKey` / `ui.stateStore` | `ui.cad.persistence` / `ui.cad.persistenceKey` / `ui.cad.stateStore` |
|
|
503
|
+
| `ui.keyboard` / `ui.unitFormatter` | `ui.cad.keyboard` / `ui.cad.unitFormatter` |
|
|
504
|
+
| `ui.preview.parts` | `ui.scene.parts` |
|
|
505
|
+
| `ui.preview.appearance.theme` | `ui.theme`,唯一初始主题来源 |
|
|
506
|
+
|
|
507
|
+
CAD 类型改为 `ViewerCadUiOptions`、`ViewerCadUiParts`、`ViewerCadUiKeyboardOptions`、
|
|
508
|
+
`ViewerCadUiStateStore`;场景类型使用 `ViewerSceneUiOptions`、`ViewerSceneUiParts`,旧名称不保留别名。
|
|
509
|
+
`ui.scene.parts` 支持 `filename`、`playback`、`reset`、`fit`、`upAxis`、`ground`、`collision`、
|
|
510
|
+
`theme`,默认开启并继续受当前资产 capabilities 门控。
|
|
511
|
+
|
|
512
|
+
### 4. 插件与事件迁移
|
|
513
|
+
|
|
514
|
+
- 插件通过 `ctx.asset.$read()` 获取活动资产 snapshot,通过 `ctx.asset.$readControl()` 获取权威控制态。
|
|
515
|
+
- 播放、复位、适应视图、up-axis、地面、碰撞和主题统一通过 `ctx.commands` 发出,并受活动 epoch、
|
|
516
|
+
ready phase 与 capabilities 门控。
|
|
517
|
+
- 按钮的 pressed 状态以 `asset-control-change` 为唯一真值;不要在插件或业务壳中维护重复状态。
|
|
518
|
+
- 第三方格式通过 `asset-loader` contribution 接入统一 session 生命周期,不应直接访问 Viewer 私有
|
|
519
|
+
Scene、Renderer、Camera 或创建额外渲染循环。
|
|
520
|
+
|
|
521
|
+
### 5. 最小迁移检查表
|
|
522
|
+
|
|
523
|
+
- [ ] 所有资产打开/关闭均已改为 `openAsset()` / `closeAsset()`。
|
|
524
|
+
- [ ] CAD 业务状态仍从 `viewer.cad` 读取,没有把 CAD 专属能力混入通用 asset API。
|
|
525
|
+
- [ ] 旧 `ui.parts`、`ui.preview`、顶层 CAD persistence/keyboard 配置已完成分组迁移。
|
|
526
|
+
- [ ] Embed 中重复 UI、GLTF 加载、clearColor 分支、抓取监听和 resize/inset 状态机已移除。
|
|
527
|
+
- [ ] Draco、MuJoCo、OpenUSD runtime URL 由宿主明确配置并已在真实部署路径验证。
|
|
528
|
+
- [ ] 远端资产的 CORS、MIME、鉴权、相对资源路径以及 USD 的跨源隔离已端到端验证。
|
|
529
|
+
|
|
530
|
+
## 通用 Three.js 对象
|
|
531
|
+
|
|
532
|
+
Viewer 同样可管理非 CAD 的 `Object3D`:
|
|
533
|
+
|
|
534
|
+
```ts
|
|
535
|
+
viewer.addObject(model, { ownership: 'borrowed' });
|
|
536
|
+
viewer.fitView(model);
|
|
537
|
+
|
|
538
|
+
const stopPicking = viewer.on('pick', ({ result }) => {
|
|
539
|
+
selectObject(result?.object ?? null);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
viewer.setHighlight(model, { color: 0x5b8cff });
|
|
543
|
+
stopPicking();
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
`addObject()`、成功的 `removeObject()` 和实际改变可见性的 `setVisible()` 都会发布一次
|
|
547
|
+
`content-change`(`source: 'object'`)。`camera-change` 与 `viewport-change` 同样覆盖没有
|
|
548
|
+
打开 CAD 文档时的用户导航和 resize,便于上层同步小地图、比例尺或协同视角。
|
|
549
|
+
|
|
550
|
+
默认点击会执行通用 Raycaster 拾取。格式专用的交互可通过 `pointerPick: false` 关闭它,
|
|
551
|
+
并监听 `pointer-click`、`pointer-move` 和 `pointer-leave`。`screenToWorldOnPlane()` 用于
|
|
552
|
+
通用的屏幕坐标到世界平面转换。
|
|
553
|
+
|
|
554
|
+
## 生命周期
|
|
555
|
+
|
|
556
|
+
Viewer 借用 `addObject()` 传入的 `Object3D`。`removeObject()` 和 `dispose()` 只解除场景
|
|
557
|
+
挂载,不会释放调用方的 Geometry、Material 或 Texture。
|
|
558
|
+
|
|
559
|
+
例如,GLTF 资源应在从 Viewer 移除后由加载它的业务统一释放:
|
|
560
|
+
|
|
561
|
+
```ts
|
|
562
|
+
import { Material, Mesh, Texture } from 'three';
|
|
563
|
+
|
|
564
|
+
viewer.removeObject(gltf.scene);
|
|
565
|
+
gltf.scene.traverse((object) => {
|
|
566
|
+
if (!(object instanceof Mesh)) return;
|
|
567
|
+
object.geometry.dispose();
|
|
568
|
+
const materials = Array.isArray(object.material) ? object.material : [object.material];
|
|
569
|
+
for (const material of materials as Material[]) {
|
|
570
|
+
for (const value of Object.values(material)) {
|
|
571
|
+
if (value instanceof Texture) value.dispose();
|
|
572
|
+
}
|
|
573
|
+
material.dispose();
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
Viewer 自行拥有 SceneRuntime、Renderer、Camera、Controls、事件监听和 CAD 会话;调用
|
|
579
|
+
`dispose()` 会统一释放这些资源。运行时依赖为 `three` peer dependency,要求 Node.js 18
|
|
580
|
+
或更高版本。
|
|
581
|
+
|
|
582
|
+
## 开发
|
|
583
|
+
|
|
584
|
+
```bash
|
|
585
|
+
npm run typecheck --workspace @nebula-spatial/viewer
|
|
586
|
+
npm test --workspace @nebula-spatial/viewer
|
|
587
|
+
npm run build --workspace @nebula-spatial/viewer
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
### USD / Isaac Sim 物理映射边界
|
|
592
|
+
|
|
593
|
+
查看器遵循 OpenUSD `UsdPhysics` 的基础刚体语义:`PhysicsCollisionAPI` 单独应用表示静态碰撞体,和 `PhysicsRigidBodyAPI` 同时应用表示动态刚体;同一刚体下可以包含多个碰撞形状。场景单位、Z/Y-up、Cube/Sphere/Cylinder/Capsule/Mesh 碰撞体、静态碰撞体、质量/密度、自由刚体初始速度和基础关节会在可验证的范围内映射到 MuJoCo。
|
|
594
|
+
|
|
595
|
+
Isaac Sim/PhysX 专有的碰撞近似、碰撞组、simulation owner、求解器/CCD、接触模型和逐对关节碰撞过滤不保证等价转换。无法可靠映射的属性会写入 `diagnostics.droppedFeatures`,不会伪造一个看似正确的 MJCF 结果;若基础几何或刚体语义无法建立,则按 USD 物理降级策略处理。
|