Skip to content

linkcad.plugin

The plugin module provides the framework for creating tools, format readers, and format writers. Use linkcad.v1.plugin in scripts and plugins; it mirrors the whole public namespace of linkcad.plugin, so both expose exactly the same API.

from linkcad.v1.plugin import (
tool, Tool, Option, TableColumn,
format_reader, FormatReader, DrawingContext, CellContext, LayerContext, DrawingBuilder,
format_writer, FormatWriter, WriterContext, ShapeInfo, WriterController,
DialogSpec, ChoiceItem,
EventLog, Phase, LayerFlags, SortOrder,
HolesMode, PolygonType, EndCap, FillRule,
PluginError, ParseError, WriteError, ValidationError,
UNIT_IN_METERS,
)

Working units

Both DrawingContext (import) and WriterContext (export) carry a units property. Set it once and every coordinate and width crossing the plugin boundary is expressed in that unit — a plugin never needs its own scale table:

drawing.units = "mm" # in a reader: the numbers you hand in are millimetres
drawing.units = "um" # in a writer: the numbers you get back are microns
AcceptedAliases
nm, um, mil, mm, cm, inch, mnanometer/nanometre, micron/micrometer/micrometre, millimeter/millimetre, centimeter/centimetre, in, meter/metre

Leaving units unset (or assigning None) means raw database units, which is exactly how plugins behaved before the property existed. An unknown name, or a drawing that does not declare its database units, raises ValidationError.

UNIT_IN_METERS is the one conversion table, exported here so no plugin has to carry its own — a second table that can silently disagree is how a thousand-fold placement error once reached a shipped tool:

from linkcad.v1.plugin import UNIT_IN_METERS
to_db_units = UNIT_IN_METERS["mm"] * drawing.units # drawing.units is per metre

Decorators

@tool()

Registers a class as a menu tool. See Tool Decorator.

@tool(name="My Tool", menu="Tools/Custom")
class MyTool(Tool):
def run(self, drawing) -> dict:
# `drawing` is a linkcad.v1.db.Drawing owned by the application.
return {"summary": f"{len(drawing.cells)} cells"}

@format_reader()

Registers a class as a file import format. See Format Decorators.

@format_reader(name="My Format", extensions=["*.myf"])
class MyReader(FormatReader):
def read(self, path: Path, drawing: DrawingContext) -> None:
...

A reader may also define an optional post_process(phase, drawing, resolution) hook, called after parsing for each of the phases LinkCAD runs — the same hook the built-in native readers use. It runs on the same instance as read(), so anything recorded there is still available. Return False to abort the import:

from linkcad.v1.plugin import Phase
class MyReader(FormatReader):
def post_process(self, phase, drawing, resolution) -> bool:
if phase == Phase.ResolvedRefs:
... # every reference now points at a real cell
return True

FormatReader also provides parse_file(filepath, builder, file_size, progress_callback), the native bridge used by LinkCAD. Implement read() in user plugins and let the bridge call it.

@format_writer()

Registers a class as a file export format. See Format Decorators.

@format_writer(name="My Format", extensions=["*.myf"])
class MyWriter(FormatWriter):
def write(self, path: Path, drawing: WriterContext) -> None:
...

FormatWriter also provides write_file(filepath, controller, set_entity_sink=None), the native bridge used by LinkCAD. Implement write() in user plugins and let the bridge call it.

Option Class

Option provides factory methods for defining typed, persistent options:

FactoryResult TypeUI Control
Option.integer(label, default, min, max)intSpin box
Option.real(label, default, min, max, decimals)floatDouble spin box
Option.boolean(label, default)boolCheckbox
Option.string(label, default)strText field
Option.choice(label, choices, default)strDropdown
Option.path(label, default, file_filter)strFile picker
Option.color(label, default)strColor picker
Option.table(label, columns, default)list[dict]Editable grid
Option.cell_choice(label, default)strCell dropdown

All factories accept optional tooltip and enabled_when parameters, plus a keyword-only name that overrides the generated option key.

Option keys share one namespace with the application’s own settings, so a key generated from a class attribute is scoped rather than merely prefixed: flatten on AsciiWriter becomes PyPlugin.AsciiWriter.flatten. The reserved PyPlugin root keeps plugin options out of the application’s namespace, and the class segment keeps two plugins that both call an option flatten apart. The attribute name is used as written, so the key and the Python attribute that reads it are spelled the same.

