コンテンツにスキップ

パネルアセンブリチュートリアル

この応用チュートリアルでは、Option.table() 型を使ってセル配置を編集可能なグリッドとしてユーザーに提示し、それをパネルに組み立てる実用的なツールを構築します。

LinkCAD に同梱されている Panel Assembly ツール(plugins/python/tools/panelize.py)と対応しているため、本ページと合わせて実際のファイルを読むこともできます。

目的

セル参照 を配置することでセルをパネルに並べるツールを作ります。ジオメトリはコピーもフラット化もされないため、パネルは編集可能なまま、どの形式にもエクスポートできます。

完全なプラグイン

import json
import logging
from 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 文字列形式——です
  • 列の型には stringintegerrealchoicecell_choice があります

TableColumn のプロパティ

プロパティ説明
keyこの列の値に対する辞書キー
label列見出しのテキスト
col_typestringintegerrealchoicecell_choice
default新しい行の既定値
choiceschoice 列の選択肢
decimalsreal 列の小数桁数
min_value数値列の最小値
max_value数値列の最大値

cell_choice 列型

cell_choice 列は、現在の図面のすべてのセル名を候補とするドロップダウンを表示します。ユーザーにセルを選ばせる場合はこれが推奨されます。

主要なパターン

セル参照を配置する

参照は、それを保持することになるセルが作成します。

assembly_cell.add_ref(ref_cell, transformation)

add_ref() は 1 回の呼び出しで配列も作成できます。位置ごとに参照を作るより効率的です。

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」メニューに表示されます。

単位変換

データベースは整数を格納しますが、その 1 単位がどれだけの大きさかは図面ごとに異なります。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 から取ってください——使用できる名前は nmummilmmcminchm です。

コンソールから実行する

ツールのオプションは通常のインスタンス属性なので、ダイアログを開かずにスクリプトエディターから直接ツールを駆動できます。

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"])