面板拼版教程
这个进阶教程构建一个真实可用的工具:它用 Option.table() 类型为用户提供一个可编辑的单元放置网格,并据此拼装出面板。
它与 LinkCAD 随附的 Panel Assembly 工具(plugins/python/tools/panelize.py)一致,因此你可以对照阅读随附文件。
目标
创建一个通过放置单元引用把单元排布成面板的工具——不复制也不展平任何几何图形,因此面板保持可编辑,并可导出到任何格式。
完整插件
import jsonimport loggingfrom typing import Any, Dict, List
from linkcad.v1.plugin import UNIT_IN_METERS, Option, TableColumn, Tool, tool
logger = logging.getLogger(__name__)
# Placement coordinates are entered in whichever unit the user picked, while the# database stores integers whose size comes from `drawing.units` (database units# per metre), so the two have to be reconciled before anything is placed. The# conversion table is linkcad.plugin's: a second table that can silently# disagree is how a thousand-fold placement error once got into this file.
@tool( name="Panel Assembly", menu="Tools/Drawing", tooltip="Build a panel by placing cell references at specified positions", requires_drawing=True,)class PanelizeTool(Tool): placements = Option.table( "Placements", columns=[ TableColumn(key="cell_name", label="Cell", col_type="cell_choice"), TableColumn(key="x", label="X", col_type="real", default=0.0, decimals=3), TableColumn(key="y", label="Y", col_type="real", default=0.0, decimals=3), TableColumn( key="rotation", label="Rotation (°)", col_type="real", default=0.0, decimals=1, min_value=-360.0, max_value=360.0, ), ], default=[], tooltip="Cell placement list: select a cell and enter coordinates", )
units = Option.choice( "Placement units", choices=["nm", "um", "mil", "mm", "cm", "inch", "m"], default="mm", tooltip="Units the X and Y columns are given in", )
output_cell_name = Option.string( "Output Cell Name", default="PANEL_ASSEMBLY", tooltip="Name of the new panel assembly cell", )
set_as_main = Option.boolean( "Set as Main Cell", default=True, tooltip="Make the panel assembly cell the main (top) cell", )
def run(self, drawing: Any) -> Dict[str, Any]: from linkcad.v1.db import Transaction from linkcad.v1.geom import Transformation
# The dialog stores table rows as a JSON string; a script may pass a list. raw = self.placements if isinstance(raw, str): rows: List[Dict[str, Any]] = json.loads(raw) if raw else [] elif isinstance(raw, list): rows = raw else: rows = []
if not rows: raise ValueError( "No placements defined. Add at least one row to the placement table." )
# Validate every referenced cell before changing anything. cell_names_needed = {r["cell_name"] for r in rows if r.get("cell_name")} missing = sorted(n for n in cell_names_needed if drawing.cell(n) is None) if missing: raise ValueError( "The following cells are not in the drawing: " + ", ".join(missing) + ". Import the required designs first." )
output_name = self.output_cell_name or "PANEL_ASSEMBLY"
# A cell placed inside itself corrupts the cell tree, so refuse it up front. if output_name in cell_names_needed: raise ValueError( f"Output cell '{output_name}' cannot also be used as a placement " f"cell. This would create a circular cell reference." )
db_units_per_meter = float(drawing.units) if db_units_per_meter <= 0.0: raise ValueError( "The drawing does not declare its database units, so placement " "coordinates cannot be converted." ) to_db_units = UNIT_IN_METERS[self.units] * db_units_per_meter
placed_count = 0
with Transaction(drawing, "Panel Assembly"): assembly_cell = drawing.add_cell(output_name)
# If the cell already existed, clear it so re-running the tool does # not duplicate every placement. for obj in assembly_cell.cell_objects: obj.destroy()
for row in rows: cell_name = row.get("cell_name", "") if not cell_name: continue
x = float(row.get("x", 0.0)) y = float(row.get("y", 0.0)) rotation = float(row.get("rotation", 0.0))
ref_cell = drawing.cell(cell_name)
# Rotate before translating. Transformation.rotate() also rotates # whatever offset is already set, so the other order would swing # each cell away from its intended spot. xform = Transformation() if rotation != 0.0: xform.rotate(rotation) xform.translate(x * to_db_units, y * to_db_units)
assembly_cell.add_ref(ref_cell, transformation=xform) placed_count += 1
if self.set_as_main: drawing.main_cell = assembly_cell
return { "title": "Panel Assembly", "cell_name": output_name, "placements": placed_count, "unique_cells": len(cell_names_needed), "summary": ( f"Created '{output_name}' with {placed_count} cell references " f"from {len(cell_names_needed)} unique cell(s)" ), }表格选项的工作方式
Option.table() 在工具对话框中创建一个可编辑网格:
- 列由
TableColumn对象定义 - 行由用户添加和删除
- 取值是把列键映射到值的
list[dict]——若由对话框保存,则是该列表的 JSON 字符串形式 - 列类型包括
string、integer、real、choice和cell_choice
TableColumn 属性
| 属性 | 说明 |
|---|---|
key | 该列取值对应的字典键 |
label | 列标题文本 |
col_type | string、integer、real、choice、cell_choice |
default | 新行的默认值 |
choices | choice 列的可选项 |
decimals | real 列的小数位数 |
min_value | 数值列的最小值 |
max_value | 数值列的最大值 |
cell_choice 列类型
cell_choice 列会渲染一个下拉列表,其中填充当前图纸的所有单元名。这是让用户选择单元的推荐方式。
关键模式
放置单元引用
引用由将要持有它的单元创建:
assembly_cell.add_ref(ref_cell, transformation)add_ref() 也能在一次调用中构建阵列,这比每个位置一个引用更划算:
assembly_cell.add_ref( ref_cell, transformation, columns=8, rows=4, column_spacing=10_000, row_spacing=8_000,)它会抛出 ValueError,而不是返回一个无法使用的值——循环层次结构(把某个单元放进它自己的后代中)如此,目标单元不存在时也是如此。这个工具仍然在改动图纸之前先自行检查自引用,好让用户得到一条点名输出单元的提示,而不是事务进行到一半时抛出的异常。
使用数据库事务
务必把对图纸的修改包裹在事务中。块正常结束时提交,异常逸出时回滚,因此中途失败不会留下半成品:
from linkcad.v1.db import Transaction
with Transaction(drawing, "Panel Assembly"): ...描述字符串会作为撤销条目显示在“Edit”菜单中。
单位换算
数据库存储整数,而一个整数代表多大,因图纸而异。drawing.units 表示 1 米等于多少数据库单位,正是它把物理尺寸转换成坐标:
from linkcad.v1.plugin import UNIT_IN_METERS
db_units_per_meter = float(drawing.units)if db_units_per_meter <= 0.0: raise ValueError("This drawing does not declare its database units")
x_db = x_mm * UNIT_IN_METERS["mm"] * db_units_per_meter在这里猜测会直接决定几何图形落在哪里。当图纸未声明其单位时,请拒绝处理而不是假设。每单位对应多少米,请取自 linkcad.plugin.UNIT_IN_METERS,不要自己写一张表——可接受的名称是 nm、um、mil、mm、cm、inch 和 m。
从控制台运行
工具选项就是普通的实例属性,因此无需打开对话框,也可以直接从脚本编辑器驱动工具:
from tools.panelize import PanelizeTool
t = PanelizeTool()t.placements = [{"cell_name": "UNIT", "x": 5.0, "y": 2.5, "rotation": 0.0}]t.units = "mm"t.output_cell_name = "PANEL"print(t.run(drawing)["summary"])