Every plugin option is also settable from the command line under that key:

linkcad --PyPlugin.AsciiWriter.precision=5 ...

Context Classes

DrawingContext

Pythonic builder interface for format readers.

  • units — the unit your coordinates are in; see Working units
  • cell(name, main=False) — context manager yielding a CellContext
  • iter_lines(path, encoding="utf-8") — line iterator with progress
  • iter_binary(path, chunk_size=8192) — binary iterator with progress
  • progress — get/set progress from 0.0 to 1.0

CellContext

Yielded by DrawingContext.cell().

  • layer(name) — context manager yielding a LayerContext

LayerContext

Yielded by CellContext.layer(). Coordinates are in the drawing’s working unit.

  • polygon(vertices) — create a closed polygon from (x, y) tuples
  • polyline(width, vertices, closed=False) — create a polyline
  • circle(center, diameter) — create a circle
def read(self, path: Path, drawing: DrawingContext) -> None:
drawing.units = "um"
with drawing.cell(path.stem, main=True) as cell:
with cell.layer("METAL1") as layer:
layer.polygon([(0, 0), (100, 0), (100, 100), (0, 100)]) # a 100 µm square
layer.polyline(2.5, [(0, 0), (200, 200)])
layer.circle((500, 500), 50)

WriterContext

Pythonic reader interface for format writers.

  • units — the unit the geometry you receive is expressed in; see Working units
  • flatten — get/set hierarchy flattening. Set it before iterating
  • shapes(cell=None, layer=None) — iterate shapes with progress
  • shapes_by_layer(cell=None) — shapes grouped by layer
  • shapes_by_cell(layer=None) — shapes grouped by cell
  • cell_names / layer_names — names, as lists
  • cell_count / shape_count — counts
  • main_cell_name — top cell name
  • progress — get/set progress from 0.0 to 1.0

Everything but shapes() and its two groupings is a noun and therefore a property. The cells() and layers() generators are gone: they yielded exactly the strings cell_names and layer_names already hold, and two spellings of one list is a choice with no right answer.

shapes() is the single traversal for geometry, flattened or not. With flatten = False it walks each cell’s own shapes, leaving references to you. With flatten = True it renders the hierarchy through the host controller, which applies the transformation stack as it descends and tessellates curves along the way — so arcs, circles, donuts and NURBS reach a writer as polygons and polylines only when flattening is on.

def write(self, path: Path, drawing: WriterContext) -> None:
drawing.units = "mm"
drawing.flatten = True
with open(path, "w") as f:
for shape in drawing.shapes():
for x, y in shape.vertices: # millimetres
f.write(f"{x:.4f} {y:.4f}\n")
f.write("\n")

Flattened traversal needs the host writer, so it is available inside FormatWriter.write(). A WriterContext you build by hand can only use flatten = False.

ShapeInfo

Data class yielded by WriterContext.shapes():

  • layer_name: str — layer name
  • cell_name: str — cell name
  • vertices: list[tuple](x, y) coordinates in the working unit
  • is_polygon: bool — polygon vs. polyline-like output
  • width: int — polyline width in the working unit
  • is_closed: bool — whether the shape is closed

Native Format Interfaces

DrawingContext and WriterContext cover most format plugins. The native DrawingBuilder and WriterController interfaces are also exposed for plugins that need direct parity with LinkCAD’s low-level format API.

DrawingBuilder

DrawingBuilder is supplied to import plugins by the LinkCAD runtime. It is not created directly by user code.

