Writing a Format Plugin
This tutorial shows how to add custom file format support to LinkCAD using Python.
Format Reader (Import)
A format reader converts an external file format into LinkCAD’s drawing database.
Example: Simple Coordinate File Reader
from pathlib import Pathfrom linkcad.v1.plugin import format_reader, FormatReader, Option, DrawingContext
@format_reader( name="Coordinate File", extensions=["*.xyz", "*.coord"], description="Simple X Y coordinate files",)class CoordReader(FormatReader): units = Option.choice( "Units", choices=["nm", "um", "mil", "mm", "cm", "inch", "m"], default="um", tooltip="Physical units of the coordinate values in the file", ) layer_name = Option.string("Layer name", default="imported")
def read(self, path: Path, drawing: DrawingContext) -> None: # Name the unit once and LinkCAD converts every coordinate you hand it. drawing.units = self.units
with drawing.cell(path.stem, main=True) as cell: with cell.layer(self.layer_name) as layer: vertices = [] for line in drawing.iter_lines(path): line = line.strip() if not line or line.startswith("#"): # Blank line = end of polygon if vertices: layer.polygon(vertices) vertices = [] continue parts = line.split() vertices.append((float(parts[0]), float(parts[1])))
# Don't forget the last polygon if vertices: layer.polygon(vertices)Key Concepts
@format_reader()registers the class as an import formatextensionsdetermines which files the reader handlesDrawingContextprovides a builder API for creating cells, layers, and shapesdrawing.unitsnames the unit your coordinates are in, so you never scale by handdrawing.iter_lines()reads lines with automatic progress trackingdrawing.iter_binary()reads binary data with progress tracking
Working Units
Setting drawing.units once means every coordinate and width you pass in is interpreted in that unit. Accepted names are nm, um, mil, mm, cm, inch, and m, plus the obvious aliases (micron, millimetre, in, …).
Leave it unset and coordinates are raw database units, exactly as before the property existed. An unknown name, or a drawing that does not declare its database units, raises ValidationError.
A reader that multiplies coordinates by its own scale table is out of date — delete the table and name the unit.
DrawingContext API
| Property / Method | Description |
|---|---|
units | The unit your coordinates are in; None for raw database units |
cell(name, main=False) | Context manager — creates/opens a cell |
iter_lines(path, encoding="utf-8") | Iterate text file lines with progress |
iter_binary(path, chunk_size=8192) | Iterate binary chunks with progress |
progress | Get/set progress (0.0 to 1.0) |
CellContext API
| Method | Description |
|---|---|
layer(name) | Context manager — selects a layer |
LayerContext API
Coordinates are in the drawing’s working unit.
| Method | Description |
|---|---|
polygon(vertices) | Create a closed polygon from (x, y) tuples |
polyline(width, vertices, closed=False) | Create a polyline |
circle(center, diameter) | Create a circle |
Post-Processing Hook
A reader may define an optional post_process() to take part in the phases LinkCAD runs after parsing — the same hook the built-in native readers use. It runs on the same instance as read(), so anything you recorded there is still available. Return False to abort the import.
from linkcad.v1.plugin import Phase
class CoordReader(FormatReader): def post_process(self, phase, drawing, resolution) -> bool: if phase == Phase.ResolvedRefs: ... # every reference now points at a real cell return TruePhases arrive in order: ParsedFile, ParsedAll, ClosedOpenCells, ResolvedRefs, SelectedMainCell, ResolvedLayersByBlock, AutoNumberedZ.
Format Writer (Export)
A format writer exports LinkCAD geometry to a file.
Example: Simple Text Writer
from pathlib import Pathfrom linkcad.v1.plugin import format_writer, FormatWriter, Option, WriterContext
@format_writer( name="Simple Text", extensions=["*.stxt"],)class SimpleWriter(FormatWriter): units = Option.choice( "Output units", choices=["nm", "um", "mil", "mm", "cm", "inch", "m"], default="um", ) separator = Option.choice( "Separator", choices=["Space", "Comma", "Tab"], default="Space", ) precision = Option.integer("Decimal places", default=3, min=0, max=10) flatten = Option.boolean("Flatten hierarchy", default=False)
def write(self, path: Path, drawing: WriterContext) -> None: sep = {"Space": " ", "Comma": ", ", "Tab": "\t"}[self.separator]
# Name the unit once and every coordinate arrives already converted. drawing.units = self.units drawing.flatten = self.flatten
with open(path, "w") as f: for layer_name, shapes in drawing.shapes_by_layer(): f.write(f"# Layer: {layer_name}\n") for shape in shapes: for x, y in shape.vertices: f.write(f"{x:.{self.precision}f}{sep}" f"{y:.{self.precision}f}\n") f.write("\n")Working Units and Flattening
drawing.units works the same way for writers as for readers, in the other direction: set it and every coordinate and width you receive from shapes() is already expressed in that unit. A writer that divides by its own scale table is out of date.
drawing.flatten selects the traversal. Set it before iterating:
flatten = Falsewalks each cell’s own shapes and leaves cell references to you. Curves come back with no vertices, because tessellating them needs the host.flatten = Truerenders the hierarchy through the host controller, which applies the transformation stack as it descends and tessellates curves along the way. Arcs, circles, donuts, and NURBS therefore reach a writer as polygons and polylines only when flattening is on.
Either way, shapes() is the one traversal — there is no separate flattened iterator.
WriterContext API
| Property / Method | Description |
|---|---|
units | The unit the geometry you receive is expressed in; None for raw database units |
flatten | Get/set hierarchy flattening. Set before iterating |
shapes(cell=None, layer=None) | Iterate shapes (optional cell/layer filter) |
shapes_by_layer(cell=None) | Iterate shapes grouped by layer |
shapes_by_cell(layer=None) | Iterate shapes grouped by cell |
cell_names / layer_names | Lists of all cell / layer names |
cell_count | Number of cells |
shape_count | Total number of shapes |
main_cell_name | Name of the top cell |
progress | Get/set progress (0.0 to 1.0) |
ShapeInfo Properties
| Property | Type | Description |
|---|---|---|
layer_name | str | Layer name |
cell_name | str | Cell name |
vertices | list[tuple] | (x, y) coordinates in the working unit |
is_polygon | bool | True for polygons, False for polylines |
width | int | Polyline width in the working unit (0 for polygons) |
is_closed | bool | Whether the shape is closed |
Error Handling
Use the built-in exception classes for clear error reporting:
from linkcad.v1.plugin import ParseError, WriteError, ValidationError
# In a reader:raise ParseError("Invalid coordinate", line=42, path=path)
# In a writer:raise WriteError("Cannot write to locked file")
# In option validation:raise ValidationError("Scale must be positive")Next Steps
- Panel Assembly Tutorial — advanced tool using table options
- Format Decorators Reference — complete decorator API