feat: add Cesium semantic inspection mode
This commit is contained in:
@@ -70,7 +70,8 @@ npm run compress:glb -- --input in.glb --output out.glb [options]
|
|||||||
- `<fileStem>-compressed-webp768.glb`
|
- `<fileStem>-compressed-webp768.glb`
|
||||||
- `<fileStem>-compressed-webp768.json`
|
- `<fileStem>-compressed-webp768.json`
|
||||||
- `<fileStem>-compressed-webp768-cesium-preview.html`
|
- `<fileStem>-compressed-webp768-cesium-preview.html`
|
||||||
- 伴生 metadata 的 `asset` 和 `assets[0].url` 改为压缩 GLB 文件名。
|
- 伴生 metadata 的 `asset` 与 `id="main"` 资产 URL 改为压缩 GLB 文件名;其余资产(包括
|
||||||
|
`category="semantic"` 的 Cesium 分类检查 GLB)必须原样保留。
|
||||||
- preview HTML 只替换 `window.OSM_ASSET_PREVIEW_CONFIG` 的 `glbName` /
|
- preview HTML 只替换 `window.OSM_ASSET_PREVIEW_CONFIG` 的 `glbName` /
|
||||||
`metadataName` 和 loading 文案,不改 preview runtime。
|
`metadataName` 和 loading 文案,不改 preview runtime。
|
||||||
- 成功时 stdout 打印 `GLB_COMPRESS_DONE <json>`,包含压缩前后大小、image /
|
- 成功时 stdout 打印 `GLB_COMPRESS_DONE <json>`,包含压缩前后大小、image /
|
||||||
@@ -100,6 +101,7 @@ npm run compress:glb -- --input in.glb --output out.glb [options]
|
|||||||
### 6. Tests Required
|
### 6. Tests Required
|
||||||
|
|
||||||
- `node --check scripts/compress-glb.js`
|
- `node --check scripts/compress-glb.js`
|
||||||
|
- `npm run test:compress-glb`:断言压缩 metadata 只替换主资产,不丢失语义资产。
|
||||||
- `node --check scripts/build-area.js`
|
- `node --check scripts/build-area.js`
|
||||||
- 对目标区域跑一次 `npm run compress:glb -- ... --metadata --preview`
|
- 对目标区域跑一次 `npm run compress:glb -- ... --metadata --preview`
|
||||||
- 对目标区域跑一次 `npm run build:area -- --stages compress`
|
- 对目标区域跑一次 `npm run build:area -- --stages compress`
|
||||||
|
|||||||
@@ -201,6 +201,73 @@ GLB 停留在**局部 ENU 坐标系**(X 东、Y 北、Z 上),靠伴生 JSO
|
|||||||
|
|
||||||
`scenePlacement(metadata)`(`:131`)负责这一步。**改动导出侧的坐标约定必须同步改这里。**
|
`scenePlacement(metadata)`(`:131`)负责这一步。**改动导出侧的坐标约定必须同步改这里。**
|
||||||
|
|
||||||
|
## 语义检查资产
|
||||||
|
|
||||||
|
### 1. 范围与触发条件
|
||||||
|
|
||||||
|
`cesium` 阶段除完整主 GLB 外,会按 Blender 顶层集合导出可选检查资产:道路、建筑、
|
||||||
|
绿化与设施、水体。它们只服务于预览检查;主 GLB 仍是下游兼容基线,不能被替换。
|
||||||
|
|
||||||
|
### 2. 调用形式
|
||||||
|
|
||||||
|
不新增 CLI 参数。正常运行 Cesium 阶段即可:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build:area -- --config config/areas/<area>.json --stages cesium
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 契约
|
||||||
|
|
||||||
|
- `blender/export_cesium.py:SEMANTIC_ASSETS` 是集合名、稳定资产 ID 与展示名的唯一映射:
|
||||||
|
`03_Roads → roads`、`04_Buildings → buildings`、`02_Green + 05_Props → vegetation`、
|
||||||
|
`01_Water → water`。
|
||||||
|
- 每个有几何的类别额外写 `<stem>-<id>.glb`,且必须保留与主 GLB 相同的局部 ENU 坐标和
|
||||||
|
已处理的 Cesium 材质。
|
||||||
|
- metadata 的主资产保持 `id="main"`、`enabled=true`;辅助项设
|
||||||
|
`category="semantic"`、`enabled=false`,并提供 `id`、`label`、`type="model"`、`url`。
|
||||||
|
- `build-area.js:semanticAssetRecords()` 必须验证 metadata 声明的每个语义文件存在后才写
|
||||||
|
Cesium manifest。
|
||||||
|
- 浏览器先加载非语义资产;只有点 `Inspect` 才加载辅助 GLB。检查模式必须隐藏主场景,
|
||||||
|
返回 `Scene` 必须隐藏辅助模型,禁止两套几何重叠渲染。
|
||||||
|
- 没有 `category="semantic"` 的旧 metadata 仍按单资产预览打开,`Inspect` 按钮禁用。
|
||||||
|
|
||||||
|
### 4. 校验与错误矩阵
|
||||||
|
|
||||||
|
| 条件 | 结果 |
|
||||||
|
|---|---|
|
||||||
|
| 类别集合没有可导出 mesh | metadata 不声明该类别,预览不显示该开关 |
|
||||||
|
| metadata 声明语义资产但文件不存在 | Cesium 阶段失败,不能写成功 manifest |
|
||||||
|
| 辅助 GLB 浏览器加载失败 | 该开关禁用并写入诊断;主场景继续可用 |
|
||||||
|
| 旧 metadata 没有语义项 | 完整场景照常显示,`Inspect` 不可点击 |
|
||||||
|
|
||||||
|
### 5. 正常、基础与错误示例
|
||||||
|
|
||||||
|
- 正常:进入 `Inspect` 后道路、建筑、绿化与设施、水体全部显示,再单独取消任一类别。
|
||||||
|
- 基础:旧的只有 `main` 资产的 metadata 不展示分类控件,所有原有控制仍可用。
|
||||||
|
- 错误:主 GLB 和语义 GLB 同时可见,导致道路、建筑等重复渲染和闪烁。
|
||||||
|
|
||||||
|
### 6. 必需测试
|
||||||
|
|
||||||
|
- `node scripts/test-preview-assets.js`:断言生成页包含模式切换与语义开关挂载点。
|
||||||
|
- `node --check scripts/build-area.js`、`node --check scripts/lib/area-preview.js`、
|
||||||
|
`node --check scripts/lib/cesium-preview.js`。
|
||||||
|
- 目标区域运行 `--stages cesium`,确认 metadata 的语义 `assets` 与同名辅助 GLB 一一对应。
|
||||||
|
- 浏览器在桌面及窄屏分别切换 `Scene` / `Inspect`,确认不重叠且控制不溢出。
|
||||||
|
|
||||||
|
### 7. 错误与正确写法
|
||||||
|
|
||||||
|
错误:把辅助模型标为默认启用,页面加载时把它们与主 GLB 一起绘制。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "id": "roads", "enabled": true, "category": "semantic" }
|
||||||
|
```
|
||||||
|
|
||||||
|
正确:默认关闭并在检查模式按需加载。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "id": "roads", "enabled": false, "category": "semantic" }
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 本地预览必须走 HTTP
|
## 本地预览必须走 HTTP
|
||||||
|
|||||||
1
.trellis/tasks/08-05-cesium-preview-controls/check.jsonl
Normal file
1
.trellis/tasks/08-05-cesium-preview-controls/check.jsonl
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||||
28
.trellis/tasks/08-05-cesium-preview-controls/design.md
Normal file
28
.trellis/tasks/08-05-cesium-preview-controls/design.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# 设计:Cesium 语义资产检查
|
||||||
|
|
||||||
|
## 边界
|
||||||
|
|
||||||
|
Blender 场景已按集合组织要素。Cesium 导出器在不修改作者场景的前提下,复用现有导出材质处理,先生成完整主 GLB,再按集合筛选对象生成道路、建筑、植被和水体四份辅助 GLB。主 GLB 继续是默认资产和兼容基线。
|
||||||
|
|
||||||
|
## 资产契约
|
||||||
|
|
||||||
|
metadata 保持现有 `asset` 字段指向主 GLB,并将 `assets` 扩展为:
|
||||||
|
|
||||||
|
- `main`:完整场景,默认启用;
|
||||||
|
- `roads`、`buildings`、`vegetation`、`water`:语义检查资产,默认关闭。
|
||||||
|
|
||||||
|
每个资产提供稳定的 `id`、中文 `label`、`type: model`、相对 `url` 与 `enabled`。旧 metadata 缺少这些额外资产时,预览按现有单资产回退路径加载。
|
||||||
|
|
||||||
|
## 预览交互
|
||||||
|
|
||||||
|
主场景模式只显示主 GLB,并保留现有 `Scene` 总开关。切入分类检查模式时隐藏主 GLB、显示四个类别复选项;选择类别时加载并显隐对应资产。离开分类模式后恢复主场景,避免完整模型和类别模型重叠渲染。
|
||||||
|
|
||||||
|
控制面板继续是贴边的紧凑工具面,不添加嵌套卡片。模式切换使用分段控件,类别开关按一行标签排列;窄屏时自然换行。颜色只用于状态和可访问性反馈,不做大面积装饰。
|
||||||
|
|
||||||
|
## 兼容与回滚
|
||||||
|
|
||||||
|
主 GLB、主 metadata 和既有预览文件名均不变。任意辅助 GLB 缺失时,预览记录该资产失败但完整场景仍可加载。回滚只需恢复旧 exporter/runtime,主产物仍可使用。
|
||||||
|
|
||||||
|
## 风险
|
||||||
|
|
||||||
|
多次导出会增加 Cesium 阶段耗时与磁盘占用,但只影响验证辅助产物,不改变主 GLB。导出时需确保每份辅助资产保留与主 GLB 相同的局部 ENU 坐标和材质处理。
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||||
21
.trellis/tasks/08-05-cesium-preview-controls/implement.md
Normal file
21
.trellis/tasks/08-05-cesium-preview-controls/implement.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# 实施计划:Cesium 语义资产检查
|
||||||
|
|
||||||
|
1. 读取 Blender 场景集合,定义四个稳定的语义导出组;完整 GLB 仍走当前路径。
|
||||||
|
2. 扩展 `export_cesium.py`:导出四份辅助 GLB,并把资产描述写进 metadata。
|
||||||
|
3. 扩展 `build-area.js` 的 Cesium 输出检查和 manifest,使辅助资产成为该阶段的受管产物。
|
||||||
|
4. 更新 `area-preview.js`、`cesium-preview.js` 与 CSS:增加完整/分类模式及分类显隐控件,保持旧 metadata 回退。
|
||||||
|
5. 为 HTML 注入、metadata 回退和模式状态编写 Node 侧测试;运行既有预览与纯 Python 测试。
|
||||||
|
6. 在本机重建一个区域,确认辅助 GLB 坐标、材质、模式切换及窄屏布局;此环境若 Blender 沙箱失败,不将其计为代码失败。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node scripts/test-preview-assets.js
|
||||||
|
python3 -m unittest discover blender/tests
|
||||||
|
git diff --check
|
||||||
|
npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages cesium
|
||||||
|
```
|
||||||
|
|
||||||
|
## 回滚点
|
||||||
|
|
||||||
|
主 GLB 与原 preview 文件保持不变。若辅助导出或分类预览异常,可删除语义资产声明并恢复为单资产加载,不影响既有场景。
|
||||||
47
.trellis/tasks/08-05-cesium-preview-controls/prd.md
Normal file
47
.trellis/tasks/08-05-cesium-preview-controls/prd.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Cesium 预览控制
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
让 Cesium 预览更便于检查已生成的园区场景,不涉及 QGIS 数据编辑或压缩产物策略。
|
||||||
|
|
||||||
|
## 已确认事实
|
||||||
|
|
||||||
|
- 预览已有 `Overview`、`Oblique`、`Detail`、`Route` 相机预设,以及车辆跟随;重复开发这些控件没有价值。
|
||||||
|
- 预览已有主场景、路线、车辆的显隐开关;多 GLB 时也会自动提供资产级开关。
|
||||||
|
- 当前导出物只有一个主 GLB,metadata 只记录场景统计和一个 `main` 资产,不含可用于点选的要素属性。
|
||||||
|
- 预览是验证层,改动不应改变 Blender 主资产的几何或 QGIS 工作流。
|
||||||
|
|
||||||
|
## 候选范围
|
||||||
|
|
||||||
|
- 按场景类别显隐,例如道路、建筑、植被、水体。
|
||||||
|
- 点击可识别对象后显示基础信息。
|
||||||
|
- 保持既有相机预设、车辆控制和单场景加载的兼容性。
|
||||||
|
|
||||||
|
## 已确认决策
|
||||||
|
|
||||||
|
- 场景按道路、建筑、植被、水体等语义类别额外导出 GLB,Cesium 预览分别加载。
|
||||||
|
- 保留现有主 GLB,作为完整场景基线和兼容入口。
|
||||||
|
- 首版不提供点选要素属性;这需要逐要素 OSM 元数据映射,另行规划。
|
||||||
|
- 控件遵循现有低干扰预览风格:紧凑分组、清晰状态、克制色彩和无装饰性卡片堆叠。
|
||||||
|
|
||||||
|
## 需求
|
||||||
|
|
||||||
|
1. Cesium 导出额外生成道路、建筑、植被、水体四类语义 GLB,并写入 metadata 资产清单。
|
||||||
|
2. 预览默认加载主 GLB;用户可切换到语义资产检查模式,在该模式独立显示或隐藏每个类别。
|
||||||
|
3. 预览保留现有相机预设、车辆巡航、主场景开关及单 GLB metadata 的兼容行为。
|
||||||
|
4. 控件在桌面和窄屏下保持可读、可操作且不遮挡关键画面。
|
||||||
|
|
||||||
|
## 不在范围内
|
||||||
|
|
||||||
|
- QGIS 图层、人工数据修补和 reimport 流程。
|
||||||
|
- 将压缩 GLB 设为默认产物。
|
||||||
|
- 地图、地形或在线底图功能。
|
||||||
|
- 点击要素属性、OSM ID 或名称映射。
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
|
||||||
|
- [ ] 一次 Cesium 导出会保留主 GLB,并产出四个可独立加载的类别 GLB。
|
||||||
|
- [ ] metadata 的 `assets` 同时声明主场景和各类别资产,旧 metadata 仍可作为单资产场景打开。
|
||||||
|
- [ ] 预览可在完整场景与分类检查模式之间切换;分类模式可独立控制道路、建筑、植被、水体。
|
||||||
|
- [ ] 现有 Overview、Oblique、Detail、Route、车辆和诊断控件仍可使用。
|
||||||
|
- [ ] 控件在常规桌面与窄屏宽度下无重叠、无溢出,视觉层级与现有预览一致。
|
||||||
26
.trellis/tasks/08-05-cesium-preview-controls/task.json
Normal file
26
.trellis/tasks/08-05-cesium-preview-controls/task.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"id": "cesium-preview-controls",
|
||||||
|
"name": "cesium-preview-controls",
|
||||||
|
"title": "Cesium preview controls",
|
||||||
|
"description": "",
|
||||||
|
"status": "in_progress",
|
||||||
|
"dev_type": null,
|
||||||
|
"scope": null,
|
||||||
|
"package": null,
|
||||||
|
"priority": "P2",
|
||||||
|
"creator": "dingkang",
|
||||||
|
"assignee": "dingkang",
|
||||||
|
"createdAt": "2026-08-05",
|
||||||
|
"completedAt": null,
|
||||||
|
"branch": null,
|
||||||
|
"base_branch": "main",
|
||||||
|
"worktree_path": null,
|
||||||
|
"commit": null,
|
||||||
|
"pr_url": null,
|
||||||
|
"subtasks": [],
|
||||||
|
"children": [],
|
||||||
|
"parent": null,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"notes": "",
|
||||||
|
"meta": {}
|
||||||
|
}
|
||||||
@@ -8,7 +8,8 @@
|
|||||||
|
|
||||||
- `<area-id>.blend`:Blender 场景,包含道路、建筑、水体、植被等
|
- `<area-id>.blend`:Blender 场景,包含道路、建筑、水体、植被等
|
||||||
- `<area-id>.png`:Blender 预览渲染
|
- `<area-id>.png`:Blender 预览渲染
|
||||||
- `<area-id>.glb`:Cesium 可加载的 3D 模型
|
- `<area-id>.glb`:Cesium 可加载的完整 3D 模型(基线产物)
|
||||||
|
- `<area-id>-roads.glb`、`-buildings.glb`、`-vegetation.glb`、`-water.glb`:Cesium 分类检查用的辅助模型
|
||||||
- `<area-id>.json`:Cesium 放置元数据和示例代码
|
- `<area-id>.json`:Cesium 放置元数据和示例代码
|
||||||
- `<area-id>-cesium-preview.html`:Cesium 本地预览页
|
- `<area-id>-cesium-preview.html`:Cesium 本地预览页
|
||||||
- `<area-id>-compressed-webp768.glb/json/html`:显式 `compress` 阶段生成的可选压缩预览产物
|
- `<area-id>-compressed-webp768.glb/json/html`:显式 `compress` 阶段生成的可选压缩预览产物
|
||||||
@@ -57,6 +58,9 @@ npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json
|
|||||||
|
|
||||||
`intermediates` 会生成 osm2streets GeoJSON、GeoPackage、QGIS 工程和 QGIS 预览图。`blender` 使用 OSM 和 osm2streets GeoJSON 生成 `.blend`/`.png`。`cesium` 从 `.blend` 导出 `.glb`/`.json`,并生成 Cesium 预览 HTML。`preview` 只在已有 `.glb/.json` 时补生成 HTML。`compress` 从已有 `.glb/.json/html` 生成并列压缩产物,不覆盖默认 GLB。`reimport` 把手工编辑过的 GeoPackage 回导为 GeoJSON,不含在 `all` 里,详见 [QGIS 手工修正工作流](#qgis-手工修正工作流)。
|
`intermediates` 会生成 osm2streets GeoJSON、GeoPackage、QGIS 工程和 QGIS 预览图。`blender` 使用 OSM 和 osm2streets GeoJSON 生成 `.blend`/`.png`。`cesium` 从 `.blend` 导出 `.glb`/`.json`,并生成 Cesium 预览 HTML。`preview` 只在已有 `.glb/.json` 时补生成 HTML。`compress` 从已有 `.glb/.json/html` 生成并列压缩产物,不覆盖默认 GLB。`reimport` 把手工编辑过的 GeoPackage 回导为 GeoJSON,不含在 `all` 里,详见 [QGIS 手工修正工作流](#qgis-手工修正工作流)。
|
||||||
|
|
||||||
|
Cesium 预览默认显示完整场景。点击 `Inspect` 后会按道路、建筑、绿化与设施、水体加载辅助 GLB;该模式用于单独检查生成结果,主 GLB 仍是完整场景和下游使用的基线。
|
||||||
|
压缩预览同样保留这些分类检查资产;重跑 `compress` 后应打开新生成的压缩预览 HTML。
|
||||||
|
|
||||||
`compress` 不含在 `all` 里,也不能从配置文件默认开启。需要重导出 Cesium 后立刻生成压缩产物时,显式跑:
|
`compress` 不含在 `all` 里,也不能从配置文件默认开启。需要重导出 Cesium 后立刻生成压缩产物时,显式跑:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -29,6 +29,16 @@ from osmassets.materials import CESIUM_EXPORT_PROPERTY, link_alpha_clip # noqa:
|
|||||||
# mesh's shared slots can recognise its own output and leave it alone.
|
# mesh's shared slots can recognise its own output and leave it alone.
|
||||||
EXPORT_PREFIX = "Cesium "
|
EXPORT_PREFIX = "Cesium "
|
||||||
|
|
||||||
|
# Top-level collections are the authoring scene's stable semantic boundary.
|
||||||
|
# Their numeric prefixes are draw-order labels in Blender, not part of the
|
||||||
|
# Cesium asset contract below.
|
||||||
|
SEMANTIC_ASSETS = (
|
||||||
|
("roads", "道路", ("03_Roads",)),
|
||||||
|
("buildings", "建筑", ("04_Buildings",)),
|
||||||
|
("vegetation", "绿化与设施", ("02_Green", "05_Props")),
|
||||||
|
("water", "水体", ("01_Water",)),
|
||||||
|
)
|
||||||
|
|
||||||
# Fraction of its own albedo a cut-out foliage material emits, to keep the
|
# Fraction of its own albedo a cut-out foliage material emits, to keep the
|
||||||
# shadowed side of a crown off Cesium's near-black ambient floor. Kept well
|
# shadowed side of a crown off Cesium's near-black ambient floor. Kept well
|
||||||
# under the 0.18 the buildings use: a tree still has to read as lit from one
|
# under the 0.18 the buildings use: a tree still has to read as lit from one
|
||||||
@@ -544,6 +554,54 @@ def triangulate_mesh(obj):
|
|||||||
obj.modifiers.remove(modifier)
|
obj.modifiers.remove(modifier)
|
||||||
|
|
||||||
|
|
||||||
|
def export_glb(filepath, meshes):
|
||||||
|
"""Export a prepared object subset without changing authoring visibility."""
|
||||||
|
bpy.ops.object.select_all(action="DESELECT")
|
||||||
|
for obj in meshes:
|
||||||
|
obj.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = meshes[0] if meshes else None
|
||||||
|
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
||||||
|
bpy.ops.export_scene.gltf(
|
||||||
|
filepath=filepath,
|
||||||
|
export_format="GLB",
|
||||||
|
use_selection=True,
|
||||||
|
export_apply=False,
|
||||||
|
export_texcoords=True,
|
||||||
|
export_normals=True,
|
||||||
|
export_materials="EXPORT",
|
||||||
|
export_image_format="AUTO",
|
||||||
|
export_tangents=True,
|
||||||
|
export_extras=True,
|
||||||
|
export_cameras=False,
|
||||||
|
export_lights=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def semantic_asset_specs(glb_path, meshes):
|
||||||
|
"""Derive optional inspection assets from the scene's named collections."""
|
||||||
|
stem, extension = os.path.splitext(glb_path)
|
||||||
|
by_collection = {}
|
||||||
|
for collection in bpy.context.scene.collection.children:
|
||||||
|
by_collection[collection.name] = set(collection.all_objects)
|
||||||
|
|
||||||
|
specs = []
|
||||||
|
for asset_id, label, collection_names in SEMANTIC_ASSETS:
|
||||||
|
objects = set()
|
||||||
|
for name in collection_names:
|
||||||
|
objects.update(by_collection.get(name, set()))
|
||||||
|
subset = [obj for obj in meshes if obj in objects]
|
||||||
|
if not subset:
|
||||||
|
continue
|
||||||
|
path = stem + "-" + asset_id + extension
|
||||||
|
specs.append({
|
||||||
|
"id": asset_id,
|
||||||
|
"label": label,
|
||||||
|
"path": path,
|
||||||
|
"meshes": subset,
|
||||||
|
})
|
||||||
|
return specs
|
||||||
|
|
||||||
|
|
||||||
def export(args):
|
def export(args):
|
||||||
if not os.path.exists(args["blend"]):
|
if not os.path.exists(args["blend"]):
|
||||||
raise FileNotFoundError(args["blend"])
|
raise FileNotFoundError(args["blend"])
|
||||||
@@ -583,30 +641,10 @@ def export(args):
|
|||||||
material_map[source.name] = make_export_material(source)
|
material_map[source.name] = make_export_material(source)
|
||||||
slot.material = material_map[source.name]
|
slot.material = material_map[source.name]
|
||||||
|
|
||||||
bpy.ops.object.select_all(action="DESELECT")
|
export_glb(args["glb"], meshes)
|
||||||
for obj in meshes:
|
semantic_assets = semantic_asset_specs(args["glb"], meshes)
|
||||||
obj.select_set(True)
|
for asset in semantic_assets:
|
||||||
bpy.context.view_layer.objects.active = meshes[0] if meshes else None
|
export_glb(asset["path"], asset["meshes"])
|
||||||
|
|
||||||
os.makedirs(os.path.dirname(args["glb"]), exist_ok=True)
|
|
||||||
bpy.ops.export_scene.gltf(
|
|
||||||
filepath=args["glb"],
|
|
||||||
export_format="GLB",
|
|
||||||
use_selection=True,
|
|
||||||
export_apply=False,
|
|
||||||
export_texcoords=True,
|
|
||||||
export_normals=True,
|
|
||||||
export_materials="EXPORT",
|
|
||||||
export_image_format="AUTO",
|
|
||||||
# The foliage materials carry a normal map, and glTF leaves tangent
|
|
||||||
# derivation to the renderer when TANGENT is absent. On thin
|
|
||||||
# double-sided leaf cards that derivation is unreliable, so ship real
|
|
||||||
# tangents — it costs four floats a vertex on meshes this small.
|
|
||||||
export_tangents=True,
|
|
||||||
export_extras=True,
|
|
||||||
export_cameras=False,
|
|
||||||
export_lights=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
scene = bpy.context.scene
|
scene = bpy.context.scene
|
||||||
try:
|
try:
|
||||||
@@ -626,7 +664,14 @@ def export(args):
|
|||||||
"type": "model",
|
"type": "model",
|
||||||
"url": os.path.basename(args["glb"]),
|
"url": os.path.basename(args["glb"]),
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
}],
|
}] + [{
|
||||||
|
"id": asset["id"],
|
||||||
|
"label": asset["label"],
|
||||||
|
"type": "model",
|
||||||
|
"url": os.path.basename(asset["path"]),
|
||||||
|
"enabled": False,
|
||||||
|
"category": "semantic",
|
||||||
|
} for asset in semantic_assets],
|
||||||
"coordinate_system": "local ENU meters (X east, Y north, Z up)",
|
"coordinate_system": "local ENU meters (X east, Y north, Z up)",
|
||||||
"heading_correction_degrees": -90.0,
|
"heading_correction_degrees": -90.0,
|
||||||
"anchor": {"longitude": center_lon, "latitude": center_lat, "height": 0.35},
|
"anchor": {"longitude": center_lon, "latitude": center_lat, "height": 0.35},
|
||||||
@@ -660,6 +705,7 @@ def export(args):
|
|||||||
print("CESIUM_EXPORT_DONE", json.dumps({
|
print("CESIUM_EXPORT_DONE", json.dumps({
|
||||||
"glb": args["glb"], "metadata": args["metadata"],
|
"glb": args["glb"], "metadata": args["metadata"],
|
||||||
"meshes": len(meshes), "materials": len(material_map),
|
"meshes": len(meshes), "materials": len(material_map),
|
||||||
|
"semantic_assets": [asset["id"] for asset in semantic_assets],
|
||||||
"anchor": [center_lon, center_lat],
|
"anchor": [center_lon, center_lat],
|
||||||
}, ensure_ascii=True))
|
}, ensure_ascii=True))
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"test:preflight": "node scripts/test-area-preflight.js",
|
"test:preflight": "node scripts/test-area-preflight.js",
|
||||||
"test:budgets": "node scripts/test-asset-budgets.js",
|
"test:budgets": "node scripts/test-asset-budgets.js",
|
||||||
"test:preview-assets": "node scripts/test-preview-assets.js",
|
"test:preview-assets": "node scripts/test-preview-assets.js",
|
||||||
|
"test:compress-glb": "node scripts/test-compress-glb.js",
|
||||||
"test:turn-lane-arrows": "node scripts/test-turn-lane-arrows.js",
|
"test:turn-lane-arrows": "node scripts/test-turn-lane-arrows.js",
|
||||||
"render:turn-lane-arrow-samples": "node scripts/render-turn-lane-arrow-samples.js"
|
"render:turn-lane-arrow-samples": "node scripts/render-turn-lane-arrow-samples.js"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -324,6 +324,7 @@ function exportCesium(area) {
|
|||||||
"--metadata",
|
"--metadata",
|
||||||
area.outputs.metadata,
|
area.outputs.metadata,
|
||||||
], "cesium");
|
], "cesium");
|
||||||
|
const semanticAssets = semanticAssetRecords(area);
|
||||||
writeCesiumPreview(area);
|
writeCesiumPreview(area);
|
||||||
const finished = Date.now();
|
const finished = Date.now();
|
||||||
const digest = glbDigest(area.outputs.glb);
|
const digest = glbDigest(area.outputs.glb);
|
||||||
@@ -340,6 +341,7 @@ function exportCesium(area) {
|
|||||||
outputs: {
|
outputs: {
|
||||||
glb: fileRecord(area.outputs.glb),
|
glb: fileRecord(area.outputs.glb),
|
||||||
metadata: fileRecord(area.outputs.metadata),
|
metadata: fileRecord(area.outputs.metadata),
|
||||||
|
semanticAssets,
|
||||||
},
|
},
|
||||||
summary: {
|
summary: {
|
||||||
glb: glbSummary(digest),
|
glb: glbSummary(digest),
|
||||||
@@ -349,6 +351,19 @@ function exportCesium(area) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function semanticAssetRecords(area) {
|
||||||
|
const metadata = JSON.parse(fs.readFileSync(area.outputs.metadata, "utf8"));
|
||||||
|
const assets = Array.isArray(metadata.assets) ? metadata.assets : [];
|
||||||
|
const records = {};
|
||||||
|
for (const asset of assets) {
|
||||||
|
if (asset.category !== "semantic") continue;
|
||||||
|
const file = path.join(area.outputs.areaDir, asset.url);
|
||||||
|
ensureFile(file, `Cesium semantic asset '${asset.id}'`);
|
||||||
|
records[asset.id] = fileRecord(file);
|
||||||
|
}
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
|
||||||
function compressCesiumGlb(area) {
|
function compressCesiumGlb(area) {
|
||||||
ensureFile(area.outputs.glb, "Cesium GLB");
|
ensureFile(area.outputs.glb, "Cesium GLB");
|
||||||
ensureFile(area.outputs.metadata, "Cesium metadata");
|
ensureFile(area.outputs.metadata, "Cesium metadata");
|
||||||
|
|||||||
@@ -144,13 +144,31 @@ function writeCompressedMetadata(sourceMetadata, outputGlb, outputMetadata) {
|
|||||||
const metadata = JSON.parse(fs.readFileSync(sourceMetadata, "utf8"));
|
const metadata = JSON.parse(fs.readFileSync(sourceMetadata, "utf8"));
|
||||||
const glbName = path.basename(outputGlb);
|
const glbName = path.basename(outputGlb);
|
||||||
metadata.asset = glbName;
|
metadata.asset = glbName;
|
||||||
metadata.assets = [{
|
const assets = Array.isArray(metadata.assets) ? metadata.assets : [];
|
||||||
|
const main = assets.find((asset) => asset.id === "main");
|
||||||
|
metadata.assets = assets.length
|
||||||
|
? assets.map((asset) => asset.id === "main" ? {
|
||||||
|
...asset,
|
||||||
|
label: "Compressed Scene",
|
||||||
|
url: glbName,
|
||||||
|
enabled: true,
|
||||||
|
} : asset)
|
||||||
|
: [{
|
||||||
id: "main",
|
id: "main",
|
||||||
label: "Compressed Scene",
|
label: "Compressed Scene",
|
||||||
type: "model",
|
type: "model",
|
||||||
url: glbName,
|
url: glbName,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
}];
|
}];
|
||||||
|
if (!main && assets.length) {
|
||||||
|
metadata.assets.unshift({
|
||||||
|
id: "main",
|
||||||
|
label: "Compressed Scene",
|
||||||
|
type: "model",
|
||||||
|
url: glbName,
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
if (typeof metadata.cesium_js === "string") {
|
if (typeof metadata.cesium_js === "string") {
|
||||||
metadata.cesium_js = metadata.cesium_js.replace(
|
metadata.cesium_js = metadata.cesium_js.replace(
|
||||||
/url: '[^']+\.glb'/,
|
/url: '[^']+\.glb'/,
|
||||||
@@ -270,9 +288,13 @@ function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (require.main === module) {
|
||||||
|
try {
|
||||||
main();
|
main();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error.message);
|
console.error(error.message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = { writeCompressedMetadata };
|
||||||
|
|||||||
@@ -42,8 +42,14 @@ function cesiumPreviewHtml(glbName, metadataName, routeName, vehicleModelName, a
|
|||||||
<span id="speedLabel">8 m/s</span>
|
<span id="speedLabel">8 m/s</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
|
<span class="control-label">View</span>
|
||||||
|
<span id="viewMode" class="segmented-control" role="group" aria-label="Scene view mode">
|
||||||
|
<button type="button" data-view-mode="scene" aria-pressed="true">Scene</button>
|
||||||
|
<button type="button" data-view-mode="inspect" aria-pressed="false">Inspect</button>
|
||||||
|
</span>
|
||||||
<label><input id="toggleScene" type="checkbox" checked> Scene</label>
|
<label><input id="toggleScene" type="checkbox" checked> Scene</label>
|
||||||
<span id="assetToggles" class="control-subgroup"></span>
|
<span id="assetToggles" class="control-subgroup"></span>
|
||||||
|
<span id="semanticToggles" class="control-subgroup hidden"></span>
|
||||||
<label><input id="toggleRoutes" type="checkbox" checked> Routes</label>
|
<label><input id="toggleRoutes" type="checkbox" checked> Routes</label>
|
||||||
<label><input id="toggleVehicles" type="checkbox" checked> Vehicles</label>
|
<label><input id="toggleVehicles" type="checkbox" checked> Vehicles</label>
|
||||||
<label><input id="toggleFps" type="checkbox"> FPS</label>
|
<label><input id="toggleFps" type="checkbox"> FPS</label>
|
||||||
|
|||||||
@@ -198,6 +198,35 @@ body.scene-error .loading-bar span {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.control-label {
|
||||||
|
color: rgba(255, 255, 255, 0.68);
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmented-control {
|
||||||
|
display: inline-flex;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.24);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmented-control button {
|
||||||
|
min-width: 58px;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
background: transparent !important;
|
||||||
|
color: rgba(255, 255, 255, 0.78) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmented-control button + button {
|
||||||
|
border-left: 1px solid rgba(255, 255, 255, 0.20) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmented-control button[aria-pressed="true"] {
|
||||||
|
background: #e7f2f2 !important;
|
||||||
|
color: #143337 !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Playback / layers / camera each read as their own row of the panel. */
|
/* Playback / layers / camera each read as their own row of the panel. */
|
||||||
.control-group {
|
.control-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -228,3 +257,32 @@ body.scene-error .loading-bar span {
|
|||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* This must outrank `#controls label { display: inline-flex }` above. */
|
||||||
|
#controls .hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
#controls {
|
||||||
|
right: 10px;
|
||||||
|
left: 10px;
|
||||||
|
top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#diagnostics {
|
||||||
|
top: auto;
|
||||||
|
right: 10px;
|
||||||
|
bottom: 56px;
|
||||||
|
left: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#status {
|
||||||
|
right: 10px;
|
||||||
|
left: 10px;
|
||||||
|
bottom: 10px;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,10 +13,12 @@
|
|||||||
const toggleFps = document.getElementById("toggleFps");
|
const toggleFps = document.getElementById("toggleFps");
|
||||||
const toggleDiagnostics = document.getElementById("toggleDiagnostics");
|
const toggleDiagnostics = document.getElementById("toggleDiagnostics");
|
||||||
const assetToggles = document.getElementById("assetToggles");
|
const assetToggles = document.getElementById("assetToggles");
|
||||||
|
const semanticToggles = document.getElementById("semanticToggles");
|
||||||
const vehicleSelect = document.getElementById("vehicleSelect");
|
const vehicleSelect = document.getElementById("vehicleSelect");
|
||||||
const speedControl = document.getElementById("speedControl");
|
const speedControl = document.getElementById("speedControl");
|
||||||
const speedLabel = document.getElementById("speedLabel");
|
const speedLabel = document.getElementById("speedLabel");
|
||||||
const cameraButtons = Array.from(document.querySelectorAll("[data-camera]"));
|
const cameraButtons = Array.from(document.querySelectorAll("[data-camera]"));
|
||||||
|
const viewModeButtons = Array.from(document.querySelectorAll("[data-view-mode]"));
|
||||||
|
|
||||||
// Status carries two kinds of message: the scene summary, which is what the
|
// Status carries two kinds of message: the scene summary, which is what the
|
||||||
// panel should read whenever nothing else is going on, and transient notes
|
// panel should read whenever nothing else is going on, and transient notes
|
||||||
@@ -39,7 +41,8 @@
|
|||||||
const cameras = createCameraPresets(viewer, metadata, placement, cruise);
|
const cameras = createCameraPresets(viewer, metadata, placement, cruise);
|
||||||
|
|
||||||
buildAssetToggles(assets);
|
buildAssetToggles(assets);
|
||||||
bindRuntimeControls(viewer, assets, cruise, cameras);
|
buildSemanticToggles(viewer, assets, placement);
|
||||||
|
bindRuntimeControls(viewer, assets, cruise, cameras, placement);
|
||||||
startDiagnostics(viewer, metadata, assets, cruise, placement);
|
startDiagnostics(viewer, metadata, assets, cruise, placement);
|
||||||
cameras.overview();
|
cameras.overview();
|
||||||
baseStatus = summaryText(metadata, assets, cruise);
|
baseStatus = summaryText(metadata, assets, cruise);
|
||||||
@@ -167,28 +170,47 @@
|
|||||||
for (const asset of normalizedAssets(metadata)) {
|
for (const asset of normalizedAssets(metadata)) {
|
||||||
const id = asset.id || "asset-" + loaded.length;
|
const id = asset.id || "asset-" + loaded.length;
|
||||||
const label = asset.label || asset.id || asset.url;
|
const label = asset.label || asset.id || asset.url;
|
||||||
try {
|
const entry = { ...asset, id, label, model: null, error: null };
|
||||||
const model = await Cesium.Model.fromGltfAsync({
|
loaded.push(entry);
|
||||||
url: asset.url,
|
// Semantic assets are inspection aids. Defer their network and GPU cost
|
||||||
modelMatrix: placement.modelMatrix,
|
// until the user explicitly switches out of the normal scene view.
|
||||||
scale: Number(asset.scale || 1.0)
|
if (entry.category === "semantic") continue;
|
||||||
});
|
await loadAsset(viewer, entry, placement);
|
||||||
model.show = asset.enabled !== false;
|
|
||||||
viewer.scene.primitives.add(model);
|
|
||||||
loaded.push({ id, label, url: asset.url, model, error: null });
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
loaded.push({ id, label, url: asset.url, model: null, error });
|
|
||||||
}
|
}
|
||||||
}
|
if (!loaded.some((asset) => asset.model && asset.category !== "semantic")) {
|
||||||
if (!loaded.some((asset) => asset.model)) {
|
|
||||||
throw new Error("No scene model could be loaded (" + loaded.length + " declared)");
|
throw new Error("No scene model could be loaded (" + loaded.length + " declared)");
|
||||||
}
|
}
|
||||||
return loaded;
|
return loaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadAsset(viewer, asset, placement) {
|
||||||
|
if (asset.model || asset.error) return asset.model;
|
||||||
|
if (asset.loading) return asset.loading;
|
||||||
|
asset.loading = Cesium.Model.fromGltfAsync({
|
||||||
|
url: asset.url,
|
||||||
|
modelMatrix: placement.modelMatrix,
|
||||||
|
scale: Number(asset.scale || 1.0)
|
||||||
|
}).then((model) => {
|
||||||
|
model.show = asset.enabled !== false;
|
||||||
|
viewer.scene.primitives.add(model);
|
||||||
|
asset.model = model;
|
||||||
|
return model;
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
asset.error = error;
|
||||||
|
return null;
|
||||||
|
}).finally(() => {
|
||||||
|
asset.loading = null;
|
||||||
|
});
|
||||||
|
return asset.loading;
|
||||||
|
}
|
||||||
|
|
||||||
function liveAssets(assets) {
|
function liveAssets(assets) {
|
||||||
return assets.filter((asset) => asset.model);
|
return assets.filter((asset) => asset.model && asset.category !== "semantic");
|
||||||
|
}
|
||||||
|
|
||||||
|
function semanticAssets(assets) {
|
||||||
|
return assets.filter((asset) => asset.category === "semantic");
|
||||||
}
|
}
|
||||||
|
|
||||||
// A single-asset scene keeps the plain "Scene" checkbox; a multi-asset one
|
// A single-asset scene keeps the plain "Scene" checkbox; a multi-asset one
|
||||||
@@ -221,8 +243,40 @@
|
|||||||
toggleScene.indeterminate = shown > 0 && shown < live.length;
|
toggleScene.indeterminate = shown > 0 && shown < live.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindRuntimeControls(viewer, assets, cruise, cameras) {
|
function buildSemanticToggles(viewer, assets, placement) {
|
||||||
|
for (const asset of semanticAssets(assets)) {
|
||||||
|
const label = document.createElement("label");
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "checkbox";
|
||||||
|
input.checked = false;
|
||||||
|
input.dataset.assetId = asset.id;
|
||||||
|
input.addEventListener("change", async () => {
|
||||||
|
if (input.checked) {
|
||||||
|
const model = await loadAsset(viewer, asset, placement);
|
||||||
|
if (!model) {
|
||||||
|
input.checked = false;
|
||||||
|
input.disabled = true;
|
||||||
|
setStatus(asset.label + " unavailable");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
model.show = true;
|
||||||
|
} else if (asset.model) {
|
||||||
|
asset.model.show = false;
|
||||||
|
}
|
||||||
|
setStatus(asset.label + (input.checked ? " visible" : " hidden"));
|
||||||
|
});
|
||||||
|
label.appendChild(input);
|
||||||
|
label.appendChild(document.createTextNode(" " + asset.label));
|
||||||
|
semanticToggles.appendChild(label);
|
||||||
|
asset.toggle = input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindRuntimeControls(viewer, assets, cruise, cameras, placement) {
|
||||||
const hasVehicles = cruise.vehicles.length > 0;
|
const hasVehicles = cruise.vehicles.length > 0;
|
||||||
|
const hasSemanticAssets = semanticAssets(assets).length > 0;
|
||||||
|
const sceneLabel = toggleScene.closest("label");
|
||||||
|
let viewMode = "scene";
|
||||||
|
|
||||||
toggleScene.addEventListener("change", () => {
|
toggleScene.addEventListener("change", () => {
|
||||||
for (const asset of liveAssets(assets)) {
|
for (const asset of liveAssets(assets)) {
|
||||||
@@ -245,6 +299,58 @@
|
|||||||
diagnosticsEl.classList.toggle("hidden", !toggleDiagnostics.checked);
|
diagnosticsEl.classList.toggle("hidden", !toggleDiagnostics.checked);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function setViewMode(nextMode) {
|
||||||
|
if (nextMode === viewMode) return;
|
||||||
|
viewMode = nextMode;
|
||||||
|
const inspecting = viewMode === "inspect";
|
||||||
|
for (const button of viewModeButtons) {
|
||||||
|
button.setAttribute("aria-pressed", String(button.dataset.viewMode === viewMode));
|
||||||
|
}
|
||||||
|
semanticToggles.classList.toggle("hidden", !inspecting);
|
||||||
|
assetToggles.classList.toggle("hidden", inspecting);
|
||||||
|
if (sceneLabel) sceneLabel.classList.toggle("hidden", inspecting);
|
||||||
|
|
||||||
|
if (!inspecting) {
|
||||||
|
for (const asset of semanticAssets(assets)) {
|
||||||
|
if (asset.model) asset.model.show = false;
|
||||||
|
}
|
||||||
|
for (const asset of liveAssets(assets)) asset.model.show = toggleScene.checked;
|
||||||
|
setStatus("Scene view");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const asset of liveAssets(assets)) asset.model.show = false;
|
||||||
|
await Promise.all(semanticAssets(assets).map(async (asset) => {
|
||||||
|
const model = await loadAsset(viewer, asset, placement);
|
||||||
|
if (!model) {
|
||||||
|
if (asset.toggle) {
|
||||||
|
asset.toggle.checked = false;
|
||||||
|
asset.toggle.disabled = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
model.show = true;
|
||||||
|
if (asset.toggle) asset.toggle.checked = true;
|
||||||
|
}));
|
||||||
|
if (viewMode !== "inspect") {
|
||||||
|
for (const asset of semanticAssets(assets)) {
|
||||||
|
if (asset.model) asset.model.show = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStatus("Inspection view");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const button of viewModeButtons) {
|
||||||
|
if (button.dataset.viewMode === "inspect" && !hasSemanticAssets) {
|
||||||
|
button.disabled = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
void setViewMode(button.dataset.viewMode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let stopFollow = function () {};
|
let stopFollow = function () {};
|
||||||
if (hasVehicles) {
|
if (hasVehicles) {
|
||||||
stopFollow = bindCruiseControls(viewer, cruise);
|
stopFollow = bindCruiseControls(viewer, cruise);
|
||||||
|
|||||||
32
scripts/test-compress-glb.js
Normal file
32
scripts/test-compress-glb.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const assert = require("assert");
|
||||||
|
const fs = require("fs");
|
||||||
|
const os = require("os");
|
||||||
|
const path = require("path");
|
||||||
|
const { writeCompressedMetadata } = require("./compress-glb");
|
||||||
|
|
||||||
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "compress-glb-"));
|
||||||
|
const source = path.join(tempDir, "scene.json");
|
||||||
|
const output = path.join(tempDir, "scene-compressed.json");
|
||||||
|
const compressedGlb = path.join(tempDir, "scene-compressed.glb");
|
||||||
|
|
||||||
|
fs.writeFileSync(source, `${JSON.stringify({
|
||||||
|
asset: "scene.glb",
|
||||||
|
assets: [
|
||||||
|
{ id: "main", label: "Scene", type: "model", url: "scene.glb", enabled: true },
|
||||||
|
{ id: "roads", label: "道路", type: "model", url: "scene-roads.glb", enabled: false, category: "semantic" },
|
||||||
|
],
|
||||||
|
}, null, 2)}\n`);
|
||||||
|
|
||||||
|
writeCompressedMetadata(source, compressedGlb, output);
|
||||||
|
const metadata = JSON.parse(fs.readFileSync(output, "utf8"));
|
||||||
|
assert.equal(metadata.asset, "scene-compressed.glb");
|
||||||
|
assert.deepEqual(metadata.assets.map((asset) => asset.id), ["main", "roads"]);
|
||||||
|
assert.equal(metadata.assets[0].url, "scene-compressed.glb");
|
||||||
|
assert.equal(metadata.assets[1].url, "scene-roads.glb");
|
||||||
|
assert.equal(metadata.assets[1].category, "semantic");
|
||||||
|
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
console.log("GLB compression metadata tests passed.");
|
||||||
@@ -80,6 +80,9 @@ assert.match(html, /<title>north<&>\u2028valley Cesium Preview<\/title
|
|||||||
assert.match(html, /Loading scene<&>\.glb/);
|
assert.match(html, /Loading scene<&>\.glb/);
|
||||||
assert.match(html, /"areaId":"north\\u003c\\u0026\\u003e\\u2028valley"/);
|
assert.match(html, /"areaId":"north\\u003c\\u0026\\u003e\\u2028valley"/);
|
||||||
assert.match(html, /"glbName":"scene\\u003c\\u0026\\u003e\.glb"/);
|
assert.match(html, /"glbName":"scene\\u003c\\u0026\\u003e\.glb"/);
|
||||||
|
assert.match(html, /id="viewMode"/);
|
||||||
|
assert.match(html, /data-view-mode="inspect"/);
|
||||||
|
assert.match(html, /id="semanticToggles" class="control-subgroup hidden"/);
|
||||||
|
|
||||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
console.log("Preview asset tests passed.");
|
console.log("Preview asset tests passed.");
|
||||||
|
|||||||
Reference in New Issue
Block a user