首次提交: OnebotCatalog 项目代码与文档(含 NX 按需生成服务二期、后台、一键启动)

This commit is contained in:
wangruiguo
2026-09-03 17:55:45 +08:00
commit fafa86d3a6
241 changed files with 78656 additions and 0 deletions

View File

@@ -0,0 +1,132 @@
# -*- coding: utf-8 -*-
"""
gen-manual-placeholders.py —— 为新系列生成手册占位文件夹与占位图片 (尺寸图窗口占位用)
背景: 老 11 个系列有手册拆页 (catalog\\manual\\<系列>\\pXX.png), 新 6 个系列
(UCBM/ULP/USP/UGP/UAGP/LAE) 暂无手册 → 尺寸图窗口空白。
本脚本为每个新系列生成一张占位 PNG (A4 比例, 灰框+系列代码大字) 并写入
series\\<系列>.meta.json 的 manual 附件引用。真实手册到货后用
render-manual-pages.py 重新拆页覆盖 p01.png 即可。
纯标准库实现 (zlib+struct 写 PNG, 5x7 点阵字体), 无任何第三方依赖。
用法: python gen-manual-placeholders.py
"""
import json
import os
import struct
import zlib
BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MANUAL_DIR = os.path.join(BASE, 'catalog', 'manual')
SERIES_DIR = os.path.join(BASE, 'catalog', 'series')
NEW_SERIES = ['UCBM', 'ULP', 'USP', 'UGP', 'UAGP', 'LAE']
# 5x7 点阵字体 (每字符 7 字节, 位=像素)
FONT = {
'A': [0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11], 'B': [0x1E, 0x11, 0x11, 0x1E, 0x11, 0x11, 0x1E],
'C': [0x0E, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0E], 'D': [0x1C, 0x12, 0x11, 0x11, 0x11, 0x12, 0x1C],
'E': [0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x1F], 'F': [0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x10],
'G': [0x0E, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0F], 'H': [0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11],
'I': [0x0E, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E], 'J': [0x07, 0x02, 0x02, 0x02, 0x12, 0x12, 0x0C],
'K': [0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11], 'L': [0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F],
'M': [0x11, 0x1B, 0x15, 0x15, 0x11, 0x11, 0x11], 'N': [0x11, 0x19, 0x15, 0x13, 0x11, 0x11, 0x11],
'O': [0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E], 'P': [0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10],
'Q': [0x0E, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0D], 'R': [0x1E, 0x11, 0x11, 0x1E, 0x14, 0x12, 0x11],
'S': [0x0F, 0x10, 0x10, 0x0E, 0x01, 0x01, 0x1E], 'T': [0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04],
'U': [0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E], 'V': [0x11, 0x11, 0x11, 0x11, 0x11, 0x0A, 0x04],
'W': [0x11, 0x11, 0x11, 0x15, 0x15, 0x1B, 0x11], 'X': [0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11],
'Y': [0x11, 0x11, 0x0A, 0x04, 0x04, 0x04, 0x04], 'Z': [0x1F, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1F],
'0': [0x0E, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0E], '1': [0x04, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x0E],
'2': [0x0E, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1F], '3': [0x1F, 0x02, 0x04, 0x06, 0x01, 0x11, 0x0E],
'4': [0x02, 0x06, 0x0A, 0x12, 0x1F, 0x02, 0x02], '5': [0x1F, 0x10, 0x1E, 0x01, 0x01, 0x11, 0x0E],
'6': [0x06, 0x08, 0x10, 0x1E, 0x11, 0x11, 0x0E], '7': [0x1F, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08],
'8': [0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E], '9': [0x0E, 0x11, 0x11, 0x0F, 0x01, 0x02, 0x0C],
'/': [0x01, 0x01, 0x02, 0x04, 0x08, 0x10, 0x10], '-': [0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00],
'.': [0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C], ':': [0x00, 0x0C, 0x0C, 0x00, 0x0C, 0x0C, 0x00],
' ': [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
}
W, H = 794, 1123 # A4 @96dpi 近似
def draw_text(buf, text, x0, y0, scale, color):
"""把 text 按 5x7 点阵放大 scale 倍画进 buf (0xFFFFFF 背景)"""
for ch in text:
glyph = FONT.get(ch.upper(), FONT[' '])
for row in range(7):
bits = glyph[row]
for col in range(5):
if bits & (0x10 >> col):
for dy in range(scale):
for dx in range(scale):
px = (x0 + col * scale + dx) * 3
py = y0 + row * scale + dy
buf[py * W * 3 + px] = color[0]
buf[py * W * 3 + px + 1] = color[1]
buf[py * W * 3 + px + 2] = color[2]
x0 += 6 * scale
def make_png(path, series):
buf = bytearray([0xF5, 0xF6, 0xF8] * (W * H)) # 浅灰底
gray = (0x9A, 0xA2, 0xAC)
dark = (0x3A, 0x45, 0x52)
# 图纸外框 + 四角标
for x in range(40, W - 40):
for y in (38, 39, H - 40, H - 39):
i = (y * W + x) * 3
buf[i], buf[i + 1], buf[i + 2] = gray
for y in range(38, H - 38):
for x in (40, 41, W - 42, W - 41):
i = (y * W + x) * 3
buf[i], buf[i + 1], buf[i + 2] = gray
draw_text(buf, series, 60, 90, 10, dark) # 系列代码大字
draw_text(buf, 'DRAWING PENDING', 60, 230, 3, gray) # 占位说明
draw_text(buf, 'SERIES: ' + series, 60, 270, 2, gray)
draw_text(buf, 'REPLACE THIS PAGE', 60, 300, 2, gray)
draw_text(buf, 'WITH REAL MANUAL', 60, 324, 2, gray)
# 对角辅助线 (工程图纸感)
for t in range(0, 300):
for x in (t, t + 1):
i = ((90 + t) * W + 60 + x) * 3
buf[i], buf[i + 1], buf[i + 2] = 0xE2, 0xE6, 0xEA
# 写 PNG (无压缩优化, 标准库)
def chunk(tag, data):
c = struct.pack('>I', len(data)) + tag + data
c += struct.pack('>I', zlib.crc32(tag + data) & 0xFFFFFFFF)
return c
raw = b''.join(b'\x00' + bytes(buf[y * W * 3:(y + 1) * W * 3]) for y in range(H))
png = (b'\x89PNG\r\n\x1a\n'
+ chunk(b'IHDR', struct.pack('>IIBBBBB', W, H, 8, 2, 0, 0, 0))
+ chunk(b'IDAT', zlib.compress(raw, 6))
+ chunk(b'IEND', b''))
with open(path, 'wb') as f:
f.write(png)
def update_meta(series):
meta_path = os.path.join(SERIES_DIR, series + '.meta.json')
with open(meta_path, 'r', encoding='utf-8') as f:
obj = json.load(f)
obj['attachments'] = [{'kind': 'manual', 'lang': '', 'source': 'manual/%s/p01.png' % series, 'model': None}]
with open(meta_path, 'w', encoding='utf-8') as f:
json.dump(obj, f, ensure_ascii=False)
def main():
made = 0
for s in NEW_SERIES:
d = os.path.join(MANUAL_DIR, s)
if not os.path.isdir(d):
os.makedirs(d)
make_png(os.path.join(d, 'p01.png'), s)
update_meta(s)
made += 1
print('OK %s: manual/%s/p01.png + meta 附件引用' % (s, s))
print('完成 %d 个系列' % made)
print('提示: 真实手册到货后跑 render-manual-pages.py 覆盖, 或直接替换 p01.png 同名文件')
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
"""
gen-mesh-preview.py —— 生成占位 STL 网格 (窗口内 3D 预览用, 开源 Helix Toolkit 渲染)
用途: 真实数模导出前, 为每个型号生成一个简单圆柱占位网格,
放在 onebot-data\\catalog\\mesh\\ 下 (与 step\\ 同名, .stl 后缀)。
桌面软件发现 mesh 侧车文件后, 直接在窗口内渲染, 不再弹浏览器。
真实网格来源: NX journal (nx-batch-export.vb) 导出 STEP 时同步导出 STL 到 mesh\\,
本脚本只在"还没拿到真实网格"时充当占位。
用法: python gen-mesh-preview.py
"""
import os
import struct
import math
MESH_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'catalog', 'mesh')
# 型号 → (半径 mm, 高度 mm): 按型号里的规格数字粗略缩放, 仅占位用
MODELS = [
('UCBM25', 12.5, 60), ('UCBM32', 16, 75), ('UCBM40', 20, 90),
('UCBM50', 25, 110), ('UCBM63', 31.5, 135), ('UCBM80', 40, 170),
('ULP32', 16, 90), ('ULP50', 25, 120), ('ULP63', 31.5, 140),
('USP32', 16, 90), ('USP50', 25, 120), ('USP63', 31.5, 140),
('UGP40', 20, 100), ('UGPS32D', 16, 100),
('LAE140', 30, 140), ('LAE225', 40, 225), ('LAE350', 55, 350),
('UAGP080_W', 40, 80), ('UAGP080_B', 40, 80), ('UAGP120_W', 50, 120),
('UAGP120_B', 50, 120), ('UAGP150_W', 50, 150), ('UAGP150_B', 50, 150),
('UAGP155_W', 50, 155), ('UAGP155_B', 50, 155), ('UAGP170_W', 62.5, 170),
('UAGP170_B', 62.5, 170), ('UAGP210_W', 62.5, 210), ('UAGP210_B', 62.5, 210),
('UAGP215_W', 62.5, 215), ('UAGP215_B', 62.5, 215), ('UAGP300_W', 80, 300),
('UAGP300_B', 80, 300), ('UAGP350_W', 80, 350), ('UAGP350_B', 80, 350),
('UAGP355_W', 80, 355), ('UAGP355_B', 80, 355), ('UAGP605_W', 100, 600),
('UAGP605_B', 100, 600),
('KC-00-32x10', 16, 100), # KC 试点真实型号: 网格预览样例
]
def cylinder_tris(r, h, n=24):
"""圆柱体三角网格: (法线, 顶点) 三元组列表, 轴为 Z, 底面 z=0, 顶面 z=h"""
tris = []
top, bot = [], []
for i in range(n):
a0, a1 = 2 * math.pi * i / n, 2 * math.pi * (i + 1) / n
x0, y0 = r * math.cos(a0), r * math.sin(a0)
x1, y1 = r * math.cos(a1), r * math.sin(a1)
# 顶面 (z=h, 法线 +Z)
tris.append(((0, 0, 1), ((0, 0, h), (x0, y0, h), (x1, y1, h))))
# 底面 (z=0, 法线 -Z)
tris.append(((0, 0, -1), ((0, 0, 0), (x1, y1, 0), (x0, y0, 0))))
# 侧面 (两块三角, 法线径向)
nx0, ny0 = math.cos(a0), math.sin(a0)
nx1, ny1 = math.cos(a1), math.sin(a1)
tris.append(((nx0, ny0, 0), ((x0, y0, 0), (x0, y0, h), (x1, y1, h))))
tris.append(((nx1, ny1, 0), ((x0, y0, 0), (x1, y1, h), (x1, y1, 0))))
return tris
def write_stl(path, tris):
with open(path, 'wb') as f:
f.write(b'ONEBOT mesh preview'.ljust(80, b'\0'))
f.write(struct.pack('<I', len(tris)))
for (nx, ny, nz), (p0, p1, p2) in tris:
f.write(struct.pack('<12f', nx, ny, nz,
p0[0], p0[1], p0[2],
p1[0], p1[1], p1[2],
p2[0], p2[1], p2[2]))
f.write(struct.pack('<H', 0))
def main():
if not os.path.isdir(MESH_DIR):
os.makedirs(MESH_DIR)
made = 0
for name, r, h in MODELS:
path = os.path.join(MESH_DIR, name + '.stl')
write_stl(path, cylinder_tris(r, h))
made += 1
print('已生成 %d 个占位 STL → %s' % (made, MESH_DIR))
print('提示: 真实网格由 NX journal 导出 STEP 时同步生成, 同名 .stl 覆盖本占位即可')
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,487 @@
# ONEBOT 直线气缸目录【源数据生成器】(统一路线, 2026-08-22):
# 系列定义 meta JSON + 参数表 CSV + 简化几何 STEP → 目录源目录 onebot-data\catalog\
# .opc 打包改由 C# 统一内核完成: 维护窗口【重建全量目录】或 CLI:
# OnebotCatalog\bin\OnebotCatalog.exe --buildfull onebot-data\catalog sample\OnebotCatalog_<版本>.opc
# 数据来源: 《ONEBOT - 直线气缸 - 中文.pdf》订购码与规格参数表 (2026-08 提取)
# 说明:
# - STEP 为简化圆柱组合占位模型 (尺寸随缸径/行程缩放), 待 NX 母模批量导出后替换同名文件即可
# - 相同几何的型号组合共用同一 STEP (磁石/固定形式/杆材/后盖不改变简化几何)
# - 行程取标准档位 10/20/30/50/75/100; 手册标注"可调行程 25~1000mm"属参数化二期
# - 本脚本只负责【生成/刷新源数据】(几何+CSV+系列定义); 目录打包/发布走 C# 统一路线
# 用法: powershell -NoProfile -ExecutionPolicy Bypass -File onebot-data\tools\gen-onebot-source.ps1
param(
[string]$OutDir = (Join-Path $PSScriptRoot '..\catalog')
)
$ProgressPreference = 'SilentlyContinue'
$inv = [System.Globalization.CultureInfo]::InvariantCulture
$stepDir = Join-Path $OutDir 'step'
$csvDir = Join-Path $OutDir 'csv'
$seriesDir = Join-Path $OutDir 'series'
$manDir = Join-Path $OutDir 'manual'
New-Item -ItemType Directory -Force -Path $stepDir | Out-Null
New-Item -ItemType Directory -Force -Path $csvDir | Out-Null
New-Item -ItemType Directory -Force -Path $seriesDir | Out-Null
# ==================== 系列与分类定义 ====================
$SERIES = New-Object System.Collections.Generic.List[object]
$SERIES.Add(@{ code='KC'; cat='AUT'; zh='KC 系列标准气缸 (ISO15552)'; en='KC Series Standard Cylinder (ISO15552)'; kw='kc iso15552 biaozhun qigang' })
$SERIES.Add(@{ code='KCB'; cat='AUT'; zh='KCB 系列倍力气缸'; en='KCB Series Tandem Cylinder'; kw='kcb beili qigang tandem' })
$SERIES.Add(@{ code='KSB'; cat='AUT'; zh='KSB 系列多位置气缸'; en='KSB Series Multi-position Cylinder'; kw='ksb duoweizhi multi-position' })
$SERIES.Add(@{ code='KCS'; cat='AUT'; zh='KCS 系列超经济型标准气缸'; en='KCS Series Economy Standard Cylinder'; kw='kcs chaojingji economy' })
$SERIES.Add(@{ code='KS'; cat='AUT'; zh='KS 系列拉杆式标准气缸'; en='KS Series Tie-rod Standard Cylinder'; kw='ks lagan tie-rod' })
$SERIES.Add(@{ code='KSU'; cat='AUT'; zh='KSU 系列拉杆内藏式标准气缸'; en='KSU Series Concealed Tie-rod Cylinder'; kw='ksu lagan neicang concealed' })
$SERIES.Add(@{ code='M'; cat='MIN'; zh='M 系列不锈钢迷你缸 (ISO6432)'; en='M Series Stainless Mini Cylinder (ISO6432)'; kw='m iso6432 minigang stainless mini' })
$SERIES.Add(@{ code='MS'; cat='MIN'; zh='MS 系列不锈钢迷你缸 (ISO6432)'; en='MS Series Stainless Mini Cylinder (ISO6432)'; kw='ms iso6432 minigang stainless mini' })
$SERIES.Add(@{ code='MSC'; cat='MIN'; zh='MSC 系列不锈钢迷你缸 (ISO6432)'; en='MSC Series Stainless Mini Cylinder (ISO6432)'; kw='msc iso6432 minigang stainless mini' })
$SERIES.Add(@{ code='MAL'; cat='ALU'; zh='MAL 系列铝合金迷你缸'; en='MAL Series Aluminium Mini Cylinder'; kw='mal lvhejin minigang aluminium mini' })
$SERIES.Add(@{ code='MALC'; cat='ALU'; zh='MALC 系列铝合金迷你缸'; en='MALC Series Aluminium Mini Cylinder'; kw='malc lvhejin minigang aluminium mini' })
$CATS = New-Object System.Collections.Generic.List[object]
$CATS.Add(@{ code='AUT'; zh='汽车行业气缸 Automotive'; en='Automotive Cylinders' })
$CATS.Add(@{ code='MIN'; zh='不锈钢迷你缸 (ISO6432)'; en='Stainless Mini Cylinders (ISO6432)' })
$CATS.Add(@{ code='ALU'; zh='铝合金迷你缸'; en='Aluminium Mini Cylinders' })
$MOUNT_DISP = @{ ''='基本型'; FA='前盖固定型 FA'; FB='后盖固定型 FB'; CA='后盖单耳固定型 CA'; CB='后盖双耳固定型 CB'; LB='前后固定型 LB'; YB='后盖支座固定型 YB'; TC='中间摇摆式 TC'; 'TC-M'='摇摆式附脚座 TC-M'; SDB='后盖摇摆式 SDB' }
$MAGNET_DISP = @{ ''='不附磁石'; M='附磁石 M' }
$ROD_DISP = @{ ''='碳钢'; E='不锈钢 E' }
$CAP_DISP = @{ ''='标准摆尾型'; U='标准平尾型 U'; CM='标准圆尾型 CM' }
$STROKES = @(10, 20, 30, 50, 75, 100)
$STROKES_KSB = @(0, 10, 20, 30, 50, 75, 100)
$BORE_STD = @(32, 40, 50, 63, 80, 100, 125)
$BORE_KC = @(32, 40, 50, 63, 80, 100, 125, 160, 200, 250, 320)
$BORE_KCS = @(32, 40, 50, 63, 80, 100)
$BORE_M = @(8, 10, 12, 16, 20, 25, 32, 40)
$BORE_MINI = @(16, 20, 25, 32, 40)
$MOUNT_STD = @('', 'FA', 'FB', 'CA', 'CB', 'LB', 'YB')
$MOUNT_TIE = @('', 'FA', 'FB', 'CA', 'CB', 'LB', 'TC', 'TC-M')
$MOUNT_MINI = @('', 'LB', 'FA', 'FB', 'SDB')
# 每系列: 参数定义 (code/zh/en/unit/type/values) + 变体组合规则
function Get-SeriesSpec([string]$code) {
switch ($code) {
'KC' {
$params = @(
@('type', '型号', 'Type', 'enum', @('00', '02', '03')),
@('bore', '缸径', 'Bore', 'number', $BORE_KC),
@('stroke', '行程', 'Stroke', 'number', $STROKES),
@('magnet', '磁石', 'Magnet', 'enum', @('', 'M')),
@('mount', '固定形式', 'Mounting', 'enum', $MOUNT_STD)
)
$rules = @()
$rules += @{ if=@{param='mount'; op='eq'; value='YB'}; then=@{param='bore'; op='in'; values=@(32,40,50,63,80,100,125)} }
return @{ params=$params; rules=$rules; twin=$false }
}
'KCB' {
$params = @(
@('bore', '缸径', 'Bore', 'number', $BORE_STD),
@('stroke', '行程', 'Stroke', 'number', $STROKES),
@('magnet', '磁石', 'Magnet', 'enum', @('', 'M')),
@('mount', '固定形式', 'Mounting', 'enum', $MOUNT_STD)
)
return @{ params=$params; rules=@(); twin=$false }
}
'KSB' {
$params = @(
@('bore', '缸径', 'Bore', 'number', $BORE_STD),
@('stroke1', '行程1', 'Stroke 1', 'number', $STROKES_KSB),
@('stroke2', '行程2', 'Stroke 2', 'number', $STROKES_KSB),
@('magnet', '磁石', 'Magnet', 'enum', @('', 'M')),
@('mount', '固定形式', 'Mounting', 'enum', $MOUNT_STD)
)
return @{ params=$params; rules=@(); twin=$false }
}
'KCS' {
$params = @(
@('type', '型号', 'Type', 'enum', @('00', '02', '03')),
@('bore', '缸径', 'Bore', 'number', $BORE_KCS),
@('stroke', '行程', 'Stroke', 'number', $STROKES),
@('magnet', '磁石', 'Magnet', 'enum', @('', 'M')),
@('mount', '固定形式', 'Mounting', 'enum', $MOUNT_STD)
)
return @{ params=$params; rules=@(); twin=$false }
}
'KS' {
$params = @(
@('type', '型号', 'Type', 'enum', @('00', '02', '03')),
@('bore', '缸径', 'Bore', 'number', $BORE_STD),
@('stroke', '行程', 'Stroke', 'number', $STROKES),
@('magnet', '磁石', 'Magnet', 'enum', @('', 'M')),
@('mount', '固定形式', 'Mounting', 'enum', $MOUNT_TIE)
)
$rules = @()
$rules += @{ if=@{param='type'; op='in'; values=@('02','03')}; then=@{param='mount'; op='in'; values=@('FA','FB','LB','TC','TC-M')} }
return @{ params=$params; rules=$rules; twin=$false }
}
'KSU' {
$params = @(
@('type', '型号', 'Type', 'enum', @('00', '02', '03')),
@('bore', '缸径', 'Bore', 'number', $BORE_STD),
@('stroke', '行程', 'Stroke', 'number', $STROKES),
@('magnet', '磁石', 'Magnet', 'enum', @('', 'M')),
@('mount', '固定形式', 'Mounting', 'enum', $MOUNT_TIE)
)
$rules = @()
$rules += @{ if=@{param='type'; op='in'; values=@('02','03')}; then=@{param='mount'; op='in'; values=@('FA','FB','LB','TC','TC-M')} }
return @{ params=$params; rules=$rules; twin=$false }
}
'M' {
$params = @(
@('action', '动作形式', 'Action', 'enum', @('N', 'S', 'T', 'ND', 'NJ')),
@('bore', '缸径', 'Bore', 'number', $BORE_M),
@('stroke', '行程', 'Stroke', 'number', $STROKES),
@('magnet', '磁石', 'Magnet', 'enum', @('', 'M')),
@('rod', '活塞杆材质', 'Rod Material', 'enum', @('E', '')),
@('mount', '固定形式', 'Mounting', 'enum', $MOUNT_MINI),
@('cap', '后盖形式', 'Cap Type', 'enum', @('', 'U', 'CM'))
)
return @{ params=$params; rules=@(); twin=$false }
}
'MS' {
$params = @(
@('action', '动作形式', 'Action', 'enum', @('N', 'D', 'J')),
@('bore', '缸径', 'Bore', 'number', $BORE_MINI),
@('stroke', '行程', 'Stroke', 'number', $STROKES),
@('magnet', '磁石', 'Magnet', 'enum', @('', 'M')),
@('rod', '活塞杆材质', 'Rod Material', 'enum', @('E', '')),
@('mount', '固定形式', 'Mounting', 'enum', $MOUNT_MINI),
@('cap', '后盖形式', 'Cap Type', 'enum', @('', 'U', 'CM'))
)
return @{ params=$params; rules=@(); twin=$false }
}
'MSC' {
$params = @(
@('action', '动作形式', 'Action', 'enum', @('C', 'D', 'J')),
@('bore', '缸径', 'Bore', 'number', $BORE_MINI),
@('stroke', '行程', 'Stroke', 'number', $STROKES),
@('magnet', '磁石', 'Magnet', 'enum', @('', 'M')),
@('rod', '活塞杆材质', 'Rod Material', 'enum', @('E', '')),
@('mount', '固定形式', 'Mounting', 'enum', $MOUNT_MINI),
@('cap', '后盖形式', 'Cap Type', 'enum', @('', 'U', 'CM'))
)
return @{ params=$params; rules=@(); twin=$false }
}
'MAL' {
$params = @(
@('type', '型号', 'Type', 'enum', @('MAL', 'MSAL', 'MTAL', 'MALD', 'MALJ')),
@('bore', '缸径', 'Bore', 'number', $BORE_MINI),
@('stroke', '行程', 'Stroke', 'number', $STROKES),
@('magnet', '磁石', 'Magnet', 'enum', @('', 'M')),
@('rod', '活塞杆材质', 'Rod Material', 'enum', @('E', '')),
@('mount', '固定形式', 'Mounting', 'enum', $MOUNT_MINI),
@('cap', '后盖形式', 'Cap Type', 'enum', @('', 'U', 'CM'))
)
return @{ params=$params; rules=@(); twin=$false }
}
'MALC' {
$params = @(
@('type', '型号', 'Type', 'enum', @('MALC', 'MALCD', 'MALCJ')),
@('bore', '缸径', 'Bore', 'number', $BORE_MINI),
@('stroke', '行程', 'Stroke', 'number', $STROKES),
@('magnet', '磁石', 'Magnet', 'enum', @('', 'M')),
@('rod', '活塞杆材质', 'Rod Material', 'enum', @('E', '')),
@('mount', '固定形式', 'Mounting', 'enum', $MOUNT_MINI),
@('cap', '后盖形式', 'Cap Type', 'enum', @('', 'U', 'CM'))
)
return @{ params=$params; rules=@(); twin=$false }
}
default { return @{ params=@(); rules=@(); twin=$false } }
}
}
# ==================== STEP 几何生成 (简化圆柱组合) ====================
function Format-Num([double]$v) { return $v.ToString('0.###', $inv) }
function New-CylinderEntities([int]$id, [double]$x, [double]$y, [double]$z, [double]$r, [double]$len) {
$X = Format-Num $x; $Y = Format-Num $y; $Z = Format-Num $z
$ZT = Format-Num ($z + $len); $R = Format-Num $r
$VY = Format-Num ($y + $r)
$e = @(
"#$($id)=DIRECTION('',(0.,0.,1.));",
"#$($id+1)=CARTESIAN_POINT('',($X,$Y,$Z));",
"#$($id+2)=CARTESIAN_POINT('',($X,$Y,$ZT));",
"#$($id+3)=DIRECTION('',(1.,0.,0.));",
"#$($id+4)=AXIS2_PLACEMENT_3D('',#$($id+1),#$($id),#$($id+3));",
"#$($id+5)=CARTESIAN_POINT('',($X,$VY,$Z));",
"#$($id+6)=CARTESIAN_POINT('',($X,$VY,$ZT));",
"#$($id+7)=VERTEX_POINT('',#$($id+5));",
"#$($id+8)=VERTEX_POINT('',#$($id+6));",
"#$($id+9)=CIRCLE('',#$($id+4),$R);",
"#$($id+10)=EDGE_CURVE('',#$($id+7),#$($id+7),#$($id+9),.T.);",
"#$($id+11)=CYLINDRICAL_SURFACE('',#$($id+4),$R);",
"#$($id+12)=PLANE('',#$($id+4));",
"#$($id+13)=DIRECTION('',(0.,0.,1.));",
"#$($id+14)=AXIS2_PLACEMENT_3D('',#$($id+2),#$($id+13),#$($id+3));",
"#$($id+15)=PLANE('',#$($id+14));",
"#$($id+16)=CIRCLE('',#$($id+14),$R);",
"#$($id+17)=EDGE_CURVE('',#$($id+8),#$($id+8),#$($id+16),.T.);",
"#$($id+18)=DIRECTION('',(0.,0.,1.));",
"#$($id+19)=VECTOR('',#$($id+18),1.);",
"#$($id+20)=LINE('',#$($id+5),#$($id+19));",
"#$($id+21)=EDGE_CURVE('',#$($id+7),#$($id+8),#$($id+20),.T.);",
"#$($id+22)=ORIENTED_EDGE('',*,*,#$($id+10),.T.);",
"#$($id+23)=ORIENTED_EDGE('',*,*,#$($id+17),.T.);",
"#$($id+24)=ORIENTED_EDGE('',*,*,#$($id+21),.T.);",
"#$($id+25)=ORIENTED_EDGE('',*,*,#$($id+17),.T.);",
"#$($id+26)=ORIENTED_EDGE('',*,*,#$($id+21),.F.);",
"#$($id+27)=ORIENTED_EDGE('',*,*,#$($id+10),.F.);",
"#$($id+28)=EDGE_LOOP('',(#$($id+22)));",
"#$($id+29)=EDGE_LOOP('',(#$($id+23)));",
"#$($id+30)=EDGE_LOOP('',(#$($id+24),#$($id+25),#$($id+26),#$($id+27)));",
"#$($id+31)=ADVANCED_FACE('',(#$($id+30)),#$($id+11),.T.);",
"#$($id+32)=ADVANCED_FACE('',(#$($id+28)),#$($id+12),.F.);",
"#$($id+33)=ADVANCED_FACE('',(#$($id+29)),#$($id+15),.T.);",
"#$($id+34)=CLOSED_SHELL('',(#$($id+31),#$($id+32),#$($id+33)));",
"#$($id+35)=MANIFOLD_SOLID_BREP('',#$($id+34));"
)
return $e
}
function Write-StepFile([string]$path, [double]$bore, [double]$stroke, [bool]$twin, [bool]$mini) {
# 尺寸 (占位近似, 真实尺寸待 NX 母模导出替换)
$wall = if ($mini) { 2.0 } else { 3.0 }
$bodyR = $bore / 2.0 + $wall
$capR = $bore / 2.0 + $(if ($mini) { 3.5 } else { 6.0 })
$capLen = if ($mini) { 6.0 } else { 8.0 }
$fcapLen = if ($mini) { 8.0 } else { 10.0 }
$bodyLen = $stroke + $bore * 1.1 + $(if ($mini) { 20.0 } else { 30.0 })
$rodR = [Math]::Max($bore / $(if ($mini) { 6.0 } else { 7.0 }), $(if ($mini) { 2.0 } else { 4.0 }))
$rodBot = $bodyLen - $(if ($mini) { 8.0 } else { 12.0 })
$rodLen = $stroke + $(if ($mini) { 20.0 } else { 26.0 })
$rodOff = if ($twin) { $rodR * 2.0 } else { 0.0 }
$lines = New-Object System.Collections.Generic.List[string]
$lines.Add('ISO-10303-21;')
$lines.Add('HEADER;')
$lines.Add("FILE_DESCRIPTION(('ONEBOT simplified sample geometry'),'2;1');")
$lines.Add("FILE_NAME('$(Split-Path $path -Leaf)','2026-08-22T00:00:00',('ONEBOT'),('ONEBOT'),'','','');")
$lines.Add("FILE_SCHEMA(('CONFIG_CONTROL_DESIGN'));")
$lines.Add('ENDSEC;')
$lines.Add('DATA;')
$ids = New-Object System.Collections.Generic.List[int]
$nextId = 1
$cyls = New-Object System.Collections.Generic.List[object]
$cyls.Add(@{ x = 0.0; y = 0.0; z = -$capLen; r = $capR; len = $capLen })
$cyls.Add(@{ x = 0.0; y = 0.0; z = 0.0; r = $bodyR; len = $bodyLen })
$cyls.Add(@{ x = 0.0; y = 0.0; z = $bodyLen; r = $capR; len = $fcapLen })
$cyls.Add(@{ x = -$rodOff; y = 0.0; z = $rodBot; r = $rodR; len = $rodLen })
if ($twin) { $cyls.Add(@{ x = $rodOff; y = 0.0; z = $rodBot; r = $rodR; len = $rodLen }) }
foreach ($cyl in $cyls) {
[string[]]$entities = New-CylinderEntities -id $nextId -x $cyl['x'] -y $cyl['y'] -z $cyl['z'] -r $cyl['r'] -len $cyl['len']
$lines.AddRange($entities)
$ids.Add($nextId + 35)
$nextId += 36
}
# 尾部共享实体: 注入 STYLED_ITEM 颜色 (occt-import-js 对无颜色模型会崩)
$styledIds = New-Object System.Collections.Generic.List[string]
$c = $nextId
foreach ($s in $ids) {
$lines.Add("#$c=COLOUR_RGB('',0.65,0.72,0.85);")
$lines.Add("#$($c+1)=FILL_AREA_STYLE_COLOUR('',#$c);")
$lines.Add("#$($c+2)=SURFACE_STYLE_FILL_AREA(#$($c+1));")
$lines.Add("#$($c+3)=SURFACE_SIDE_STYLE('',(#$($c+2)));")
$lines.Add("#$($c+4)=SURFACE_STYLE_USAGE(.BOTH.,#$($c+3));")
$lines.Add("#$($c+5)=PRESENTATION_STYLE_ASSIGNMENT((#$($c+4)));")
$lines.Add("#$($c+6)=STYLED_ITEM('',(#$($c+5)),#$s);")
$styledIds.Add("#$($c+6)")
$c += 7
}
$items = (($ids | ForEach-Object { "#$_" }) + $styledIds) -join ','
$lines.Add("#$c=SHAPE_REPRESENTATION('',($items),#$($c+1));")
$lines.Add("#$($c+1)=(GEOMETRIC_REPRESENTATION_CONTEXT(3)GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#$($c+2)))GLOBAL_UNIT_ASSIGNED_CONTEXT((#$($c+3),#$($c+4),#$($c+5)))REPRESENTATION_CONTEXT('',''));")
$lines.Add("#$($c+2)=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-06),#$($c+3),'');")
$lines.Add("#$($c+3)=(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MILLI.,.METRE.));")
$lines.Add("#$($c+4)=(NAMED_UNIT(*)PLANE_ANGLE_UNIT()SI_UNIT($,.RADIAN.));")
$lines.Add("#$($c+5)=(NAMED_UNIT(*)SI_UNIT($,.STERADIAN.)SOLID_ANGLE_UNIT());")
$lines.Add("#$($c+6)=PRODUCT('ONEBOT','ONEBOT','',(#$($c+7)));")
$lines.Add("#$($c+7)=PRODUCT_CONTEXT('',#$($c+8),'mechanical');")
$lines.Add("#$($c+8)=APPLICATION_CONTEXT('configuration controlled 3d designs of mechanical parts and assemblies');")
$lines.Add("#$($c+9)=PRODUCT_DEFINITION_FORMATION('','',#$($c+6));")
$lines.Add("#$($c+10)=PRODUCT_DEFINITION('design','',#$($c+9),#$($c+11));")
$lines.Add("#$($c+11)=PRODUCT_DEFINITION_CONTEXT('part definition',#$($c+8),'design');")
$lines.Add("#$($c+12)=PRODUCT_DEFINITION_SHAPE('','',#$($c+10));")
$lines.Add("#$($c+13)=SHAPE_DEFINITION_REPRESENTATION(#$($c+12),#$c);")
$lines.Add('ENDSEC;')
$lines.Add('END-ISO-10303-21;')
$enc = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllLines($path, $lines.ToArray(), $enc)
}
# 双轴型号判定 (两根活塞杆)
function Is-TwinType([string]$code, [string]$type) {
if ($code -eq 'M') { return ($type -eq 'ND' -or $type -eq 'NJ') }
if ($code -eq 'MS') { return ($type -eq 'D' -or $type -eq 'J') }
if ($code -eq 'MSC') { return ($type -eq 'D' -or $type -eq 'J') }
if ($code -eq 'MAL') { return ($type -eq 'MALD' -or $type -eq 'MALJ') }
if ($code -eq 'MALC') { return ($type -eq 'MALCD' -or $type -eq 'MALCJ') }
return ($type -eq '02' -or $type -eq '03')
}
# 规则求值 (与 C# Configurator 同语义: 违反规则的组合不生成)
function Cond-Matches($cond, $combo) {
if (-not $combo.ContainsKey($cond['param'])) { return $false }
$val = [string]$combo[$cond['param']]
switch ($cond['op']) {
'eq' { return ([string]$val -eq [string]$cond['value']) }
'ne' { return ([string]$val -ne [string]$cond['value']) }
'in' { foreach ($v in $cond['values']) { if ([string]$val -eq [string]$v) { return $true } } return $false }
default { return $false }
}
}
function Rule-Violated($rules, $combo) {
foreach ($r in $rules) {
if ((Cond-Matches $r['if'] $combo) -and -not (Cond-Matches $r['then'] $combo)) { return $true }
}
return $false
}
function Cond-ToJson($c) {
if ($c['op'] -eq 'in') {
$vals = ($c['values'] | ForEach-Object { '"' + $_ + '"' }) -join ','
return '{"Param":"' + $c['param'] + '","Op":"in","Value":[' + $vals + ']}'
}
return '{"Param":"' + $c['param'] + '","Op":"' + $c['op'] + '","Value":"' + $c['value'] + '"}'
}
function Rule-ToJson($r) {
return '{"If":' + (Cond-ToJson $r['if']) + ',"Then":' + (Cond-ToJson $r['then']) + '}'
}
# ==================== 主流程 ====================
$seriesJsonList = New-Object System.Collections.Generic.List[string]
$stepNames = New-Object System.Collections.Generic.HashSet[string]
$totalVariants = 0
$totalSteps = 0
foreach ($s in $SERIES) {
$code = $s.code
$spec = Get-SeriesSpec $code
$mini = ($s.cat -eq 'MIN' -or $s.cat -eq 'ALU')
# 变体组合: 参数值笛卡尔积
$combos = New-Object System.Collections.Generic.List[object]
$combos.Add(@{})
foreach ($p in $spec.params) {
$next = New-Object System.Collections.Generic.List[object]
foreach ($c in $combos) {
foreach ($v in $p[4]) {
$nc = @{}
foreach ($k in $c.Keys) { $nc[$k] = $c[$k] }
$nc[$p[0]] = $v
$next.Add($nc)
}
}
$combos = $next
}
# CSV 行 + 变体 JSON + STEP (按 几何键: 系列+型号段+缸径+行程)
$csvLines = New-Object System.Collections.Generic.List[string]
$headerCols = @('model_code')
foreach ($p in $spec.params) { $headerCols += $p[0] }
$headerCols += 'step_file'
$csvLines.Add(($headerCols -join ','))
$varJson = New-Object System.Collections.Generic.List[string]
$geoCodes = New-Object System.Collections.Generic.HashSet[string]
foreach ($c in $combos) {
# 规则过滤: 违反手册约束的组合不生成 (如 大缸径+YB、双轴型+受限固定形式)
if (Rule-Violated $spec.rules $c) { continue }
$typeSeg = ''
if ($spec.params[0][0] -eq 'type' -or $spec.params[0][0] -eq 'action') { $typeSeg = [string]$c[$spec.params[0][0]] }
$bore = [double]$c['bore']
$stroke = if ($c.ContainsKey('stroke')) { [double]$c['stroke'] } else { [double]$c['stroke1'] }
# 型号编码
$model = ''
if ($code -eq 'KSB') {
$model = "KSB$($c['bore'])-$($c['stroke1'])x$($c['stroke2'])$($c['magnet'])$($c['mount'])"
} elseif ($s.cat -eq 'MIN' -or $code -eq 'MAL' -or $code -eq 'MALC') {
$model = "$typeSeg$($c['bore'])-$($c['stroke'])$($c['magnet'])$($c['rod'])$($c['mount'])$($c['cap'])"
} else {
$model = "$code$typeSeg$($c['bore'])-$($c['stroke'])$($c['magnet'])$($c['mount'])"
}
# 几何 STEP (共用)
$geoKey = "$code-$typeSeg-${bore}x$stroke"
$stepName = "$code-$typeSeg-${bore}x$stroke.step"
if (-not $geoCodes.Contains($geoKey)) {
[void]$geoCodes.Add($geoKey)
if (-not $stepNames.Contains($stepName)) {
Write-StepFile (Join-Path $stepDir $stepName) $bore $stroke (Is-TwinType $code $typeSeg) $mini
[void]$stepNames.Add($stepName)
$totalSteps++
}
}
# 变体 JSON 与 CSV
$paramsJson = New-Object System.Collections.Generic.List[string]
foreach ($p in $spec.params) {
$v = $c[$p[0]]
if ($p[3] -eq 'number') { $paramsJson.Add('"' + $p[0] + '":' + [string]$v) }
else { $paramsJson.Add('"' + $p[0] + '":"' + $v + '"') }
}
$varJson.Add('{"modelCode":"' + $model + '","params":{' + ($paramsJson -join ',') + '},"step":"step/' + $stepName + '"}')
$rowVals = @($model)
foreach ($p in $spec.params) { $rowVals += [string]$c[$p[0]] }
$rowVals += $stepName
$csvLines.Add(($rowVals -join ','))
$totalVariants++
}
# 系列 meta JSON (series\<code>.meta.json, 与向导保存配置同构; 变体不存, 构建时从 CSV 读)
$paramsJson = New-Object System.Collections.Generic.List[string]
foreach ($p in $spec.params) {
$display = ''
if ($p[0] -eq 'mount') {
$entries = New-Object System.Collections.Generic.List[string]
foreach ($k in $MOUNT_DISP.Keys) { $entries.Add('"' + $k + '":"' + $MOUNT_DISP[$k] + '"') }
$display = '"display":{' + ($entries -join ',') + '}'
} elseif ($p[0] -eq 'magnet') {
$display = '"display":{"":"不附磁石","M":"附磁石 M"}'
} elseif ($p[0] -eq 'rod') {
$display = '"display":{"":"碳钢","E":"不锈钢 E"}'
} elseif ($p[0] -eq 'cap') {
$display = '"display":{"":"标准摆尾型","U":"标准平尾型 U","CM":"标准圆尾型 CM"}'
}
$vals = New-Object System.Collections.Generic.List[string]
foreach ($v in $p[4]) {
if ($p[3] -eq 'number') { $vals.Add([string]$v) }
else { $vals.Add('"' + $v + '"') }
}
$paramsJson.Add('{"code":"' + $p[0] + '","nameZh":"' + $p[1] + '","nameEn":"' + $p[2] + '","unit":"' + $(if ($p[3] -eq 'number') { 'mm' } else { '' }) + '","type":"' + $p[3] + '","keywords":"' + $p[0] + '","values":[' + ($vals -join ',') + ']' + $(if ($display) { ',' + $display } else { '' }) + '}')
}
$rulesJson = (($spec.rules | ForEach-Object { Rule-ToJson $_ }) -join ',')
# 手册页附件 (尺寸图窗口对照用): catalog\manual\<系列>\pXX.png → source 相对路径
$manAtt = New-Object System.Collections.Generic.List[string]
if (Test-Path (Join-Path $manDir $code)) {
foreach ($mf in (Get-ChildItem (Join-Path $manDir $code) -Filter '*.png' | Sort-Object Name)) {
$manAtt.Add('{"kind":"manual","lang":"","source":"manual/' + $code + '/' + $mf.Name + '","model":null}')
}
}
$sjson = '{"code":"' + $code + '","nameZh":"' + $s.zh + '","nameEn":"' + $s.en + '","keywords":"' + $s.kw + '",' +
'"parameters":[' + ($paramsJson -join ',') + '],' +
'"rules":[' + $rulesJson + '],' +
'"naming":{"stepNameTemplate":"{model}.step"},' +
'"attachments":[],"variants":[]}'
$metaJson = '{"series":' + $sjson + ',"attachments":[' + ($manAtt -join ',') + ']}'
[System.IO.File]::WriteAllText((Join-Path $seriesDir ($code + '.meta.json')), $metaJson, (New-Object System.Text.UTF8Encoding($false)))
# 写 CSV
[System.IO.File]::WriteAllLines((Join-Path $csvDir ($code + '.csv')), $csvLines.ToArray(), (New-Object System.Text.UTF8Encoding($true)))
Write-Output ("系列 " + $code + ": " + $combos.Count + " 个变体, 几何 STEP " + $geoCodes.Count + "")
}
# catalog.json (源目录: 目录级定义 + 分类系列列表; series 定义在 series\*.meta.json)
$catJson = New-Object System.Collections.Generic.List[string]
foreach ($c in $CATS) {
$codes = ($SERIES | Where-Object { $_.cat -eq $c.code } | ForEach-Object { '"' + $_.code + '"' }) -join ','
$catJson.Add('{"code":"' + $c.code + '","nameZh":"' + $c.zh + '","nameEn":"' + $c.en + '","keywords":"","series":[' + $codes + ']}')
}
$catalogJson = '{"schemaVersion":"1.0","catalogName":"ONEBOT 气动元件目录","catalogNameEn":"ONEBOT Pneumatic Catalog",' +
'"catalogVersion":"2026.08","defaultLang":"zh","langs":["zh","en"],' +
'"categories":[' + ($catJson -join ',') + '],' +
'"series":[]}'
[System.IO.File]::WriteAllText((Join-Path $OutDir 'catalog.json'), $catalogJson, (New-Object System.Text.UTF8Encoding($false)))
Write-Output ""
Write-Output ("完成: " + $SERIES.Count + " 个系列, " + $totalVariants + " 个变体, " + $totalSteps + " 个几何 STEP")
Write-Output ("源目录: " + $OutDir + " (catalog.json + series\*.meta.json + csv\ + step\)")
Write-Output ("下一步打包 (C# 统一内核): OnebotCatalog\bin\OnebotCatalog.exe --buildfull <源目录> <输出.opc>")

