printbench 0.0.0-stage → 0.1.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/README.md CHANGED
@@ -1,3 +1,224 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # PrintBench
2
+
3
+ 纯浏览器 3D 打印模型工作台(Vue 3 + three.js + Manifold)。在本地解析、体检、修复网格,再导出给切片软件。几何处理在浏览器内完成,审核报告默认不上传后端。
4
+
5
+ 开发服务路径为 `/print-bench/`(根路径会重定向到该前缀)。
6
+
7
+ ## 环境
8
+
9
+ - Node.js(建议 20+)
10
+ - 现代浏览器(需 WebGL、Web Worker、WASM)
11
+
12
+ ```bash
13
+ npm install
14
+ npm run dev # 打开提示的地址,进入 /print-bench/
15
+ npm run build # 产出 SPA 静态资源(写入 dist/)
16
+ npm run build:lib # 产出可被宿主安装的库(写入 dist/,与 SPA 互斥覆盖)
17
+ npm run preview # 预览 SPA 构建结果
18
+ npm test
19
+ ```
20
+
21
+ 静态部署时,站点 `base` 为 `/print-bench/`,需按该子路径托管。
22
+
23
+ ## 作为 npm 库嵌入(独立 Vue 宿主)
24
+
25
+ 本仓可打成 ESM 组件包,供其它 Vue 3 项目引用。
26
+
27
+ ### 1. 构建产物
28
+
29
+ ```bash
30
+ npm run build:lib
31
+ ```
32
+
33
+ 产出:`dist/printbench.js`(内含内联 Worker + WASM)、`dist/printbench.css`、`dist/printbench.d.ts`。
34
+
35
+ Worker 与 `manifold.wasm` 都打进主包,宿主 Vite 不再请求 `/@fs/.../manifold.worker.js` 或 `/@fs/.../manifold.wasm`。
36
+
37
+ ### 2. 宿主安装
38
+
39
+ 联调(同机独立仓库):
40
+
41
+ ```json
42
+ {
43
+ "dependencies": {
44
+ "printbench": "file:../PrintBench",
45
+ "vue": "^3.5.0",
46
+ "pinia": "^3.0.0"
47
+ }
48
+ }
49
+ ```
50
+
51
+ 安装前请先在本仓执行 `npm run build:lib`。正式环境改为私有 npm 版本号即可。
52
+
53
+ ### 3. 宿主使用
54
+
55
+ ```ts
56
+ import { createApp } from 'vue'
57
+ import { createPinia } from 'pinia'
58
+ import { PrintBench } from 'printbench'
59
+ import 'printbench/style.css'
60
+ import App from './App.vue'
61
+
62
+ createApp(App).use(createPinia()).mount('#app')
63
+ ```
64
+
65
+ ```vue
66
+ <template>
67
+ <PrintBench
68
+ height="100vh"
69
+ upload-url="https://api.example.com/api/printbench/exports"
70
+ :model-url="modelUrl"
71
+ @ready="onReady"
72
+ @error="onError"
73
+ @exported="onExported"
74
+ @report="onReport"
75
+ />
76
+ </template>
77
+ ```
78
+
79
+ | props | 说明 |
80
+ |-------|------|
81
+ | `uploadUrl` | 导出上传地址;优先于构建时的 `VITE_UPLOAD_URL` |
82
+ | `modelUrl` | 挂载后自动按 URL 导入模型 |
83
+ | `height` | 根容器高度,默认 `100%` |
84
+
85
+ | emits | 说明 |
86
+ |-------|------|
87
+ | `ready` | 几何内核就绪 |
88
+ | `error` | 可展示错误信息 |
89
+ | `exported` | `{ format, fileName, blob, unit, axis? }` |
90
+ | `report` | 审核报告对象(下载 JSON/HTML 时触发) |
91
+
92
+ peer:`vue`、`pinia`。运行时依赖(`three`、`manifold-3d` 等)随本包装入 `dependencies`,由 npm 安装传递。
93
+
94
+ 宿主 Vite 建议排除预打包(避免改写 `printbench.js` 的 `import.meta.url`):
95
+
96
+ ```ts
97
+ optimizeDeps: {
98
+ exclude: ['printbench', 'manifold-3d'],
99
+ },
100
+ ```
101
+
102
+ Worker 与 WASM 已内联,npm 与 `file:` 都不应再请求独立的 `manifold.worker.js` / `manifold.wasm`。
103
+
104
+ ## 推荐流程
105
+
106
+ 1. **导入模型** → 确认单位(看底栏包围盒)
107
+ 2. **看右侧诊断** → 点条目飞到缺陷;需要时 **一键修复**(红绿对拍后确认)
108
+ 3. **导出** STL / 3MF → 在切片软件(Cura、PrusaSlicer 等)里按毫米(或导出时所选单位)解释坐标
109
+
110
+ 存在红色阻断项时无法导出,会定位到对应缺陷。黄色警告可选择继续,但可能影响打印质量。
111
+
112
+ ## 导入
113
+
114
+ 支持 **GLB / GLTF / STL / OBJ / 3MF**,以及含上述模型的 **ZIP**,单文件约 **500MB** 上限。可:
115
+
116
+ - 顶栏 **导入** → 从本地选择
117
+ - 拖入画布,或粘贴文件
118
+ - 填写 **http(s) 直链**(粘贴链接也会打开导入框)
119
+
120
+ 导入后先按毫米预览包围盒,再在对话框里选真实单位(mm / cm / m)。选错会差 10–1000 倍。3MF 会读取文件内的 model unit。动画、材质不进入打印网格。
121
+
122
+ **网络地址**:须目标服务器允许浏览器跨域(CORS)。GitHub 的 `blob` 链接会自动改成 `raw.githubusercontent.com`。无 CORS 时请下载到本地再导入。
123
+
124
+ ## 界面
125
+
126
+ | 区域 | 作用 |
127
+ |------|------|
128
+ | 顶栏 | 见下节「顶栏菜单」 |
129
+ | 左栏 | 零件树与编辑工具;标题栏按钮可收起/展开(默认展开) |
130
+ | 画布 | 轨道旋转缩放;双击或 **F** 按模型自适应;**复位视角** 回到默认相机;未导入时显示引导 |
131
+ | 右栏 | 诊断与测量;标题栏按钮可收起/展开(默认展开) |
132
+ | 底栏 | 包围盒、内部单位 mm、体积、文件名 |
133
+
134
+ 首次打开可走三步引导;**帮助** 里可重看。发现本地草稿时会询问是否恢复。未导入模型时,除导入、草稿、设置、帮助外,顶栏多数按钮不可用。
135
+
136
+ ## 顶栏菜单
137
+
138
+ 从左到右大致是:进文件 → 看模型 → 量尺寸 → 修复与导出;最右侧是草稿、设置、帮助。左栏的零件树、布尔、切底座等不在顶栏。
139
+
140
+ **文件与历史**
141
+
142
+ | 项 | 说明 |
143
+ |----|------|
144
+ | 导入 | 本地文件或 http(s) 地址;拖放、粘贴文件或链接也可 |
145
+ | 撤销 / 重做 | 操作树历史(Ctrl+Z / Ctrl+Y) |
146
+ | 一键修复 | 对当前模型修复,画布红/绿对拍,确认才保留 |
147
+ | 导出 | 红级诊断会拦截;黄级可选择继续。对话框内可下载 STL/3MF,或一键上传 |
148
+ | 审核报告 | 体积、包围盒、校验与测量;可下 JSON/HTML;无红级时可本地标「可打印」 |
149
+
150
+ **观察(不改网格)**
151
+
152
+ | 项 | 说明 |
153
+ |----|------|
154
+ | 复位视角 | 相机回到默认朝向与距离,不改零件姿态。画布 **双击** 或 **F** 是按模型自适应取景,与复位不同 |
155
+ | 实体 / 线框 / 半透明 / 法线 | 仅改变显示,不改变几何 |
156
+ | 壁厚热力图 | 按设置中的最小壁厚给顶点着色,便于看薄壁 |
157
+ | 剖切 | 画布出现高度滑条,按 Y 切开看内部。与左栏 **切割 +Y**(真裁掉底座)不是同一功能 |
158
+
159
+ **测量**
160
+
161
+ 关 / 长度 / 角度 / 半径。在模型上点吸附点(顶点、边中点、面心)。选「长度」时出现 **目标 mm**,用来算偏差。结果列在右栏。**Esc** 退出测量。
162
+
163
+ **右侧(随时可用)**
164
+
165
+ | 项 | 说明 |
166
+ |----|------|
167
+ | 草稿 | 命名保存 / 打开 / 删除;Ctrl+S 另有自动草稿 |
168
+ | 设置 | 默认导入单位、默认导出格式、网格/灯光/包围盒、最小壁厚 |
169
+ | 帮助 | 教程与快捷键,可重看引导 |
170
+
171
+ ## 变换与零件
172
+
173
+ - 工具 **1 / 2 / 3**:移动、旋转、缩放;画布上拖手柄
174
+ - **打印拆件**:左栏放置切面,移动/旋转后确认。沿同一平面拆成两块(默认锯缝 0 mm)。可选球形/方形卡扣:正侧挖卯、负侧加榫,原位互补对插。须先是封闭实体;与「拆分碎片」不同
175
+ - **轴向吸附(Shift)**:移动 1 mm、旋转 15°、缩放 0.1;勾选则常开
176
+ - **数值变换**:位移(mm)、欧拉角(度,XYZ 序)、缩放;点「应用数值」写入当前零件
177
+ - **Ctrl** 点选 / 框选;右键菜单:显隐、复制、删除、合并所选
178
+ - 可添加立方体 / 圆柱 / 球 / 棱柱,再做并集、差集、交集(先预览,再确认精算)
179
+
180
+ ## 诊断与修复
181
+
182
+ 导入后自动体检(封闭、流形、薄壁等)。点诊断飞到开边、薄壁等位置。
183
+
184
+ **一键修复**:红为修复前、绿为修复后,确认才保留。破洞过多可先 **拆分碎片** 或 **简化对拍**。
185
+
186
+ **打印拆件**:左栏放置切面并确认,Manifold 按平面裁成两块(可设锯缝)。可选球形/方形卡扣(切口互补对插)。须水密。与顶栏剖切、切底座不是同一功能。
187
+
188
+ **切底座 / 切割 +Y**:按给定高度用 +Y 平面裁掉下方,便于贴床。
189
+
190
+ **简化对拍**:降低面数,红绿对比后确认或撤销。
191
+
192
+ 顶栏 **剖切** 只改显示,不改网格。**壁厚热力图** 按设置里的最小壁厚着色。
193
+
194
+ ## 测量
195
+
196
+ 见上文「顶栏菜单 · 测量」。结果列在右栏。
197
+
198
+ ## 导出与审核
199
+
200
+ - **STL**:二进制;轴向与当前导入或编辑后的网格一致。单个零件为 `.stl`;多个零件打包为 `.zip`(每件一个 STL),再导入可保持零件个数。STL 无单位字段,切片软件需与导出单位一致(推荐毫米)
201
+ - **3MF**:自带毫米单位;每个零件单独写成 object,导入时按对象拆开
202
+ - **一键上传**:生成与下载相同的文件后,以 `multipart/form-data` POST 到服务端预留接口(字段 `file`、`meta` JSON)。配置环境变量 `VITE_UPLOAD_URL`(参见 `.env.example`)后生效;未配置时提示接口已预留。
203
+
204
+ **审核报告**:体积、包围盒、校验与测量;可下载 JSON / HTML。无红级阻断时可本地标记为可打印。上传走同一预留接口。
205
+
206
+ ## 草稿与设置
207
+
208
+ - **Ctrl+S**:自动草稿到浏览器本地
209
+ - **草稿**:命名保存 / 打开 / 删除
210
+ - **设置**:默认导入单位、默认导出格式、网格/灯光/包围盒、最小壁厚(mm)
211
+
212
+ ## 快捷键
213
+
214
+ | 按键 | 作用 |
215
+ |------|------|
216
+ | Ctrl+Z / Ctrl+Y | 撤销 / 重做 |
217
+ | Ctrl+S | 保存自动草稿 |
218
+ | 1 / 2 / 3 | 移动 / 旋转 / 缩放 |
219
+ | Esc | 取消手柄、预览、测量 |
220
+ | F | 按模型自适应视角(与「复位视角」不同) |
221
+ | Shift | 变换吸附 |
222
+ | Ctrl | 多选、框选 |
223
+
224
+ 出错时可复制错误信息;模型仍可尝试继续导出。
@@ -0,0 +1 @@
1
+ :root{--bg: #0e1116;--panel: #171b22;--panel-2: #1e242e;--border: #2c3440;--text: #e8edf4;--muted: #8b95a5;--accent: #5b8def;--danger: #ff5c5c;--warn: #f0b429;--ok: #3dcc7a;--shadow: 0 8px 32px rgba(0, 0, 0, .35);font-family:Segoe UI,PingFang SC,Microsoft YaHei,sans-serif}.printbench-root,.printbench-root *{box-sizing:border-box}.printbench-root{height:100%;min-height:0;background:var(--bg);color:var(--text);color-scheme:dark;font-family:Segoe UI,PingFang SC,Microsoft YaHei,sans-serif}.printbench-root button,.printbench-root input,.printbench-root select,.printbench-root textarea{font:inherit;color:inherit;color-scheme:dark}.printbench-root select option{background:var(--panel-2);color:var(--text)}.workbench{display:grid;grid-template-rows:48px 1fr 28px;grid-template-columns:292px 1fr 300px;height:100%}.workbench.no-left{grid-template-columns:36px 1fr 300px}.workbench.no-right{grid-template-columns:292px 1fr 36px}.workbench.no-left.no-right{grid-template-columns:36px 1fr 36px}.topbar,.statusbar{grid-column:1 / -1}.panel{background:var(--panel);border-right:1px solid var(--border);display:flex;flex-direction:column;min-height:0}.panel.right{border-right:none;border-left:1px solid var(--border)}.panel-hd{display:flex;align-items:center;gap:6px;padding:10px 12px;font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);border-bottom:1px solid var(--border)}.panel-hd .ttl{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.panel-toggle{flex:none;width:24px;height:24px;padding:0;background:var(--panel-2);border:1px solid var(--border);border-radius:4px;color:var(--text);cursor:pointer;line-height:1;font-size:16px}.panel-toggle:hover{border-color:var(--accent);color:var(--accent)}.panel.collapsed .panel-hd{flex-direction:column;height:100%;padding:8px 4px;border-bottom:none;justify-content:flex-start}.panel.collapsed .panel-toggle{order:-1}.panel.collapsed .ttl{writing-mode:vertical-rl;flex:none;letter-spacing:.2em}.panel-bd{padding:10px;overflow:auto;flex:1;display:flex;flex-direction:column;gap:8px}.topbar{display:flex;align-items:center;gap:8px;padding:0 10px;background:var(--panel);border-bottom:1px solid var(--border)}.brand{font-weight:700;margin-right:12px}.brand span{color:var(--accent)}.btn{background:var(--panel-2);border:1px solid var(--border);border-radius:6px;padding:5px 10px;cursor:pointer}.btn:hover{border-color:var(--accent)}.btn.primary{background:var(--accent);border-color:var(--accent);color:#fff}.btn.danger{border-color:var(--danger);color:var(--danger)}.btn:disabled{opacity:.45;cursor:not-allowed}.canvas-wrap{position:relative;min-width:0;min-height:0;background:#12151c}.canvas-wrap canvas{width:100%;height:100%;display:block}.empty{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none;color:var(--muted);text-align:center}.empty h2{color:var(--text);margin:0 0 8px;font-weight:600}.steps{display:flex;gap:18px;margin-top:16px;font-size:13px}.steps b{color:var(--accent)}.tree{display:flex;flex-direction:column;gap:2px;background:var(--panel-2);border:1px solid var(--border);border-radius:8px;padding:4px;min-height:120px;overflow-y:auto}.tree-empty{margin:0;padding:14px 8px;text-align:center;font-size:12px;color:var(--muted)}.item{display:flex;align-items:center;gap:8px;padding:7px 8px;border-radius:6px;cursor:pointer}.item:hover{background:#5b8def14}.item.active{background:#263044}.item .idx{flex:none;width:18px;font-size:11px;color:var(--muted);text-align:right}.item .grow{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px}.item input[type=checkbox]{accent-color:var(--accent);margin:0}.hint{margin:0;font-size:11px;line-height:1.45;color:var(--muted)}.sec{margin:4px 0 0;padding:10px 0 2px;border-top:1px solid var(--border);font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}.btn-grid{display:grid;grid-template-columns:1fr 1fr;gap:6px}.btn-grid .btn,.btn-row .btn{width:100%;font-size:12px;padding:6px 8px}.btn-grid select{width:100%;min-width:0;background:var(--panel-2);border:1px solid var(--border);border-radius:6px;padding:6px 8px;font-size:12px}.btn-row{display:flex;gap:6px}.rename{display:grid;grid-template-columns:1fr auto;gap:6px}.rename input{width:100%;min-width:0;background:var(--panel-2);border:1px solid var(--border);border-radius:6px;padding:5px 8px}.xyz{display:grid;grid-template-columns:48px 1fr 1fr 1fr;gap:4px;align-items:center;font-size:12px}.xyz>span{color:var(--muted)}.xyz input,.xyz select{width:100%;min-width:0;background:var(--panel-2);border:1px solid var(--border);border-radius:4px;padding:4px 5px;color:var(--text);color-scheme:dark}.xyz input{text-align:center}.xyz select{grid-column:2 / -1}.xyz-hd{display:grid;grid-template-columns:48px 1fr 1fr 1fr;gap:4px;font-size:10px;color:var(--muted);text-align:center;margin-bottom:-2px}.stack{display:flex;flex-direction:column;gap:6px}.stack select,.stack input[type=number]{width:100%;min-width:0;background:var(--panel-2);border:1px solid var(--border);border-radius:6px;padding:5px 8px}.check{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted)}.diag{border-left:3px solid var(--border);padding:8px 10px;margin-bottom:8px;background:var(--panel-2);border-radius:0 6px 6px 0;cursor:pointer}.diag.red{border-color:var(--danger)}.diag.yellow{border-color:var(--warn)}.diag.green{border-color:var(--ok)}.diag h4{margin:0 0 4px;font-size:13px}.diag p{margin:0;font-size:12px;color:var(--muted)}.statusbar{display:flex;align-items:center;gap:16px;padding:0 12px;font-size:12px;color:var(--muted);background:var(--panel);border-top:1px solid var(--border)}.overlay{position:fixed;inset:0;background:#0000008c;display:flex;align-items:center;justify-content:center;z-index:40}.dialog{width:min(480px,92vw);background:var(--panel);border:1px solid var(--border);border-radius:10px;box-shadow:var(--shadow);padding:18px}.dialog h3{margin:0 0 10px}.dialog .row{margin:10px 0;display:flex;gap:8px;align-items:center}.dialog select{background:var(--panel-2);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:5px 8px;color-scheme:dark}.dialog select option{background:var(--panel-2);color:var(--text)}.dialog .actions{display:flex;justify-content:flex-end;gap:8px;margin-top:16px}.dialog.help{width:min(640px,94vw);max-height:min(82vh,720px);overflow:auto}.dialog.help p{font-size:13px;line-height:1.55}.dialog .url-input{width:100%;background:var(--panel-2);border:1px solid var(--border);border-radius:6px;padding:8px 10px}.toasts{position:fixed;right:16px;bottom:40px;display:flex;flex-direction:column;gap:8px;z-index:50}.toast{padding:10px 14px;border-radius:8px;background:var(--panel-2);border:1px solid var(--border)}.toast.ok{border-color:var(--ok)}.toast.fail{border-color:var(--danger)}.busy{position:absolute;inset:0;background:#0e11168c;display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:5}.bar{width:180px;height:4px;background:#2a303c;border-radius:4px;overflow:hidden}.bar i{display:block;height:100%;width:40%;background:var(--accent);animation:slide 1s infinite}@keyframes slide{0%{transform:translate(-100%)}to{transform:translate(280%)}}.field{display:flex;align-items:center;gap:8px;margin:6px 0;font-size:13px}.field input,.field select{background:var(--panel-2);border:1px solid var(--border);border-radius:4px;padding:4px 6px;width:90px}.tour{position:absolute;left:50%;top:72px;transform:translate(-50%);background:var(--panel);border:1px solid var(--accent);border-radius:10px;padding:14px 16px;z-index:8;width:360px}.preview-tag{position:absolute;top:10px;left:10px;background:#1d3a24;color:#9dffb0;padding:4px 8px;border-radius:4px;font-size:12px}.marquee{position:absolute;border:1px dashed var(--accent);background:#5b8def1f;pointer-events:none}.ctx{position:fixed;z-index:60;background:var(--panel);border:1px solid var(--border);border-radius:6px;min-width:140px;padding:4px}.ctx button{display:block;width:100%;text-align:left;background:none;border:none;padding:6px 8px;cursor:pointer}.ctx button:hover{background:var(--panel-2)}.clip-slider{position:absolute;top:12px;left:50%;transform:translate(-50%);background:#141820e0;padding:8px 12px;border-radius:8px;font-size:12px;display:flex;align-items:center;gap:8px;z-index:4}.clip-slider input{width:180px}.diag.active{outline:1px solid var(--accent)}.live-read{position:absolute;left:12px;bottom:12px;background:#141820d9;padding:6px 10px;border-radius:6px;font-size:13px;color:#ffe08a}
@@ -0,0 +1,28 @@
1
+ import type { DefineComponent } from 'vue'
2
+
3
+ export interface PrintBenchExportedPayload {
4
+ format: 'stl' | '3mf'
5
+ fileName: string
6
+ blob: Blob
7
+ unit: 'mm' | 'cm' | 'm'
8
+ axis?: 'y-up' | 'z-up'
9
+ }
10
+
11
+ export interface ReviewReport {
12
+ generatedAt: string
13
+ fileName: string
14
+ units: string
15
+ printable?: boolean
16
+ [key: string]: unknown
17
+ }
18
+
19
+ export interface PrintBenchProps {
20
+ uploadUrl?: string
21
+ modelUrl?: string
22
+ height?: string
23
+ }
24
+
25
+ declare const PrintBench: DefineComponent<PrintBenchProps>
26
+ export { PrintBench }
27
+ export default PrintBench
28
+ export type { ReviewReport }