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 millimetresdrawing.units = "um" # in a writer: the numbers you get back are microns| Accepted | Aliases |
|---|---|
nm, um, mil, mm, cm, inch, m | nanometer/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 metreDecorators
@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 TrueFormatReader 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:
| Factory | Result Type | UI Control |
|---|---|---|
Option.integer(label, default, min, max) | int | Spin box |
Option.real(label, default, min, max, decimals) | float | Double spin box |
Option.boolean(label, default) | bool | Checkbox |
Option.string(label, default) | str | Text field |
Option.choice(label, choices, default) | str | Dropdown |
Option.path(label, default, file_filter) | str | File picker |
Option.color(label, default) | str | Color picker |
Option.table(label, columns, default) | list[dict] | Editable grid |
Option.cell_choice(label, default) | str | Cell 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 unitscell(name, main=False)— context manager yielding aCellContextiter_lines(path, encoding="utf-8")— line iterator with progressiter_binary(path, chunk_size=8192)— binary iterator with progressprogress— get/set progress from0.0to1.0
CellContext
Yielded by DrawingContext.cell().
layer(name)— context manager yielding aLayerContext
LayerContext
Yielded by CellContext.layer(). Coordinates are in the drawing’s working unit.
polygon(vertices)— create a closed polygon from(x, y)tuplespolyline(width, vertices, closed=False)— create a polylinecircle(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 unitsflatten— get/set hierarchy flattening. Set it before iteratingshapes(cell=None, layer=None)— iterate shapes with progressshapes_by_layer(cell=None)— shapes grouped by layershapes_by_cell(layer=None)— shapes grouped by cellcell_names/layer_names— names, as listscell_count/shape_count— countsmain_cell_name— top cell nameprogress— get/set progress from0.0to1.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 namecell_name: str— cell namevertices: list[tuple]—(x, y)coordinates in the working unitis_polygon: bool— polygon vs. polyline-like outputwidth: int— polyline width in the working unitis_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.
| Property | Description |
|---|---|
builder.resolution | Curve tessellation settings |
builder.cell | Current cell, or None |
builder.layer | Current layer, or None |
builder.cell_object | Most recently created cell object, or None |
builder.drawing | Drawing being constructed |
| Method group | Methods |
|---|---|
| Drawing metadata | set_drawing_name(name), set_drawing_modif_time(time), set_drawing_access_time(time), set_progress(percent) |
| Cells | open_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) |
| Layers | select_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) |
| Shapes | create_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) |
| Text | create_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) |
| References | create_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) |
| Context | save_context(), enter_context(handle), leave_context() |
| Logging | log_info(message), log_warning(message), log_error(message) |
| Properties and styles | set_entity_layer_style(flags), set_current_cell_object_real_property(name, value), set_current_cell_object_bool_property(name, value) |
| Fonts | DrawingBuilder.register_odb_fonts(fonts_dir) |
from linkcad.v1.db import EndCap, TextStyle, TextStyleMaskfrom 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.
| Property | Description |
|---|---|
controller.file_name | Output file path |
controller.resolution | Curve tessellation settings |
controller.drawing | Drawing being exported |
controller.main_cell | Main cell |
controller.units_per_meter | Database units per metre, or 0 if the drawing does not declare them |
controller.layers | Every enabled layer, in regular order |
controller.layer_names | Names of every enabled layer |
controller.layer_count | Number of enabled layers |
controller.cells | Every sub-cell, child-first (excludes the main cell) |
controller.cell_names | Names of every cell, including the main cell |
controller.cell_count | Number of cells, including the main cell |
controller.fonts | Every font used in the drawing |
controller.object_count | Current processed-object count |
controller.total_object_count | Total objects to export |
controller.fill_rule | Current FillRule |
controller.transformation | The live top of the coordinate transformation stack |
| Method group | Methods |
|---|---|
| Logging and progress | log_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 configuration | set_polygon_mode(holes_mode, polygon_type) |
| Narrowed accessors | filtered_layers(sort_order=SortOrder.Regular), filtered_cells(layer=None) |
| Step-wise enumeration | start_enum_layers(sort_order=SortOrder.Regular), next_layer(), start_enum_cells(), next_cell(layer=None), start_enum_fonts(), next_font() |
| Transformations | transform_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()| Type | Purpose |
|---|---|
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 method | Description |
|---|---|
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_NAMESfmt.layer_max_length = 32Exceptions
| Exception | Use |
|---|---|
PluginError | Base class for all plugin errors |
ParseError | File parsing errors; accepts line and path keyword arguments |
WriteError | File writing errors |
ValidationError | Option validation errors |
TableColumn
Data class for defining table option columns:
key: str— dict keylabel: str— column headercol_type: str—string,integer,real,choice,cell_choicedefault: Any— default valuechoices: list[str]— forchoicecolumnsdecimals: int— forrealcolumnsmin_value/max_value— for numeric columns
Enums
| Enum | Values | Use |
|---|---|---|
Phase | ParsedFile, ParsedAll, ClosedOpenCells, ResolvedRefs, SelectedMainCell, ResolvedLayersByBlock, AutoNumberedZ | Reader post-processing phase hooks |
LayerFlags | Normal, ByLayer, ByBlock | Entity layer-property inheritance for DrawingBuilder.set_entity_layer_style() |
SortOrder | Regular, Reverse | Layer enumeration order for WriterController |
EndCap | Round, SquareExtended, SquareFlat | Re-export of linkcad.v1.db.EndCap for builder shape creation |
FillRule | NonZero, EvenOdd | Re-export of linkcad.v1.db.FillRule for writer fill-rule handling |
Writer polygon-mode enums are available from linkcad.v1.plugin:
| Enum | Values | Use |
|---|---|---|
HolesMode | Link, Split, Extract, Keep | How holes are represented during polygon export |
PolygonType | AllowComplex, ForceSimple | Whether rendered polygons may be complex |