View File

@@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-
# 从佳尔灵英文目录 PDF 生成平替对照表 (JELPC → 我方型号)
# 原理: 系列对照 (SQ↔KCS, SI↔KC, SIB↔KCB, SC/SU↔KS/KSU, SCT↔KSB, MAL/MALC 同名)
# + 抽取 PDF 中的 系列+缸径x行程 组合 → 构造我方基础型型号 → 必须存在于我方 CSV 才写入
# 输出: OnebotCatalog\onebot-data\catalog\crossref.csv
# 用法: python tools\jelpc-crossref.py
import fitz, re, os, sys
# 路径基于脚本自身位置 (OnebotCatalog\onebot-data\tools\ → 上溯两级到 OnebotCatalog)
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
PDF = os.path.join(ROOT, '4_JELPC_E-Catalog_EN.pdf')
CSV_DIR = os.path.join(ROOT, 'onebot-data', 'catalog', 'csv')
OUT = os.path.join(ROOT, 'onebot-data', 'catalog', 'crossref.csv')
# 佳尔灵系列 → (我方系列, 我方基础型号段, 是否双轴字母映射)
# 我方型号构造规则 (与 build-onebot 一致):
# KC类: <我方系列><type><缸径>-<行程> type: 00 标准 / 02 双轴 / 03 双轴可调
# MAL类: <type段><缸径>-<行程> type段: MAL / MALC
SERIES_MAP = {
"SI": ("KC", "00"),
"SIB": ("KCB", ""),
"SQ": ("KCS", "00"),
"SC": ("KS", "00"),
"SU": ("KSU", "00"),
"SCT": ("KSB", ""),
"MAL": ("MAL", "MAL"),
"MALC": ("MALC", "MALC"),
}
def main():
# 1) 我方型号集合 (CSV 第 1 列)
ours = set()
for fn in os.listdir(CSV_DIR):
if not fn.endswith('.csv'):
continue
with open(os.path.join(CSV_DIR, fn), encoding='utf-8-sig') as f:
for line in f:
line = line.strip()
if not line or line.startswith('model_code'):
continue
code = line.split(',')[0].strip()
if code:
ours.add(code)
# 2) 佳尔灵 PDF 全文本
doc = fitz.open(PDF)
full = '\n'.join(doc[i].get_text() for i in range(doc.page_count))
doc.close()
# 3) 抽取 系列+型号段+缸径-行程 组合
rows = [] # (jpc_code, our_code)
seen = set()
for jseries, (oseries, otype) in SERIES_MAP.items():
# 型号段字母: 标准(无)/D(双轴)/J(双轴可调) 等
pat = re.compile(r'\b' + jseries + r'([A-Z]?)(\d{1,3})-(\d{1,4})\b')
for m in pat.finditer(full):
seg = m.group(1)
bore = m.group(2)
stroke = m.group(3)
if len(stroke) > 3: # 过滤表格数字误匹配 (行程一般 1~3 位)
continue
jpc_code = m.group(0)
# 构造我方型号
if oseries in ("MAL", "MALC"):
our = otype + bore + '-' + stroke
elif oseries == "KSB":
our = oseries + bore + '-0x' + stroke # 双行程: 行程1=0 行程2=stroke (占位)
else:
t = {"": "00", "D": "02", "J": "03"}.get(seg, "00")
our = oseries + t + bore + '-' + stroke
if our not in ours:
continue
key = (jpc_code, our)
if key in seen:
continue
seen.add(key)
rows.append((jpc_code, our))
rows.sort(key=lambda x: (x[1], x[0]))
print(f'生成对照 {len(rows)} 行 (我方型号已校验存在)')
# 4) 写 crossref.csv (覆盖; 其余品牌行请自行追加)
with open(OUT, 'w', encoding='utf-8-sig', newline='') as f:
f.write('brand,foreign_model,our_model,note\n')
for jpc, our in rows:
f.write(f'JELPC,{jpc},{our},基础型平替(待核对)\n')
print('已写:', OUT)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,176 @@
# -*- coding: utf-8 -*-
"""
manual-pages-to-pdf.py —— 把各系列手册页 PNG 打包成 PDF (供"下载数据表 PDF"用)
背景: 尺寸图窗口显示的手册页是 PNG (manual\\<系列>\\pXX.png)。客户还需要能
下载 PDF 文档 → 本脚本把每个系列的手册页按序合成一个 PDF, 输出
manual\\<系列>\\<系列>_manual.pdf, 并把 datasheet 附件写进 series\\<系列>.meta.json
(软件里【下载数据表 PDF】按钮直接可用, 网页/桌面两端一致)。
纯标准库实现 (zlib 解/压缩 PNG + 手写 PDF), 无任何第三方依赖。
真实手册替换 pXX.png 后重跑本脚本即可更新 PDF。
用法: python manual-pages-to-pdf.py
"""
import json
import os
import struct
import zlib
BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MANUAL_DIR = os.path.join(BASE, 'catalog', 'manual')
SERIES_DIR = os.path.join(BASE, 'catalog', 'series')
# A4 页面 (pt)
PAGE_W, PAGE_H = 595.28, 841.89
def parse_png(path):
"""解析 PNG → (width, height, RGB 字节串)。支持 8bit 灰度/RGB/RGBA, 非隔行。"""
with open(path, 'rb') as f:
data = f.read()
assert data[:8] == b'\x89PNG\r\n\x1a\n', '非 PNG: %s' % path
pos, idat, w, h, bd, ct, interlace = 8, b'', 0, 0, 0, 0, 0
while pos < len(data):
(length,) = struct.unpack('>I', data[pos:pos + 4])
typ = data[pos + 4:pos + 8]
chunk = data[pos + 8:pos + 8 + length]
if typ == b'IHDR':
w, h, bd, ct, _, _, interlace = struct.unpack('>IIBBBBB', chunk)
elif typ == b'IDAT':
idat += chunk
elif typ == b'IEND':
break
pos += 12 + length
assert bd == 8 and interlace == 0, '仅支持 8bit 非隔行 PNG'
channels = {0: 1, 2: 3, 4: 2, 6: 4}.get(ct)
assert channels, '不支持的色彩类型 %d' % ct
raw = zlib.decompress(idat)
bpp = channels
stride = w * bpp
out = bytearray()
prev = bytearray(stride)
for y in range(h):
f = raw[y * (stride + 1)]
line = bytearray(raw[y * (stride + 1) + 1:(y + 1) * (stride + 1)])
if f == 1: # Sub
for i in range(bpp, stride):
line[i] = (line[i] + line[i - bpp]) & 0xFF
elif f == 2: # Up
for i in range(stride):
line[i] = (line[i] + prev[i]) & 0xFF
elif f == 3: # Average
for i in range(stride):
a = line[i - bpp] if i >= bpp else 0
line[i] = (line[i] + ((a + prev[i]) >> 1)) & 0xFF
elif f == 4: # Paeth
for i in range(stride):
a = line[i - bpp] if i >= bpp else 0
b = prev[i]
c = prev[i - bpp] if i >= bpp else 0
p = a + b - c
pa, pb, pc = abs(p - a), abs(p - b), abs(p - c)
pr = a if (pa <= pb and pa <= pc) else (b if pb <= pc else c)
line[i] = (line[i] + pr) & 0xFF
if channels == 4: # RGBA → RGB (白底合成)
rgb = bytearray(w * 3)
for i in range(w):
a4 = line[i * 4 + 3]
for k in range(3):
rgb[i * 3 + k] = (line[i * 4 + k] * a4 + 255 * (255 - a4)) // 255
out += rgb
elif channels == 1: # 灰度 → RGB
rgb = bytearray(w * 3)
for i in range(w):
rgb[i * 3] = rgb[i * 3 + 1] = rgb[i * 3 + 2] = line[i]
out += rgb
else:
out += line
prev = line
return w, h, bytes(out)
def build_pdf(images):
"""images: [(w, h, rgb_bytes)] → PDF 字节串 (每图一页 A4 等比铺满)"""
objs = [] # (num, bytes)
kids = []
def add(body):
objs.append((len(objs) + 1, body))
return len(objs)
for (w, h, rgb) in images:
# 等比缩放铺满 A4
scale = min(PAGE_W / w, PAGE_H / h)
dw, dh = w * scale, h * scale
x0, y0 = (PAGE_W - dw) / 2.0, (PAGE_H - dh) / 2.0
stream = b'q %f 0 0 %f %f %f cm /Im0 Do Q' % (dw, dh, x0, y0)
img_num = add(b'<< /Type /XObject /Subtype /Image /Width %d /Height %d '
b'/ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /FlateDecode '
b'/Length %d >>\nstream\n' % (w, h, len(zlib.compress(rgb, 6)))
+ zlib.compress(rgb, 6) + b'\nendstream')
content_num = add(b'<< /Length %d >>\nstream\n' % len(stream) + stream + b'\nendstream')
page_num = add(b'<< /Type /Page /Parent %d 0 R /MediaBox [0 0 %f %f] '
b'/Resources << /XObject << /Im0 %d 0 R >> >> /Contents %d 0 R >>\n'
% (0, PAGE_W, PAGE_H, img_num, content_num))
kids.append(page_num)
pages_num = len(objs) + 1
kids_ref = b' '.join(b'%d 0 R' % k for k in kids)
pages_obj = (b'<< /Type /Pages /Kids [%s] /Count %d >>' % (kids_ref, len(kids)))
objs.append((pages_num, pages_obj))
catalog_num = pages_num + 1
objs.append((catalog_num, b'<< /Type /Catalog /Pages %d 0 R >>' % pages_num))
# 修正 page 里的 Parent 引用
for i, (n, body) in enumerate(objs):
if b'/Parent 0 0 R' in body:
objs[i] = (n, body.replace(b'/Parent 0 0 R', b'/Parent %d 0 R' % pages_num))
out = bytearray(b'%PDF-1.4\n%\xe2\xe3\xcf\xd3\n')
offsets = []
for n, body in objs:
offsets.append(len(out))
out += b'%d 0 obj\n' % n + body + b'\nendobj\n'
xref_pos = len(out)
out += b'xref\n0 %d\n' % (len(objs) + 1)
out += b'0000000000 65535 f \n'
for off in offsets:
out += b'%010d 00000 n \n' % off
out += b'trailer\n<< /Size %d /Root %d 0 R >>\nstartxref\n%d\n%%%%EOF\n' % (len(objs) + 1, catalog_num, xref_pos)
return bytes(out)
def update_meta(series, pdf_name):
meta_path = os.path.join(SERIES_DIR, series + '.meta.json')
with open(meta_path, 'r', encoding='utf-8') as f:
obj = json.load(f)
atts = list(obj.get('attachments', []))
atts = [a for a in atts if a.get('kind') != 'datasheet']
atts.append({'kind': 'datasheet', 'lang': '', 'source': 'manual/%s/%s' % (series, pdf_name), 'model': None})
obj['attachments'] = atts
with open(meta_path, 'w', encoding='utf-8') as f:
json.dump(obj, f, ensure_ascii=False)
def main():
made = 0
for series in sorted(os.listdir(MANUAL_DIR)):
d = os.path.join(MANUAL_DIR, series)
if not os.path.isdir(d):
continue
pngs = sorted(f for f in os.listdir(d) if f.lower().endswith('.png'))
if not pngs:
continue
images = [parse_png(os.path.join(d, p)) for p in pngs]
pdf_name = series + '_manual.pdf'
with open(os.path.join(d, pdf_name), 'wb') as f:
f.write(build_pdf(images))
update_meta(series, pdf_name)
made += 1
print('OK %s: %d 页 → %s' % (series, len(images), pdf_name))
print('完成 %d 个系列 PDF' % made)
print('提示: 替换手册页 PNG 后重跑本脚本更新 PDF, 再【重建全量目录】+ 发布')
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
# 把 ONEBOT 手册 PDF 的系列页拆出渲染成图片 (供目录软件"尺寸图"窗口对照查看)
# 用法:
# python tools\render-manual-pages.py # 渲染全部系列
# python tools\render-manual-pages.py KC # 只渲染一个系列
# 输出: OnebotCatalog\onebot-data\catalog\manual\<系列>\p<物理页码>.png (灰度 PNG 110dpi, 源目录布局)
import fitz, os, sys
PDF = r"OnebotCatalog\ONEBOT - 直线气缸 - 中文.pdf"
OUT = r"OnebotCatalog\onebot-data\catalog\manual"
DPI = 110
# 系列 → 手册物理页码 (1-based, 含首尾); 依据: 手册印刷页码 = 物理页码 - 1
SERIES_PAGES = {
"KC": [2, 3, 4, 5, 6, 11, 12], # KC 全缸径: 32~125 (p2-6) + 125~320 (p11-12)
"KCB": [9, 10],
"KSB": [16, 17],
"KCS": [13],
"KS": [14, 15],
"KSU": [14, 15],
"M": [22, 23, 24, 25, 26, 27],
"MS": [28, 29, 30, 31, 32],
"MSC": [33, 34, 35],
"MAL": [36, 37, 38, 39, 40],
"MALC": [41, 42, 43],
}
def main():
only = sys.argv[1] if len(sys.argv) > 1 else None
doc = fitz.open(PDF)
todo = {k: v for k, v in SERIES_PAGES.items()} if only is None else {only: SERIES_PAGES.get(only, [])}
total = 0
for series, pages in todo.items():
if not pages:
print(f"[warn] 未知系列: {only}")
continue
d = os.path.join(OUT, series)
os.makedirs(d, exist_ok=True)
for p in pages:
out = os.path.join(d, f"p{p:02d}.png")
pix = doc[p - 1].get_pixmap(dpi=DPI, colorspace=fitz.csGRAY)
pix.save(out)
kb = os.path.getsize(out) // 1024
total += kb
print(f"{series} p{p:02d} -> {kb} KB")
doc.close()
print(f"完成, 共 {total // 1024} MB")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# 扫描 ONEBOT 手册 PDF: 定位每个系列的章节页范围 + 参数/尺寸关键词页
# 用法: python tools\scan-manual-pages.py
import fitz, os, re, sys
pdf = r"OnebotCatalog\ONEBOT - 直线气缸 - 中文.pdf"
doc = fitz.open(pdf)
DIGITS = {0xF6B1 + i: str(i) for i in range(10)}
def dec(text):
return "".join(DIGITS.get(ord(ch), ch) for ch in text)
# 系列代码 → 显示名 (来自 build-onebot-catalog.ps1)
SERIES = ["KC", "KCB", "KSB", "KCS", "KS", "KSU", "M", "MS", "MSC", "MAL", "MALC"]
KEYWORDS = ["订购", "规格", "参数", "外形尺寸", "符号", "特性", "理论出力", "重量", "结构"]
print("总页数:", doc.page_count)
pages = []
for i in range(doc.page_count):
t = dec(doc[i].get_text())
pages.append(t)
# 1) 每页出现的系列码标题 (页首大字号? 简化: 整页文本匹配)
hits = {}
for i, t in enumerate(pages):
found = set()
for s in SERIES:
# 系列标题一般单独成行, 如 "KC 系列" / "KC系列"
if re.search(r"(?m)^\s*" + s + r"\s*系列", t) or re.search(r"(?m)^\s*" + s + r"系列", t):
found.add(s)
if found:
hits[i + 1] = sorted(found)
kw = [k for k in KEYWORDS if k in t]
print(f"p{i+1:3d} 系列: {found} 关键词: {kw[:4]}")
print("\n--- 每页系列线索 (含页码的系列码出现) ---")
for i, t in enumerate(pages):
for s in SERIES:
if re.search(r"(?m)^\s*" + s + r"\s*$", t): # 整页只有系列码的目录页可能
print(f"p{i+1:3d} 独立行: {s}")

