printbench 0.4.5 → 0.5.2

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
@@ -51,8 +51,10 @@ import type { PrintBenchExportedPayload } from 'printbench'
51
51
  import 'printbench/style.css'
52
52
 
53
53
  function onExported(payload: PrintBenchExportedPayload) {
54
+ if (payload.cancelled) return
54
55
  // payload.action: 'download' | 'upload'
55
- // payload.blob / payload.fileName / payload.format / payload.unit
56
+ // payload.blob / fileName / format / unit / size
57
+ // payload.longestEdgeMm — 若导出时指定了按最长边缩放
56
58
  }
57
59
  </script>
58
60
 
@@ -62,16 +64,18 @@ function onExported(payload: PrintBenchExportedPayload) {
62
64
  upload-url="https://api.example.com/api/printbench/exports"
63
65
  :model-url="modelUrl"
64
66
  :custom-export="false"
67
+ :features="{ layout: true, importUnit: true, extras: true }"
68
+ :bed-size-mm="{ x: 220, y: 250, z: 220 }"
69
+ :show-bed-volume="false"
65
70
  @ready="onReady"
66
71
  @error="onError"
67
72
  @exported="onExported"
68
73
  @report="onReport"
74
+ @bbox="onBBox"
69
75
  />
70
76
  </template>
71
77
  ```
72
78
 
73
-
74
-
75
79
  ### Props
76
80
 
77
81
 
@@ -84,23 +88,41 @@ function onExported(payload: PrintBenchExportedPayload) {
84
88
  | `mode` | `'edit' | 'preview'` | `'edit'` | `preview` 为嵌入几何只读:隐藏工作台壳,画布可旋转与点选零件,不能改网格 |
85
89
  | `selectedPartIds` | `string[]` | — | 受控选中。传入后画布点击只触发 `select-part`,由宿主回写 |
86
90
  | `partColors` | `Record<string, string>` | — | 受控零件色,`#RRGGBB`,key 为零件 `id` |
91
+ | `bedSizeMm` | `{ x, y, z }` | — | 宿主默认打印床体积(mm:X/Z 床面,Y 高度)。挂载时写入设置一次,之后仍可在布局区/设置中修改 |
92
+ | `showBedVolume` | `boolean` | — | 宿主默认是否显示打印床线框。挂载时写入设置一次,之后仍可开关(未传时默认隐藏) |
87
93
  | `diagnosisZIndex` | `number` | `100` | 诊断浮层 `z-index`。也可在外层设 CSS 变量 `--printbench-diagnosis-z` |
88
- | `features` | `{ numeric?, primitives?, split?, drafts?, extras?, importUnit?, hideEmpty?, hideStatusBar?, hidePanelModeToggle?, … }` | 能力项默认 `false` | 进阶能力。缺省精简:左栏仅导入 / 零件 / 导出 / 快照。未开 `drafts` 时不读不写自动草稿。未开 `importUnit` 时导入不弹单位确认。`hideEmpty` / `hideStatusBar` / `hidePanelModeToggle` 为 `true` 时分别隐藏空画布提示、底栏状态条、零件栏悬浮/通栏切换按钮。 |
94
+ | `features` | `{ numeric?, primitives?, split?, autoSplit?, drafts?, extras?, layout?, importUnit?, hideEmpty?, hideStatusBar?, hidePanelModeToggle?, … }` | 能力项默认 `false` | 进阶能力。缺省精简:左栏仅导入 / 零件 / 导出 / 快照。未开 `drafts` 时不读不写自动草稿。导入单位优先用文件声明,否则按包围盒尺寸推测 mm/cm/m 再换算到内部毫米;开 `importUnit` 时可再确认。`split` 为切面拆件;`autoSplit` 为「自动拆件」按钮(默认隐藏,需显式打开)。`layout` 打开贴地 / 镜像 / 目标尺寸缩放 / 选面朝下与打印床体积。`hideEmpty` / `hideStatusBar` / `hidePanelModeToggle` 为 `true` 时分别隐藏空画布提示、底栏状态条、零件栏悬浮/通栏切换按钮。 |
89
95
 
90
96
 
91
- 嵌入预览示例(宿主列表 + 色板 + 自定义导出):
97
+ 嵌入预览示例(宿主列表 + 色板 + 自定义导出 + 按最长边缩放):
92
98
 
93
99
  ```vue
94
100
  <script setup lang="ts">
95
101
  import { ref } from 'vue'
96
102
  import { PrintBench } from 'printbench'
97
- import type { PrintBenchExpose } from 'printbench'
103
+ import type { PrintBenchExpose, PrintBenchPartInfo } from 'printbench'
98
104
 
99
105
  const bench = ref<PrintBenchExpose | null>(null)
106
+ const modelUrl = ref('https://example.com/model.glb')
107
+ const selectedIds = ref<string[]>([])
108
+ const colorMap = ref<Record<string, string>>({})
109
+ const parts = ref<PrintBenchPartInfo[]>([])
110
+
111
+ function onSelectPart(payload: { id: string; index: number; additive: boolean }) {
112
+ selectedIds.value = payload.additive
113
+ ? [...new Set([...selectedIds.value, payload.id])]
114
+ : [payload.id]
115
+ }
100
116
 
101
117
  async function download() {
102
- const file = await bench.value?.exportFile({ action: 'download', format: '3mf' })
103
- if (!file) return
118
+ const file = await bench.value?.exportFile({
119
+ action: 'download',
120
+ format: '3mf',
121
+ unit: 'mm',
122
+ longestEdgeMm: 120, // 可选:等比缩放到最长边 120mm 后再导出
123
+ silent: true,
124
+ })
125
+ if (!file || file.cancelled) return
104
126
  const a = document.createElement('a')
105
127
  a.href = URL.createObjectURL(file.blob)
106
128
  a.download = file.fileName
@@ -108,9 +130,13 @@ async function download() {
108
130
  }
109
131
 
110
132
  async function upload() {
111
- const file = await bench.value?.exportFile({ action: 'upload', format: '3mf' })
112
- if (!file) return
113
- // 宿主自己的上传,例如 FormData POST,不必传 uploadUrl
133
+ const file = await bench.value?.exportFile({
134
+ action: 'upload',
135
+ format: 'glb',
136
+ longestEdgeMm: 120,
137
+ })
138
+ if (!file || file.cancelled) return
139
+ // 宿主自己的上传;custom-export 时插件不 POST,只返回 blob
114
140
  await fetch('/api/your-export', { method: 'POST', body: file.blob })
115
141
  }
116
142
  </script>
@@ -125,15 +151,20 @@ async function upload() {
125
151
  :model-url="modelUrl"
126
152
  :selected-part-ids="selectedIds"
127
153
  :part-colors="colorMap"
154
+ :bed-size-mm="{ x: 256, y: 256, z: 256 }"
155
+ :show-bed-volume="false"
128
156
  @parts="parts = $event"
129
157
  @select-part="onSelectPart"
130
158
  @exported="onExported"
159
+ @bbox="onBBox"
131
160
  />
132
161
  </template>
133
162
  ```
134
163
 
135
164
  `mode="preview"` 时插件不显示导出 / 快照按钮。宿主用 `ref.exportFile()` / `ref.exportSnapshot()` 只要文件:`customExport` 为 true 时不本地下载、不 POST,返回 `blob` 后由宿主保存或上传。工作台左栏导出与快照仍可用 `uploadUrl` 走插件上传。
136
165
 
166
+ `exportFile` 的 `longestEdgeMm` 与导出对话框「最长边」一致:按内部毫米将整模 AABB 最长边缩到该值后再换算 `unit` 写出。省略则不缩放。
167
+
137
168
  `@parts` 在导入/求值/变换后给出零件列表。`id` 为 meshId 或基本体/布尔节点 id,不要用下标当主键。每项含 `size`(当前 AABB 边长 mm)与 `initialSize`(导入时该 id 的边长;导入后新建的零件为 `null`)。
138
169
 
139
170
  **填色优先级**:有 `partColors` / 打印色时用纯色(盖过导入贴图);否则用导入外观;再否则默认蓝。
@@ -145,7 +176,7 @@ async function upload() {
145
176
  | -------------------- | ------------------------------------------------- | -------------------------------------------------- |
146
177
  | `ready` | — | 几何内核就绪 |
147
178
  | `error` | `string` | 可展示错误 |
148
- | `exported` | `{ action, format, fileName, blob, unit, size, axis? }` | 网格导出完成。`action` 为 `'download'`(下载)或 `'upload'`(一键上传);`size` 为导出单位下的包围盒 `[x,y,z]` |
179
+ | `exported` | `{ action, format, fileName, blob, unit, size, longestEdgeMm?, axis? }` | 网格导出完成。`action` 为 `'download'`(下载)或 `'upload'`(一键上传);`size` 为导出单位下的包围盒 `[x,y,z]`;若按最长边缩放则带 `longestEdgeMm`(mm) |
149
180
  | `snapshot` | `{ action, format, fileName, blob, mime, width, height }` | 模型快照完成。`format` 为 `'png'` \| `'jpeg'` |
150
181
  | `report` | 审核报告对象 | 下载 JSON / HTML 审核报告时触发 |
151
182
  | `parts` | `{ id, index, name, initialSize, size, unit }[]` | 零件列表;`size` 为当前尺寸,`initialSize` 为导入时尺寸(无则为 `null`) |
@@ -163,8 +194,9 @@ async function upload() {
163
194
 
164
195
  | 方法 | 说明 |
165
196
  | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
166
- | `exportFile(options?)` | 不打开插件对话框。`action`: `'download'` \| `'upload'`;`format`: `'stl'` \| `'3mf'` \| `'glb'`;`unit` 默认 mm;`silent: true` 跳过黄级确认。红级阻断返回 `null`。成功返回与 `exported` 相同的 payload。 |
197
+ | `exportFile(options?)` | 不打开插件对话框。`action`: `'download'` \| `'upload'`;`format`: `'stl'` \| `'3mf'` \| `'glb'`;`unit` 默认 mm;`longestEdgeMm` 可选,按该毫米最长边等比缩放后再导出;`silent: true` 跳过黄级确认。红级阻断返回 `null`。成功返回与 `exported` 相同的 payload。 |
167
198
  | `exportSnapshot(options?)` | 按当前视角截取模型图(不含网格、手柄、切面)。`action`: `'download'` \| `'upload'`;`format`: `'png'`(默认)\| `'jpeg'`;`quality` 仅 JPEG。成功返回与 `snapshot` 相同的 payload。`customExport` 时不本地下载、不 POST。 |
199
+ | `autoSplit(options?)` | 多零件接触面自动装配(≥2 件)。检测接触后一端保留凸起、一端挖槽,再加卡扣。`connector`: `'sphere'` \| `'square'` \| `'ballSnap'`(默认 `sphere`);`connectorSize` 可选;`silent: true` 时不弹修复确认,未封闭则返回 `{ ok: false, needsRepair: true, repairPartNames }`。成功 `{ ok: true }`。 |
168
200
  | `getPartBounds()` | 同步返回与 `@parts` 相同结构的零件列表(含 `initialSize` / `size`)。无模型时为 `[]`。 |
169
201
  | `getBBox()` | 同步返回与 `@bbox` 相同结构的整模载荷(含 `parts`)。尚无诊断包围盒时为 `null`。 |
170
202
 
@@ -188,10 +220,10 @@ async function upload() {
188
220
 
189
221
  | 区域 | 作用 |
190
222
  | --- | ------------------------------------------------------ |
191
- | 左栏 | 导入、导出、快照、零件树、上色、移动/旋转/缩放;`features` 打开后才有数值变换、基本体、拆件、草稿与观察工具 |
192
- | 画布 | 轨道旋转缩放;双击或 **F** 自适应;**复位视角** 回默认相机 |
223
+ | 左栏 | 导入、导出、快照、零件树、上色、移动/旋转/缩放;`features` 打开后才有数值变换、布局(贴地/镜像/缩放/选面朝下)、基本体、拆件、草稿与观察工具 |
224
+ | 画布 | 轨道旋转缩放;双击或 **F** 自适应;**复位视角** 回默认相机;开启 `layout` 后可在布局区/设置中显示打印床线框(默认隐藏) |
193
225
  | 右栏 | 诊断与测量浮层(右上,默认 `z-index: 100`),默认收起 |
194
- | 底栏 | 包围盒、内部单位 mm、体积、文件名 |
226
+ | 底栏 | 包围盒、内部单位 mm、体积、文件名;超床时提示超出轴 |
195
227
 
196
228
 
197
229
 
@@ -201,10 +233,9 @@ async function upload() {
201
233
  - **STL**:二进制;无单位字段,切片软件需与导出单位一致(推荐毫米)。单零件 `.stl`,多零件 `.zip`(每件一个 STL)。不含颜色。
202
234
  - **3MF**:自带毫米单位;每零件单独 object,并写入 `displaycolor`(零件打印色)
203
235
  - **GLB**:单文件多网格;零件打印色写入材质。无打印单位字段,坐标按导出单位解释。
236
+ - **最长边缩放**:导出对话框可填「最长边 mm」(默认预填当前尺寸);或 `exportFile({ longestEdgeMm })`。先按毫米等比缩放,再按所选 `unit` 写出。
204
237
  - **快照**:PNG / JPEG。按当前相机截取模型,不含工作台网格、变换手柄、拆件切面与测量。下载或一键上传(同 `uploadUrl` / `customExport`)。嵌入预览用 `exportSnapshot()`。
205
238
 
206
-
207
-
208
239
  ### 快捷键
209
240
 
210
241
 
@@ -214,13 +245,15 @@ async function upload() {
214
245
  | Ctrl+S | 保存自动草稿(需打开 `features.drafts`) |
215
246
  | 1 / 2 / 3 | 移动 / 旋转 / 缩放 |
216
247
  | P | 切换工作台观察预览 |
217
- | Esc | 取消手柄、预览、测量 |
248
+ | Esc | 取消手柄、预览、测量、选面朝下 |
218
249
  | F | 按模型自适应视角 |
219
250
  | Shift | 变换吸附 |
220
251
  | Ctrl | 多选、框选 |
221
252
 
222
253
 
223
- 草稿保存在浏览器本地,仅当 `features.drafts` 为 true 时读写并提示恢复。设置含默认导入单位、默认导出格式、网格/灯光、最小壁厚。
254
+ 草稿保存在浏览器本地,仅当 `features.drafts` 为 true 时读写并提示恢复。设置含默认导入单位、默认导出格式、网格/灯光、最小壁厚、打印床尺寸(默认 220×220×250 mm,Y 为高度)。打印床线框默认隐藏,可在布局区或设置中打开;宿主可用 props `bedSizeMm` / `showBedVolume` 提供初始默认值(只写入一次,不锁定控件)。
255
+
256
+ 开启 `features.layout` 后:贴地将零件落到 Y=0;镜像相对包围盒中心;「缩放到」按最长边或指定轴等比缩放并自动贴地;「选面朝下」点击表面使该面朝下并贴地。超出打印床时底栏提示;显示床线框时超界会变为警示色。
224
257
 
225
258
  ## 开发本仓库
226
259
 
@@ -1 +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{position:relative;display:grid;grid-template-rows:1fr 28px;grid-template-columns:292px 1fr;height:100%}.workbench.no-left{grid-template-columns:36px 1fr}.workbench.left-float{grid-template-columns:1fr}.workbench.left-float>.panel.parts{position:absolute;left:12px;top:12px;width:280px;max-height:calc(100% - 52px);z-index:12;grid-column:1;grid-row:1;border:1px solid var(--border);border-radius:10px;box-shadow:var(--shadow);background:color-mix(in srgb,var(--panel) 94%,transparent);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.workbench.left-float>.panel.parts.collapsed{width:36px;max-height:none;bottom:40px}.workbench.left-float>.canvas-wrap{grid-column:1;grid-row:1;min-width:0}.workbench.preview-shell{grid-template-rows:1fr;grid-template-columns:1fr}.item input.swatch{flex:none;width:22px;height:18px;padding:0;border:1px solid var(--border);border-radius:4px;background:transparent;cursor:pointer}.statusbar{grid-column:1 / -1}.panel{background:var(--panel);border-right:1px solid var(--border);display:flex;flex-direction:column;min-height:0}.workbench>.panel.right{position:absolute;right:12px;top:12px;width:280px;max-height:calc(100% - 52px);z-index:var(--printbench-diagnosis-z, 100);border:1px solid var(--border);border-radius:10px;box-shadow:var(--shadow);background:color-mix(in srgb,var(--panel) 94%,transparent);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border-right:1px solid var(--border);border-left:1px solid var(--border)}.workbench>.panel.right.collapsed{width:36px;max-height:none}.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;display:inline-flex;align-items:center;justify-content:center}.panel-toggle:hover{border-color:var(--accent);color:var(--accent)}.panel-toggle .ico{width:14px;height:14px;display:block}.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}.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{position:relative;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 .rename-btn{position:absolute;right:6px;top:50%;transform:translateY(-50%);width:auto;padding:2px 6px;font-size:11px;line-height:1.3;color:var(--text);opacity:0;pointer-events:none}.item:hover .rename-btn,.item:focus-within .rename-btn{opacity:1;pointer-events:auto}.item:hover .grow,.item:focus-within .grow{padding-right:40px}.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.core-actions.compact,.btn-grid.tool-mode.compact{gap:4px}.btn-grid.core-actions.icon-text{grid-template-columns:1fr 1fr}.btn-grid.core-actions.icons-only{grid-template-columns:repeat(5,1fr)}.btn-grid.tool-mode{grid-template-columns:repeat(3,1fr)}.btn-grid.core-actions .btn.icon-btn,.btn-grid.tool-mode .btn.icon-btn{display:inline-flex;align-items:center;justify-content:center;gap:4px;padding:4px 6px;font-size:11px;min-height:28px}.btn-grid.core-actions.icons-only .btn.icon-btn,.btn-grid.tool-mode.icons-only .btn.icon-btn{padding:4px}.btn-grid.core-actions .btn.icon-btn .ico,.btn-grid.tool-mode .btn.icon-btn .ico{width:14px;height:14px;flex:none;display:block}.btn-grid.core-actions .btn.icon-btn span,.btn-grid.tool-mode .btn.icon-btn span{line-height:1.2;white-space:nowrap}.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}.split-list{display:flex;flex-direction:column;gap:6px}.split-list .btn-row .btn:first-child{flex:1;width:auto}.split-list .btn-row .btn.danger{flex:none;width:auto}.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:absolute;inset:0;background:#0000008c;display:flex;align-items:center;justify-content:center;z-index:40}.workbench>.overlay{grid-column:1 / -1;grid-row:1 / -1}.dialog{width:min(480px,calc(100% - 24px),92vw);max-width:100%;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;flex-wrap:wrap;justify-content:flex-end;gap:8px;margin-top:16px}.dialog .actions>.btn{flex:0 0 auto;white-space:nowrap}.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}.dialog .snapshot-preview{display:block;width:100%;max-height:min(52vh,420px);object-fit:contain;background:#12151c;border:1px solid var(--border);border-radius:8px}.toasts{position:fixed;top:16px;left:50%;transform:translate(-50%);display:flex;flex-direction:column;align-items:center;gap:8px;z-index:50;pointer-events:none}.toast{pointer-events:auto;padding:10px 14px;border-radius:8px;background:#1e242e;border:1px solid var(--border);color:var(--text);box-shadow:var(--shadow)}.toast.ok{background:#15261c;border-color:var(--ok)}.toast.fail{background:#2a1518;border-color:var(--danger)}.toast.info{background:#1e242e}.busy{position:absolute;inset:0;background:#0e11168c;display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:30;cursor:wait;-webkit-user-select:none;user-select:none}.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}
1
+ .bed-xyz[data-v-d2771f68]{display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px;margin-top:6px}.bed-presets[data-v-d2771f68]{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}.warn[data-v-b085aeeb]{color:#e07040;font-weight:600}: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{position:relative;display:grid;grid-template-rows:1fr 28px;grid-template-columns:292px 1fr;height:100%}.workbench.no-left{grid-template-columns:36px 1fr}.workbench.left-float{grid-template-columns:1fr}.workbench.left-float>.panel.parts{position:absolute;left:12px;top:12px;width:280px;max-height:calc(100% - 52px);z-index:12;grid-column:1;grid-row:1;border:1px solid var(--border);border-radius:10px;box-shadow:var(--shadow);background:color-mix(in srgb,var(--panel) 94%,transparent);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.workbench.left-float>.panel.parts.collapsed{width:36px;max-height:none;bottom:40px}.workbench.left-float>.canvas-wrap{grid-column:1;grid-row:1;min-width:0}.workbench.preview-shell{grid-template-rows:1fr;grid-template-columns:1fr}.item input.swatch{flex:none;width:22px;height:18px;padding:0;border:1px solid var(--border);border-radius:4px;background:transparent;cursor:pointer}.statusbar{grid-column:1 / -1}.panel{background:var(--panel);border-right:1px solid var(--border);display:flex;flex-direction:column;min-height:0}.workbench>.panel.right{position:absolute;right:12px;top:12px;width:280px;max-height:calc(100% - 52px);z-index:var(--printbench-diagnosis-z, 100);border:1px solid var(--border);border-radius:10px;box-shadow:var(--shadow);background:color-mix(in srgb,var(--panel) 94%,transparent);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border-right:1px solid var(--border);border-left:1px solid var(--border)}.workbench>.panel.right.collapsed{width:36px;max-height:none}.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;display:inline-flex;align-items:center;justify-content:center}.panel-toggle:hover{border-color:var(--accent);color:var(--accent)}.panel-toggle .ico{width:14px;height:14px;display:block}.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}.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{position:relative;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 .rename-btn{position:absolute;right:6px;top:50%;transform:translateY(-50%);width:auto;padding:2px 6px;font-size:11px;line-height:1.3;color:var(--text);opacity:0;pointer-events:none}.item:hover .rename-btn,.item:focus-within .rename-btn{opacity:1;pointer-events:auto}.item:hover .grow,.item:focus-within .grow{padding-right:40px}.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.core-actions.compact,.btn-grid.tool-mode.compact{gap:4px}.btn-grid.core-actions.icon-text{grid-template-columns:1fr 1fr}.btn-grid.core-actions.icons-only{grid-template-columns:repeat(5,1fr)}.btn-grid.tool-mode{grid-template-columns:repeat(3,1fr)}.btn-grid.core-actions .btn.icon-btn,.btn-grid.tool-mode .btn.icon-btn{display:inline-flex;align-items:center;justify-content:center;gap:4px;padding:4px 6px;font-size:11px;min-height:28px}.btn-grid.core-actions.icons-only .btn.icon-btn,.btn-grid.tool-mode.icons-only .btn.icon-btn{padding:4px}.btn-grid.core-actions .btn.icon-btn .ico,.btn-grid.tool-mode .btn.icon-btn .ico{width:14px;height:14px;flex:none;display:block}.btn-grid.core-actions .btn.icon-btn span,.btn-grid.tool-mode .btn.icon-btn span{line-height:1.2;white-space:nowrap}.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}.split-list{display:flex;flex-direction:column;gap:6px}.split-list .btn-row .btn:first-child{flex:1;width:auto}.split-list .btn-row .btn.danger{flex:none;width:auto}.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}.scale-row{display:flex;align-items:center;gap:6px;font-size:12px}.scale-row>span{color:var(--muted);flex:none}.scale-row input,.scale-row select{min-width:0;background:var(--panel-2);border:1px solid var(--border);border-radius:4px;padding:4px 5px;color:var(--text);color-scheme:dark}.scale-row input{width:4.5rem;flex:none;text-align:center}.scale-row select{flex: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:absolute;inset:0;background:#0000008c;display:flex;align-items:center;justify-content:center;z-index:40}.workbench>.overlay{grid-column:1 / -1;grid-row:1 / -1}.dialog{width:min(480px,calc(100% - 24px),92vw);max-width:100%;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;flex-wrap:wrap;justify-content:flex-end;gap:8px;margin-top:16px}.dialog .actions>.btn{flex:0 0 auto;white-space:nowrap}.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}.dialog .snapshot-preview{display:block;width:100%;max-height:min(52vh,420px);object-fit:contain;background:#12151c;border:1px solid var(--border);border-radius:8px}.toasts{position:fixed;top:16px;left:50%;transform:translate(-50%);display:flex;flex-direction:column;align-items:center;gap:8px;z-index:50;pointer-events:none}.toast{pointer-events:auto;padding:10px 14px;border-radius:8px;background:#1e242e;border:1px solid var(--border);color:var(--text);box-shadow:var(--shadow)}.toast.ok{background:#15261c;border-color:var(--ok)}.toast.fail{background:#2a1518;border-color:var(--danger)}.toast.info{background:#1e242e}.busy{position:absolute;inset:0;background:#0e11168c;display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:30;cursor:wait;-webkit-user-select:none;user-select:none}.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}