11 KiB
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 更新、否则插最前),并 postMessagelibrary-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 的
#viewportleft 是内联样式(会压过 CSS),手机态要style.left=''
- 抽屉 =
七、测试
playwright-core + 系统 Edge(headless),脚本在 %TEMP%\nds3dtest\:
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)
- dev 内容放站点根目录(3d.ruiguo.wang 根即入口);本地 FTP 目录与生产双向同步
- Web Station → 网站门户 → 该站点 → 编辑 → PHP 选 PHP 8.x(不选=纯静态返回源码)
- File Station 在 web 目录外建
/volume1/stepviewer_private、/volume1/dwgviewer_private, 给http用户组读写权限 - 两个查看器 api/ 的
$SECRET已设为随机值(同查看器三处一致) - nginx 下
.htaccess不生效——防下载靠私有目录方案,不需要 nginx 规则 - 验证清单:token.php 返回 JSON;上传返回裸文件名;直开
.stp/.dwg已不在 web 目录; 嵌入链接能打开;库表增删改持久化
另外
ftp.ruiguo.wang/上海新迪3D/dev/也是同一套文件(PHP 未启用), 上传的加密模型在那里打不开属预期(只读回退),正式使用请走 3d.ruiguo.wang。