170 lines
11 KiB
Markdown
170 lines
11 KiB
Markdown
# 2D / 3D 在线查看器 · 开发文档
|
||
|
||
> 更新:2026-08-21。本文覆盖 `dev/` 目录下的自研查看器(不含 2D/、3D/ 里的新迪官方案例)。
|
||
|
||
## 一、项目结构
|
||
|
||
```
|
||
dev/
|
||
├── index.html ← 统一入口:落地页 + 2D/3D 数模库 + 嵌入预览弹窗
|
||
├── library.php ← 数模库元数据接口(GET 读 / POST 保存)
|
||
├── library.json ← 数模库记录(上传自动收录 + 手动增删改)
|
||
├── DWGViewer/ ← 2D 图纸查看器(.dwg / .dxf)
|
||
│ ├── index.html / app.js / style.css
|
||
│ ├── js/ ← source.js(解析) → flatten.js(展平) → render.js(Canvas2D) → tools.js
|
||
│ ├── libs/dxf-parser(MIT) libs/libredwg-web(GPL-3.0, 仅打开 .dwg 时动态 import)
|
||
│ ├── samples/ ← XQD601.dwg、patent.dwg、pulley.dxf、cushion.dxf
|
||
│ └── api/ ← token.php / model.php / upload.php(防下载三件套)
|
||
└── STEPViewer/ ← 3D 模型查看器(.step / .stp,浏览器内 WASM 直读,无云服务)
|
||
├── index.html / app.js / style.css
|
||
├── libs/three.js + occt-import-js(0.0.23)
|
||
├── samples/ ← as1-oc-214.stp 等
|
||
└── api/ ← 同款三件套(SECRET 与 DWGViewer 不同)
|
||
```
|
||
|
||
## 二、本地开发
|
||
|
||
- 服务器:python http.server 脚本(`%TEMP%\nds_server_step.py` 等),**root 指向 dev/**,
|
||
端口 **8920**;日志 `%TEMP%\req_log_*.txt`
|
||
- 打开 `http://127.0.0.1:8920/`(入口)或直接开 `DWGViewer/index.html?file=samples/XQD601.dwg`
|
||
- **本地没有 PHP**:查看器会自动退回直连(判断依据是接口响应的 content-type 是否为
|
||
`application/json`——python 会把 token.php 源码当静态文件 200 发回来,只看状态码会误判);
|
||
数模库只读(增删改走 library.php 会 501 并给出明确提示)
|
||
- 改完 JS 用 `node --check app.js` 做语法检查
|
||
|
||
## 三、架构要点
|
||
|
||
**2D 数据流**:`source.js`(解析+归一化:libredwg 全给弧度,dxf-parser 混用——统一成弧度)
|
||
→ `flatten.js`(展平成世界坐标显示列表,INSERT 递归烘焙变换,**只做一次**;缩放平移只重算
|
||
屏幕坐标 → 2.9 万实体 6ms/帧)→ `render.js`(Canvas2D,支持旋转/三种背景/线宽)。
|
||
|
||
**3D 数据流**:拖入/`?file=` 取到 STEP 字节 → `occt-import-js` WASM 转换 → three.js 场景
|
||
(MeshPhongMaterial + Ambient/2×Directional,30° 法线平滑)。零件树按 mesh 名分组,
|
||
`S.meshEntryMap` 做 mesh→树行精确映射(视口点选用它,不要按树顺序 first-match)。
|
||
|
||
**入口分发**(dev/index.html):
|
||
- 按扩展名路由:dwg/dxf → DWGViewer,step/stp → STEPViewer;iframe 加载
|
||
- 本地文件:postMessage(viewer-ready 握手后发 viewer-file,只传 ArrayBuffer 不落地)
|
||
- 路径换算:优先「查看器目录内」(HEAD 探测),入口目录兜底;**选中候选后要换算成
|
||
相对查看器的路径**,否则双目录 404
|
||
- 参数透传:`embed=1`、`bg=` 必须显式转发给查看器(入口 URL 不会自动带过去)
|
||
|
||
**嵌入模式**:`?file=路径&embed=1` 隐藏顶栏(3D 另强制高精度);`bg=grey|beige|black` 定背景。
|
||
嵌入地址规则:私有目录文件用**裸文件名**(`?file=xxx.enc`);web 目录内文件写相对路径
|
||
(`samples/xxx.stp`);**`../` 逃出查看器目录的路径会被令牌通道拒绝**(防目录穿越)。
|
||
|
||
## 四、防下载(令牌三件套,PHP 部署后自动生效)
|
||
|
||
```
|
||
查看器 fetch → api/token.php(同源校验 + HMAC 5 分钟令牌)
|
||
→ api/model.php(验令牌 → .enc XOR 解密 / 原文件原样 → gzip 传输)
|
||
上传:POST api/upload.php(X-Filename 头;客户端 gzip → 服务端再 gzip → XOR 加密落盘)
|
||
```
|
||
|
||
- `SECRET`:同一查看器内 token/model/upload 三处一致;DWG 与 STEP 的 SECRET 不同
|
||
- **私有目录**:`$PRIVATE_DIR = '/volume1/stepviewer_private'`(DWG 为 dwgviewer_private)——
|
||
模型存 web 目录外,web 里根本没有文件可下载;model.php 按 `basename($f)` 在私有目录找,
|
||
**找不到时退回 web 目录**(留在 web 目录里的样例仍可按原路径用,但可直链下载)
|
||
- 落盘名:STEP = 原名+8 位随机 hex+.enc;DWG = 纯随机 hex.enc(原文件名藏加密体内,
|
||
model.php 用 `X-Filename` 头带回)
|
||
- 客户端 `apiRequest`(STEP):`.php` 必须拼在 `?` 之前(曾经拼成
|
||
`api/token?f=xxx.stp.php` 导致 PHP 部署下令牌流程整体失效——血泪教训)
|
||
- 接口未部署自动退回直连;接口在跑但拒绝时明确报错,不悄悄回退
|
||
- 网页端无法绝对防抓包,此方案挡的是直接下载、批量爬取、链接外传猜名
|
||
|
||
## 五、数模库(首页库表)
|
||
|
||
- 记录结构:`{path, name, type(2d|3d), note, time}` 存 `library.json`(入口目录)
|
||
- **path(=链接核心)只在新增时可填、之后锁定 🔒**;name/type/note 可编辑;删除只删记录
|
||
- `library.php`:GET 返回全量(静态 JSON 也能读,无 PHP 只读可用);POST 整体保存
|
||
(清洗:path 拒绝 `..`、限制字符长度;LOCK_EX 写文件);Sec-Fetch-Site 弱校验
|
||
- **自动收录**:两个查看器 upload.php 成功后调 `add_to_library()`(`../../library.json`,
|
||
同 path 更新、否则插最前),并 postMessage `library-updated` 让入口刷新
|
||
- 库表功能:打开(按 type 进对应查看器)、复制链接(embed=1 完整地址)、预览
|
||
(弹窗内 iframe 以 embed=1 直载查看器 + 复制 iframe 代码)、编辑、删除、新增、
|
||
搜索(名称/路径/备注)、类型筛选
|
||
|
||
## 六、手机适配(≤768px)
|
||
|
||
- 2D 图层 / 3D 模型树:桌面**默认显示**(按钮 toggle 隐藏);手机变**抽屉**
|
||
(默认隐藏,点「图层」/「结构树」弹出,抽屉内 ✕ 或点空白处收起)
|
||
- 顶栏收起 logo/文件名;底部工具栏横向滚动、图标缩小;浮动面板限宽可滚动
|
||
- 首页:库表外层 `overflow-x:auto` 横向滚动、预览弹窗全屏
|
||
- 实现要点:
|
||
- 抽屉 = `body.tree-open` 类 + `transform: translateX(-100%→0)` + transition
|
||
- **「点外部收起」的 document click 监听会把打开按钮的那次点击也当外部点击**——
|
||
按钮 handler 必须 `stopPropagation()`
|
||
- **媒体查询必须写在被覆盖规则之后**(同特异性后写生效;放前面会被基础规则吃掉)
|
||
- STEP 的 `#viewport` left 是内联样式(会压过 CSS),手机态要 `style.left=''`
|
||
|
||
## 七、测试
|
||
|
||
playwright-core + 系统 Edge(headless),脚本在 `%TEMP%\nds3dtest\`:
|
||
|
||
```js
|
||
const { chromium } = require('playwright-core')
|
||
const browser = await chromium.launch({
|
||
executablePath: 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
|
||
headless: true, args: ['--enable-unsafe-swiftshader', '--disable-gpu', '--ignore-certificate-errors'],
|
||
})
|
||
```
|
||
|
||
关键脚本:`embed_matrix3.js`(嵌入矩阵)、`dwg_php_sim.js` / route 拦截模拟 PHP 令牌流程、
|
||
`prod_final_all.js`(生产回归)、`mobile_test2.js`(手机行为)、`diag_*.js`(诊断)。
|
||
|
||
测试坑:
|
||
- 加载断点别用 `#fileName` 默认文本(非空,会提前 break 假失败)——等含「实体」/「加载完成」
|
||
或 errBox 出现
|
||
- 嵌入模式下顶栏隐藏,`#btnEmbed` 点不到——测嵌入面板用非嵌入模式
|
||
- playwright 点击偶报「tree 拦截指针事件」而 `elementFromPoint` 实际命中按钮——加载完
|
||
等 1.5s 再点 + fallback 坐标点击
|
||
- headless 全新加载测不出缓存问题——**线上用户复现的白屏往往是缓存混版**(见下)
|
||
|
||
## 八、⚠️ 缓存版本号(每次改文件必做)
|
||
|
||
两个查看器的 index.html 用固定 `?v=` 引用 app.js/style.css。**任何一轮修改都必须升级
|
||
版本号**(当前:DWG `app.js?v=2`/`style.css?v=2`;STEP `app.js?v=5`/`style.css?v=5`),
|
||
否则用户浏览器缓存新旧混合(旧 index.html + 新 app.js 引用新 DOM 元素 → 模块崩溃白屏)。
|
||
跨文件 DOM 引用(如 `#btnTreeClose`)一律加 null 容错。
|
||
|
||
## 九、已踩过的坑(速查)
|
||
|
||
**2D**:
|
||
- 大半径圆弧包围盒按**实际弧段**算,不能按整圆(R=49000 的过渡弧会把图纸撑大 20 倍)
|
||
- HATCH 的 offset(DXF 45/46) 是已旋转到世界系的位移向量;ANSI31 等预定义图案要内置图案表现场展开
|
||
- 文档内单位一律弧度;`$INSUNITS` 国内图纸常年默认值,只认显式公制,默认 mm
|
||
- dxf-parser 的 dist 是无扩展名相对 import + 依赖 loglevel(importmap 指到本地 shim)
|
||
- libredwg 的块表在 `db.tables.BLOCK_RECORD.entries[]`,模型空间取 `*Model_Space` 项
|
||
- 该 wasm 构建禁用了 DXF 读写(错误码 2048);alpha:false canvas 要先画一次白底
|
||
|
||
**3D**:
|
||
- occt 0.0.23 的 `attributes.position/index` 是 `{array}` 包装(转 TypedArray);树节点字段是
|
||
`meshes`;`brep_faces` 是 `{first,last,color}[]`
|
||
- 顶点颜色必须 `Color.setRGB(..., THREE.SRGBColorSpace)`,否则模型洗白
|
||
- ⚠️ 曾试 PBR(MeshStandard+RoomEnvironment+ACES) 被用户否掉(上色被洗淡),**不要再加
|
||
色调映射/环境反射**;过暗 CAD 面色(<0.3)自动提亮到 0.35 保色相
|
||
- 剖切平面必须按模型包围盒中心重算(固定原点切不到)
|
||
|
||
**PHP/接口**:
|
||
- PHP 没执行时 token.php 返回源码 200——判部署看 content-type 是否 JSON
|
||
- 群晖 Web Station 的 PHP **按「网站门户」逐个启用**,新域名不会自动继承
|
||
- 同源校验:Sec-Fetch-Site 优先(不受父页面 Referrer-Policy 影响、跨站 iframe 嵌入可用),
|
||
Referer 兜底;直链/curl 两者都过不了
|
||
|
||
**许可**:DWGViewer 的 libredwg-web 是 GPL-3.0(前端分发有传染性,商用需评估);换服务端
|
||
dwg2dxf / ODA SDK 只需替换 `source.js` 的 `loadDwg()`。
|
||
|
||
## 十、部署(群晖 NAS Web Station,生产 3d.ruiguo.wang)
|
||
|
||
1. dev 内容放站点根目录(3d.ruiguo.wang 根即入口);本地 FTP 目录与生产双向同步
|
||
2. Web Station → 网站门户 → 该站点 → 编辑 → **PHP 选 PHP 8.x**(不选=纯静态返回源码)
|
||
3. File Station 在 web 目录外建 `/volume1/stepviewer_private`、`/volume1/dwgviewer_private`,
|
||
给 `http` 用户组读写权限
|
||
4. 两个查看器 api/ 的 `$SECRET` 已设为随机值(同查看器三处一致)
|
||
5. nginx 下 `.htaccess` 不生效——防下载靠私有目录方案,不需要 nginx 规则
|
||
6. 验证清单:token.php 返回 JSON;上传返回裸文件名;直开 `.stp/.dwg` 已不在 web 目录;
|
||
嵌入链接能打开;库表增删改持久化
|
||
|
||
> 另外 `ftp.ruiguo.wang/上海新迪3D/dev/` 也是同一套文件(PHP 未启用),
|
||
> 上传的加密模型在那里打不开属预期(只读回退),正式使用请走 3d.ruiguo.wang。
|