printbench 0.2.2 → 0.3.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
@@ -19,13 +19,13 @@ Peer 依赖(宿主需自行安装,大版本对齐):
19
19
 
20
20
  ### Vite 宿主
21
21
 
22
- 开发时建议排除预打包:
22
+ Draco 解码器已打进包,宿主不必再准备 `public/draco/`,也不必为 Draco 去 `optimizeDeps.exclude` PrintBench。`manifold-3d` 仍建议排除预打包:
23
23
 
24
24
  ```ts
25
25
  // vite.config.ts
26
26
  export default defineConfig({
27
27
  optimizeDeps: {
28
- exclude: ['printbench', 'manifold-3d'],
28
+ exclude: ['manifold-3d'],
29
29
  },
30
30
  })
31
31
  ```
@@ -70,17 +70,22 @@ function onExported(payload: PrintBenchExportedPayload) {
70
70
  </template>
71
71
  ```
72
72
 
73
+
74
+
73
75
  ### Props
74
76
 
75
- | 属性 | 类型 | 默认 | 说明 |
76
- |------|------|------|------|
77
- | `height` | `string` | `'100%'` | 根容器高度 |
78
- | `modelUrl` | `string` | — | 挂载后按 URL 自动导入模型(需 CORS) |
79
- | `uploadUrl` | `string` | — | 「一键上传」的 POST 地址;未设则无法上传 |
80
- | `customExport` | `boolean` | `false` | 为 `true` 时,下载 / 一键上传只生成文件并触发 `exported`(用 `action` 区分),不本地下载、不 POST |
81
- | `mode` | `'edit' \| 'preview'` | `'edit'` | `preview` 为嵌入几何只读:隐藏工作台壳,画布可旋转与点选零件,不能改网格 |
82
- | `selectedPartIds` | `string[]` | — | 受控选中。传入后画布点击只触发 `select-part`,由宿主回写 |
83
- | `partColors` | `Record<string, string>` | — | 受控零件色,`#RRGGBB`,key 为零件 `id` |
77
+
78
+ | 属性 | 类型 | 默认 | 说明 |
79
+ | ----------------- | ----------------------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------- |
80
+ | `height` | `string` | `'100%'` | 根容器高度 |
81
+ | `modelUrl` | `string` | — | 挂载后按 URL 自动导入模型(需 CORS) |
82
+ | `uploadUrl` | `string` | — | 「一键上传」的 POST 地址;未设则无法上传 |
83
+ | `customExport` | `boolean` | `false` | 为 `true` 时,下载 / 一键上传只生成文件并触发 `exported`(用 `action` 区分),不本地下载、不 POST |
84
+ | `mode` | `'edit' | 'preview'` | `'edit'` | `preview` 为嵌入几何只读:隐藏工作台壳,画布可旋转与点选零件,不能改网格 |
85
+ | `selectedPartIds` | `string[]` | — | 受控选中。传入后画布点击只触发 `select-part`,由宿主回写 |
86
+ | `partColors` | `Record<string, string>` | — | 受控零件色,`#RRGGBB`,key 为零件 `id` |
87
+ | `features` | `{ numeric?, primitives?, split?, drafts?, extras?, importUnit? }` | 全 `false` | 进阶能力。缺省精简:左栏仅导入 / 零件 / 导出。未开 `drafts` 时不读不写自动草稿。未开 `importUnit` 时导入不弹单位确认,直接用文件声明单位或设置里的默认单位。 |
88
+
84
89
 
85
90
  嵌入预览示例(宿主列表 + 色板 + 自定义导出):
86
91
 
@@ -126,7 +131,7 @@ async function upload() {
126
131
  </template>
127
132
  ```
128
133
 
129
- `mode="preview"` 时插件不显示导出按钮。宿主用 `ref.exportFile()` 只要文件:`customExport` 为 true 时不本地下载、不 POST,返回 `blob` 后由宿主保存或上传。工作台顶栏导出仍可用 `uploadUrl` 走插件上传。
134
+ `mode="preview"` 时插件不显示导出按钮。宿主用 `ref.exportFile()` 只要文件:`customExport` 为 true 时不本地下载、不 POST,返回 `blob` 后由宿主保存或上传。工作台左栏导出仍可用 `uploadUrl` 走插件上传。
130
135
 
131
136
  `@parts` 在导入/求值后给出 `{ id, index, name }[]`。`id` 为 meshId 或基本体/布尔节点 id,不要用下标当主键。
132
137
 
@@ -134,15 +139,17 @@ async function upload() {
134
139
 
135
140
  ### Events
136
141
 
137
- | 事件 | 负载 | 说明 |
138
- |------|------|------|
139
- | `ready` | — | 几何内核就绪 |
140
- | `error` | `string` | 可展示错误 |
141
- | `exported` | `{ action, format, fileName, blob, unit, axis? }` | 导出完成。`action` 为 `'download'`(下载)或 `'upload'`(一键上传) |
142
- | `report` | 审核报告对象 | 下载 JSON / HTML 审核报告时触发 |
143
- | `parts` | `{ id, index, name }[]` | 当前零件列表 |
144
- | `select-part` | `{ id, index, additive }` | 画布或零件树点选 |
145
- | `part-colors-change` | `Record<string, string>` | 工作台内改色时抛出(嵌入预览以 `partColors` 为准) |
142
+
143
+ | 事件 | 负载 | 说明 |
144
+ | -------------------- | ------------------------------------------------- | -------------------------------------------------- |
145
+ | `ready` | — | 几何内核就绪 |
146
+ | `error` | `string` | 可展示错误 |
147
+ | `exported` | `{ action, format, fileName, blob, unit, axis? }` | 导出完成。`action` 为 `'download'`(下载)或 `'upload'`(一键上传) |
148
+ | `report` | 审核报告对象 | 下载 JSON / HTML 审核报告时触发 |
149
+ | `parts` | `{ id, index, name }[]` | 当前零件列表 |
150
+ | `select-part` | `{ id, index, additive }` | 画布或零件树点选 |
151
+ | `part-colors-change` | `Record<string, string>` | 工作台内改色时抛出(嵌入预览以 `partColors` 为准) |
152
+
146
153
 
147
154
  `exported` 的 `format` 为 `'stl' | '3mf' | 'glb'`;多零件 STL 时 `fileName` 为 zip,`blob` 为打包后的文件。GLB 为单文件,零件色写入材质。
148
155
 
@@ -150,31 +157,38 @@ async function upload() {
150
157
 
151
158
  ### 实例方法(`ref`)
152
159
 
153
- | 方法 | 说明 |
154
- |------|------|
155
- | `exportFile(options?)` | 不打开插件对话框。`action`: `'download'` \| `'upload'`;`format`: `'stl'` \| `'3mf'` \| `'glb'`;`unit` 默认 mm;`silent: true` 跳过黄级确认。红级阻断返回 `null`。成功返回与 `exported` 相同的 payload。 |
160
+
161
+ | 方法 | 说明 |
162
+ | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
163
+ | `exportFile(options?)` | 不打开插件对话框。`action`: `'download'` | `'upload'`;`format`: `'stl'` | `'3mf'` | `'glb'`;`unit` 默认 mm;`silent: true` 跳过黄级确认。红级阻断返回 `null`。成功返回与 `exported` 相同的 payload。 |
164
+
165
+
166
+
156
167
 
157
168
  ## 工作台能力
158
169
 
159
- 推荐流程:导入模型并确认单位 → 看右侧诊断,必要时一键修复 → 导出 STL / 3MF 给切片软件(坐标按毫米或导出时所选单位解释)。
170
+ 推荐流程:导入模型并确认单位 → 需要时展开右侧诊断并一键修复 → 导出 STL / 3MF 给切片软件(坐标按毫米或导出时所选单位解释)。
160
171
 
161
172
  有红色阻断项时无法导出。黄色警告可继续,但可能影响打印质量。
162
173
 
163
174
  ### 导入
164
175
 
165
- 支持 **GLB / GLTF / STL / OBJ / 3MF**,以及含上述模型的 **ZIP**,单文件约 **500MB**。可从本地选择、拖入画布、粘贴文件,或填写 http(s) 直链(需 CORS;GitHub `blob` 会改成 raw)。
176
+ 支持 **GLB / GLTF / STL / OBJ / 3MF**,以及含上述模型的 **ZIP**,单文件约 **500MB**。可从本地选择、拖入画布、粘贴文件,或填写 http(s) 直链(需 CORS;GitHub `blob` 会改成 raw)。Draco 压缩的 GLB/GLTF 可直接导入,无需宿主再配解码器路径。
166
177
 
167
178
  导入后按毫米预览包围盒,再选真实单位(mm / cm / m)。3MF 会读文件内 model unit。动画、材质不进入打印网格。
168
179
 
169
180
  ### 界面
170
181
 
171
- | 区域 | 作用 |
172
- |------|------|
173
- | 顶栏 | 导入、撤销/重做、预览/编辑、修复、导出、草稿、设置、帮助 |
174
- | 左栏 | 零件树、上色、编辑(布尔、拆件等),可收起 |
175
- | 画布 | 轨道旋转缩放;双击或 **F** 自适应;**复位视角** 回默认相机 |
176
- | 右栏 | 诊断与测量,可收起 |
177
- | 底栏 | 包围盒、内部单位 mm、体积、文件名 |
182
+
183
+ | 区域 | 作用 |
184
+ | --- | ------------------------------------------------------ |
185
+ | 左栏 | 导入、导出、零件树、上色、移动/旋转/缩放;`features` 打开后才有数值变换、基本体、拆件、草稿与观察工具 |
186
+ | 画布 | 轨道旋转缩放;双击或 **F** 自适应;**复位视角** 回默认相机 |
187
+ | 右栏 | 诊断与测量,默认收起 |
188
+ | 底栏 | 包围盒、内部单位 mm、体积、文件名 |
189
+
190
+
191
+
178
192
 
179
193
  ### 导出
180
194
 
@@ -182,20 +196,24 @@ async function upload() {
182
196
  - **3MF**:自带毫米单位;每零件单独 object,并写入 `displaycolor`(零件打印色)
183
197
  - **GLB**:单文件多网格;零件打印色写入材质。无打印单位字段,坐标按导出单位解释。
184
198
 
199
+
200
+
185
201
  ### 快捷键
186
202
 
187
- | 按键 | 作用 |
188
- |------|------|
189
- | Ctrl+Z / Ctrl+Y | 撤销 / 重做 |
190
- | Ctrl+S | 保存自动草稿(IndexedDB) |
191
- | 1 / 2 / 3 | 移动 / 旋转 / 缩放 |
192
- | P | 切换工作台观察预览 |
193
- | Esc | 取消手柄、预览、测量 |
194
- | F | 按模型自适应视角 |
195
- | Shift | 变换吸附 |
196
- | Ctrl | 多选、框选 |
197
203
 
198
- 草稿保存在浏览器本地。设置含默认导入单位、默认导出格式、网格/灯光、最小壁厚。
204
+ | 按键 | 作用 |
205
+ | --------------- | ----------------------------- |
206
+ | Ctrl+Z / Ctrl+Y | 撤销 / 重做 |
207
+ | Ctrl+S | 保存自动草稿(需打开 `features.drafts`) |
208
+ | 1 / 2 / 3 | 移动 / 旋转 / 缩放 |
209
+ | P | 切换工作台观察预览 |
210
+ | Esc | 取消手柄、预览、测量 |
211
+ | F | 按模型自适应视角 |
212
+ | Shift | 变换吸附 |
213
+ | Ctrl | 多选、框选 |
214
+
215
+
216
+ 草稿保存在浏览器本地,仅当 `features.drafts` 为 true 时读写并提示恢复。设置含默认导入单位、默认导出格式、网格/灯光、最小壁厚。
199
217
 
200
218
  ## 开发本仓库
201
219
 
@@ -209,11 +227,13 @@ npm test
209
227
 
210
228
  开发站路由(仅本仓库 SPA,不进入 npm 包):
211
229
 
212
- | 路径 | 说明 |
213
- |------|------|
214
- | `/print-bench/` | 完整工作台 |
230
+
231
+ | 路径 | 说明 |
232
+ | ---------------------- | --------------------------------------------------- |
233
+ | `/print-bench/` | 完整工作台 |
215
234
  | `/print-bench/preview` | 嵌入只读壳 + 左侧宿主色板,用来测 `selectedPartIds` / `partColors` |
216
235
 
217
- 预览页可在左侧选择本地文件或 `?model=` 直链;下载/上传按钮走宿主逻辑(`custom-export` + `exportFile` 只拿 blob)。工作台内观察态仍用顶栏「预览」或快捷键 `P`。
218
236
 
219
- 发布前请使用 `npm run build:lib`,不要用 SPA 的 `build` 覆盖 `dist/`。
237
+ 预览页可在左侧选择本地文件或 `?model=` 直链;下载/上传按钮走宿主逻辑(`custom-export` + `exportFile` 只拿 blob)。工作台内观察态仍用左栏「预览」或快捷键 `P`(需 `features.extras`)。
238
+
239
+ 发布前请使用 `npm run build:lib`,不要用 SPA 的 `build` 覆盖 `dist/`。
@@ -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{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}.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}.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}
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 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}.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}.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}.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 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: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: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}