# twinsight **Repository Path**: zdsiyan/twinsight ## Basic Information - **Project Name**: twinsight - **Description**: 前端+nodejs写了一个简单的厂区用数字孪生功能,还不是系统,只用来展示3D模型,有些简单的交互 - **Primary Language**: NodeJS - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-20 - **Last Updated**: 2026-09-22 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 厂区数字孪生 Demo 模型上传 → 后端自动切片为 **3D Tiles** → **Babylon.js** 前端动态流式加载。 ## 文档导航 | 文档 | 内容 | | --- | --- | | **`README.md`**(本文) | 快速开始、常用命令、使用指导 | | **[`AGENTS.md`](./AGENTS.md)** | **硬约束与编码规约**(动代码前必读)、排查手册 | | **[`DEVPLAN.md`](./DEVPLAN.md)** | 开发计划、里程碑进度、下一步待办 | ## 架构 ``` apps/web Vue3 + Vite + TS + Pinia + Element Plus + Babylon.js apps/server Node + Express 5 + 任务队列 + SSE 进度 + 瓦片静态托管 packages/pipeline 3D Tiles 处理流水线(S0 识别 → S6 发布) packages/shared 前后端共用类型与常量 ``` 数据流: ``` 上传 → S0 识别 → S1 归一化(单GLB, X/Z居中+底部在y=0) → S2 优化(weld/draco) → S3 切分(flatten → 划分树 → 多瓦片 + LOD 代理) → S4 生成 tileset.json(1.1, box+误差树) → S5 3d-tiles-tools 后处理(可选) → S6 发布到 data/tilesets/{id} → 前端 TilesRenderer(/tiles/{id}/tileset.json) 按需拉取 ``` ## 快速开始 ```bash npm install cp .env.example .env # Windows: copy .env.example .env npm run seed # 可选:生成示例数据集,便于立刻看到效果 npm run dev ``` - 前端:http://localhost:5173 - 后端:http://localhost:8787/api/health ## 常用命令 ```bash npm run dev # 同时启动前后端 npm run dev:server # 仅后端 npm run dev:web # 仅前端 npm run typecheck # 全工作区类型检查(提交前必过) npm run build # 构建前端产物 npm run seed # 生成示例数据集并发布 # 离线跑一次切片(便于调试 pipeline,不启服务) npm run pipeline -- -i ./samples/sample-factory.glb -o ./data/tilesets/demo --strategy grid ``` ## 怎么用 1. 打开前端,点顶部「模型上传 / 转换」,或直接点左侧数据集面板的「+ 上传模型」(会在当前页弹出上传抽屉)。 2. 拖入 `.glb`(也支持 `.gltf` 打包的 zip;`.obj`/`.fbx` 需额外配置转换器,见 `AGENTS.md §3`)。 3. 选切分策略与参数,点「开始转换」。转换完成后会自动选中新数据集并加载到场景。 4. 在场景里点击构件 → 高亮并在右侧显示属性;在左侧构件树里搜索、显隐、定位。 底部工具栏(左→右):**视角预设**(轴测/俯视/正视/侧视)|**取景 / 线框 / 参考面 / 隔离 / FXAA**| **交互工具**(测量 / 巡检 / 剖切)|**清晰度**。选中某个工具后,工具栏上方会浮出该工具的参数面板。 | 工具 | 用法 | | --- | --- | | **测量** | 在地面或构件上依次点击取点;「距离/折线」给分段长度与总长,「面积」给多边形面积(Newell 法,允许测点不严格共面) | | **巡检** | 点击地面添加航点(≥2 个)→ 播放,相机沿路径贴地巡游;可调速度与视点高度、循环播放 | | **剖切** | 只保留指定高度以下的部分(`y=0` 即地面),拖动滑块切开楼体看内部 | | **隔离** | 选中构件后开启,只显示该构件;配合「淡出」让其余构件半透明,便于观察空间关系 | ## 施工进度着色 按构件的施工状态给模型上色,用于 4D 进度可视化。右侧「施工状态」页签是图例, 可一键把状态色覆盖到模型、点图例只看某一状态(其余半透明淡出)。 | 状态 | 取值 | 颜色 | | --- | --- | --- | | 可施工 | `ready` | 绿 `#22C55E` | | 待料停工 | `material-hold` | 蓝 `#3B82F6` | | 待图停工 | `drawing-hold` | 橙 `#F59E0B` | | 双重滞后 | `delayed` | 红 `#EF4444` | | 已完成 | `completed` | 灰 `#94A3B8` | 状态是**构件级**的:左侧构件树每一行都有状态色点,右侧「构件属性」页会展开该构件的 完成度 / 计划与实际完成日期 / 责任班组 / 备注。 ### 构件角色:不是所有构件都「该」有进度状态 园区模型里混着两类东西: | 角色 | 取值 | 是什么 | | --- | --- | --- | | 进度对象 | `subject` | 正在施工的建筑、正在生产的产品 —— **才有**进度状态 | | 环境背景 | `context` | 园区既有环境(已建成建筑、路灯、道路、绿化、车辆)—— 不归进度管 | **不区分的代价是统计失真**:411 个构件里若有 300 个是树木车辆,覆盖率会被算成 32%, 面板上长期挂着一个**假的**「数据没覆盖全」告警,**真正的漏推送反而被淹没**。 所以覆盖率与「未覆盖」只统计进度对象。 声明方式(优先级 高 → 低,**具体优于笼统**): ```jsonc // 1. 模型节点 extras.role —— 跟随构件本身,最权威 { "assetId": "路灯.001", "role": "context" } // 2. 进度数据里的 contextPatterns(正则数组)—— 一次性声明一批 { "contextPatterns": ["^路灯", "^绿化", "^道路", "^车"], "items": [ /* ... */ ] } ``` 都不声明时**全部按进度对象处理**(保守取值:宁可多算,不要静默漏算)。 环境构件在场景里默认**统一标记成中性色**而不是留白 —— 留白会被误读成「这里没数据」,统一标记才表示「这里不归进度管」; 构件树里用空心环区分。开关在「施工状态」面板里。 > 完整示例见 [`samples/progress-context-example.json`](./samples/progress-context-example.json)。 ### 构件 3D 标签 工具栏「标注」把构件名与状态直接浮在模型上,用来一眼定位问题构件。 - **标注范围**:只看问题(默认,仅停工与滞后)/ 全部 / 仅选中 - **内容密度**:自动(默认,拉远用单行简洁、拉近补上停工原因)/ 详细 / 简洁 - 同屏最多 40 个,按「双重滞后 → 待料 / 待图 → 其它」优先显示;位置互相压叠的会自动让位, 所以**拉近能看到的构件更多** - 点标签即可选中该构件,右侧属性页会展开完整进度信息 > 实时数据接口**已就绪**,等进度系统按契约推送即可: > `PUT/PATCH/DELETE /api/tilesets/{id}/progress`,详见上一节。 > 需要「文件方式」时把文件放 `data/progress/{id}.json`,两条路径共用同一套解析与校验。 > ⚠️ **IoT 传感器可视化已搁置**(见 [`DEVPLAN.md`](./DEVPLAN.md) M5)。 > `apps/web/src/mocks/iot.ts` 与 `components/panel/IotPanel.vue` 保留但未挂载,请勿继续投入。 > 注意 IoT 的「运行/待机/告警」与施工进度是**两套独立语义**,将来接入时不要复用同一套颜色。 ### 接入真实进度数据(外部系统推送) 进度系统把**构件级状态**推给后端即可,前端点「刷新」就能看到。 ```bash # 全量替换 curl -X PUT http://localhost:8787/api/tilesets/{数据集id}/progress \ -H 'content-type: application/json' \ -d '[{ "assetId": "M-001", "status": "ready", "percent": 30, "team": "土建一班" }]' # 增量合并:只推有变化的构件,其余保持不动 curl -X PATCH http://localhost:8787/api/tilesets/{数据集id}/progress \ -H 'content-type: application/json' \ -d '{ "M-006": "material-hold" }' # 清除,回落到演示数据 curl -X DELETE http://localhost:8787/api/tilesets/{数据集id}/progress ``` body 支持三种写法,怎么省事怎么来(完整示例见 `samples/progress-example.json`): ```jsonc // A. 数组 —— 信息最全 [{ "assetId": "M-001", "status": "ready", "percent": 30, "plannedFinish": "2026-10-01", "actualFinish": null, "team": "土建一班", "note": "已具备开工条件" }] // B. 包一层 items { "items": [ /* 同上 */ ] } // C. 映射表 —— 手写最省事 { "M-001": "ready", "M-002": "completed" } ``` **写入接口会返回回执**,让推送方自查数据质量: ```jsonc { "mode": "replace", "accepted": 25, // 成功写入的构件数 "rejected": 2, // 格式非法被丢弃(状态取值未知 / 缺 assetId) "unknownAssetIds": [], // ⚠ 数据里有、模型里没有的构件 ID —— 永远不会被着色,必须核对 "uncovered": 2, // 写入后仍没有进度记录的构件数 "total": 25 } ``` 其它约定: - `assetId` 必须与模型节点的 `extras.assetId`(或节点名)一致。对不上的 ID 会被**写入但永远不着色**, 所以放在 `unknownAssetIds` 里显式暴露,前端面板也会告警 —— 这是最容易踩的坑。 - `percent` / `plannedFinish` / `actualFinish` / `team` / `note` 都是可选的,会显示在「构件属性」页。 - 未知的 `status` 取值会被丢弃并计入 `rejected`,不会污染统计,也不会让整批数据失败。 - **不用接口也可以**:直接把文件放到 `data/progress/{数据集id}.json`(文件名必须等于数据集 id)。 两条路径共用同一套解析与校验,行为完全一致。 - 不提供任何数据时自动回落到确定性演示数据,读取接口的 `source` 字段会标明是 `file` 还是 `mock`。 > 换数据源(改成直连进度系统 / 数据库)只需替换 `apps/server/src/services/progress-service.ts`, > 路由与前端不用动 —— 这是刻意的分层,详见 [`AGENTS.md §4.7`](./AGENTS.md)。 > > ⚠️ 当前接口**无鉴权**,仅适合内网 Demo。对外暴露前必须加认证与限流。 ## 切片策略怎么选 | 策略 | 适用 | 说明 | | --- | --- | --- | | `none` | 小模型 / 快速验证 | 整模型 1 个瓦片,不做空间索引 | | `grid`(默认) | 扁平厂区 | XZ 平面递归四叉,最常用 | | `octree` | 多层厂房、立体管廊 | 三维递归八叉,切分更碎 | 关键参数(界面上可调,`/api/health` 能看到当前默认值): - **叶格边长**(`maxLeafSize`,默认 50m)与**叶格最大图元数**(`maxLeafPrimitives`,默认 64): 两者任一满足即停止细分。调大 → 瓦片更少更大;调小 → 瓦片更多更小。 ⚠️ 模型带纹理图集时,瓦片越多纹理副本越多、总体积会成倍膨胀,此时应**调大**这两个值。 - **生成 LOD**:内部节点用 `weld + simplify` 生成粗模型。实现会实测抽面效果, **降幅不足(< 10%)时该节点不写内容**,只当空间索引用 —— 近处再拉取真实几何。 tileset 的 `refine` **恒为 `REPLACE`**(父瓦片内容在子瓦片加载后被替换)。 ⚠️ 不要改成 `ADD`:它的语义是「父内容始终保留、子内容额外叠加」, 会让同一张表面在多层同时绘制、共面重叠产生深度冲突,表现为「某些构件在特定视角下 被一大片平面挡住,转动视角又好了」。详见 [`AGENTS.md §2.3`](./AGENTS.md)。 - **跨瓦片共享纹理**(`shareTextures`,默认开启):瓦片输出为 `.gltf` + `.bin`,纹理按内容哈希去重后 集中放在 `content/textures/`,**同一张纹理全数据集只存一份**。关闭后每个瓦片是自包含 `.glb` (文件数更少),但共享图集会被逐个复制——实测园区模型源 4.9MB 切成 37 片后会涨到 80MB。 - **Draco 压缩**:默认开启;编码器不可用时只告警不中断。 > 常见问题的「症状 → 原因 → 看哪里」对照表在 [`AGENTS.md §7 排查手册`](./AGENTS.md)。 ## 目录说明 | 路径 | 说明 | | --- | --- | | `packages/shared/src/types.ts` | 前后端共用类型(`JobInfo` / `TilingConfig` / `TilesetManifest`),单一事实来源 | | `packages/pipeline/src/stages/` | 流水线各阶段实现 | | `packages/pipeline/src/tiler/` | 切分核心:`bounds` / `partition` / `lod` / `export-tile` | | `packages/pipeline/src/scripts/cli.ts` | 离线切片 CLI | | `apps/server/src/services/job-service.ts` | 任务队列(并发可配)+ 状态机 + SSE 广播 | | `apps/web/src/babylon/` | Babylon 封装层:运行时 / TilesLayer / 相机 / 拾取 / 高亮 / 测量 / 巡检 / 状态着色 | | `apps/server/src/services/progress-service.ts` | 施工进度取数(真实 JSON 优先 + 演示数据兜底),换数据源只改这里 | | `data/` | 运行期生成:`uploads/` `work/` `tilesets/` `progress/` `db.json`(已 gitignore) | ## 进度 里程碑状态与下一步待办见 [`DEVPLAN.md §3 实施进度`](./DEVPLAN.md)。