PropertyDescription
builder.resolutionCurve tessellation settings
builder.cellCurrent cell, or None
builder.layerCurrent layer, or None
builder.cell_objectMost recently created cell object, or None
builder.drawingDrawing being constructed
Method groupMethods
Drawing metadataset_drawing_name(name), set_drawing_modif_time(time), set_drawing_access_time(time), set_progress(percent)
Cellsopen_cell(name_or_number, is_main_cell=False, reopen=False), close_cell(), delete_cell(), set_cell_name(name), set_cell_modif_time(time), set_cell_access_time(time), find_cell(name)
Layersselect_layer(name_or_number), select_layer(major, minor), set_layer_comment(comment), set_layer_color(rgba), set_layer_enabled(enabled), set_layer_z(z), set_layer_polarity_positive(positive=True), set_layer_polarity_group(group_name), set_layer_polarity_sequence(sequence)
Shapescreate_polygon(vertices, make_simple=False), create_polygon_with_bulges(vertices, bulges), create_rectangle(corner1, corner2), create_polyline(width, vertices, closed=False, end_cap=EndCap.Round), create_circle(center, diameter, donut=False), create_arc(center, radius, width, start_angle=0.0, end_angle=360.0, end_cap=EndCap.Round), create_donut(center, mean_diameter, width), create_nurbs(width, degree, knots, control_points, periodic=False), create_nurbs_weighted(width, degree, knots, control_points, weights, periodic=False)
Textcreate_text(), set_text_position(position), set_text_height(height), set_text_stroke_width(width), set_text_style(flags, mask=TextStyleMask.None_), set_formatted_text(text), set_unformatted_text(text), set_text_font(font_name), set_text_width_factor(factor), set_text_obliquing_angle(angle_degrees), set_text_mirrored_x(mirror=True), set_text_mirrored_y(mirror=True), set_text_rotation(angle_degrees, absolute=False), set_text_box_width(width), set_text_line_spacing(spacing)
Referencescreate_ref(cell_name_or_number), scale_ref(scale, absolute=False), mirror_ref_x(negate=True), mirror_ref_y(negate=True), rotate_ref(angle_degrees, absolute=False), translate_ref(position), set_ref_array_spacing(dx, dy), set_ref_array_size(cols, rows)
Contextsave_context(), enter_context(handle), leave_context()
Logginglog_info(message), log_warning(message), log_error(message)
Properties and stylesset_entity_layer_style(flags), set_current_cell_object_real_property(name, value), set_current_cell_object_bool_property(name, value)
FontsDrawingBuilder.register_odb_fonts(fonts_dir)
from linkcad.v1.db import EndCap, TextStyle, TextStyleMask
from linkcad.v1.geom import Point
builder.open_cell("TOP", is_main_cell=True)
builder.select_layer("metal1")
builder.create_arc(Point(0, 0), radius=10_000, width=500, end_cap=EndCap.Round)
builder.create_text()
builder.set_text_position(Point(0, 12_000))
builder.set_text_height(1_000)
builder.set_text_style(TextStyle.AlignHCenter, TextStyleMask.AlignH)
builder.set_unformatted_text("TOP")
builder.close_cell()

WriterController

WriterController is supplied to export plugins by the LinkCAD runtime. It is not created directly by user code.

PropertyDescription
controller.file_nameOutput file path
controller.resolutionCurve tessellation settings
controller.drawingDrawing being exported
controller.main_cellMain cell
controller.units_per_meterDatabase units per metre, or 0 if the drawing does not declare them
controller.layersEvery enabled layer, in regular order
controller.layer_namesNames of every enabled layer
controller.layer_countNumber of enabled layers
controller.cellsEvery sub-cell, child-first (excludes the main cell)
controller.cell_namesNames of every cell, including the main cell
controller.cell_countNumber of cells, including the main cell
controller.fontsEvery font used in the drawing
controller.object_countCurrent processed-object count
controller.total_object_countTotal objects to export
controller.fill_ruleCurrent FillRule
controller.transformationThe live top of the coordinate transformation stack
Method groupMethods
Logging and progresslog_info(message), log_warning(message), log_error(message), set_progress(percent), init_progress_counter(force_flattened=False), set_object_count(count), set_total_object_count(count)
Export configurationset_polygon_mode(holes_mode, polygon_type)
Narrowed accessorsfiltered_layers(sort_order=SortOrder.Regular), filtered_cells(layer=None)
Step-wise enumerationstart_enum_layers(sort_order=SortOrder.Regular), next_layer(), start_enum_cells(), next_cell(layer=None), start_enum_fonts(), next_font()
Transformationstransform_point(point), transform_distance(distance)

layers and cells are the common case; filtered_layers(sort_order) and filtered_cells(layer) expose the parameters those properties hide — the same pair as cell.shapes / cell.filtered_shapes.

from linkcad.v1.plugin import SortOrder
for layer in controller.filtered_layers(SortOrder.Reverse):
for cell in controller.filtered_cells(layer):
...

The push side belongs to the framework

