Skip to content

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 Path
from 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 format
  • extensions determines which files the reader handles
  • DrawingContext provides a builder API for creating cells, layers, and shapes
  • drawing.units names the unit your coordinates are in, so you never scale by hand
  • drawing.iter_lines() reads lines with automatic progress tracking
  • drawing.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 / MethodDescription
unitsThe 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
progressGet/set progress (0.0 to 1.0)

CellContext API

MethodDescription
layer(name)Context manager — selects a layer

LayerContext API

Coordinates are in the drawing’s working unit.

MethodDescription
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 True

Phases 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 Path
from 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 = False walks each cell’s own shapes and leaves cell references to you. Curves come back with no vertices, because tessellating them needs the host.
  • flatten = True renders 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 / MethodDescription
unitsThe unit the geometry you receive is expressed in; None for raw database units
flattenGet/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_namesLists of all cell / layer names
cell_countNumber of cells
shape_countTotal number of shapes
main_cell_nameName of the top cell
progressGet/set progress (0.0 to 1.0)

ShapeInfo Properties

PropertyTypeDescription
layer_namestrLayer name
cell_namestrCell name
verticeslist[tuple](x, y) coordinates in the working unit
is_polygonboolTrue for polygons, False for polylines
widthintPolyline width in the working unit (0 for polygons)
is_closedboolWhether 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