Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc5ee1dd36 |
158
docs/更改说明-v2.1.md
Normal file
158
docs/更改说明-v2.1.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# v2.1 更改说明
|
||||
|
||||
发布日期:2026-09-15
|
||||
上一版:v2.0(4 分类真实数据替换 + 手册彩色重做 + 网页版改版)
|
||||
|
||||
本版聚焦两件事:**选型交互改为 DESTACO 风格**,以及**参数组合补全(缺组合也能出零件号)**。
|
||||
|
||||
---
|
||||
|
||||
## 一、选型界面:DESTACO 风格(网页版)
|
||||
|
||||
参照 DESTACO 配置器的交互做了改版。**只改网页版**,桌面版保持原样,后续再同步。
|
||||
|
||||
### 1. 零件号实时条(新增)
|
||||
|
||||
参数面板顶部新增一条零件号显示区,**边选边拼**:
|
||||
|
||||
- 已确定的片段用深青色实心显示,未确定的用浅灰虚化占位
|
||||
- 底部进度条随选择推进
|
||||
- 选满且唯一命中时,整条转为青色实心渐变(完成反馈)
|
||||
- 浅色配色,与页面整体风格一致(初版曾用深色块,观感不协调,已改)
|
||||
|
||||
**技术说明**:40 个系列中只有 KC 有编码模板,无法靠模板拼号。改用**「当前候选型号的最长公共前缀」**——
|
||||
选的越多候选越少、公共前缀越长,自然拼出完整零件号。已验证 40/40 系列前缀单调增长(不回退)。
|
||||
|
||||
### 2. 参数选择改为可视化选项块(替代下拉菜单)
|
||||
|
||||
- 每个维度显示为**选项块网格**,而不是 `<select>` 下拉
|
||||
- 悬停抬升 + 青色投影;选中青色填充 + ✓ 角标
|
||||
- **不可选的取值置灰**(虚线框 + 划线),而不是从列表消失——用户能看到"该选项存在但当前组合不适用"
|
||||
- 再点一次可取消选择
|
||||
- 有说明文字的(如传感器 `K` = 电子 PNP M12)显示代号大字 + 说明小字;纯数值显示紧凑文字块
|
||||
- 连续域参数(range,如 KC 的行程)保留输入框 + 标准档位
|
||||
|
||||
**语义未变**:参数联动仍复用原有 `allowedOptions` 规则引擎,只改了渲染方式。
|
||||
|
||||
---
|
||||
|
||||
## 二、参数组合补全:库中缺的组合现在也能出零件号
|
||||
|
||||
### 背景
|
||||
|
||||
原逻辑是「**必须有真实数模才能生成型号**」:型号表由真实 STEP 文件扫描而来,构建器要求每个型号的数模必须存在。
|
||||
结果是——**库里没有的组合,用户选不出来,也拿不到零件号**。
|
||||
|
||||
### 做法
|
||||
|
||||
**1. 从已有型号反推编码模板**
|
||||
|
||||
40 个系列里只有 1 个(KC)有现成模板。写了 `infer-sku-template.py` 从已有型号反推编码规则,
|
||||
得到 **23 个系列 100% 可复现**的模板,例如:
|
||||
|
||||
```
|
||||
ULP{size}{stroke:pad3}{rod}{orient}{sensor}0 → ULP + 32 + 020 + A + E + K + 0
|
||||
ULA{size}{stroke:pad3}{rod}{mount}{sensor}0
|
||||
USSL{size}{pcmodel}{hook_pos}{pin_dia}{locating}{prog}{sensor}0
|
||||
```
|
||||
|
||||
**并加了自检门禁**(`verify-templates.py`):用模板重新渲染**每一个已有型号**,必须与库中编码逐字一致。
|
||||
这道检查抓出了一处真实错误——ULM/ULP 的行程补零位数写错(2 位写成 3 位),
|
||||
**若无此校验会生成 3,000 个不存在的零件号发给客户**。
|
||||
|
||||
**2. 补齐缺失组合**
|
||||
|
||||
只对模板验证通过的 23 个系列补全(其余 17 个系列因旧码/新码混用、编码结构特殊,未补)。
|
||||
|
||||
| 系列 | 补全前 | 补全后 |
|
||||
|---|---|---|
|
||||
| ULP | 168 | **2,970** |
|
||||
| ULM | 70 | 200 |
|
||||
| ULA | 19 | 192 |
|
||||
| ULL | 14 | 40 |
|
||||
| ULY / ULZ | 各 10 | 各 20 |
|
||||
| USSL | 7 | 12 |
|
||||
| ULT | 3 | 4 |
|
||||
| **合计新增** | | **+3,157 个型号** |
|
||||
|
||||
全库型号数:**54,136 → 57,293**。构建通过(0 错误 0 警告)。
|
||||
|
||||
**3. 占位数模**
|
||||
|
||||
新增的 3,157 个型号共用 **1 个 `PLACEHOLDER.step`**(4.5KB 方块)。
|
||||
构建器按路径打包,同一路径只存一份,因此**数据包仅从 2.10GB 增至 2.14GB**。
|
||||
|
||||
> 中途走过的弯路:先按"每型号一个硬链接文件"实现,但 NTFS 硬链接上限为 1024,
|
||||
> 超出部分退化为真实拷贝,白占约 500MB。改为共享单文件后归零。
|
||||
|
||||
**4. 待补数模清单**
|
||||
|
||||
`onebot-data/catalog/_pending_models.csv` —— 3,157 行,列出每个待补型号及其参数,供 AI 补真实数模。
|
||||
补完后把该行的 `step_file` 从 `PLACEHOLDER.step` 改为 `<型号>.step` 即可。
|
||||
|
||||
---
|
||||
|
||||
## 三、新增脚本
|
||||
|
||||
| 脚本 | 用途 |
|
||||
|---|---|
|
||||
| `infer-sku-template.py` | 从已有型号反推编码模板 |
|
||||
| `verify-templates.py` | 校验模板能 100% 复现已有型号(生成新号前的门禁) |
|
||||
| `quantify-combo-gap.py` | 统计参数组合缺口规模 |
|
||||
| `gen-missing-combos.py` | 补全缺失组合(`--report` 只统计,`--write` 落盘) |
|
||||
| `gen-placeholder-step.py` | 生成占位数模方块 |
|
||||
| `check-combo-unique.py` | 检查"参数组合 ↔ 型号"是否一对一 |
|
||||
|
||||
---
|
||||
|
||||
## 四、发现但未处理的问题
|
||||
|
||||
### 1. 夹紧缸 9 个系列:一组参数对应多个型号
|
||||
|
||||
| 系列 | 变体数 | 重复组数 | 一组最多对应 |
|
||||
|---|---|---|---|
|
||||
| UCBP | 358 | 46 组 | 18 个型号 |
|
||||
| UCBM | 266 | 26 组 | 15 个型号 |
|
||||
| UCBH | 61 | 7 组 | 5 个型号 |
|
||||
| UCBQ / UCDP / UCIM / UCIP / UFL / UCBT | — | 2-4 组 | 2-6 个型号 |
|
||||
|
||||
共 **310 个型号**受影响。原因是这些型号的编码里有**参数之外的尾缀**(版本号/变体标记),
|
||||
如 `UCBP25AK00` vs `UCBP25AK010`(差末尾版本号)、`UBP25010` vs `UCBP25BK0`(旧码/新码混用)。
|
||||
|
||||
**影响**:用户选完参数后仍无法唯一确定型号。界面已提示"该组合对应 N 个型号",但需后续补维度或清理。
|
||||
|
||||
### 2. UAGP / LAGP 参数顺序与官方编码结构不符
|
||||
|
||||
官方订购码顺序为 `规格 - 台位 - 传感器 - 转角 - 制动 - 接口`,
|
||||
当前 meta 里传感器排在最后。不影响选型正确性(参数按名索引),但导致编码模板推不出来,
|
||||
且界面展示顺序与官方不一致。
|
||||
|
||||
### 3. 未补全的 17 个系列
|
||||
|
||||
模板验证未通过的系列保持原状——**宁可少补,也不生成可能不存在的零件号**。
|
||||
|
||||
---
|
||||
|
||||
## 五、涉及文件
|
||||
|
||||
| 文件 | 改动 |
|
||||
|---|---|
|
||||
| `web/app.js` | 零件号实时条 + 选项块渲染(+148 行) |
|
||||
| `web/style.css` | SKU 条与选项块样式(+111 行) |
|
||||
| `web/index.html` | 新增 SKU 条容器 |
|
||||
| `onebot-data/catalog/csv/*.csv` | 8 个系列追加 3,157 行 |
|
||||
| `onebot-data/catalog/step/PLACEHOLDER.step` | 新增占位数模 |
|
||||
| `onebot-data/catalog/_pending_models.csv` | 新增待补数模清单 |
|
||||
| `onebot-data/tools/*.py` | 新增 6 个数据管线脚本 |
|
||||
|
||||
---
|
||||
|
||||
## 六、验证方式
|
||||
|
||||
1. **构建**:`bin\OnebotCatalog.exe --buildfull onebot-data\catalog sample\OnebotCatalog_2026.08.opc`
|
||||
→ 应输出 `系列数: 40 | 变体数: 57293`,`校验: 0 错误, 0 警告`
|
||||
2. **网页预览**:`tools\serve-web.ps1` → 打开 `http://localhost:8080`(Ctrl+F5 强刷)
|
||||
- 选定位销 PIN → ULP → 选一个原来选不到的组合(如 63 缸 / 110 行程)
|
||||
- 应能看到零件号实时拼出、可下载(下载到的是占位方块)
|
||||
3. **模板门禁**:`PYTHONUTF8=1 python onebot-data\tools\verify-templates.py`
|
||||
→ 应输出「所有模板对已有型号 100% 复现」
|
||||
3158
onebot-data/catalog/_pending_models.csv
Normal file
3158
onebot-data/catalog/_pending_models.csv
Normal file
File diff suppressed because it is too large
Load Diff
@@ -18,3 +18,176 @@ ULA63040AD0,63,40,A,,D,ULA63040AD0.step
|
||||
ULA63040AK0,63,40,A,,K,ULA63040AK0.step
|
||||
ULA63050AK0,63,50,A,,K,ULA63050AK0.step
|
||||
ULA63060AK0,63,60,A,,K,ULA63060AK0.step
|
||||
ULA40020AD0,40,20,A,,D,PLACEHOLDER.step
|
||||
ULA40020AK0,40,20,A,,K,PLACEHOLDER.step
|
||||
ULA40020GSD0,40,20,G,S,D,PLACEHOLDER.step
|
||||
ULA40020GD0,40,20,G,,D,PLACEHOLDER.step
|
||||
ULA40020GK0,40,20,G,,K,PLACEHOLDER.step
|
||||
ULA40020NSD0,40,20,N,S,D,PLACEHOLDER.step
|
||||
ULA40020ND0,40,20,N,,D,PLACEHOLDER.step
|
||||
ULA40020NK0,40,20,N,,K,PLACEHOLDER.step
|
||||
ULA40020PSD0,40,20,P,S,D,PLACEHOLDER.step
|
||||
ULA40020PD0,40,20,P,,D,PLACEHOLDER.step
|
||||
ULA40020PK0,40,20,P,,K,PLACEHOLDER.step
|
||||
ULA40040AD0,40,40,A,,D,PLACEHOLDER.step
|
||||
ULA40040AK0,40,40,A,,K,PLACEHOLDER.step
|
||||
ULA40040GD0,40,40,G,,D,PLACEHOLDER.step
|
||||
ULA40040GK0,40,40,G,,K,PLACEHOLDER.step
|
||||
ULA40040ND0,40,40,N,,D,PLACEHOLDER.step
|
||||
ULA40040NK0,40,40,N,,K,PLACEHOLDER.step
|
||||
ULA40040PD0,40,40,P,,D,PLACEHOLDER.step
|
||||
ULA40040PK0,40,40,P,,K,PLACEHOLDER.step
|
||||
ULA40015ASD0,40,15,A,S,D,PLACEHOLDER.step
|
||||
ULA40015ASK0,40,15,A,S,K,PLACEHOLDER.step
|
||||
ULA40015AD0,40,15,A,,D,PLACEHOLDER.step
|
||||
ULA40015AK0,40,15,A,,K,PLACEHOLDER.step
|
||||
ULA40015GSD0,40,15,G,S,D,PLACEHOLDER.step
|
||||
ULA40015GSK0,40,15,G,S,K,PLACEHOLDER.step
|
||||
ULA40015GD0,40,15,G,,D,PLACEHOLDER.step
|
||||
ULA40015GK0,40,15,G,,K,PLACEHOLDER.step
|
||||
ULA40015NSD0,40,15,N,S,D,PLACEHOLDER.step
|
||||
ULA40015NSK0,40,15,N,S,K,PLACEHOLDER.step
|
||||
ULA40015ND0,40,15,N,,D,PLACEHOLDER.step
|
||||
ULA40015NK0,40,15,N,,K,PLACEHOLDER.step
|
||||
ULA40015PSD0,40,15,P,S,D,PLACEHOLDER.step
|
||||
ULA40015PSK0,40,15,P,S,K,PLACEHOLDER.step
|
||||
ULA40015PD0,40,15,P,,D,PLACEHOLDER.step
|
||||
ULA40015PK0,40,15,P,,K,PLACEHOLDER.step
|
||||
ULA40025ASD0,40,25,A,S,D,PLACEHOLDER.step
|
||||
ULA40025ASK0,40,25,A,S,K,PLACEHOLDER.step
|
||||
ULA40025AD0,40,25,A,,D,PLACEHOLDER.step
|
||||
ULA40025AK0,40,25,A,,K,PLACEHOLDER.step
|
||||
ULA40025GSD0,40,25,G,S,D,PLACEHOLDER.step
|
||||
ULA40025GSK0,40,25,G,S,K,PLACEHOLDER.step
|
||||
ULA40025GD0,40,25,G,,D,PLACEHOLDER.step
|
||||
ULA40025GK0,40,25,G,,K,PLACEHOLDER.step
|
||||
ULA40025NSD0,40,25,N,S,D,PLACEHOLDER.step
|
||||
ULA40025NSK0,40,25,N,S,K,PLACEHOLDER.step
|
||||
ULA40025ND0,40,25,N,,D,PLACEHOLDER.step
|
||||
ULA40025NK0,40,25,N,,K,PLACEHOLDER.step
|
||||
ULA40025PSD0,40,25,P,S,D,PLACEHOLDER.step
|
||||
ULA40025PSK0,40,25,P,S,K,PLACEHOLDER.step
|
||||
ULA40025PD0,40,25,P,,D,PLACEHOLDER.step
|
||||
ULA40025PK0,40,25,P,,K,PLACEHOLDER.step
|
||||
ULA40050ASD0,40,50,A,S,D,PLACEHOLDER.step
|
||||
ULA40050ASK0,40,50,A,S,K,PLACEHOLDER.step
|
||||
ULA40050AD0,40,50,A,,D,PLACEHOLDER.step
|
||||
ULA40050AK0,40,50,A,,K,PLACEHOLDER.step
|
||||
ULA40050GSD0,40,50,G,S,D,PLACEHOLDER.step
|
||||
ULA40050GSK0,40,50,G,S,K,PLACEHOLDER.step
|
||||
ULA40050GD0,40,50,G,,D,PLACEHOLDER.step
|
||||
ULA40050GK0,40,50,G,,K,PLACEHOLDER.step
|
||||
ULA40050NSD0,40,50,N,S,D,PLACEHOLDER.step
|
||||
ULA40050NSK0,40,50,N,S,K,PLACEHOLDER.step
|
||||
ULA40050ND0,40,50,N,,D,PLACEHOLDER.step
|
||||
ULA40050NK0,40,50,N,,K,PLACEHOLDER.step
|
||||
ULA40050PSD0,40,50,P,S,D,PLACEHOLDER.step
|
||||
ULA40050PSK0,40,50,P,S,K,PLACEHOLDER.step
|
||||
ULA40050PD0,40,50,P,,D,PLACEHOLDER.step
|
||||
ULA40050PK0,40,50,P,,K,PLACEHOLDER.step
|
||||
ULA40060ASD0,40,60,A,S,D,PLACEHOLDER.step
|
||||
ULA40060ASK0,40,60,A,S,K,PLACEHOLDER.step
|
||||
ULA40060AD0,40,60,A,,D,PLACEHOLDER.step
|
||||
ULA40060AK0,40,60,A,,K,PLACEHOLDER.step
|
||||
ULA40060GSD0,40,60,G,S,D,PLACEHOLDER.step
|
||||
ULA40060GSK0,40,60,G,S,K,PLACEHOLDER.step
|
||||
ULA40060GD0,40,60,G,,D,PLACEHOLDER.step
|
||||
ULA40060GK0,40,60,G,,K,PLACEHOLDER.step
|
||||
ULA40060NSD0,40,60,N,S,D,PLACEHOLDER.step
|
||||
ULA40060NSK0,40,60,N,S,K,PLACEHOLDER.step
|
||||
ULA40060ND0,40,60,N,,D,PLACEHOLDER.step
|
||||
ULA40060NK0,40,60,N,,K,PLACEHOLDER.step
|
||||
ULA40060PSD0,40,60,P,S,D,PLACEHOLDER.step
|
||||
ULA40060PSK0,40,60,P,S,K,PLACEHOLDER.step
|
||||
ULA40060PD0,40,60,P,,D,PLACEHOLDER.step
|
||||
ULA40060PK0,40,60,P,,K,PLACEHOLDER.step
|
||||
ULA63020ASD0,63,20,A,S,D,PLACEHOLDER.step
|
||||
ULA63020ASK0,63,20,A,S,K,PLACEHOLDER.step
|
||||
ULA63020AD0,63,20,A,,D,PLACEHOLDER.step
|
||||
ULA63020AK0,63,20,A,,K,PLACEHOLDER.step
|
||||
ULA63020GSD0,63,20,G,S,D,PLACEHOLDER.step
|
||||
ULA63020GSK0,63,20,G,S,K,PLACEHOLDER.step
|
||||
ULA63020GD0,63,20,G,,D,PLACEHOLDER.step
|
||||
ULA63020GK0,63,20,G,,K,PLACEHOLDER.step
|
||||
ULA63020NSD0,63,20,N,S,D,PLACEHOLDER.step
|
||||
ULA63020NSK0,63,20,N,S,K,PLACEHOLDER.step
|
||||
ULA63020ND0,63,20,N,,D,PLACEHOLDER.step
|
||||
ULA63020NK0,63,20,N,,K,PLACEHOLDER.step
|
||||
ULA63020PSD0,63,20,P,S,D,PLACEHOLDER.step
|
||||
ULA63020PSK0,63,20,P,S,K,PLACEHOLDER.step
|
||||
ULA63020PD0,63,20,P,,D,PLACEHOLDER.step
|
||||
ULA63020PK0,63,20,P,,K,PLACEHOLDER.step
|
||||
ULA63040ASD0,63,40,A,S,D,PLACEHOLDER.step
|
||||
ULA63040ASK0,63,40,A,S,K,PLACEHOLDER.step
|
||||
ULA63040GSD0,63,40,G,S,D,PLACEHOLDER.step
|
||||
ULA63040GSK0,63,40,G,S,K,PLACEHOLDER.step
|
||||
ULA63040GD0,63,40,G,,D,PLACEHOLDER.step
|
||||
ULA63040GK0,63,40,G,,K,PLACEHOLDER.step
|
||||
ULA63040NSD0,63,40,N,S,D,PLACEHOLDER.step
|
||||
ULA63040NSK0,63,40,N,S,K,PLACEHOLDER.step
|
||||
ULA63040ND0,63,40,N,,D,PLACEHOLDER.step
|
||||
ULA63040NK0,63,40,N,,K,PLACEHOLDER.step
|
||||
ULA63040PSD0,63,40,P,S,D,PLACEHOLDER.step
|
||||
ULA63040PSK0,63,40,P,S,K,PLACEHOLDER.step
|
||||
ULA63040PD0,63,40,P,,D,PLACEHOLDER.step
|
||||
ULA63040PK0,63,40,P,,K,PLACEHOLDER.step
|
||||
ULA63015ASD0,63,15,A,S,D,PLACEHOLDER.step
|
||||
ULA63015ASK0,63,15,A,S,K,PLACEHOLDER.step
|
||||
ULA63015AD0,63,15,A,,D,PLACEHOLDER.step
|
||||
ULA63015GSD0,63,15,G,S,D,PLACEHOLDER.step
|
||||
ULA63015GSK0,63,15,G,S,K,PLACEHOLDER.step
|
||||
ULA63015GD0,63,15,G,,D,PLACEHOLDER.step
|
||||
ULA63015GK0,63,15,G,,K,PLACEHOLDER.step
|
||||
ULA63015NSD0,63,15,N,S,D,PLACEHOLDER.step
|
||||
ULA63015NSK0,63,15,N,S,K,PLACEHOLDER.step
|
||||
ULA63015ND0,63,15,N,,D,PLACEHOLDER.step
|
||||
ULA63015NK0,63,15,N,,K,PLACEHOLDER.step
|
||||
ULA63015PSD0,63,15,P,S,D,PLACEHOLDER.step
|
||||
ULA63015PSK0,63,15,P,S,K,PLACEHOLDER.step
|
||||
ULA63015PD0,63,15,P,,D,PLACEHOLDER.step
|
||||
ULA63015PK0,63,15,P,,K,PLACEHOLDER.step
|
||||
ULA63025ASD0,63,25,A,S,D,PLACEHOLDER.step
|
||||
ULA63025ASK0,63,25,A,S,K,PLACEHOLDER.step
|
||||
ULA63025AD0,63,25,A,,D,PLACEHOLDER.step
|
||||
ULA63025GSD0,63,25,G,S,D,PLACEHOLDER.step
|
||||
ULA63025GSK0,63,25,G,S,K,PLACEHOLDER.step
|
||||
ULA63025GD0,63,25,G,,D,PLACEHOLDER.step
|
||||
ULA63025GK0,63,25,G,,K,PLACEHOLDER.step
|
||||
ULA63025NSD0,63,25,N,S,D,PLACEHOLDER.step
|
||||
ULA63025NSK0,63,25,N,S,K,PLACEHOLDER.step
|
||||
ULA63025ND0,63,25,N,,D,PLACEHOLDER.step
|
||||
ULA63025NK0,63,25,N,,K,PLACEHOLDER.step
|
||||
ULA63025PSD0,63,25,P,S,D,PLACEHOLDER.step
|
||||
ULA63025PSK0,63,25,P,S,K,PLACEHOLDER.step
|
||||
ULA63025PD0,63,25,P,,D,PLACEHOLDER.step
|
||||
ULA63025PK0,63,25,P,,K,PLACEHOLDER.step
|
||||
ULA63050ASD0,63,50,A,S,D,PLACEHOLDER.step
|
||||
ULA63050ASK0,63,50,A,S,K,PLACEHOLDER.step
|
||||
ULA63050AD0,63,50,A,,D,PLACEHOLDER.step
|
||||
ULA63050GSD0,63,50,G,S,D,PLACEHOLDER.step
|
||||
ULA63050GSK0,63,50,G,S,K,PLACEHOLDER.step
|
||||
ULA63050GD0,63,50,G,,D,PLACEHOLDER.step
|
||||
ULA63050GK0,63,50,G,,K,PLACEHOLDER.step
|
||||
ULA63050NSD0,63,50,N,S,D,PLACEHOLDER.step
|
||||
ULA63050NSK0,63,50,N,S,K,PLACEHOLDER.step
|
||||
ULA63050ND0,63,50,N,,D,PLACEHOLDER.step
|
||||
ULA63050NK0,63,50,N,,K,PLACEHOLDER.step
|
||||
ULA63050PSD0,63,50,P,S,D,PLACEHOLDER.step
|
||||
ULA63050PSK0,63,50,P,S,K,PLACEHOLDER.step
|
||||
ULA63050PD0,63,50,P,,D,PLACEHOLDER.step
|
||||
ULA63050PK0,63,50,P,,K,PLACEHOLDER.step
|
||||
ULA63060ASD0,63,60,A,S,D,PLACEHOLDER.step
|
||||
ULA63060ASK0,63,60,A,S,K,PLACEHOLDER.step
|
||||
ULA63060AD0,63,60,A,,D,PLACEHOLDER.step
|
||||
ULA63060GSD0,63,60,G,S,D,PLACEHOLDER.step
|
||||
ULA63060GSK0,63,60,G,S,K,PLACEHOLDER.step
|
||||
ULA63060GD0,63,60,G,,D,PLACEHOLDER.step
|
||||
ULA63060GK0,63,60,G,,K,PLACEHOLDER.step
|
||||
ULA63060NSD0,63,60,N,S,D,PLACEHOLDER.step
|
||||
ULA63060NSK0,63,60,N,S,K,PLACEHOLDER.step
|
||||
ULA63060ND0,63,60,N,,D,PLACEHOLDER.step
|
||||
ULA63060NK0,63,60,N,,K,PLACEHOLDER.step
|
||||
ULA63060PSD0,63,60,P,S,D,PLACEHOLDER.step
|
||||
ULA63060PSK0,63,60,P,S,K,PLACEHOLDER.step
|
||||
ULA63060PD0,63,60,P,,D,PLACEHOLDER.step
|
||||
ULA63060PK0,63,60,P,,K,PLACEHOLDER.step
|
||||
|
||||
|
@@ -13,3 +13,29 @@ ULL50040EXK0,50,40,E,X,K,ULL50040EXK0.step
|
||||
ULL50040FXK0,50,40,F,X,K,ULL50040FXK0.step
|
||||
ULL50040HXK0,50,40,H,X,K,ULL50040HXK0.step
|
||||
ULL50040IXK0,50,40,I,X,K,ULL50040IXK0.step
|
||||
ULL50040AXK0,50,40,A,X,K,PLACEHOLDER.step
|
||||
ULL50040BEK0,50,40,B,E,K,PLACEHOLDER.step
|
||||
ULL50040BNK0,50,40,B,N,K,PLACEHOLDER.step
|
||||
ULL50040BOK0,50,40,B,O,K,PLACEHOLDER.step
|
||||
ULL50040BSK0,50,40,B,S,K,PLACEHOLDER.step
|
||||
ULL50040CEK0,50,40,C,E,K,PLACEHOLDER.step
|
||||
ULL50040CNK0,50,40,C,N,K,PLACEHOLDER.step
|
||||
ULL50040COK0,50,40,C,O,K,PLACEHOLDER.step
|
||||
ULL50040CSK0,50,40,C,S,K,PLACEHOLDER.step
|
||||
ULL50040DXK0,50,40,D,X,K,PLACEHOLDER.step
|
||||
ULL50040EEK0,50,40,E,E,K,PLACEHOLDER.step
|
||||
ULL50040ENK0,50,40,E,N,K,PLACEHOLDER.step
|
||||
ULL50040EOK0,50,40,E,O,K,PLACEHOLDER.step
|
||||
ULL50040ESK0,50,40,E,S,K,PLACEHOLDER.step
|
||||
ULL50040FEK0,50,40,F,E,K,PLACEHOLDER.step
|
||||
ULL50040FNK0,50,40,F,N,K,PLACEHOLDER.step
|
||||
ULL50040FOK0,50,40,F,O,K,PLACEHOLDER.step
|
||||
ULL50040FSK0,50,40,F,S,K,PLACEHOLDER.step
|
||||
ULL50040HEK0,50,40,H,E,K,PLACEHOLDER.step
|
||||
ULL50040HNK0,50,40,H,N,K,PLACEHOLDER.step
|
||||
ULL50040HOK0,50,40,H,O,K,PLACEHOLDER.step
|
||||
ULL50040HSK0,50,40,H,S,K,PLACEHOLDER.step
|
||||
ULL50040IEK0,50,40,I,E,K,PLACEHOLDER.step
|
||||
ULL50040INK0,50,40,I,N,K,PLACEHOLDER.step
|
||||
ULL50040IOK0,50,40,I,O,K,PLACEHOLDER.step
|
||||
ULL50040ISK0,50,40,I,S,K,PLACEHOLDER.step
|
||||
|
||||
|
@@ -69,3 +69,133 @@ ULM50060EXK0,50,60,E,X,K,ULM50060EXK0.step
|
||||
ULM50060FXK0,50,60,F,X,K,ULM50060FXK0.step
|
||||
ULM50060HXK0,50,60,H,X,K,ULM50060HXK0.step
|
||||
ULM50060IXK0,50,60,I,X,K,ULM50060IXK0.step
|
||||
ULM50015AXK0,50,15,A,X,K,PLACEHOLDER.step
|
||||
ULM50015BEK0,50,15,B,E,K,PLACEHOLDER.step
|
||||
ULM50015BNK0,50,15,B,N,K,PLACEHOLDER.step
|
||||
ULM50015BOK0,50,15,B,O,K,PLACEHOLDER.step
|
||||
ULM50015BSK0,50,15,B,S,K,PLACEHOLDER.step
|
||||
ULM50015CEK0,50,15,C,E,K,PLACEHOLDER.step
|
||||
ULM50015CNK0,50,15,C,N,K,PLACEHOLDER.step
|
||||
ULM50015COK0,50,15,C,O,K,PLACEHOLDER.step
|
||||
ULM50015CSK0,50,15,C,S,K,PLACEHOLDER.step
|
||||
ULM50015DXK0,50,15,D,X,K,PLACEHOLDER.step
|
||||
ULM50015EEK0,50,15,E,E,K,PLACEHOLDER.step
|
||||
ULM50015ENK0,50,15,E,N,K,PLACEHOLDER.step
|
||||
ULM50015EOK0,50,15,E,O,K,PLACEHOLDER.step
|
||||
ULM50015ESK0,50,15,E,S,K,PLACEHOLDER.step
|
||||
ULM50015FEK0,50,15,F,E,K,PLACEHOLDER.step
|
||||
ULM50015FNK0,50,15,F,N,K,PLACEHOLDER.step
|
||||
ULM50015FOK0,50,15,F,O,K,PLACEHOLDER.step
|
||||
ULM50015FSK0,50,15,F,S,K,PLACEHOLDER.step
|
||||
ULM50015HEK0,50,15,H,E,K,PLACEHOLDER.step
|
||||
ULM50015HNK0,50,15,H,N,K,PLACEHOLDER.step
|
||||
ULM50015HOK0,50,15,H,O,K,PLACEHOLDER.step
|
||||
ULM50015HSK0,50,15,H,S,K,PLACEHOLDER.step
|
||||
ULM50015IEK0,50,15,I,E,K,PLACEHOLDER.step
|
||||
ULM50015INK0,50,15,I,N,K,PLACEHOLDER.step
|
||||
ULM50015IOK0,50,15,I,O,K,PLACEHOLDER.step
|
||||
ULM50015ISK0,50,15,I,S,K,PLACEHOLDER.step
|
||||
ULM50025AXK0,50,25,A,X,K,PLACEHOLDER.step
|
||||
ULM50025BEK0,50,25,B,E,K,PLACEHOLDER.step
|
||||
ULM50025BNK0,50,25,B,N,K,PLACEHOLDER.step
|
||||
ULM50025BOK0,50,25,B,O,K,PLACEHOLDER.step
|
||||
ULM50025BSK0,50,25,B,S,K,PLACEHOLDER.step
|
||||
ULM50025CEK0,50,25,C,E,K,PLACEHOLDER.step
|
||||
ULM50025CNK0,50,25,C,N,K,PLACEHOLDER.step
|
||||
ULM50025COK0,50,25,C,O,K,PLACEHOLDER.step
|
||||
ULM50025CSK0,50,25,C,S,K,PLACEHOLDER.step
|
||||
ULM50025DXK0,50,25,D,X,K,PLACEHOLDER.step
|
||||
ULM50025EEK0,50,25,E,E,K,PLACEHOLDER.step
|
||||
ULM50025ENK0,50,25,E,N,K,PLACEHOLDER.step
|
||||
ULM50025EOK0,50,25,E,O,K,PLACEHOLDER.step
|
||||
ULM50025ESK0,50,25,E,S,K,PLACEHOLDER.step
|
||||
ULM50025FEK0,50,25,F,E,K,PLACEHOLDER.step
|
||||
ULM50025FNK0,50,25,F,N,K,PLACEHOLDER.step
|
||||
ULM50025FOK0,50,25,F,O,K,PLACEHOLDER.step
|
||||
ULM50025FSK0,50,25,F,S,K,PLACEHOLDER.step
|
||||
ULM50025HEK0,50,25,H,E,K,PLACEHOLDER.step
|
||||
ULM50025HNK0,50,25,H,N,K,PLACEHOLDER.step
|
||||
ULM50025HOK0,50,25,H,O,K,PLACEHOLDER.step
|
||||
ULM50025HSK0,50,25,H,S,K,PLACEHOLDER.step
|
||||
ULM50025IEK0,50,25,I,E,K,PLACEHOLDER.step
|
||||
ULM50025INK0,50,25,I,N,K,PLACEHOLDER.step
|
||||
ULM50025IOK0,50,25,I,O,K,PLACEHOLDER.step
|
||||
ULM50025ISK0,50,25,I,S,K,PLACEHOLDER.step
|
||||
ULM50040AXK0,50,40,A,X,K,PLACEHOLDER.step
|
||||
ULM50040BEK0,50,40,B,E,K,PLACEHOLDER.step
|
||||
ULM50040BNK0,50,40,B,N,K,PLACEHOLDER.step
|
||||
ULM50040BOK0,50,40,B,O,K,PLACEHOLDER.step
|
||||
ULM50040BSK0,50,40,B,S,K,PLACEHOLDER.step
|
||||
ULM50040CEK0,50,40,C,E,K,PLACEHOLDER.step
|
||||
ULM50040CNK0,50,40,C,N,K,PLACEHOLDER.step
|
||||
ULM50040COK0,50,40,C,O,K,PLACEHOLDER.step
|
||||
ULM50040CSK0,50,40,C,S,K,PLACEHOLDER.step
|
||||
ULM50040DXK0,50,40,D,X,K,PLACEHOLDER.step
|
||||
ULM50040EEK0,50,40,E,E,K,PLACEHOLDER.step
|
||||
ULM50040ENK0,50,40,E,N,K,PLACEHOLDER.step
|
||||
ULM50040EOK0,50,40,E,O,K,PLACEHOLDER.step
|
||||
ULM50040ESK0,50,40,E,S,K,PLACEHOLDER.step
|
||||
ULM50040FEK0,50,40,F,E,K,PLACEHOLDER.step
|
||||
ULM50040FNK0,50,40,F,N,K,PLACEHOLDER.step
|
||||
ULM50040FOK0,50,40,F,O,K,PLACEHOLDER.step
|
||||
ULM50040FSK0,50,40,F,S,K,PLACEHOLDER.step
|
||||
ULM50040HEK0,50,40,H,E,K,PLACEHOLDER.step
|
||||
ULM50040HNK0,50,40,H,N,K,PLACEHOLDER.step
|
||||
ULM50040HOK0,50,40,H,O,K,PLACEHOLDER.step
|
||||
ULM50040HSK0,50,40,H,S,K,PLACEHOLDER.step
|
||||
ULM50040IEK0,50,40,I,E,K,PLACEHOLDER.step
|
||||
ULM50040INK0,50,40,I,N,K,PLACEHOLDER.step
|
||||
ULM50040IOK0,50,40,I,O,K,PLACEHOLDER.step
|
||||
ULM50040ISK0,50,40,I,S,K,PLACEHOLDER.step
|
||||
ULM50050AXK0,50,50,A,X,K,PLACEHOLDER.step
|
||||
ULM50050BEK0,50,50,B,E,K,PLACEHOLDER.step
|
||||
ULM50050BNK0,50,50,B,N,K,PLACEHOLDER.step
|
||||
ULM50050BOK0,50,50,B,O,K,PLACEHOLDER.step
|
||||
ULM50050BSK0,50,50,B,S,K,PLACEHOLDER.step
|
||||
ULM50050CEK0,50,50,C,E,K,PLACEHOLDER.step
|
||||
ULM50050CNK0,50,50,C,N,K,PLACEHOLDER.step
|
||||
ULM50050COK0,50,50,C,O,K,PLACEHOLDER.step
|
||||
ULM50050CSK0,50,50,C,S,K,PLACEHOLDER.step
|
||||
ULM50050DXK0,50,50,D,X,K,PLACEHOLDER.step
|
||||
ULM50050EEK0,50,50,E,E,K,PLACEHOLDER.step
|
||||
ULM50050ENK0,50,50,E,N,K,PLACEHOLDER.step
|
||||
ULM50050EOK0,50,50,E,O,K,PLACEHOLDER.step
|
||||
ULM50050ESK0,50,50,E,S,K,PLACEHOLDER.step
|
||||
ULM50050FEK0,50,50,F,E,K,PLACEHOLDER.step
|
||||
ULM50050FNK0,50,50,F,N,K,PLACEHOLDER.step
|
||||
ULM50050FOK0,50,50,F,O,K,PLACEHOLDER.step
|
||||
ULM50050FSK0,50,50,F,S,K,PLACEHOLDER.step
|
||||
ULM50050HEK0,50,50,H,E,K,PLACEHOLDER.step
|
||||
ULM50050HNK0,50,50,H,N,K,PLACEHOLDER.step
|
||||
ULM50050HOK0,50,50,H,O,K,PLACEHOLDER.step
|
||||
ULM50050HSK0,50,50,H,S,K,PLACEHOLDER.step
|
||||
ULM50050IEK0,50,50,I,E,K,PLACEHOLDER.step
|
||||
ULM50050INK0,50,50,I,N,K,PLACEHOLDER.step
|
||||
ULM50050IOK0,50,50,I,O,K,PLACEHOLDER.step
|
||||
ULM50050ISK0,50,50,I,S,K,PLACEHOLDER.step
|
||||
ULM50060AXK0,50,60,A,X,K,PLACEHOLDER.step
|
||||
ULM50060BEK0,50,60,B,E,K,PLACEHOLDER.step
|
||||
ULM50060BNK0,50,60,B,N,K,PLACEHOLDER.step
|
||||
ULM50060BOK0,50,60,B,O,K,PLACEHOLDER.step
|
||||
ULM50060BSK0,50,60,B,S,K,PLACEHOLDER.step
|
||||
ULM50060CEK0,50,60,C,E,K,PLACEHOLDER.step
|
||||
ULM50060CNK0,50,60,C,N,K,PLACEHOLDER.step
|
||||
ULM50060COK0,50,60,C,O,K,PLACEHOLDER.step
|
||||
ULM50060CSK0,50,60,C,S,K,PLACEHOLDER.step
|
||||
ULM50060DXK0,50,60,D,X,K,PLACEHOLDER.step
|
||||
ULM50060EEK0,50,60,E,E,K,PLACEHOLDER.step
|
||||
ULM50060ENK0,50,60,E,N,K,PLACEHOLDER.step
|
||||
ULM50060EOK0,50,60,E,O,K,PLACEHOLDER.step
|
||||
ULM50060ESK0,50,60,E,S,K,PLACEHOLDER.step
|
||||
ULM50060FEK0,50,60,F,E,K,PLACEHOLDER.step
|
||||
ULM50060FNK0,50,60,F,N,K,PLACEHOLDER.step
|
||||
ULM50060FOK0,50,60,F,O,K,PLACEHOLDER.step
|
||||
ULM50060FSK0,50,60,F,S,K,PLACEHOLDER.step
|
||||
ULM50060HEK0,50,60,H,E,K,PLACEHOLDER.step
|
||||
ULM50060HNK0,50,60,H,N,K,PLACEHOLDER.step
|
||||
ULM50060HOK0,50,60,H,O,K,PLACEHOLDER.step
|
||||
ULM50060HSK0,50,60,H,S,K,PLACEHOLDER.step
|
||||
ULM50060IEK0,50,60,I,E,K,PLACEHOLDER.step
|
||||
ULM50060INK0,50,60,I,N,K,PLACEHOLDER.step
|
||||
ULM50060IOK0,50,60,I,O,K,PLACEHOLDER.step
|
||||
ULM50060ISK0,50,60,I,S,K,PLACEHOLDER.step
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -2,3 +2,4 @@
|
||||
ULT50040AK0,50,40,A,K,ULT50040AK0.step
|
||||
ULT50040GK0,50,40,G,K,ULT50040GK0.step
|
||||
ULT50040GN0,50,40,G,N,ULT50040GN0.step
|
||||
ULT50040AN0,50,40,A,N,PLACEHOLDER.step
|
||||
|
||||
|
@@ -9,3 +9,13 @@ ULY40040DEK0,40,40,D,E,K,ULY40040DEK0.step
|
||||
ULY40040DNK0,40,40,D,N,K,ULY40040DNK0.step
|
||||
ULY40040DOK0,40,40,D,O,K,ULY40040DOK0.step
|
||||
ULY40040DSK0,40,40,D,S,K,ULY40040DSK0.step
|
||||
ULY40040AXK0,40,40,A,X,K,PLACEHOLDER.step
|
||||
ULY40040BEK0,40,40,B,E,K,PLACEHOLDER.step
|
||||
ULY40040BNK0,40,40,B,N,K,PLACEHOLDER.step
|
||||
ULY40040BOK0,40,40,B,O,K,PLACEHOLDER.step
|
||||
ULY40040BSK0,40,40,B,S,K,PLACEHOLDER.step
|
||||
ULY40040CEK0,40,40,C,E,K,PLACEHOLDER.step
|
||||
ULY40040CNK0,40,40,C,N,K,PLACEHOLDER.step
|
||||
ULY40040COK0,40,40,C,O,K,PLACEHOLDER.step
|
||||
ULY40040CSK0,40,40,C,S,K,PLACEHOLDER.step
|
||||
ULY40040DXK0,40,40,D,X,K,PLACEHOLDER.step
|
||||
|
||||
|
@@ -9,3 +9,13 @@ ULZ40040DEK0,40,40,D,E,K,ULZ40040DEK0.step
|
||||
ULZ40040DNK0,40,40,D,N,K,ULZ40040DNK0.step
|
||||
ULZ40040DOK0,40,40,D,O,K,ULZ40040DOK0.step
|
||||
ULZ40040DSK0,40,40,D,S,K,ULZ40040DSK0.step
|
||||
ULZ40040AXK0,40,40,A,X,K,PLACEHOLDER.step
|
||||
ULZ40040BEK0,40,40,B,E,K,PLACEHOLDER.step
|
||||
ULZ40040BNK0,40,40,B,N,K,PLACEHOLDER.step
|
||||
ULZ40040BOK0,40,40,B,O,K,PLACEHOLDER.step
|
||||
ULZ40040BSK0,40,40,B,S,K,PLACEHOLDER.step
|
||||
ULZ40040CEK0,40,40,C,E,K,PLACEHOLDER.step
|
||||
ULZ40040CNK0,40,40,C,N,K,PLACEHOLDER.step
|
||||
ULZ40040COK0,40,40,C,O,K,PLACEHOLDER.step
|
||||
ULZ40040CSK0,40,40,C,S,K,PLACEHOLDER.step
|
||||
ULZ40040DXK0,40,40,D,X,K,PLACEHOLDER.step
|
||||
|
||||
|
@@ -6,3 +6,8 @@ USSL50JB51001N0,50,J,B,5,1,001,N,USSL50JB51001N0.step
|
||||
USSL50JC31001N0,50,J,C,3,1,001,N,USSL50JC31001N0.step
|
||||
USSL50JC31002N0,50,J,C,3,1,002,N,USSL50JC31002N0.step
|
||||
USSL50JC51001N0,50,J,C,5,1,001,N,USSL50JC51001N0.step
|
||||
USSL50JB41002N0,50,J,B,4,1,002,N,PLACEHOLDER.step
|
||||
USSL50JB51002N0,50,J,B,5,1,002,N,PLACEHOLDER.step
|
||||
USSL50JC41001N0,50,J,C,4,1,001,N,PLACEHOLDER.step
|
||||
USSL50JC41002N0,50,J,C,4,1,002,N,PLACEHOLDER.step
|
||||
USSL50JC51002N0,50,J,C,5,1,002,N,PLACEHOLDER.step
|
||||
|
||||
|
31
onebot-data/tools/check-combo-unique.py
Normal file
31
onebot-data/tools/check-combo-unique.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""统计各系列"参数组合 → 型号"是否一对一 (多处多 vs 一处多)。"""
|
||||
import json, io, collections
|
||||
|
||||
CAT = r'D:/开发/OnebotCatalog/release/web_2026.08/catalog.json'
|
||||
d = json.load(io.open(CAT, encoding='utf-8-sig'))
|
||||
|
||||
print(f'{"系列":8s} {"变体":>6s} {"组合":>6s} {"重复组":>6s} {"最大一对多":>9s} 判定')
|
||||
print('-' * 58)
|
||||
tot_codes = 0
|
||||
bad = []
|
||||
for s in d['series']:
|
||||
vs = s.get('variants') or []
|
||||
if not vs:
|
||||
continue
|
||||
params = [p['code'] for p in s['parameters']]
|
||||
sig = collections.Counter(
|
||||
tuple(str(v['params'].get(pc)) for pc in params) for v in vs)
|
||||
dup = {k: n for k, n in sig.items() if n > 1}
|
||||
mx = max(sig.values()) if sig else 0
|
||||
verdict = 'OK 一对一' if not dup else f'!! {len(dup)} 组一对多'
|
||||
if dup:
|
||||
bad.append((s['code'], len(vs), len(dup), mx))
|
||||
tot_codes += sum(n - 1 for n in dup.values())
|
||||
print(f'{s["code"]:8s} {len(vs):6d} {len(sig):6d} {len(dup):6d} {mx:9d} {verdict}')
|
||||
|
||||
print(f'\n存在一对多的系列: {len(bad)} 个, 受影响型号 {tot_codes} 个')
|
||||
if bad:
|
||||
print('\n明细 (系列 / 变体 / 重复组数 / 一组最多几个型号):')
|
||||
for c, n, dup, mx in sorted(bad, key=lambda x: -x[3]):
|
||||
print(f' {c:8s} {n:5d} 变体 {dup:3d} 组 max={mx}')
|
||||
225
onebot-data/tools/gen-missing-combos.py
Normal file
225
onebot-data/tools/gen-missing-combos.py
Normal file
@@ -0,0 +1,225 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
补齐参数组合空间: 为"规则内合法但库中缺失"的组合生成零件号, 并指向系列级占位数模。
|
||||
|
||||
背景: 当前型号表由真实 STEP 文件扫描而来 —— 没有数模就没有型号, 用户选完也拿不到代号。
|
||||
本脚本按规则反向生成缺失组合, 使其可选、可出号; 数模用每系列一个占位件, 后续 AI 补真实数模。
|
||||
|
||||
用法:
|
||||
PYTHONUTF8=1 python gen-missing-combos.py --report # 只统计, 不写文件
|
||||
PYTHONUTF8=1 python gen-missing-combos.py --write # 写入 CSV + meta 模板 + 占位清单
|
||||
"""
|
||||
import json, io, os, csv, itertools, sys, collections
|
||||
|
||||
CAT = r'D:/开发/OnebotCatalog/onebot-data/catalog'
|
||||
REPORT = '--report' in sys.argv
|
||||
WRITE = '--write' in sys.argv
|
||||
|
||||
# 只有"模板能 100% 对齐"的系列才补 (由 infer-sku-template.py 验证)。
|
||||
# 其余系列 (旧码混用/编码结构特殊) 保持现状, 不生成可能不存在的零件号。
|
||||
SAFE = {
|
||||
'KC', 'KCB', 'KCS', 'KS', 'KSB', 'KSU', 'M', 'MS', 'MSC', 'MAL', 'MALC',
|
||||
'ULA', 'ULL', 'ULM', 'ULO', 'ULP', 'ULT', 'ULX', 'ULY', 'ULZ', 'USL', 'USSL',
|
||||
}
|
||||
|
||||
# 各系列的编码模板 (由 infer-sku-template.py 反推, 100% 对齐)
|
||||
# {参数:补零位数}; 空值渲染为空串
|
||||
TEMPLATES = {
|
||||
'KC': ('KC{type}{bore}-{stroke}{magnet}{mount}', {}),
|
||||
'KCB': ('KCB{bore}-{stroke}{magnet}{mount}', {}),
|
||||
'KCS': ('KCS{type}{bore}-{stroke}{magnet}{mount}', {}),
|
||||
'KS': ('KS{type}{bore}-{stroke}{magnet}{mount}', {}),
|
||||
'KSB': ('KSB{bore}-{stroke1}x{stroke2}{magnet}{mount}', {}),
|
||||
'KSU': ('KSU{type}{bore}-{stroke}{magnet}{mount}', {}),
|
||||
'M': ('{action}{bore}-{stroke}{magnet}{rod}{mount}{cap}', {}),
|
||||
'MS': ('{action}{bore}-{stroke}{magnet}{rod}{mount}{cap}', {}),
|
||||
'MSC': ('{action}{bore}-{stroke}{magnet}{rod}{mount}{cap}', {}),
|
||||
'MAL': ('{type}{bore}-{stroke}{magnet}{rod}{mount}{cap}', {}),
|
||||
'MALC': ('{type}{bore}-{stroke}{magnet}{rod}{mount}{cap}', {}),
|
||||
'ULA': ('ULA{size}{stroke:pad3}{rod}{mount}{sensor}0', {}),
|
||||
'ULL': ('ULL{size}{stroke:pad3}{rod}{orient}{sensor}0', {}),
|
||||
'ULM': ('ULM{size}{stroke:pad3}{rod}{orient}{sensor}0', {}),
|
||||
'ULO': ('ULO{size}{stroke:pad3}{rod}{sensor}0', {}),
|
||||
'ULP': ('ULP{size}{stroke:pad3}{rod}{orient}{sensor}0', {}),
|
||||
'ULT': ('ULT{size}{stroke:pad3}{rod}{sensor}0', {}),
|
||||
'ULX': ('ULX{size}{stroke:pad3}{rod}{sensor}0', {}),
|
||||
'ULY': ('ULY{size}{stroke:pad3}{rod}{orient}{sensor}0', {}),
|
||||
'ULZ': ('ULZ{size}{stroke:pad3}{rod}{orient}{sensor}0', {}),
|
||||
'USL': ('USL{size}{pcmodel}{pin_dia}{locating}{prog}{sensor}0', {}),
|
||||
'USSL': ('USSL{size}{pcmodel}{hook_pos}{pin_dia}{locating}{prog}{sensor}0', {}),
|
||||
}
|
||||
|
||||
PLACEHOLDER_STEP = 'PLACEHOLDER.step' # 每系列共用
|
||||
|
||||
|
||||
def S(v):
|
||||
return '' if v is None else str(v)
|
||||
|
||||
|
||||
def eq(a, b):
|
||||
return S(a).lower() == S(b).lower()
|
||||
|
||||
|
||||
def cond_match(c, prm):
|
||||
if not c:
|
||||
return True
|
||||
val = prm.get(c['Param'])
|
||||
if val is None:
|
||||
return False
|
||||
op, cv = c['Op'], c['Value']
|
||||
if op == 'eq':
|
||||
return eq(val, cv)
|
||||
if op == 'ne':
|
||||
return not eq(val, cv)
|
||||
if op == 'in':
|
||||
return isinstance(cv, list) and any(eq(val, x) for x in cv)
|
||||
try:
|
||||
a, b = float(S(val)), float(S(cv))
|
||||
except ValueError:
|
||||
return False
|
||||
return {'gt': a > b, 'ge': a >= b, 'lt': a < b, 'le': a <= b}.get(op, False)
|
||||
|
||||
|
||||
def violated(rules, prm):
|
||||
for r in rules or []:
|
||||
if cond_match(r.get('If'), prm) and not cond_match(r.get('Then'), prm):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def render(tpl, prm):
|
||||
"""模板渲染: {field} / {field:padN}; 空值渲染为空串。"""
|
||||
out, i = '', 0
|
||||
while i < len(tpl):
|
||||
if tpl[i] == '{':
|
||||
j = tpl.index('}', i)
|
||||
tok = tpl[i + 1:j]
|
||||
name, _, pad = tok.partition(':')
|
||||
name = name.strip()
|
||||
v = S(prm.get(name, ''))
|
||||
if pad.startswith('pad') and v != '':
|
||||
try:
|
||||
v = v.rjust(int(pad[3:]), '0')
|
||||
except ValueError:
|
||||
return None
|
||||
out += v
|
||||
i = j + 1
|
||||
else:
|
||||
out += tpl[i]
|
||||
i += 1
|
||||
return out
|
||||
|
||||
|
||||
def load_meta(code):
|
||||
p = os.path.join(CAT, 'series', code + '.meta.json')
|
||||
return json.load(io.open(p, encoding='utf-8'))
|
||||
|
||||
|
||||
def link_or_copy(src, dst):
|
||||
"""建硬链接; 不支持则回退复制。返回 True 表示已创建。
|
||||
注: 必须先建链接再考虑失败回退, 绝不能先删目标再建 (删了又建失败 = 文件丢失)。"""
|
||||
if os.path.exists(dst):
|
||||
return False
|
||||
try:
|
||||
os.link(src, dst)
|
||||
return True
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
import shutil
|
||||
shutil.copy2(src, dst)
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
summary = []
|
||||
plan = {}
|
||||
for code in sorted(SAFE):
|
||||
meta = load_meta(code)
|
||||
s = meta['series']
|
||||
params = s['parameters']
|
||||
csvp = os.path.join(CAT, 'csv', code + '.csv')
|
||||
if not os.path.exists(csvp):
|
||||
print(f'[{code}] 无 CSV, 跳过')
|
||||
continue
|
||||
rows = list(csv.DictReader(io.open(csvp, encoding='utf-8-sig')))
|
||||
existing = {r['model_code'] for r in rows}
|
||||
|
||||
# 值域: 数值参数取 CSV 实际出现过的值 (不用 meta.values, 避免含非法/未出现值)
|
||||
doms = collections.OrderedDict()
|
||||
for p in params:
|
||||
pc = p['code']
|
||||
if p['type'] == 'range':
|
||||
doms[pc] = None # 连续域不枚举
|
||||
continue
|
||||
vals = list(dict.fromkeys(r.get(pc, '') for r in rows))
|
||||
vals = [v for v in vals if v != '' or True]
|
||||
doms[pc] = vals
|
||||
if any(v is None for v in doms.values()):
|
||||
print(f'[{code}] 含 range 参数, 跳过 (连续域无法枚举)')
|
||||
continue
|
||||
|
||||
tpl, _ = TEMPLATES[code]
|
||||
codes = list(doms.keys())
|
||||
combos = list(itertools.product(*[doms[k] for k in codes]))
|
||||
added = []
|
||||
for combo in combos:
|
||||
prm = dict(zip(codes, combo))
|
||||
if violated(s.get('rules'), prm):
|
||||
continue
|
||||
mc = render(tpl, prm)
|
||||
if not mc or mc in existing:
|
||||
continue
|
||||
existing.add(mc)
|
||||
added.append((mc, prm))
|
||||
|
||||
summary.append((code, len(rows), len(combos), len(added)))
|
||||
if added:
|
||||
plan[code] = (tpl, params, added, rows)
|
||||
|
||||
# 输出
|
||||
print(f'{"系列":7s} {"现有":>7s} {"笛卡尔":>9s} {"待补":>7s}')
|
||||
print('-' * 36)
|
||||
tot_new = 0
|
||||
for code, n, cart, add in sorted(summary, key=lambda x: -x[3]):
|
||||
tot_new += add
|
||||
print(f'{code:7s} {n:7d} {cart:9,d} {add:7,d}')
|
||||
print(f'\n合计待补 {tot_new:,} 个型号')
|
||||
|
||||
if not WRITE:
|
||||
print('\n(--report 模式, 未写文件)')
|
||||
return
|
||||
|
||||
# ---------- 写入 ----------
|
||||
step_dir = os.path.join(CAT, 'step')
|
||||
placeholder = os.path.join(step_dir, PLACEHOLDER_STEP)
|
||||
manifest = [] # 待 AI 补真实数模的清单
|
||||
n_csv = n_ph = 0
|
||||
for code, (tpl, params, added, rows) in plan.items():
|
||||
csvp = os.path.join(CAT, 'csv', code + '.csv')
|
||||
header = list(rows[0].keys())
|
||||
dims = [h for h in header if h not in ('model_code', 'step_file')]
|
||||
# 新增行: step_file 统一指向 PLACEHOLDER.step ——
|
||||
# 构建器按 v.step 的路径复制 (同路径只存一份), 3157 个型号共用一个占位件,
|
||||
# 既不占磁盘也不撑大数据包。AI 补真实数模后把该行改成 "<型号>.step" 即可。
|
||||
with io.open(csvp, 'a', encoding='utf-8-sig', newline='') as fp:
|
||||
w = csv.writer(fp)
|
||||
for mc, prm in added:
|
||||
w.writerow([mc] + [S(prm.get(d, '')) for d in dims] + [PLACEHOLDER_STEP])
|
||||
manifest.append(f'{code},{mc},{",".join(d + "=" + S(prm.get(d, "")) for d in dims)}')
|
||||
n_csv += 1
|
||||
print(f'[{code}] CSV +{len(added)}')
|
||||
|
||||
# 待补清单 (给 AI 补数模用)
|
||||
mf = os.path.join(CAT, '_pending_models.csv')
|
||||
with io.open(mf, 'w', encoding='utf-8-sig', newline='') as fp:
|
||||
fp.write('series,model_code,params\n')
|
||||
fp.write('\n'.join(manifest) + '\n')
|
||||
print(f'\n写入完成: CSV 新增 {n_csv} 行, 占位 STEP {n_ph} 个')
|
||||
print(f'待补数模清单: {mf} ({len(manifest)} 个型号)')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
135
onebot-data/tools/gen-placeholder-step.py
Normal file
135
onebot-data/tools/gen-placeholder-step.py
Normal file
@@ -0,0 +1,135 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
生成占位数模 (每系列一个通用的"待补数模"方块)。
|
||||
|
||||
用途: 为「参数组合补齐」生成的型号提供占位 STEP —— 构建器要求每个型号必须有数模文件。
|
||||
真实数模后续由 AI 参照同系列已有型号补出, 覆盖同名文件即可。
|
||||
|
||||
产出:
|
||||
catalog/step/PLACEHOLDER.step 通用的方块 (ISO-10303-21, 标准 B-rep)
|
||||
catalog/step/_placeholder/<系列>.step 每系列一份 (便于按系列替换/识别)
|
||||
|
||||
方块边长 60mm, 居中于原点。
|
||||
用法: PYTHONUTF8=1 python gen-placeholder-step.py
|
||||
"""
|
||||
import os
|
||||
|
||||
CAT = r'D:/开发/OnebotCatalog/onebot-data/catalog'
|
||||
STEP_DIR = os.path.join(CAT, 'step')
|
||||
|
||||
# 方块尺寸 (mm)
|
||||
SX, SY, SZ = 60.0, 60.0, 60.0
|
||||
HX, HY, HZ = SX / 2, SY / 2, SZ / 2
|
||||
|
||||
|
||||
def build_step(label):
|
||||
"""生成一个合法的 STEP 文件内容: 单位立方体 B-rep。"""
|
||||
L = []
|
||||
L.append('ISO-10303-21;')
|
||||
L.append('HEADER;')
|
||||
L.append("FILE_DESCRIPTION(('PLACEHOLDER - real CAD model pending'),'2;1');")
|
||||
L.append("FILE_NAME('%s','2026-01-01T00:00:00',('ONEBOT'),('ONEBOT'),"
|
||||
"'gen-placeholder-step.py','ONEBOT Catalog','');" % label)
|
||||
L.append("FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));")
|
||||
L.append('ENDSEC;')
|
||||
L.append('DATA;')
|
||||
L.append("#1=APPLICATION_CONTEXT('core data for automotive mechanical design processes');")
|
||||
L.append("#2=APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',2000,#1);")
|
||||
L.append("#3=PRODUCT_CONTEXT('',#1,'mechanical');")
|
||||
L.append("#4=PRODUCT_DEFINITION_CONTEXT('part definition',#1,'design');")
|
||||
L.append("#5=PRODUCT('%s','PLACEHOLDER',$,(#3));" % label)
|
||||
L.append("#6=PRODUCT_DEFINITION_FORMATION('','',#5);")
|
||||
L.append("#7=PRODUCT_DEFINITION('design','',#6,#4);")
|
||||
L.append("#8=PRODUCT_DEFINITION_SHAPE('','',#7);")
|
||||
L.append("#9=(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MILLI.,.METRE.));")
|
||||
L.append("#10=(NAMED_UNIT(*)PLANE_ANGLE_UNIT()SI_UNIT($,.RADIAN.));")
|
||||
L.append("#11=(NAMED_UNIT(*)SI_UNIT($,.STERADIAN.)SOLID_ANGLE_UNIT());")
|
||||
L.append("#12=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(0.001),#9,"
|
||||
"'distance_accuracy_value','');")
|
||||
L.append("#13=(GEOMETRIC_REPRESENTATION_CONTEXT(3)"
|
||||
"GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#12))"
|
||||
"GLOBAL_UNIT_ASSIGNED_CONTEXT((#9,#10,#11))REPRESENTATION_CONTEXT('',''));")
|
||||
|
||||
# 8 个顶点坐标
|
||||
corners = [
|
||||
(-HX, -HY, -HZ), (HX, -HY, -HZ), (HX, HY, -HZ), (-HX, HY, -HZ),
|
||||
(-HX, -HY, HZ), (HX, -HY, HZ), (HX, HY, HZ), (-HX, HY, HZ),
|
||||
]
|
||||
eid = 20
|
||||
pt_ids, vx_ids = [], []
|
||||
for (x, y, z) in corners:
|
||||
L.append('#%d=CARTESIAN_POINT('',(%.3f,%.3f,%.3f));' % (eid, x, y, z))
|
||||
pt_ids.append(eid); eid += 1
|
||||
L.append('#%d=DIRECTION('',(0.,0.,1.));' % eid); dz = eid; eid += 1
|
||||
L.append('#%d=DIRECTION('',(1.,0.,0.));' % eid); dx = eid; eid += 1
|
||||
for pid in pt_ids:
|
||||
L.append('#%d=VERTEX_POINT('',#%d);' % (eid, pid))
|
||||
vx_ids.append(eid); eid += 1
|
||||
|
||||
# 12 条棱
|
||||
edges = [(0, 1), (1, 2), (2, 3), (3, 0),
|
||||
(4, 5), (5, 6), (6, 7), (7, 4),
|
||||
(0, 4), (1, 5), (2, 6), (3, 7)]
|
||||
edge_ids = []
|
||||
for (a, b) in edges:
|
||||
L.append('#%d=LINE('',#%d,#%d);' % (eid, pt_ids[a], pt_ids[b]))
|
||||
ln = eid; eid += 1
|
||||
L.append('#%d=VECTOR('',#%d,1.0);' % (eid, dx)); vc = eid; eid += 1
|
||||
L.append('#%d=EDGE_CURVE('',#%d,#%d,#%d,.T.);'
|
||||
% (eid, vx_ids[a], vx_ids[b], ln))
|
||||
ec = eid; eid += 1
|
||||
L.append('#%d=ORIENTED_EDGE('',*,*,#%d,.T.);' % (eid, ec))
|
||||
edge_ids.append(eid); eid += 1
|
||||
|
||||
# 6 个面
|
||||
faces = [
|
||||
[0, 1, 2, 3], [4, 5, 6, 7],
|
||||
[0, 1, 5, 4], [1, 2, 6, 5],
|
||||
[2, 3, 7, 6], [3, 0, 4, 7],
|
||||
]
|
||||
normals = [(0, 0, -1), (0, 0, 1), (0, -1, 0), (1, 0, 0), (0, 1, 0), (-1, 0, 0)]
|
||||
face_ids = []
|
||||
for fi, f in enumerate(faces):
|
||||
loops = []
|
||||
for k in range(4):
|
||||
loops.append(edge_ids[f[k]])
|
||||
L.append('#%d=EDGE_LOOP('',(%s));'
|
||||
% (eid, ','.join('#%d' % x for x in loops)))
|
||||
el = eid; eid += 1
|
||||
nx, ny, nz = normals[fi]
|
||||
L.append('#%d=DIRECTION('',(%.1f,%.1f,%.1f));' % (eid, nx, ny, nz))
|
||||
nd = eid; eid += 1
|
||||
L.append("#%d=AXIS2_PLACEMENT_3D('',#%d,#%d,$);" % (eid, pt_ids[f[0]], nd))
|
||||
ax = eid; eid += 1
|
||||
L.append('#%d=PLANE(\'\',#%d);' % (eid, ax))
|
||||
pl = eid; eid += 1
|
||||
L.append('#%d=ADVANCED_FACE(\'\',(#%d),#%d,.T.);' % (eid, el, pl))
|
||||
face_ids.append(eid); eid += 1
|
||||
|
||||
L.append('#%d=CLOSED_SHELL(\'\',(%s));'
|
||||
% (eid, ','.join('#%d' % x for x in face_ids)))
|
||||
sh = eid; eid += 1
|
||||
L.append("#%d=MANIFOLD_SOLID_BREP('PLACEHOLDER',#%d);" % (eid, sh))
|
||||
msb = eid; eid += 1
|
||||
L.append('#%d=ADVANCED_BREP_SHAPE_REPRESENTATION(\'PLACEHOLDER\',(#%d,#%d),#13);'
|
||||
% (eid, msb, eid + 1))
|
||||
abr = eid; eid += 1
|
||||
L.append("#%d=AXIS2_PLACEMENT_3D('',#%d,#%d,#%d);" % (eid, pt_ids[0], dz, dx))
|
||||
ax2 = eid; eid += 1
|
||||
L.append("#%d=SHAPE_DEFINITION_REPRESENTATION(#8,#%d);" % (eid, abr))
|
||||
L.append('ENDSEC;')
|
||||
L.append('END-ISO-10303-21;')
|
||||
return '\n'.join(L) + '\n'
|
||||
|
||||
|
||||
def main():
|
||||
os.makedirs(STEP_DIR, exist_ok=True)
|
||||
content = build_step('PLACEHOLDER')
|
||||
p = os.path.join(STEP_DIR, 'PLACEHOLDER.step')
|
||||
with open(p, 'w', encoding='ascii', newline='\r\n') as f:
|
||||
f.write(content)
|
||||
print(f'写入 {p} ({os.path.getsize(p)} 字节)')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
109
onebot-data/tools/infer-sku-template.py
Normal file
109
onebot-data/tools/infer-sku-template.py
Normal file
@@ -0,0 +1,109 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
编码模板反推 v2 —— 支持零填充与空值占位符。
|
||||
|
||||
v1 的 explain() 贪心匹配"非零填充", 把 ULP50040AEK0 的 "040" 拆成 字面量"0" + 值"40",
|
||||
产生大量假阴性。真实规则是:
|
||||
- 数值参数常带前导零 (stroke=40 -> "040")
|
||||
- 枚举参数为空时, 编码里用 '_' 占位 (UAGP075_K045WS0)
|
||||
|
||||
本脚本对每个参数尝试 {原值, 各前导零位数, '_'占位} 的所有组合, 取能对齐的;
|
||||
若某系列的"字面量序列"完全一致 → 可模板化。
|
||||
|
||||
用法: PYTHONUTF8=1 python infer-sku-template.py
|
||||
"""
|
||||
import json, io, collections, sys
|
||||
|
||||
CAT = r'D:/开发/OnebotCatalog/release/web_2026.08/catalog.json'
|
||||
d = json.load(io.open(CAT, encoding='utf-8-sig'))
|
||||
|
||||
MAXPAD = 4
|
||||
|
||||
|
||||
def align_all(code, params, values):
|
||||
"""返回全部可能的字面量序列 (元组)。每个参数尝试: 原值 / 前导零 / '_'占位。"""
|
||||
results = []
|
||||
|
||||
def dfs(i, pos, lits):
|
||||
if i == len(params):
|
||||
results.append(tuple(lits + [code[pos:]]))
|
||||
return
|
||||
pc = params[i]
|
||||
v = str(values.get(pc, ''))
|
||||
cands = []
|
||||
if v == '':
|
||||
cands = ['_', ''] # 空值: 占位符或什么都不写
|
||||
else:
|
||||
cands = [v] + ['0' * z + v for z in range(1, MAXPAD + 1)]
|
||||
seen = set()
|
||||
for pat in cands:
|
||||
if pat in seen:
|
||||
continue
|
||||
seen.add(pat)
|
||||
if pat == '':
|
||||
dfs(i + 1, pos, lits + [''])
|
||||
continue
|
||||
idx = code.find(pat, pos)
|
||||
if idx >= 0:
|
||||
dfs(i + 1, idx + len(pat), lits + [code[pos:idx]])
|
||||
dfs(0, 0, [])
|
||||
return results
|
||||
|
||||
|
||||
def render_template(params, lits):
|
||||
"""字面量序列 -> 模板串。空值参数渲染为 '_'。"""
|
||||
out = ''
|
||||
for i, pc in enumerate(params):
|
||||
out += lits[i] + '{' + pc + '}'
|
||||
out += lits[len(params)]
|
||||
return out
|
||||
|
||||
|
||||
good, bad = [], []
|
||||
for s in d['series']:
|
||||
vs = s.get('variants') or []
|
||||
if not vs:
|
||||
continue
|
||||
params = [p['code'] for p in s['parameters']]
|
||||
lit_sets = []
|
||||
unaligned = 0
|
||||
for v in vs:
|
||||
alts = align_all(v['modelCode'], params, v['params'])
|
||||
if not alts:
|
||||
unaligned += 1
|
||||
continue
|
||||
lit_sets.append(alts)
|
||||
|
||||
if not lit_sets:
|
||||
bad.append((s['code'], len(vs), '完全无法对齐', [], 0))
|
||||
continue
|
||||
|
||||
# 统计"出现在最多变体里"的字面量序列 (每个变体取任一可对齐解)
|
||||
cnt = collections.Counter()
|
||||
for alts in lit_sets:
|
||||
for a in set(alts):
|
||||
cnt[a] += 1
|
||||
top, hit = cnt.most_common(1)[0]
|
||||
rate = hit / len(vs) * 100
|
||||
|
||||
if rate >= 99.5 and unaligned == 0:
|
||||
good.append((s['code'], len(vs), rate, render_template(params, top), len(cnt)))
|
||||
else:
|
||||
bad.append((s['code'], len(vs), f'{rate:.1f}% ({len(cnt)} 种)', cnt.most_common(3), unaligned))
|
||||
|
||||
print('=' * 96)
|
||||
print(f'可模板化的系列 ({len(good)} 个)')
|
||||
print('=' * 96)
|
||||
for code, n, rate, tpl, k in sorted(good):
|
||||
print(f'{code:7s} {n:6d} 变体 {rate:6.1f}% {tpl}')
|
||||
|
||||
print()
|
||||
print('=' * 96)
|
||||
print(f'不可模板化的系列 ({len(bad)} 个)')
|
||||
print('=' * 96)
|
||||
for code, n, why, tops, un in sorted(bad, key=lambda x: -x[1]):
|
||||
print(f'{code:7s} {n:6d} 变体 {why}' + (f' 未对齐 {un}' if un else ''))
|
||||
for lits, c in tops[:3]:
|
||||
print(f' {c:5d}× {lits}')
|
||||
|
||||
print(f'\n合计: 可模板化 {len(good)} / 共 {len(good) + len(bad)} 个系列')
|
||||
101
onebot-data/tools/quantify-combo-gap.py
Normal file
101
onebot-data/tools/quantify-combo-gap.py
Normal file
@@ -0,0 +1,101 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
量化"参数组合缺口": 合法组合 (域 + 规则内) vs 库中已有变体, 以及补全的规模。
|
||||
|
||||
对每个系列:
|
||||
- 现有变体数
|
||||
- 笛卡尔积规模 (各参数值域相乘)
|
||||
- 规则过滤后的合法组合数 (复用 C# Configurator 同语义的规则求值)
|
||||
- 缺口 = 合法组合 - 已覆盖
|
||||
这决定"补全组合数据"的可行性与数据量。
|
||||
"""
|
||||
import json, io, itertools
|
||||
|
||||
CAT = r'D:/开发/OnebotCatalog/release/web_2026.08/catalog.json'
|
||||
d = json.load(io.open(CAT, encoding='utf-8-sig'))
|
||||
|
||||
|
||||
def S(v):
|
||||
return '' if v is None else str(v)
|
||||
|
||||
|
||||
def eq(a, b):
|
||||
return S(a).lower() == S(b).lower()
|
||||
|
||||
|
||||
def cond_match(c, params):
|
||||
"""规则条件求值 (与 app.js condMatches / C# CondMatches 同语义)。"""
|
||||
if not c:
|
||||
return True
|
||||
val = params.get(c['Param'])
|
||||
if val is None:
|
||||
return False
|
||||
op = c['Op']
|
||||
cv = c['Value']
|
||||
if op == 'eq':
|
||||
return eq(val, cv)
|
||||
if op == 'ne':
|
||||
return not eq(val, cv)
|
||||
if op == 'in':
|
||||
return isinstance(cv, list) and any(eq(val, x) for x in cv)
|
||||
try:
|
||||
a, b = float(S(val)), float(S(cv))
|
||||
except ValueError:
|
||||
return False
|
||||
return {'gt': a > b, 'ge': a >= b, 'lt': a < b, 'le': a <= b}.get(op, False)
|
||||
|
||||
|
||||
def rule_violated(rules, params):
|
||||
for r in rules or []:
|
||||
if cond_match(r.get('If'), params) and not cond_match(r.get('Then'), params):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
print(f'{"系列":7s} {"现有":>6s} {"笛卡尔":>9s} {"规则内":>9s} {"缺口":>9s} {"倍数":>5s}')
|
||||
print('-' * 62)
|
||||
tot_exist = tot_gap = 0
|
||||
rows = []
|
||||
for s in d['series']:
|
||||
vs = s.get('variants') or []
|
||||
if not vs:
|
||||
continue
|
||||
params = s['parameters']
|
||||
doms = []
|
||||
for p in params:
|
||||
vals = p.get('values') or []
|
||||
if not vals:
|
||||
doms = None
|
||||
break
|
||||
doms.append(vals)
|
||||
if doms is None:
|
||||
rows.append((s['code'], len(vs), None, None, None))
|
||||
continue
|
||||
cart = 1
|
||||
for vals in doms:
|
||||
cart *= len(vals)
|
||||
# 规则过滤 (笛卡尔过大时抽样估算)
|
||||
legal = 0
|
||||
if cart <= 400000:
|
||||
for combo in itertools.product(*doms):
|
||||
prm = {p['code']: v for p, v in zip(params, combo)}
|
||||
if not rule_violated(s.get('rules'), prm):
|
||||
legal += 1
|
||||
else:
|
||||
legal = None # 太大, 跳过精确统计
|
||||
gap = (legal - len(vs)) if legal is not None else None
|
||||
rows.append((s['code'], len(vs), cart, legal, gap))
|
||||
if legal is not None:
|
||||
tot_exist += len(vs)
|
||||
tot_gap += max(0, gap)
|
||||
|
||||
for code, n, cart, legal, gap in sorted(rows, key=lambda x: -(x[4] or -1)):
|
||||
c = f'{cart:,}' if cart else '—'
|
||||
l = f'{legal:,}' if legal else '—'
|
||||
g = f'{gap:,}' if gap is not None else '—'
|
||||
mult = f'{legal/n:.1f}x' if legal and n else '—'
|
||||
flag = ' <-- 缺口大' if gap and gap > 50000 else ''
|
||||
print(f'{code:7s} {n:6d} {c:>9s} {l:>9s} {g:>9s} {mult:>5s}{flag}')
|
||||
|
||||
print(f'\n合计: 现有 {tot_exist:,} 变体, 缺口 {tot_gap:,} 组合')
|
||||
print(f'补全后约 {tot_exist + tot_gap:,} 变体 (是现在的 {(tot_exist+tot_gap)/tot_exist:.1f} 倍)')
|
||||
40
onebot-data/tools/verify-templates.py
Normal file
40
onebot-data/tools/verify-templates.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
验证: 用模板对【已有型号】重新渲染, 必须与库中编码完全一致。
|
||||
凡不一致 → 模板有误, 绝不能用它生成新编码 (会造出不存在的零件号)。
|
||||
"""
|
||||
import json, io, os, csv, sys, importlib.util
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
spec = importlib.util.spec_from_file_location('gmc', os.path.join(HERE, 'gen-missing-combos.py'))
|
||||
gmc = importlib.util.module_from_spec(spec)
|
||||
sys.argv = ['x'] # 防止执行 main
|
||||
spec.loader.exec_module(gmc)
|
||||
|
||||
CAT = gmc.CAT
|
||||
print(f'{"系列":7s} {"变体":>6s} {"模板一致":>9s} {"失败":>6s} 反例')
|
||||
print('-' * 74)
|
||||
allok = True
|
||||
for code in sorted(gmc.SAFE):
|
||||
if code not in gmc.TEMPLATES:
|
||||
continue
|
||||
tpl, _ = gmc.TEMPLATES[code]
|
||||
rows = list(csv.DictReader(io.open(os.path.join(CAT, 'csv', code + '.csv'), encoding='utf-8-sig')))
|
||||
ok = fail = 0
|
||||
bad = []
|
||||
for r in rows:
|
||||
prm = {k: v for k, v in r.items() if k not in ('model_code', 'step_file')}
|
||||
got = gmc.render(tpl, prm)
|
||||
if got == r['model_code']:
|
||||
ok += 1
|
||||
else:
|
||||
fail += 1
|
||||
if len(bad) < 3:
|
||||
bad.append(f'{r["model_code"]} -> 渲染"{got}"')
|
||||
flag = '' if fail == 0 else ' <-- 模板有误!'
|
||||
if fail:
|
||||
allok = False
|
||||
print(f'{code:7s} {len(rows):6d} {ok:9d} {fail:6d} {bad[0] if bad else ""}{flag}')
|
||||
|
||||
print()
|
||||
print('结论:', '所有模板对已有型号 100% 复现 ✅' if allok else '存在模板错误, 不可用于生成新编码 ❌')
|
||||
148
web/app.js
148
web/app.js
@@ -514,12 +514,17 @@
|
||||
s.parameters.forEach(function (p) {
|
||||
var row = document.createElement('div');
|
||||
row.className = 'param-row';
|
||||
row.id = 'row-' + p.code;
|
||||
var label = document.createElement('label');
|
||||
label.textContent = paramName(p);
|
||||
label.id = 'lbl-' + p.code;
|
||||
label.textContent = paramName(p);
|
||||
var picked = document.createElement('span');
|
||||
picked.className = 'picked';
|
||||
picked.id = 'picked-' + p.code;
|
||||
label.appendChild(picked);
|
||||
row.appendChild(label);
|
||||
if (p.type === 'range') {
|
||||
// 参数化 range 域: 输入框 + 标准档位快捷选择 (与离线版可编辑下拉一致)
|
||||
// 连续域 (range) 无法枚举成卡片 → 保留输入框 + 标准档位
|
||||
var inp = document.createElement('input');
|
||||
inp.id = 'sel-' + p.code;
|
||||
inp.className = 'param-input';
|
||||
@@ -545,20 +550,16 @@
|
||||
panel.appendChild(row);
|
||||
return;
|
||||
}
|
||||
var sel = document.createElement('select');
|
||||
sel.id = 'sel-' + p.code;
|
||||
sel.onchange = function () {
|
||||
var raw = sel.value;
|
||||
if (raw === '') { delete state.sel[p.code]; }
|
||||
else {
|
||||
var v = raw === '__EMPTY__' ? '' : (p.type === 'number' ? num(raw) : raw);
|
||||
state.sel[p.code] = v;
|
||||
}
|
||||
refreshOptions();
|
||||
};
|
||||
row.appendChild(sel);
|
||||
// 枚举维度 → 选项块 (有图卡片 / 无图文字块)
|
||||
var grid = document.createElement('div');
|
||||
grid.className = 'opt-grid';
|
||||
grid.id = 'opts-' + p.code;
|
||||
// 选项较多时用更紧凑的列宽
|
||||
if ((p.values || []).length > 8) grid.classList.add('wide');
|
||||
row.appendChild(grid);
|
||||
panel.appendChild(row);
|
||||
});
|
||||
renderSkuBar();
|
||||
$('modelTitle').textContent = t(s.nameZh, s.nameEn);
|
||||
$('btnDatasheet').classList.add('hidden');
|
||||
// 重置尺寸图区: 选型完成前只显示提示文字, 不显示占位图
|
||||
@@ -600,11 +601,12 @@
|
||||
if (!s) return;
|
||||
var allowed = allowedOptions(s, state.sel);
|
||||
s.parameters.forEach(function (p) {
|
||||
var sel = $('sel-' + p.code);
|
||||
var vals = allowed[p.code] || [];
|
||||
if (p.type === 'range') {
|
||||
// 输入框: 更新 datalist 标准档位 (标注 标准); 已选值回显
|
||||
var inp = $('sel-' + p.code);
|
||||
var dl = $('dl-' + p.code);
|
||||
if (!inp || !dl) return;
|
||||
dl.innerHTML = '';
|
||||
vals.forEach(function (v) {
|
||||
var opt = document.createElement('option');
|
||||
@@ -613,23 +615,59 @@
|
||||
opt.label = isGrid ? str(v) + ' (' + t('标准', 'Std') + ')' : str(v);
|
||||
dl.appendChild(opt);
|
||||
});
|
||||
sel.value = state.sel[p.code] !== undefined ? str(state.sel[p.code]) : '';
|
||||
inp.value = state.sel[p.code] !== undefined ? str(state.sel[p.code]) : '';
|
||||
return;
|
||||
}
|
||||
var grid = $('opts-' + p.code);
|
||||
if (!grid) return;
|
||||
// 已选值变得不可选时清除
|
||||
if (state.sel[p.code] !== undefined &&
|
||||
!vals.some(function (v) { return str(v) === str(state.sel[p.code]); })) {
|
||||
delete state.sel[p.code];
|
||||
}
|
||||
sel.innerHTML = '<option value="">' + t('请选择', 'Select') + '</option>';
|
||||
vals.forEach(function (v) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = optValue(v);
|
||||
opt.textContent = displayValue(p, v);
|
||||
if (state.sel[p.code] !== undefined && str(v) === str(state.sel[p.code])) opt.selected = true;
|
||||
sel.appendChild(opt);
|
||||
// 候选值: 定义域全集 (保证卡片位置稳定, 不可选的置灰而非消失)
|
||||
var all = (p.values || []);
|
||||
grid.innerHTML = '';
|
||||
all.forEach(function (v) {
|
||||
var usable = vals.some(function (x) { return str(x) === str(v); });
|
||||
var on = state.sel[p.code] !== undefined && str(v) === str(state.sel[p.code]);
|
||||
var tile = document.createElement('div');
|
||||
tile.className = 'opt' + (on ? ' on' : '') + (usable ? '' : ' off');
|
||||
var disp = displayValue(p, v);
|
||||
// 有中文说明时: 代号大字 + 说明小字; 无说明 (如纯数字) 只显示值
|
||||
var hasDesc = disp && disp !== str(v);
|
||||
var k = document.createElement('div');
|
||||
k.className = 'opt-k';
|
||||
k.textContent = hasDesc ? optValue(v) .replace('__EMPTY__', '—') : disp;
|
||||
tile.appendChild(k);
|
||||
if (hasDesc) {
|
||||
var dd = document.createElement('div');
|
||||
dd.className = 'opt-d';
|
||||
dd.textContent = disp;
|
||||
tile.appendChild(dd);
|
||||
} else {
|
||||
tile.classList.add('txt');
|
||||
}
|
||||
if (usable) {
|
||||
tile.onclick = function () {
|
||||
if (state.sel[p.code] !== undefined && str(v) === str(state.sel[p.code])) {
|
||||
delete state.sel[p.code]; // 再点一次取消
|
||||
} else {
|
||||
state.sel[p.code] = p.type === 'number' ? num(v) : (str(v) === '__EMPTY__' ? '' : v);
|
||||
}
|
||||
refreshOptions();
|
||||
};
|
||||
}
|
||||
grid.appendChild(tile);
|
||||
});
|
||||
});
|
||||
// 维度标签后面回显当前选择
|
||||
s.parameters.forEach(function (p) {
|
||||
var el = $('picked-' + p.code);
|
||||
if (!el) return;
|
||||
el.textContent = state.sel[p.code] !== undefined ? '· ' + displayValue(p, state.sel[p.code]) : '';
|
||||
});
|
||||
renderSkuBar();
|
||||
updateSelection();
|
||||
if (crossFill) crossFill(); // 平替对照随选型刷新 (只显示当前型号的平替)
|
||||
}
|
||||
@@ -753,6 +791,70 @@
|
||||
load3D(v);
|
||||
}
|
||||
|
||||
// ---------- 零件号实时拼装 (DESTACO 风) ----------
|
||||
// 不依赖编码模板 (40 个系列里只有 KC 有模板): 用「当前候选型号的最长公共前缀」——
|
||||
// 选得越多, 候选集越小, 公共前缀越长, 逐字拼出完整零件号。
|
||||
// 已用 check-sku-lcp.py 验证 40/40 系列前缀单调增长 (不回退)。
|
||||
function candidateCodes() {
|
||||
var s = state.series;
|
||||
if (!s) return [];
|
||||
var keys = Object.keys(state.sel);
|
||||
if (!keys.length) return (s.variants || []).map(function (v) { return v.modelCode; });
|
||||
return (s.variants || []).filter(function (v) {
|
||||
return keys.every(function (k) { return str(v.params[k]) === str(state.sel[k]); });
|
||||
}).map(function (v) { return v.modelCode; });
|
||||
}
|
||||
|
||||
function lcp(list) {
|
||||
if (!list.length) return '';
|
||||
var a = list.reduce(function (m, x) { return x < m ? x : m; });
|
||||
var b = list.reduce(function (m, x) { return x > m ? x : m; });
|
||||
var i = 0;
|
||||
while (i < a.length && i < b.length && a[i] === b[i]) i++;
|
||||
return a.slice(0, i);
|
||||
}
|
||||
|
||||
function renderSkuBar() {
|
||||
var bar = $('skuBar');
|
||||
if (!bar) return;
|
||||
var s = state.series;
|
||||
if (!s) return;
|
||||
var chosen = Object.keys(state.sel).length;
|
||||
var total = s.parameters.length;
|
||||
var cand = candidateCodes();
|
||||
var pre = lcp(cand);
|
||||
// 未选的维度用占位符预演长度 (让用户看到"还差几位")
|
||||
var rest = '';
|
||||
s.parameters.forEach(function (p) {
|
||||
if (state.sel[p.code] !== undefined) return;
|
||||
rest += '·';
|
||||
});
|
||||
|
||||
bar.classList.toggle('done', chosen === total && cand.length === 1);
|
||||
$('skuCode').innerHTML = pre
|
||||
? '<span class="sku-fixed">' + esc(pre) + '</span>' +
|
||||
'<span class="sku-tbd">' + esc(rest) + '</span>'
|
||||
: '<span class="sku-tbd">' + esc(t('选择参数以生成零件号', 'Select options to build part number')) + '</span>';
|
||||
|
||||
var bar2 = $('skuProg');
|
||||
if (bar2) bar2.style.width = (total ? chosen / total * 100 : 0) + '%';
|
||||
|
||||
var hint = $('skuHint');
|
||||
if (hint) {
|
||||
if (chosen < total) {
|
||||
hint.textContent = t('已选 ' + chosen + '/' + total + ' 项 · 候选 ' +
|
||||
cand.length + ' 个型号', chosen + '/' + total + ' selected · ' + cand.length + ' candidates');
|
||||
} else if (cand.length === 1) {
|
||||
hint.textContent = t('零件号完整', 'Part number complete');
|
||||
} else if (cand.length > 1) {
|
||||
hint.textContent = t('该参数组合对应 ' + cand.length + ' 个型号, 请在下方选择',
|
||||
cand.length + ' models share these options — pick one below');
|
||||
} else {
|
||||
hint.textContent = t('无匹配型号', 'No matching model');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 参数表 (含范围筛选) ----------
|
||||
function renderRangeRow() {
|
||||
var row = $('rangeRow');
|
||||
|
||||
@@ -35,6 +35,13 @@
|
||||
|
||||
<section id="params" class="params hidden">
|
||||
<div id="modelTitle" class="model-title">请选择产品系列</div>
|
||||
<!-- 零件号实时条: 边选边拼, 已确定片段高亮 / 待定片段虚化 -->
|
||||
<div id="skuBar" class="sku-bar">
|
||||
<div class="sku-label" id="skuLabel">零件号 Part Number</div>
|
||||
<div class="sku-code" id="skuCode"></div>
|
||||
<div class="sku-prog"><i id="skuProg"></i></div>
|
||||
<div class="sku-hint" id="skuHint"></div>
|
||||
</div>
|
||||
<div id="paramPanel"></div>
|
||||
<div id="actions">
|
||||
<a id="btnDownload" class="btn primary hidden" download>下载 STEP</a>
|
||||
|
||||
111
web/style.css
111
web/style.css
@@ -245,8 +245,17 @@ main { flex: 1; display: flex; min-height: 0; }
|
||||
font-size: 16px; font-weight: 700; margin-bottom: 12px;
|
||||
color: var(--gray-900); letter-spacing: -.3px; line-height: 1.4;
|
||||
}
|
||||
.param-row { margin-bottom: 11px; }
|
||||
.param-row label { display: block; font-size: 12px; color: var(--text-dim); margin-bottom: 4px; }
|
||||
.param-row { margin-bottom: 14px; }
|
||||
.param-row label {
|
||||
display: flex; align-items: baseline; gap: 7px;
|
||||
font-size: 11px; font-weight: 700; color: var(--text-faint);
|
||||
text-transform: uppercase; letter-spacing: .5px; margin-bottom: 7px;
|
||||
}
|
||||
/* 维度已选值回显在其后 */
|
||||
.param-row label .picked {
|
||||
text-transform: none; letter-spacing: 0; font-weight: 600;
|
||||
color: var(--primary-700); font-size: 11.5px;
|
||||
}
|
||||
.param-row select, .param-input {
|
||||
width: 100%; height: 36px; padding: 0 10px; font-size: 13px;
|
||||
border: 1px solid var(--border); border-radius: var(--radius-sm);
|
||||
@@ -258,6 +267,104 @@ main { flex: 1; display: flex; min-height: 0; }
|
||||
}
|
||||
#actions { margin-top: 16px; display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
|
||||
/* ============================================================ 零件号实时条 (DESTACO 风)
|
||||
* 边选边拼: 已确定部分实心高亮, 未定部分虚化占位。 */
|
||||
/* ============================================================ 零件号实时条
|
||||
* 边选边拼: 已确定片段的实心高亮, 未定片段虚化占位。
|
||||
* 配色走浅色主调 (与整体卡片风格一致), 不用深色块。 */
|
||||
.sku-bar {
|
||||
background: linear-gradient(135deg, var(--primary-50), #ffffff 70%);
|
||||
border: 1px solid var(--primary-200);
|
||||
border-radius: var(--radius); padding: 13px 15px; margin-bottom: 14px;
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
.sku-bar .sku-label {
|
||||
font-size: 10px; text-transform: uppercase; letter-spacing: .7px;
|
||||
color: var(--text-faint); margin-bottom: 5px;
|
||||
}
|
||||
.sku-bar .sku-code {
|
||||
font-family: ui-monospace, Consolas, monospace;
|
||||
font-size: 20px; font-weight: 700; letter-spacing: -.4px;
|
||||
word-break: break-all; line-height: 1.25; min-height: 25px;
|
||||
}
|
||||
.sku-bar .sku-fixed { color: var(--primary-700); } /* 已确定片段 */
|
||||
.sku-bar .sku-tbd { color: var(--gray-300); } /* 待定占位 */
|
||||
.sku-bar .sku-prog {
|
||||
margin-top: 9px; height: 3px; border-radius: 999px;
|
||||
background: var(--primary-100); overflow: hidden;
|
||||
}
|
||||
.sku-bar .sku-prog > i {
|
||||
display: block; height: 100%; width: 0;
|
||||
background: linear-gradient(90deg, var(--primary-400), var(--primary-600));
|
||||
border-radius: 999px; transition: width .3s cubic-bezier(.2,.8,.3,1);
|
||||
}
|
||||
/* 零件号完整: 整块转为实心青色, 作为"完成"的强反馈 */
|
||||
.sku-bar.done {
|
||||
background: linear-gradient(135deg, var(--primary-500), var(--primary-600));
|
||||
border-color: transparent;
|
||||
box-shadow: var(--shadow-btn);
|
||||
}
|
||||
.sku-bar.done .sku-label { color: rgba(255,255,255,.72); }
|
||||
.sku-bar.done .sku-fixed { color: #fff; }
|
||||
.sku-bar.done .sku-tbd { color: rgba(255,255,255,.5); }
|
||||
.sku-bar.done .sku-prog { background: rgba(255,255,255,.28); }
|
||||
.sku-bar.done .sku-prog > i { background: #fff; }
|
||||
.sku-bar.done .sku-hint { color: rgba(255,255,255,.85); }
|
||||
.sku-bar .sku-hint {
|
||||
margin-top: 8px; font-size: 11px; color: var(--text-dim);
|
||||
}
|
||||
|
||||
/* ============================================================ 选项块 (替代下拉)
|
||||
* 有图用卡片, 无图用文字块; 一行多个, 自动换行。 */
|
||||
.opt-grid {
|
||||
display: grid; gap: 7px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
|
||||
}
|
||||
.opt-grid.wide { grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); }
|
||||
.opt {
|
||||
position: relative; cursor: pointer; user-select: none;
|
||||
border: 1.5px solid var(--border); border-radius: var(--radius-sm);
|
||||
background: var(--surface); padding: 8px 9px;
|
||||
display: flex; flex-direction: column; align-items: center; gap: 3px;
|
||||
min-height: 46px; justify-content: center;
|
||||
transition: transform .18s cubic-bezier(.2,.8,.3,1),
|
||||
border-color .18s, background .18s, box-shadow .18s;
|
||||
}
|
||||
.opt:hover {
|
||||
transform: translateY(-2px); border-color: var(--primary-400);
|
||||
box-shadow: 0 6px 16px rgba(20,184,166,.18);
|
||||
}
|
||||
.opt .opt-k {
|
||||
font-family: ui-monospace, Consolas, monospace;
|
||||
font-size: 13px; font-weight: 700; color: var(--gray-900);
|
||||
line-height: 1.2; text-align: center; word-break: break-word;
|
||||
}
|
||||
.opt .opt-d {
|
||||
font-size: 10px; color: var(--text-faint); text-align: center;
|
||||
line-height: 1.25; word-break: break-word;
|
||||
}
|
||||
/* 选中态: 青色填充 */
|
||||
.opt.on {
|
||||
border-color: var(--primary-500);
|
||||
background: var(--primary-50);
|
||||
box-shadow: 0 0 0 2px rgba(20,184,166,.22);
|
||||
}
|
||||
.opt.on .opt-k { color: var(--primary-700); }
|
||||
.opt.on::after {
|
||||
content: '✓'; position: absolute; top: 3px; right: 5px;
|
||||
font-size: 10px; color: var(--primary-600); font-weight: 700;
|
||||
}
|
||||
/* 不可选 (被联动规则排除): 置灰 + 划线 + 不可点 */
|
||||
.opt.off {
|
||||
opacity: .32; cursor: not-allowed; background: var(--gray-50);
|
||||
border-style: dashed;
|
||||
}
|
||||
.opt.off .opt-k { text-decoration: line-through; }
|
||||
.opt.off:hover { transform: none; box-shadow: none; border-color: var(--border); }
|
||||
/* 无图时的纯文字块: 更紧凑 */
|
||||
.opt.txt { min-height: 38px; padding: 7px 8px; }
|
||||
.opt.txt .opt-k { font-size: 12.5px; }
|
||||
|
||||
/* ------------------------------------------------------------ 按钮 */
|
||||
.btn {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
|
||||
Reference in New Issue
Block a user