LinkCAD exports geometry two ways: a plugin pulls shapes out, or the controller pushes rendered entities at the writer. Only the push direction can flatten, because only the controller holds the transformation stack built while descending through references — so a plugin that drives it by hand gets shapes without their placement and no error to say so.

_render_cell, _render_cell_in_layer_order, _flatten_cell_hierarchy and _get_shapes are therefore underscore-prefixed: they belong to the framework, and WriterContext drives them for you. Set drawing.flatten = True and iterate drawing.shapes(), which is the same traversal with the placement already applied.

HolesMode and PolygonType are available from the stable plugin module and are used by WriterController.set_polygon_mode():

from linkcad.v1.plugin import HolesMode, PolygonType
controller.set_polygon_mode(HolesMode.Link, PolygonType.AllowComplex)

DialogSpec

DialogSpec describes a format or tool options dialog declaratively. It mirrors the Rust plugin dialog model and serializes to the JSON format consumed by the LinkCAD UI.

from linkcad.v1.plugin import DialogSpec, ChoiceItem
spec = (
DialogSpec("Import Options")
.group("Geometry")
.bool_field("merge", "Merge polygons", True)
.int_field("facets", "Minimum facets", 4, 256, 32)
.combo_field("units", "Units", ["nm", "um", "mm"], "um")
.group_in_row("Layers", row=1)
.single_select_list(
"layer",
"Layer",
[ChoiceItem.with_display("metal1", "Metal 1")],
)
)
json_payload = spec.to_json()
TypePurpose
DialogSpec(title)Top-level dialog descriptor
DialogGroup(label, row=None)Group of related fields; groups with the same row can be laid out side-by-side
UiWidget(option_name, label, kind)Single UI element in a group
WidgetKind(kind_type, **kwargs)Widget type plus configuration
ChoiceItem(value, display, description=None)List/combo-box choice item
Builder methodDescription
group(label)Start a group
group_in_row(label, row)Start a group assigned to a row
bool_field(option_name, label, default)Checkbox
int_field(option_name, label, min, max, default)Integer spin box
real_field(option_name, label, min, max, default)Floating-point spin box
string_field(option_name, label, default)Text line
combo_field(option_name, label, choices, default)Combo box from string choices
single_select_list(option_name, label, static_choices=None, inspection_key=None)Single-select list
multi_select_list(option_name, label, static_choices=None, inspection_key=None, value_separator=",")Multi-select list
note(text)Display-only note
separator()Horizontal separator
to_json()Serialize to JSON

ChoiceItem provides simple(value), with_display(value, display), and full(value, display, description) factory methods.

Format Metadata

Reader and writer decorators attach a FormatInfo object to the plugin class. For lower-level format constraints, use linkcad.v1.conv.Format and FormatAttributes; see linkcad.conv.

from linkcad.v1.conv import Format, FormatAttributes
fmt = Format()
fmt.attributes = FormatAttributes.LAYER_NAMES | FormatAttributes.CELL_NAMES
fmt.layer_max_length = 32

Exceptions

ExceptionUse
PluginErrorBase class for all plugin errors
ParseErrorFile parsing errors; accepts line and path keyword arguments
WriteErrorFile writing errors
ValidationErrorOption validation errors

TableColumn

Data class for defining table option columns:

  • key: str — dict key
  • label: str — column header
  • col_type: strstring, integer, real, choice, cell_choice
  • default: Any — default value
  • choices: list[str] — for choice columns
  • decimals: int — for real columns
  • min_value / max_value — for numeric columns

Enums

EnumValuesUse
PhaseParsedFile, ParsedAll, ClosedOpenCells, ResolvedRefs, SelectedMainCell, ResolvedLayersByBlock, AutoNumberedZReader post-processing phase hooks
LayerFlagsNormal, ByLayer, ByBlockEntity layer-property inheritance for DrawingBuilder.set_entity_layer_style()
SortOrderRegular, ReverseLayer enumeration order for WriterController
EndCapRound, SquareExtended, SquareFlatRe-export of linkcad.v1.db.EndCap for builder shape creation
FillRuleNonZero, EvenOddRe-export of linkcad.v1.db.FillRule for writer fill-rule handling

Writer polygon-mode enums are available from linkcad.v1.plugin:

EnumValuesUse
HolesModeLink, Split, Extract, KeepHow holes are represented during polygon export
PolygonTypeAllowComplex, ForceSimpleWhether rendered polygons may be complex