View File

@@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
"""
simplify-stl.py —— 简化过大的 STL 网格 (窗口内 3D 预览用, 纯 numpy 顶点聚类, 零额外依赖)
用途: 屏幕预览不需要几十 MB 的精细网格; 本脚本把 mesh\\ 里 > 阈值的 STL
按网格边长 (GRID_MM) 做顶点聚类, 体积缩小 3~10 倍, 外形基本不变。
用法: python simplify-stl.py (处理 mesh\\ 下所有 > 5MB 的 .stl, 就地覆盖)
"""
import os
import re
import struct
import numpy as np
BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MESH_DIR = os.path.join(BASE, 'catalog', 'mesh')
SIZE_MIN = 2 * 1024 * 1024 # 只处理 2MB 以上的网格 (控制窗口内渲染内存)
GRID_MM = 0.75 # 聚类网格边长 (mm): 越大越简 (0.5~1.0 预览合适)
def read_stl(path):
"""自动识别二进制/ASCII STL, 返回 (N,3,3) 三角顶点数组"""
with open(path, 'rb') as f:
head = f.read(5)
if head[:5] == b'solid':
# ASCII STL: 正则抽出所有 vertex 行
with open(path, 'r', errors='ignore') as f:
txt = f.read()
arr = np.array(re.findall(r'vertex\s+([-\deE+.]+)\s+([-\deE+.]+)\s+([-\deE+.]+)', txt),
dtype=np.float32)
return arr.reshape(-1, 3, 3)
with open(path, 'rb') as f:
f.read(80)
(n,) = struct.unpack('<I', f.read(4))
raw = np.frombuffer(f.read(n * 50), dtype=np.uint8).reshape(n, 50)
# 每条记录 50 字节: 12 个 float (48B) + 2B 属性; 截取前 48B 再按 float 视图
f32 = np.ascontiguousarray(raw[:, :48]).view('<f4').reshape(n, 12)
return f32[:, 3:].reshape(n, 3, 3)
def write_binary_stl(path, tris):
n = len(tris)
with open(path, 'wb') as f:
f.write(b'ONEBOT simplified mesh'.ljust(80, b'\0'))
f.write(struct.pack('<I', n))
for tri in tris:
p0, p1, p2 = tri[0], tri[1], tri[2]
nrm = np.cross(p1 - p0, p2 - p0)
norm = np.linalg.norm(nrm)
nrm = nrm / norm if norm > 1e-12 else np.array([0.0, 0.0, 1.0])
f.write(struct.pack('<12f', nrm[0], nrm[1], nrm[2],
p0[0], p0[1], p0[2], p1[0], p1[1], p1[2], p2[0], p2[1], p2[2]))
f.write(struct.pack('<H', 0))
def simplify(path):
tris = read_stl(path)
n_in = len(tris)
verts = tris.reshape(-1, 3)
# 顶点聚类: 落到同一网格单元的顶点合并为均值点
cell = np.floor(verts / GRID_MM).astype(np.int64)
_, inv, counts = np.unique(cell, axis=0, return_inverse=True, return_counts=True)
sums = np.zeros((counts.shape[0], 3))
np.add.at(sums, inv, verts)
new_verts = sums / counts[:, None]
# 重映射三角形, 丢弃退化 (两顶点落入同一单元)
idx = inv.reshape(-1, 3)
keep = (idx[:, 0] != idx[:, 1]) & (idx[:, 1] != idx[:, 2]) & (idx[:, 0] != idx[:, 2])
out = new_verts[idx[keep]]
write_binary_stl(path, out)
return n_in, len(out)
def main():
if not os.path.isdir(MESH_DIR):
print('mesh 目录不存在')
return
for name in sorted(os.listdir(MESH_DIR)):
if not name.lower().endswith('.stl'):
continue
p = os.path.join(MESH_DIR, name)
size_in = os.path.getsize(p)
if size_in < SIZE_MIN:
continue
n_in, n_out = simplify(p)
print('%s: %d%d 三角面 (%.1fMB → %.1fMB)' % (
name, n_in, n_out, size_in / 1048576.0, os.path.getsize(p) / 1048576.0))
print('完成')
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
"""
step-to-stl.py —— 真实 STEP 批量转 STL 网格 (桌面软件窗口内 3D 预览用)
内核: OpenCASCADE 的 Python 绑定 (cadquery/OCP, 开源 LGPL)。
用途: 把 catalog\\step\\ 里的真实数模 (体积 > 20KB 的 .step/.stp, 占位几何 ~7KB 自动跳过)
转成 catalog\\mesh\\ 下同名 .stl 网格。桌面 3D 标签页发现有网格就在窗口内直渲。
断点续跑: 同名 STL 已存在且不比 STEP 旧 → 跳过。
以后新增真实 STEP 后重跑本脚本即可 (或用 NX journal 的 exportStl 自动导出)。
用法: python step-to-stl.py
"""
import os
import sys
BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
STEP_DIR = os.path.join(BASE, 'catalog', 'step')
MESH_DIR = os.path.join(BASE, 'catalog', 'mesh')
REAL_MIN_BYTES = 20 * 1024 # 20KB 以上算真实数模 (占位几何 ~7KB)
LIN_DEFLECTION = 1.5 # 网格线性偏差 (mm): 越小越精细、文件越大 (1.5mm 屏幕预览足够, 保证流畅旋转)
def convert(step_path, stl_path):
from cadquery.occ_impl.shapes import Shape
from OCP.STEPControl import STEPControl_Reader
from OCP.StlAPI import StlAPI_Writer
from OCP.BRepMesh import BRepMesh_IncrementalMesh
from OCP.IFSelect import IFSelect_RetDone
reader = STEPControl_Reader()
if reader.ReadFile(str(step_path)) != IFSelect_RetDone:
raise RuntimeError('STEP 读取失败: %s' % step_path)
reader.TransferRoots()
shape = Shape(reader.OneShape())
BRepMesh_IncrementalMesh(shape.wrapped, LIN_DEFLECTION, False, 0.5, True).Perform()
writer = StlAPI_Writer()
writer.SetASCIIMode(False) # 二进制 STL (比 ASCII 小 ~3 倍)
if not writer.Write(shape.wrapped, str(stl_path)):
raise RuntimeError('STL 写出失败: %s' % stl_path)
def main():
if not os.path.isdir(MESH_DIR):
os.makedirs(MESH_DIR)
step_files = [f for f in os.listdir(STEP_DIR)
if f.lower().endswith(('.step', '.stp'))]
done = skip = 0
errors = []
for name in sorted(step_files):
step_path = os.path.join(STEP_DIR, name)
if os.path.getsize(step_path) < REAL_MIN_BYTES:
continue # 占位几何, 不转
stl_path = os.path.join(MESH_DIR, os.path.splitext(name)[0] + '.stl')
# 跳过条件: STL 存在、比 STEP 新、且不是占位网格 (<50KB 视为占位, 强制重转)
if (os.path.exists(stl_path) and os.path.getsize(stl_path) > 50 * 1024
and os.path.getmtime(stl_path) >= os.path.getmtime(step_path)):
skip += 1
continue
try:
convert(step_path, stl_path)
done += 1
print(' OK %-24s -> %s (%.1f KB)' % (name, os.path.basename(stl_path),
os.path.getsize(stl_path) / 1024.0))
except Exception as e:
errors.append('%s: %s' % (name, e))
print('FAIL %s: %s' % (name, e))
print('完成: 转换 %d, 跳过 %d, 失败 %d%s' % (done, skip, len(errors), MESH_DIR))
if errors:
sys.exit(1)
if __name__ == '__main__':
main()