v1.0: 夹紧缸模块真实数据替换 + 仓库瘦身
数据 (CLM 气动夹紧缸分类): - 1 系列 demo -> 11 个官方系列, 6 型号 -> 859 个真实型号 - 参数化升级: 仅缸径 -> 最多 5 维 (缸径/臂位置/臂类型/臂样式/传感器) - 11 系列各含官方图册附图 (84 张) + datasheet PDF - 修复: 枚举维度空串必须为合法值 (否则级联过滤后下拉全空) - 修复: catalog.json 分类 series 数组未更新导致新系列不进包 - 修复: _step 副本后缀归一化错误 (产生假型号) 工具: - gen-clamp-series.py 数据清洗 (X盘数模 -> CSV/meta/STEP) - copy-clamp-step.py 8线程并行复制 (网络盘 I/O 优化) - gen-clamp-manual.py 官方图册 PDF -> 附图 - update-clamp-meta-attach.py / fix-clamp-meta-empty-enum.py 仓库瘦身: - .gitignore 排除数模(5GB)/.opc(1.2GB)/编译产物/发布产物 - 移出索引: release/ bin产物 STEP数模 .opc 大型PDF 等 - 提交体积 17MB (原 15.97GB) 其他: SelfTest.cs 可解析选型逻辑放宽 (稀疏数据用真实变体回验) Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
BIN
onebot-data/tools/__pycache__/gen-clamp-series.cpython-314.pyc
Normal file
BIN
onebot-data/tools/__pycache__/gen-clamp-series.cpython-314.pyc
Normal file
Binary file not shown.
44
onebot-data/tools/copy-clamp-step.py
Normal file
44
onebot-data/tools/copy-clamp-step.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""并行复制夹紧缸 STEP 文件(去重后)到 catalog/step/,用归一化 .step 命名。"""
|
||||
import os, sys, shutil, collections
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
# 复用 gen-clamp-series 的 collect()(仅复制,不重写 CSV/meta)
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location('gcs', os.path.join(os.path.dirname(__file__), 'gen-clamp-series.py'))
|
||||
gcs = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(gcs)
|
||||
|
||||
DST = r'D:/开发/OnebotCatalog/onebot-data/catalog/step'
|
||||
os.makedirs(DST, exist_ok=True)
|
||||
|
||||
models = gcs.collect()
|
||||
print(f'去重后需复制 {len(models)} 个文件')
|
||||
|
||||
todo = []
|
||||
for m in models.values():
|
||||
dst = os.path.join(DST, m['model'] + '.step')
|
||||
if not os.path.exists(dst):
|
||||
todo.append((m['src'], dst, m['model']))
|
||||
print(f'尚未复制 {len(todo)} 个')
|
||||
|
||||
def cp(item):
|
||||
src, dst, model = item
|
||||
try:
|
||||
shutil.copy2(src, dst)
|
||||
return (model, 'ok', None)
|
||||
except Exception as e:
|
||||
return (model, 'err', str(e))
|
||||
|
||||
done = 0
|
||||
with ThreadPoolExecutor(max_workers=8) as ex:
|
||||
futs = [ex.submit(cp, it) for it in todo]
|
||||
for fut in as_completed(futs):
|
||||
model, status, err = fut.result()
|
||||
done += 1
|
||||
if status == 'err':
|
||||
print(f'[ERR] {model}: {err}')
|
||||
elif done % 50 == 0:
|
||||
print(f' 进度 {done}/{len(todo)}')
|
||||
print(f'完成,共复制 {done} 个文件')
|
||||
49
onebot-data/tools/fix-clamp-meta-empty-enum.py
Normal file
49
onebot-data/tools/fix-clamp-meta-empty-enum.py
Normal file
@@ -0,0 +1,49 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
修复已有 meta.json: 枚举维度的空串 '' 必须是合法值(参考 KC magnet values=['','M'])。
|
||||
原因: DomainValid 要求 enum 值在 values 内, 数据里大量型号该维度为空 → 判非法
|
||||
→ AllowedOptions 过滤后下拉全空 (用户反馈"只有 UCBP 能选, 其他都空")。
|
||||
用法: PYTHONUTF8=1 python fix-clamp-meta-empty-enum.py
|
||||
"""
|
||||
import json, csv, os
|
||||
|
||||
CAT = r'D:/开发/OnebotCatalog/onebot-data/catalog'
|
||||
SERIES = ['UCBP','UCBM','UCBT','UCBQ','UCBH','UCDP','UCIM','UCIP','UCNM','UCNP','UFL']
|
||||
ENUM_CODES = ['pos', 'type', 'style', 'sensor']
|
||||
DISPLAY_EMPTY = {
|
||||
'pos': '不适用', 'type': '不适用', 'style': '不适用', 'sensor': '不适用',
|
||||
}
|
||||
|
||||
for s in SERIES:
|
||||
mp = os.path.join(CAT, 'series', s + '.meta.json')
|
||||
with open(mp, encoding='utf-8') as f:
|
||||
meta = json.load(f)
|
||||
params = meta['series']['parameters']
|
||||
changed = []
|
||||
for p in params:
|
||||
if p['code'] not in ENUM_CODES:
|
||||
continue
|
||||
vals = p['values']
|
||||
if '' in vals:
|
||||
continue
|
||||
# 从 CSV 确认该维度是否真有空值
|
||||
cp = os.path.join(CAT, 'csv', s + '.csv')
|
||||
has_empty = False
|
||||
with open(cp, encoding='utf-8-sig') as f:
|
||||
for row in csv.DictReader(f):
|
||||
if p['code'] in row and (row[p['code']] or '') == '':
|
||||
has_empty = True
|
||||
break
|
||||
if not has_empty:
|
||||
continue
|
||||
p['values'] = [''] + vals
|
||||
d = p.get('display') or {}
|
||||
d[''] = DISPLAY_EMPTY.get(p['code'], '不适用')
|
||||
p['display'] = d
|
||||
changed.append(f"{p['code']}:{p['values']}")
|
||||
if changed:
|
||||
with open(mp, 'w', encoding='utf-8') as f:
|
||||
json.dump(meta, f, ensure_ascii=False, indent=1)
|
||||
print(f'[{s}] 补空值 -> {" | ".join(changed)}')
|
||||
else:
|
||||
print(f'[{s}] 无需修改')
|
||||
128
onebot-data/tools/gen-clamp-manual.py
Normal file
128
onebot-data/tools/gen-clamp-manual.py
Normal file
@@ -0,0 +1,128 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
为夹紧缸 11 个系列生成附图(多页尺寸图 + 裁剪版数据表 PDF)。
|
||||
来源:Univer 官方图册 PDF(X 盘)。
|
||||
用法: PYTHONUTF8=1 python gen-clamp-manual.py [--clean]
|
||||
--clean 先清空各系列 manual 目录再生成(避免旧页残留)
|
||||
输出: manual/<系列>/p<NN>.png + <系列>_manual.pdf(裁成该系列的页)
|
||||
"""
|
||||
import fitz, os, shutil, sys
|
||||
|
||||
SRC = r'X:/新开发产品-Research and Development/2_产品资料/Univer资料/官方图册'
|
||||
DST = r'D:/开发/OnebotCatalog/onebot-data/catalog/manual'
|
||||
DPI = 110
|
||||
CLEAN = '--clean' in sys.argv
|
||||
MAIN_CATALOG = 'katalog-pneumatische-spanner-univer-13032019-en.pdf'
|
||||
|
||||
# 系列 -> 官方 PDF 列表(多缸径系列合并成一个系列的图,按缸径顺序)
|
||||
SERIES_PDFS = {
|
||||
'UCBP': ['ucbp25cten_en380', 'ucbp40en_en382', 'ucbp50cten_en391', 'ucbp63cten_en401', 'ucbp80cten_en392'],
|
||||
'UCBM': ['ucbm32cten_en432', 'ucbm50cten_en394', 'ucbm63cten_en395', 'ucbm80cten_en414'],
|
||||
'UCBH': ['ucbh63cten_en393'],
|
||||
'UCBQ': ['ucbq63cten_en397'],
|
||||
'UCDP': ['ucdp40cten_en417', 'ucdp63cten_en418'],
|
||||
'UCIM': ['ucim40cten_en415'],
|
||||
'UCIP': ['ucip40cten_en402'],
|
||||
'UCNM': ['ucnm50cten_en387', 'ucnm63cten_en389', 'ucnm80cten_en429'],
|
||||
'UCNP': ['ucnp50cten_en385', 'ucnp63cten_en403', 'ucnp80cten_en388'],
|
||||
# UFL450/UHL463 实为官方 UCBL(Manual power clamps Ø50/Ø63)
|
||||
'UFL': ['ucbl450cten_en398', 'ucbl463cten_en478'],
|
||||
}
|
||||
UCBT_FROM_MAIN = True # UCBT 无独立图册,从主目录提取
|
||||
|
||||
|
||||
def classify(page):
|
||||
"""按内容标记归类页面: char(特性) / dim(尺寸) / arm(夹臂配件) / other"""
|
||||
t = page.get_text().upper()
|
||||
if 'CLAMPING ARMS' in t or 'ACCESSORIES' in t:
|
||||
return 'arm'
|
||||
if 'DIMENSIONS' in t:
|
||||
return 'dim'
|
||||
if 'CHARACTERISTICS' in t or 'CODIFICATION' in t:
|
||||
return 'char'
|
||||
return 'other'
|
||||
|
||||
|
||||
def render(pages, out_dir, start_no):
|
||||
"""pages = [(doc, page_idx), ...] 渲染为 p<NN>.png,返回下一个编号"""
|
||||
n = start_no
|
||||
for doc, i in pages:
|
||||
pix = doc[i].get_pixmap(dpi=DPI, colorspace=fitz.csGRAY)
|
||||
pix.save(os.path.join(out_dir, f'p{n:02d}.png'))
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
def main():
|
||||
os.makedirs(DST, exist_ok=True)
|
||||
for series, pdfs in SERIES_PDFS.items():
|
||||
d = os.path.join(DST, series)
|
||||
if CLEAN and os.path.isdir(d):
|
||||
shutil.rmtree(d)
|
||||
os.makedirs(d, exist_ok=True)
|
||||
|
||||
all_pages = [] # (doc, page_idx, kind)
|
||||
src_docs = []
|
||||
for name in pdfs:
|
||||
p = os.path.join(SRC, name + '.pdf')
|
||||
if not os.path.exists(p):
|
||||
print(f'[{series}] 源缺失: {name}')
|
||||
continue
|
||||
doc = fitz.open(p)
|
||||
src_docs.append((name, doc))
|
||||
for i in range(doc.page_count):
|
||||
all_pages.append((doc, i, classify(doc[i])))
|
||||
|
||||
if not all_pages:
|
||||
print(f'[{series}] 无可用源 PDF')
|
||||
continue
|
||||
|
||||
# 渲染顺序: 特性页 -> 尺寸页 -> 夹臂页(稳定顺序,便于对照)
|
||||
order = {'char': 0, 'dim': 1, 'arm': 2, 'other': 3}
|
||||
all_pages.sort(key=lambda x: order[x[2]])
|
||||
n = render([(doc, i) for doc, i, k in all_pages], d, 1)
|
||||
kinds = [k for _, _, k in all_pages]
|
||||
print(f'[{series}] {len(all_pages)} 页 -> {kinds}')
|
||||
|
||||
# 数据表 PDF: 只保留该系列的页(合并多缸径 PDF)
|
||||
out_pdf = fitz.open()
|
||||
for name, doc in src_docs:
|
||||
out_pdf.insert_pdf(doc)
|
||||
out_path = os.path.join(d, series + '_manual.pdf')
|
||||
out_pdf.save(out_path, garbage=3, deflate=True)
|
||||
out_pdf.close()
|
||||
print(f'[{series}] datasheet <- {len(pdfs)} 个PDF合并 ({os.path.getsize(out_path)/1024:.0f}KB)')
|
||||
|
||||
# UCBT: 从主目录提取
|
||||
if UCBT_FROM_MAIN:
|
||||
d = os.path.join(DST, 'UCBT')
|
||||
if CLEAN and os.path.isdir(d):
|
||||
shutil.rmtree(d)
|
||||
os.makedirs(d, exist_ok=True)
|
||||
main_pdf = os.path.join(SRC, MAIN_CATALOG)
|
||||
doc = fitz.open(main_pdf)
|
||||
# 找 UCBT40 相关页(数据页)
|
||||
idxs = []
|
||||
for i in range(doc.page_count):
|
||||
t = doc[i].get_text().upper()
|
||||
if 'UCBT40' in t and any(m in t for m in ('DIMENSIONS', 'CODIFICATION', 'CHARACTERISTICS')):
|
||||
idxs.append(i)
|
||||
if idxs:
|
||||
n = render([(doc, i) for i in idxs], d, 1)
|
||||
print(f'[UCBT] {len(idxs)} 页 <- 主目录 idx={idxs}')
|
||||
# datasheet: 只裁 UCBT 那几页
|
||||
out_pdf = fitz.open()
|
||||
for i in idxs:
|
||||
out_pdf.insert_pdf(doc, from_page=i, to_page=i)
|
||||
out_path = os.path.join(d, 'UCBT_manual.pdf')
|
||||
out_pdf.save(out_path, garbage=3, deflate=True)
|
||||
out_pdf.close()
|
||||
print(f'[UCBT] datasheet <- 裁剪 {len(idxs)} 页 ({os.path.getsize(out_path)/1024:.0f}KB)')
|
||||
else:
|
||||
print('[UCBT] 未找到数据页')
|
||||
|
||||
print('完成')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
248
onebot-data/tools/gen-clamp-series.py
Normal file
248
onebot-data/tools/gen-clamp-series.py
Normal file
@@ -0,0 +1,248 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
夹紧缸数据清洗脚本 v2(Univer 真实数模 -> OnebotCatalog 目录数据)
|
||||
最终方案:11 个系列 + 5 维参数(缸径/臂位置/臂类型/臂样式/传感器)
|
||||
用法:
|
||||
PYTHONUTF8=1 python gen-clamp-series.py --dry-run # 只分析打印,不写文件
|
||||
PYTHONUTF8=1 python gen-clamp-series.py # 正式生成
|
||||
"""
|
||||
import os, re, sys, shutil, collections, json
|
||||
|
||||
SRC = r'X:/新开发产品-Research and Development/1_产品3D数模(发客户仿真)/1-夹紧缸'
|
||||
DST = r'D:/开发/OnebotCatalog/onebot-data/catalog'
|
||||
BORES = ['25缸', '32缸', '40缸', '50缸', '63缸', '80缸']
|
||||
DRY = '--dry-run' in sys.argv
|
||||
|
||||
LEGAL_BORES = [80, 63, 50, 40, 32, 25]
|
||||
|
||||
# 系列前缀映射(旧码 -> 官方 UC 前缀码;UFL/UHL 单独系列)
|
||||
PREFIX_MAP = [
|
||||
('UCBP', 'UCBP'), ('UBP', 'UCBP'),
|
||||
('UCBM', 'UCBM'), ('UBM', 'UCBM'),
|
||||
('UCBT', 'UCBT'), ('UBT', 'UCBT'),
|
||||
('UCBQ', 'UCBQ'), ('UBQ', 'UCBQ'),
|
||||
('UCBH', 'UCBH'), ('UBH', 'UCBH'),
|
||||
('UCDP', 'UCDP'), ('UDP', 'UCDP'),
|
||||
('UCIM', 'UCIM'), ('IBM', 'UCIM'),
|
||||
('UCIP', 'UCIP'), ('IBP', 'UCIP'),
|
||||
('UCNM', 'UCNM'), ('UNM', 'UCNM'),
|
||||
('UCNP', 'UCNP'), ('UNP', 'UCNP'),
|
||||
('UFL', 'UFL'), ('UHL', 'UFL'),
|
||||
]
|
||||
|
||||
SERIES_NAME = {
|
||||
'UCBP': ('UCBP 标准气动夹紧缸', 'UCBP Standard Pneumatic Power Clamp', 'ucbp jiajin clamping standard'),
|
||||
'UCBM': ('UCBM 带手杆气动夹紧缸', 'UCBM Pneumatic Power Clamp with Hand Lever', 'ucbm jiajin lever'),
|
||||
'UCBT': ('UCBT 带中央手杆气动夹紧缸', 'UCBT Pneumatic Power Clamp with Central Hand Lever', 'ucbt jiajin central lever'),
|
||||
'UCBQ': ('UCBQ 带手杆补偿夹紧缸', 'UCBQ Power Clamp with Hand Lever & Compensation', 'ucbq jiajin compensation'),
|
||||
'UCBH': ('UCBH 夹紧缸 Ø63/Ø22', 'UCBH Power Clamp Ø63/Ø22', 'ucbh jiajin'),
|
||||
'UCDP': ('UCDP 双夹臂气动夹紧缸', 'UCDP Double Arm Pneumatic Power Clamp', 'ucdp jiajin double arm'),
|
||||
'UCIM': ('UCIM 可互换带手杆夹紧缸', 'UCIM Interchangeable Power Clamp with Hand Lever', 'ucim jiajin interchangeable'),
|
||||
'UCIP': ('UCIP 可互换气动夹紧缸', 'UCIP Interchangeable Pneumatic Power Clamp', 'ucip jiajin interchangeable'),
|
||||
'UCNM': ('UCNM NAAMS 带手杆夹紧缸', 'UCNM NAAMS Power Clamp with Hand Lever', 'ucnm naams jiajin'),
|
||||
'UCNP': ('UCNP NAAMS 气动夹紧缸', 'UCNP NAAMS Pneumatic Power Clamp', 'ucnp naams jiajin'),
|
||||
'UFL': ('UFL/UHL 停产气动夹紧缸', 'UFL/UHL Legacy Pneumatic Power Clamp (Discontinued)', 'ufl uhl jiajin legacy'),
|
||||
}
|
||||
|
||||
POS_DISPLAY = {'V': '90°', 'O': '180°', 'N': '无臂', '': '不适用'}
|
||||
TYPE_DISPLAY = {'C': '中央 15mm', 'D': '右 15mm', 'S': '左 15mm', 'E': '中央 45mm',
|
||||
'F': '右 45mm', 'L': '左 45mm', 'N': '无臂', '': '不适用'}
|
||||
STYLE_DISPLAY = {'A': '铝臂', 'S': '钢臂', 'N': '无臂', '': '不适用'}
|
||||
SENSOR_DISPLAY = {'K': '电子 PNP M12', 'N': '无传感器', 'Y': '电子 PNP 白LED', 'D': '气动传感器', '': '不适用'}
|
||||
|
||||
# 杂音标记(这些出现说明文件是测试/副本,归一化时优先丢弃)
|
||||
NOISE = re.compile(r'20\d{12,14}|PROVA|_ALLCATPART|ALLCATPART|-\d{2}-\d{2}-\d{4}')
|
||||
|
||||
|
||||
def _extract_bore(s):
|
||||
for b in LEGAL_BORES:
|
||||
bs = str(b)
|
||||
if s.startswith(bs):
|
||||
return b, s[len(bs):]
|
||||
return None
|
||||
|
||||
|
||||
def normalize(name):
|
||||
"""归一化 -> (model_code, series, bore, pos, type, style, sensor) 或 None"""
|
||||
raw = os.path.splitext(name)[0]
|
||||
# 排除含非 ASCII(中文夹臂/大孔等)
|
||||
if not all(ord(c) < 128 for c in raw):
|
||||
return None
|
||||
base = raw.upper()
|
||||
# 先剥离 "_step"/"_1" 副本后缀(必须在去下划线之前,否则 _ 已被抹掉匹配不到)
|
||||
base = re.sub(r'[_\s]*_?STEP$', '', base) # UCBM40OESK0_step -> UCBM40OESK0
|
||||
base = re.sub(r'_1$', '', base)
|
||||
base = base.replace(' ', '').replace('_', '')
|
||||
# 记录是否含杂音(用于去重优先级)
|
||||
has_noise = bool(NOISE.search(base))
|
||||
base = NOISE.sub('', base)
|
||||
base = base.replace('-S', '\x01') # 钢臂标记占位(保护 -S 不被后续清理)
|
||||
base = base.replace('-', '') # 去连字符
|
||||
base = base.replace('\x01', '-S') # 恢复 -S
|
||||
if not base:
|
||||
return None
|
||||
# 系列识别
|
||||
series = rest = None
|
||||
for pfx, code in PREFIX_MAP:
|
||||
if base.startswith(pfx):
|
||||
series, rest = code, base[len(pfx):]
|
||||
break
|
||||
if series is None or rest == '':
|
||||
return None
|
||||
# UFL/UHL 特殊缸径:UFL450->50, UHL463->63
|
||||
if series == 'UFL':
|
||||
if rest.startswith('450'):
|
||||
bore, tail = 50, rest[3:]
|
||||
elif rest.startswith('463'):
|
||||
bore, tail = 63, rest[3:]
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
r = _extract_bore(rest)
|
||||
if r is None:
|
||||
return None
|
||||
bore, tail = r
|
||||
# 解析 pos/type/style/sensor
|
||||
pos = type_ = style = sensor = ''
|
||||
i = 0
|
||||
if i < len(tail) and tail[i] in 'VON':
|
||||
pos, i = tail[i], i + 1
|
||||
if i < len(tail) and tail[i] in 'CDSEFLN':
|
||||
type_, i = tail[i], i + 1
|
||||
if i < len(tail) and tail[i] in 'ASN':
|
||||
style, i = tail[i], i + 1
|
||||
if i < len(tail) and tail[i] in 'KNYD':
|
||||
sensor, i = tail[i], i + 1
|
||||
# 旧命名 -S 后缀 => 钢臂
|
||||
if tail.endswith('-S') and style == '':
|
||||
style = 'S'
|
||||
return (base, series, bore, pos, type_, style, sensor, has_noise)
|
||||
|
||||
|
||||
def collect():
|
||||
search_roots = [os.path.join(SRC, b) for b in BORES] + [SRC]
|
||||
found = collections.defaultdict(list)
|
||||
for root in search_roots:
|
||||
for dirpath, dirs, files in os.walk(root):
|
||||
for f in files:
|
||||
if f.lower().endswith(('.stp', '.step')):
|
||||
found[f].append(os.path.join(dirpath, f))
|
||||
# 候选:model -> 最佳源文件
|
||||
best = {}
|
||||
for fname, paths in found.items():
|
||||
r = normalize(fname)
|
||||
if r is None:
|
||||
continue
|
||||
model, series, bore, pos, type_, style, sensor, has_noise = r
|
||||
cur = best.get(model)
|
||||
# 优先级:干净名 > 杂音名;同优先级取 mtime 新
|
||||
better = (cur is None
|
||||
or (cur['has_noise'] and not has_noise)
|
||||
or (cur['has_noise'] == has_noise
|
||||
and os.path.getmtime(paths[0]) > os.path.getmtime(cur['src'])))
|
||||
if better:
|
||||
best[model] = dict(model=model, series=series, bore=bore, pos=pos,
|
||||
type=type_, style=style, sensor=sensor,
|
||||
src=paths[0], has_noise=has_noise)
|
||||
return best
|
||||
|
||||
|
||||
def main():
|
||||
models = collect()
|
||||
by_series = collections.defaultdict(list)
|
||||
for m in models.values():
|
||||
by_series[m['series']].append(m)
|
||||
|
||||
print('=== 归一化去重后型号总数:', len(models), '===')
|
||||
for series in sorted(by_series, key=lambda s: -len(by_series[s])):
|
||||
ms = by_series[series]
|
||||
bs = sorted(set(m['bore'] for m in ms))
|
||||
print(f'{series:6s} 型号 {len(ms):4d} 缸径 {bs}')
|
||||
|
||||
if DRY:
|
||||
print()
|
||||
for series in sorted(by_series):
|
||||
ms = sorted(by_series[series], key=lambda m: (m['bore'], m['model']))
|
||||
print(f'\n## {series} ({len(ms)} 个)')
|
||||
for m in ms:
|
||||
code = f"pos={m['pos'] or '-'} type={m['type'] or '-'} style={m['style'] or '-'} sensor={m['sensor'] or '-'}"
|
||||
print(f" {m['model']:30s} bore={m['bore']:<3} {code}")
|
||||
return
|
||||
|
||||
# 1) CSV
|
||||
csv_dir = os.path.join(DST, 'csv')
|
||||
for series, ms in by_series.items():
|
||||
ms = sorted(ms, key=lambda m: (m['bore'], m['model']))
|
||||
rows = ['model_code,bore,pos,type,style,sensor,step_file']
|
||||
for m in ms:
|
||||
rows.append(f"{m['model']},{m['bore']},{m['pos']},{m['type']},{m['style']},{m['sensor']},{m['model']}.step")
|
||||
with open(os.path.join(csv_dir, series + '.csv'), 'w', encoding='utf-8-sig', newline='') as fp:
|
||||
fp.write('\r\n'.join(rows) + '\r\n')
|
||||
print(f'写 CSV: {series}.csv ({len(ms)} 行)')
|
||||
|
||||
# 2) meta.json
|
||||
series_dir = os.path.join(DST, 'series')
|
||||
for series, ms in by_series.items():
|
||||
bores = sorted(set(m['bore'] for m in ms))
|
||||
# 枚举维度: 空串 '' 是合法值(表示该维度"无/未指定"), 必须保留在 values 里
|
||||
# (参考 KC 的 magnet values=['', 'M']) —— 否则 DomainValid 会把 "" 判非法,
|
||||
# AllowedOptions 过滤后下拉全空。仅当除空串外无真值时, 才剔除该维度。
|
||||
poss = [''] + sorted({m['pos'] for m in ms if m['pos']})
|
||||
types = [''] + sorted({m['type'] for m in ms if m['type']})
|
||||
styles = [''] + sorted({m['style'] for m in ms if m['style']})
|
||||
sensors = [''] + sorted({m['sensor'] for m in ms if m['sensor']})
|
||||
poss = poss if len(poss) > 1 else []
|
||||
types = types if len(types) > 1 else []
|
||||
styles = styles if len(styles) > 1 else []
|
||||
sensors = sensors if len(sensors) > 1 else []
|
||||
name_zh, name_en, kw = SERIES_NAME[series]
|
||||
params = [
|
||||
{"code": "bore", "nameZh": "缸径", "nameEn": "Bore", "keywords": "bore gangjing", "unit": "mm", "type": "number", "values": bores, "display": {}},
|
||||
{"code": "pos", "nameZh": "臂位置", "nameEn": "Arm Position", "keywords": "pos arm position weizhi", "unit": "", "type": "enum", "values": poss, "display": {k: POS_DISPLAY.get(k, k) for k in poss}},
|
||||
{"code": "type", "nameZh": "臂类型", "nameEn": "Arm Type", "keywords": "type arm leixing", "unit": "", "type": "enum", "values": types, "display": {k: TYPE_DISPLAY.get(k, k) for k in types}},
|
||||
{"code": "style", "nameZh": "臂样式", "nameEn": "Arm Style", "keywords": "style arm yangshi", "unit": "", "type": "enum", "values": styles, "display": {k: STYLE_DISPLAY.get(k, k) for k in styles}},
|
||||
{"code": "sensor", "nameZh": "传感器", "nameEn": "Sensor", "keywords": "sensor chuanganqi", "unit": "", "type": "enum", "values": sensors, "display": {k: SENSOR_DISPLAY.get(k, k) for k in sensors}},
|
||||
]
|
||||
# 值域为空的参数维度直接剔除(避免 UI 出现无值下拉;该维度在本系列数据里不存在)
|
||||
params = [p for p in params if p['values']]
|
||||
meta = {
|
||||
"series": {
|
||||
"code": series, "nameZh": name_zh, "nameEn": name_en, "keywords": kw,
|
||||
"parameters": params,
|
||||
"rules": [], "naming": {"stepNameTemplate": "{model}.step"},
|
||||
"attachments": [], "variants": []
|
||||
},
|
||||
"attachments": [
|
||||
{"kind": "manual", "lang": "", "source": f"manual/{series}/p01.png", "model": None},
|
||||
{"kind": "datasheet", "lang": "", "source": f"manual/{series}/{series}_manual.pdf", "model": None},
|
||||
]
|
||||
}
|
||||
with open(os.path.join(series_dir, series + '.meta.json'), 'w', encoding='utf-8') as fp:
|
||||
json.dump(meta, fp, ensure_ascii=False, indent=1)
|
||||
print(f'写 meta: {series}.meta.json bore={bores} pos={poss} type={types} style={styles} sensor={sensors}')
|
||||
|
||||
# 3) 复制 STEP
|
||||
step_dir = os.path.join(DST, 'step')
|
||||
copied = 0
|
||||
for m in models.values():
|
||||
dst_file = os.path.join(step_dir, m['model'] + '.step')
|
||||
if not os.path.exists(dst_file):
|
||||
shutil.copy2(m['src'], dst_file)
|
||||
copied += 1
|
||||
print(f'复制 STEP: {copied} 个(共 {len(models)} 型号)')
|
||||
|
||||
# 4) 更新 catalog.json:CLM 分类 series 数组
|
||||
cat_path = os.path.join(DST, 'catalog.json')
|
||||
with open(cat_path, 'r', encoding='utf-8-sig') as fp:
|
||||
cat = json.load(fp)
|
||||
series_order = ['UCBP', 'UCBM', 'UCBT', 'UCBQ', 'UCBH', 'UCDP', 'UCIM', 'UCIP', 'UCNM', 'UCNP', 'UFL']
|
||||
for c in cat['categories']:
|
||||
if c['code'] == 'CLM':
|
||||
c['series'] = series_order
|
||||
with open(cat_path, 'w', encoding='utf-8-sig') as fp:
|
||||
json.dump(cat, fp, ensure_ascii=False, indent=1)
|
||||
print('更新 catalog.json: CLM.series =', series_order)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
27
onebot-data/tools/update-clamp-meta-attach.py
Normal file
27
onebot-data/tools/update-clamp-meta-attach.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""按 manual/<系列>/ 下实际存在的 p*.png 更新 meta.json 的附件引用。"""
|
||||
import json, os, glob, re
|
||||
|
||||
CAT = r'D:/开发/OnebotCatalog/onebot-data/catalog'
|
||||
SERIES = ['UCBP','UCBM','UCBT','UCBQ','UCBH','UCDP','UCIM','UCIP','UCNM','UCNP','UFL']
|
||||
|
||||
for s in SERIES:
|
||||
d = os.path.join(CAT, 'manual', s)
|
||||
if not os.path.isdir(d):
|
||||
print(f'[{s}] 无 manual 目录,跳过'); continue
|
||||
pngs = sorted(glob.glob(os.path.join(d, 'p*.png')))
|
||||
if not pngs:
|
||||
print(f'[{s}] 无 png,跳过'); continue
|
||||
atts = []
|
||||
for p in pngs:
|
||||
atts.append({"kind": "manual", "lang": "", "source": f"manual/{s}/{os.path.basename(p)}", "model": None})
|
||||
pdf = os.path.join(d, f'{s}_manual.pdf')
|
||||
if os.path.exists(pdf):
|
||||
atts.append({"kind": "datasheet", "lang": "", "source": f"manual/{s}/{s}_manual.pdf", "model": None})
|
||||
mp = os.path.join(CAT, 'series', s + '.meta.json')
|
||||
with open(mp, encoding='utf-8') as f:
|
||||
meta = json.load(f)
|
||||
meta['attachments'] = atts
|
||||
with open(mp, 'w', encoding='utf-8') as f:
|
||||
json.dump(meta, f, ensure_ascii=False, indent=1)
|
||||
print(f'[{s}] 附件 {len(atts)} 个 ({len(pngs)}图 + {"1PDF" if os.path.exists(pdf) else "无PDF"})')
|
||||
Reference in New Issue
Block a user