Skip to content

linkcad.db

The database module provides access to LinkCAD’s in-memory drawing database. All database objects are backed by native implementations for performance.

Use the stable linkcad.v1.db namespace in scripts and plugins. It mirrors the whole public namespace of linkcad.db, so the objects are the same ones.

from linkcad.v1 import db
with db.Drawing("scratch") as dwg:
cell = dwg.add_cell("TOP")

Or import the names you use:

from linkcad.v1.db import (
Drawing, Cell, Layer, Object, DrawingObject, CellObject, Shape,
Polygon, Polyline, Arc, Ellipse, Donut, Nurbs, Text, Ref,
Color, Property, ReadLock, WriteLock, Transaction,
Unit, ObjectType, CellContext, EndCap, FillRule,
BooleanOperation, MergeLayerPolarityResult, TextStyle, TextStyleMask,
)

Rules worth knowing first

The API follows the same conventions throughout — in this module and in every other one. Knowing them means you rarely have to look anything up:

  1. A noun is a property, a verb is a method. shape.area, cell.shapes, polygon.vertices, arc.radius, obj.bounds, obj.selected — no parentheses. destroy(), clone(), rotate(), reverse(), toggle_selection() keep theirs.
  2. A getter/setter pair is one read-write property, never two names. Assign instead of calling a setter: arc.center = Point(0, 0), layer.enabled = False, obj.selected = True. There are no set_* methods on geometry.
  3. Geometry is created by the object that will own it. A drawing makes cells and layers (drawing.add_cell, drawing.add_layer); a cell makes shapes (cell.add_polygon, cell.add_arc, cell.add_ref, …). There are no free-standing constructors and no static factories.
  4. Where an accessor needs parameters, the property covers the common case and a filtered_* method takes the rest — cell.shapes is every shape, cell.filtered_shapes(selected_only=True) narrows it.

Two more conventions save a conversion at every call site:

  • A coordinate is a Point or an (x, y) pair, interchangeably, everywhere linkcad.db takes one — vertex sequences, add_arc(center=…), add_text(position=…), text.position = ….
  • A layer is a Layer, a layer name, or None. obj.layer = "METAL1" moves the object to that layer and creates it if it does not exist; obj.layer = None moves it to the default layer.

Classes

Drawing

The top-level container for all layout data.

A drawing holds the whole database in memory and must be released deterministically — letting one survive to interpreter shutdown crashes the process. Use it as a context manager and that cannot be forgotten:

from linkcad.v1.db import Drawing
with Drawing("scratch") as dwg:
cell = dwg.add_cell("TOP")
dwg.main_cell = cell
metal = dwg.add_layer("METAL1")
cell.add_polygon(metal, [(0, 0), (10_000, 0), (10_000, 10_000), (0, 10_000)])

Inside a tool plugin, the drawing is handed to run() and is owned by the application — do not destroy it there.

Property / MethodDescription
Drawing(name="")Create a drawing. If name is omitted, LinkCAD assigns a unique name
with Drawing(...) as dwg:Recommended form; releases the database on exit
drawing.nameDrawing name
drawing.unitsDatabase units per metre, as a float (e.g. 1e9 for a nanometre database). 0.0 means the drawing does not declare its units. Assigning raises RuntimeError once the database holds anything, so set it first
drawing.main_cellGet or set the main (top) cell
drawing.cellsAll cells, as a list
drawing.layersAll layers, as a list
drawing.cell(name)Find a cell by name; returns None if not found
drawing.layer(name)Find a layer by name; returns None if not found
drawing.add_cell(name)Add a cell, or return the existing cell of that name
drawing.add_layer(name)Add a layer, or return the existing layer of that name
drawing.modif_time / drawing.access_timeTimestamps, as Unix seconds
drawing.undo_enabledEnable or disable undo recording
drawing.begin_undo_marker(tag=0) / drawing.end_undo_marker()Bracket an undoable transaction manually
drawing.undo() / drawing.redo()Undo or redo the last transaction
drawing.can_undo / drawing.can_redo(available, tag) tuple
drawing.destroy_layer_by_name(name)Delete a layer by name; returns True if it existed
drawing.rename_layer(from_name, to_name)Rename a layer; returns True on success
drawing.boolean_layers_by_name(op, result_layer, operand_layer, maximum_error=100, minimum_facets=16)Boolean two layers by name; result_layer is operand A and receives the result
drawing.merge_layer_polarity_group(group_name, maximum_error=100, minimum_facets=16, progress_from=0, progress_to=100)Merge one deferred-polarity layer group
drawing.merge_all_polarity_groups(maximum_error=100, minimum_facets=16, progress_from=0, progress_to=100)Merge every deferred-polarity layer group
drawing.destroy()Destroy the drawing and its contents
drawing.memory_usageNative database memory usage in bytes
drawing.lockedTrue if any thread holds a lock on the database
drawing.locked_by_this_threadTrue if the calling thread holds the write lock

The last three read process-wide state, so they can be read from the class as well as from an instance: Drawing.memory_usage and dwg.memory_usage give the same number.

Layer helper methods that mutate the drawing create their own undoable transaction.

from linkcad.v1.db import BooleanOperation, Drawing, MergeLayerPolarityResult
with Drawing("example") as dwg:
ok = dwg.boolean_layers_by_name(
BooleanOperation.Or,
result_layer="metal",
operand_layer="vias",
)
result = dwg.merge_all_polarity_groups()
if result != MergeLayerPolarityResult.Success:
raise RuntimeError("Polarity merge failed")

Compare enum values with ==, not is. The bindings hand back a fresh Python object for each read, so result is MergeLayerPolarityResult.Success is False even when the merge succeeded.

drawing.units reports how many database units make up one metre, which is what you need to convert a physical size into the integers the database stores:

units_per_meter = drawing.units
if units_per_meter <= 0.0:
raise ValueError("This drawing does not declare its database units")
seven_microns = 7.0 * 1e-6 * units_per_meter

Plugins have an easier route: name a working unit on the DrawingContext or WriterContext and LinkCAD does the conversion for you. See linkcad.plugin.

Cell

A named container of shapes and cell references. Cells come from the drawing that owns them:

cell = drawing.add_cell("TOP") # creates, or returns the existing "TOP"
cell = drawing.cell("TOP") # looks up; None if not found

Creating geometry

Every shape type follows the same shape: add_<thing>(layer, …), returning the new object.

Coordinates are Point objects or plain (x, y) tuples, interchangeably — both in a vertex sequence and as a single coordinate such as the center of an arc, circle or donut, or the position of a text. A pair is exactly two coordinates; anything else is refused rather than truncated.

MethodDescription
cell.add_polygon(layer, vertices)Add a closed polygon
cell.add_polyline(layer, vertices, width=0, closed=False, end_cap=EndCap.Round)Add a polyline
cell.add_arc(layer, center, radius, width=0, start_angle=None, end_angle=None)Add an arc, drawn counter-clockwise when start_angle <= end_angle. Omitting the angles gives a full circle outline
cell.add_circle(layer, center, diameter)Add a filled circle
cell.add_donut(layer, center, mean_diameter, width)Add a ring; mean_diameter is (outer + inner) / 2
cell.add_text(layer, content, position, height=1.0, font="")Add a text shape
cell.add_nurbs(layer, control_points, degree, knots, width=0, weights=None, periodic=False)Add a NURBS curve; degree must be 1, 2, 3, or 5
cell.add_ref(cell, transformation=None, columns=1, rows=1, column_spacing=0, row_spacing=0, layer=None)Place another cell here. Set columns/rows above 1 for an array
from linkcad.v1.db import Drawing, EndCap
from linkcad.v1.geom import Angle, Point, Transformation
with Drawing("shapes") as dwg:
top = dwg.add_cell("TOP")
dwg.main_cell = top
metal = dwg.add_layer("METAL1")
top.add_polygon(metal, [(0, 0), (1000, 0), (1000, 1000), (0, 1000)])
top.add_polyline(metal, [(0, 0), (2000, 2000)], width=100, end_cap=EndCap.SquareFlat)
top.add_arc(metal, Point(5000, 0), radius=1000, width=50,
start_angle=Angle.from_degrees(0), end_angle=Angle.from_degrees(90))
top.add_circle(metal, Point(0, 5000), diameter=800)
top.add_donut(metal, Point(3000, 5000), mean_diameter=800, width=100)
top.add_text(metal, "TOP", Point(0, 8000), height=500)
stamp = dwg.add_cell("STAMP")
stamp.add_circle(metal, Point(0, 0), diameter=200)
top.add_ref(stamp, Transformation().translate(10_000, 0), columns=4, rows=2,
column_spacing=1000, row_spacing=1000)

Inspecting a cell

Property / MethodDescription
cell.nameCell name
cell.shapesEvery shape the cell owns, each as its actual type (Polygon, Polyline, …)
cell.filtered_shapes(selected_only=False)Shapes, narrowed. shapes is this with the defaults
cell.cell_objectsEverything the cell holds, including Ref objects, each as its actual type
cell.filtered_cell_objects(selected_only=False)Cell objects, narrowed
cell.boundsBounding box of everything in this cell and its sub-cells
cell.filtered_bounds(layer=None)Bounding box narrowed to one layer
cell.nesting_levelHighest nesting level (0 for the top cell)
cell.child_levelsNumber of child levels below this cell
cell.enabledTrue if the cell is enabled in any context
cell.enabled_in_context(context_cell)Whether the cell is enabled within another cell
cell.enable(enabled=True, context_cell=None, context=CellContext.Descend)Enable or disable the cell
cell.uses_layer(layer, context=CellContext.Descend, enabled_only=False)Whether the cell holds anything on a layer
cell.first_shape_layerFirst layer used by a contained shape
cell.selected_countNumber of selected cell objects
cell.first_selectedFirst selected cell object, or None
cell.modif_time / cell.access_timeTimestamps, as Unix seconds
cell.clone(name)Copy the cell under a new name

Because cell_objects hands back actual types, isinstance works as you would expect:

from linkcad.v1 import db
for obj in cell.cell_objects:
if isinstance(obj, db.Ref):
print(f"reference to {obj.ref_cell.name}")
elif isinstance(obj, db.Polygon):
print(f"polygon with {obj.vertex_count} vertices")

Layer

A named layer with display properties. Layers come from the drawing:

metal = drawing.add_layer("METAL1") # creates, or returns the existing layer
metal = drawing.layer("METAL1") # looks up; None if not found
Property / MethodDescription
layer.nameLayer name
layer.colorLayer colour as a packed RGBA int
layer.enabledWhether the layer is enabled; assignable
layer.hiddenWhether the layer is hidden; assignable
layer.usedTrue if any enabled cell puts something on the layer
layer.move_before(other_layer=None)Reorder within the layer list
layer.clone(drawing, name)Copy the layer, optionally into another drawing. Also takes both arguments by keyword, with drawing=None meaning this drawing
layer.destroy()Destroy the layer

There are no static factories: drawing.add_layer() creates a layer and drawing.layer() looks one up.

Object

Base class for every database object.

Property / MethodDescription
obj.idObject ID
obj.validFalse once the object has been destroyed
obj.dynamic_typeThe object’s actual type, as an ObjectType
obj.drawingThe Drawing this object belongs to
obj.destroy()Remove and destroy the object. It becomes invalid afterwards

DrawingObject (base of Layer and Cell) adds nothing beyond Object.

CellObject

Base class for everything that lives inside a cell — all shapes, plus Ref.

Property / MethodDescription
obj.owning_cellThe Cell containing this object
obj.boundsBounding box (Bounds)
obj.layerThe object’s Layer; assignable
obj.layer_nameLayer name
obj.selectedSelection state; assignable
obj.toggle_selection()Flip the selection state

layer is read-write on every cell object, Ref included, and accepts a Layer, a layer name, or None:

obj.layer = drawing.layer("METAL1") # a Layer
obj.layer = "METAL2" # by name; created if it does not exist
obj.layer = None # the default layer

toggle_selection() survives the getter/setter rule because it is not selected = not selected: that would be a read and a write under two separate locks, and the database offers the flip as one operation.

Shape

Base class for geometric shapes: polygons, polylines, arcs, ellipses, donuts, NURBS, and text. Extends CellObject.

Property / MethodDescription
shape.closedTrue if the shape is closed
shape.widthTrace width in database units (0 for filled shapes)
shape.areaEnclosed area, in square database units
shape.equivalent_to(other, ignore_sense=True)Compare two shapes for equivalence
shape.layer / shape.boundsInherited from CellObject; layer is read-write there
shape.destroy()Remove and destroy this shape

vertices is not part of Shape: only Polygon, Polyline and Nurbs have one. Curves — arcs, circles, donuts — are described by centre and radius, and are tessellated on demand by the host.

total = sum(shape.area for shape in cell.shapes if shape.closed)
print(f"filled area: {total}")

Polygon

A closed filled shape. Extends Shape. Created with cell.add_polygon(layer, vertices).

Property / MethodDescription
polygon.verticesVertices as a list of Point. Assign Point objects or (x, y) tuples
polygon.vertex_countNumber of vertices
polygon.head / polygon.tailFirst and last vertex
polygon.is_boxTrue if the polygon is an axis-aligned rectangle
polygon.is_self_intersectingTrue if the outline crosses itself
polygon.has_bulgesTrue if any edge carries a non-zero bulge
polygon.add_vertex(point)Append one vertex
polygon.add_vertices(points)Append several vertices
poly = cell.add_polygon(layer, [(0, 0), (1000, 0), (1000, 1000)])
poly.add_vertex((0, 1000))
poly.vertices = [(p.x * 2, p.y * 2) for p in poly.vertices]

Polyline

An open or closed path with an optional width. Extends Shape. Created with cell.add_polyline(...).

Property / MethodDescription
polyline.verticesVertices as a list of Point; assignable
polyline.vertex_countNumber of vertices
polyline.head / polyline.tailFirst and last vertex
polyline.widthPath width in database units
polyline.closedWhether the path is closed
polyline.end_cap_styleEndCap value
polyline.add_vertex(point) / polyline.add_vertices(points)Append vertices
polyline.reverse()Reverse the vertex order

Arc

A circular arc. Extends Shape. Created with cell.add_arc(...).

from linkcad.v1.geom import Angle, Point
arc = cell.add_arc(
layer,
center=Point(0, 0),
radius=10_000,
width=500,
start_angle=Angle.from_degrees(0),
end_angle=Angle.from_degrees(90),
)
arc.center = Point(1000, 1000)
arc.radius = 12_000
PropertyDescription
arc.centerCentre point; assignable
arc.radiusRadius in database units; assignable
arc.widthStroke width in database units; assignable
arc.start_angleStart angle as an Angle, measured counter-clockwise from the x-axis
arc.end_angleEnd angle as an Angle

Ellipse

A circle primitive. Extends Shape. Created with cell.add_circle(layer, center, diameter).

PropertyDescription
ellipse.centerCentre point; assignable
ellipse.diameterDiameter in database units; assignable
ellipse.radiusRadius, half the diameter (read-only)

Donut

A ring/annulus primitive. Extends Shape. Created with cell.add_donut(layer, center, mean_diameter, width).

PropertyDescription
donut.centerCentre point; assignable
donut.mean_diameterMean diameter, (outer + inner) / 2; assignable
donut.widthRing width; assignable
donut.outer_diameter / donut.inner_diameterDerived diameters (read-only)
donut.mean_radius / donut.outer_radius / donut.inner_radiusDerived radii (read-only)

Nurbs

A non-uniform B-spline curve. Extends Shape. Created with cell.add_nurbs(...).

from linkcad.v1.geom import Point
nurbs = cell.add_nurbs(
layer,
control_points=[Point(0, 0), Point(10, 20), Point(20, 20), Point(30, 0)],
degree=3,
knots=[0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0],
width=5,
)
Property / MethodDescription
nurbs.widthStroke width; assignable
nurbs.degreeCurve degree
nurbs.knots / nurbs.knot_countKnot vector and count
nurbs.vertices / nurbs.control_pointsControl points as Point objects (the two names are equivalent)
nurbs.control_point_countNumber of control points
nurbs.weightsWeight vector
nurbs.rationalTrue when weights are present
nurbs.periodicTrue when the curve is periodic/closed; assignable

Text

Formatted text geometry. Extends Shape. Created with cell.add_text(...).

from linkcad.v1.geom import Angle, Point
label = cell.add_text(
layer,
content="Hello LinkCAD",
position=Point(100, 200),
height=12.5,
font="simplex.shx",
)
label.content = "Goodbye"
label.rotate(Angle.from_degrees(90))
Property / MethodDescription
text.contentFormatted text content; assignable. text.text is an alias
text.fontFont name; assignable
text.positionAttachment point; assignable
text.heightText height; assignable
text.line_spacingLine spacing; assignable
text.box_widthWrapping width; assignable
text.rotationRotation Angle. Assigning rotates relative to any enclosing reference
text.rotate(angle, absolute=False)Rotate; with absolute=True, rotations applied by enclosing references are ignored
text.width_factorHorizontal scaling; assignable
text.stroke_widthStroke width; assignable
Text.escape(content)Escape plain text for formatted-text storage

Ref

A placed instance of another cell, with an associated transformation. Extends CellObject. Created with cell.add_ref(...).

from linkcad.v1.geom import Transformation
xform = Transformation()
xform.rotate(45.0)
xform.translate(1000, 2000)
ref = parent_cell.add_ref(child_cell, xform)
ref.columns = 4
ref.column_spacing = 5000
Property / MethodDescription
ref.ref_cellThe referenced Cell; assignable
ref.transformationPlacement Transformation (applies to the whole array); assignable
ref.columns / ref.rowsArray size; 1 means a single instance
ref.column_spacing / ref.row_spacingArray pitch in database units
ref.apply_transformation(transformation)Apply an additional transformation
ref.clone(cell=None, transformation=None)Copy the reference, optionally into another cell
ref.destroy()Remove and destroy this reference

cell.add_ref() raises ValueError rather than returning something unusable: for a circular hierarchy — placing a cell inside one of its own descendants — and for a missing target cell, which used to produce a reference pointing at nothing. Catch it if the cells come from user input:

try:
ref = parent_cell.add_ref(child_cell, xform)
except ValueError as exc:
print(f"cannot place '{child_cell.name}': {exc}")

Color

An RGB colour value.

Property / MethodDescription
Color() / Color(red, green, blue)Construct a colour
color.red / color.green / color.blueChannels (0-255); assignable

layer.color is a packed RGBA integer rather than a Color; use Color where an API asks for one.

Property

A named property attached to a database object.

Property / MethodDescription
prop.owner_typeType of object the property is attached to, as an ObjectType
prop.destroy()Destroy the property
Property.properties(drawing, holding_type)List property names for an object type
Property.lookup_type(drawing, name, holding_type)Look up a property’s type by name
from linkcad.v1.db import ObjectType, Property
for name in Property.properties(drawing, ObjectType.Cell):
print(name, Property.lookup_type(drawing, name, ObjectType.Cell))

Enums

ObjectType

The concrete type of a database object, returned by obj.dynamic_type and prop.owner_type and taken by the Property class methods.

GroupValues
ContainersDrawing, Layer, Cell
Cell objectsRef, Arc, Polygon, Polyline, Donut, Text, Ellipse, Nurbs
Base classesObject, DrawingObject, CellObject, Shape, Property
PropertiesBooleanProperty, IntegerProperty, RealProperty, StringProperty, each also in a Drawing… / Cell… / CellObject… / Layer… form
SentinelInvalid

CellContext

Controls whether an operation descends into sub-cells.

ValueDescription
CellContext.DescendInclude sub-cells
CellContext.DontDescendThis cell only

Unit

Named measurement units, used by unit-aware APIs and option dialogs.

ValueDescription
Unit.Picometer, Unit.Nanometer, Unit.Micron, Unit.Millimeter, Unit.Centimeter, Unit.MeterMetric units
Unit.Mil, Unit.Inch, Unit.FeetImperial units
Unit.PointTypographic point (1/72 inch)
Unit.DatabaseDatabase units (picometres)
Unit.DotsPerInch, Unit.FacetsResolution and count “units” used by option dialogs

There is also a “no unit” member, but because None is a Python keyword it can only be reached as getattr(Unit, "None").

drawing.units does not return a Unit. It is a float giving database units per metre — see Drawing.

EndCap

Polyline end-cap style. Also re-exported from linkcad.v1.plugin.

ValueDescription
EndCap.RoundRound cap
EndCap.SquareExtendedSquare cap extended past the endpoint
EndCap.SquareFlatSquare cap ending at the endpoint

FillRule

Polygon fill rule. Also re-exported from linkcad.v1.plugin for writer APIs.

ValueDescription
FillRule.NonZeroNon-zero winding rule
FillRule.EvenOddEven-odd fill rule

BooleanOperation

Layer-level boolean operation for drawing.boolean_layers_by_name().

ValueDescription
BooleanOperation.OrUnion operand layer into result layer
BooleanOperation.AMinusBSubtract operand layer from result layer

MergeLayerPolarityResult

Result from the deferred-polarity merge helpers.

ValueDescription
MergeLayerPolarityResult.SuccessMerge completed successfully
MergeLayerPolarityResult.FailureMerge failed

TextStyle and TextStyleMask

TextStyle contains bit flags for text alignment, orientation, and line spacing. TextStyleMask selects which groups of style bits are changed by DrawingBuilder.set_text_style().

EnumCommon values
TextStyleDefault, AlignHLeft, AlignHCenter, AlignHRight, AlignVBaseline, AlignVBottom, AlignVMiddle, AlignVMiddleAscent, AlignVTop, OrientH, OrientV, LineSpacingExact, LineSpacingCompact
TextStyleMaskNone_, AlignH, AlignV, Orient, LineSpacing

Locking

The locking requirement depends on the execution context.

Plugin context (@tool, @format_reader, @format_writer)

The framework holds the appropriate lock before calling run(), read(), or write(). No explicit lock is needed for read operations. Write operations that should be undoable require a Transaction:

from linkcad.v1.db import Transaction
def run(self, drawing):
for cell in drawing.cells:
print(cell.name)
with Transaction(drawing, "My Operation"):
result = drawing.add_cell("RESULT")
layer = drawing.add_layer("OUTPUT")
result.add_polygon(layer, [(0, 0), (100, 0), (100, 100), (0, 100)])
return {"summary": "Created RESULT"}

Standalone scripts (linkcad --python-script)

Scripts run outside the plugin framework must acquire locks explicitly:

from linkcad.v1.db import Drawing, ReadLock, WriteLock
with Drawing("scratch") as dwg:
with ReadLock():
for cell in dwg.cells:
print(cell.name)
with WriteLock():
for layer in list(dwg.layers):
if layer.name.startswith("TEMP_"):
dwg.destroy_layer_by_name(layer.name)

ReadLock

Context manager for read-only access in standalone scripts.

from linkcad.v1.db import ReadLock
with ReadLock():
main = dwg.main_cell
for shape in main.shapes:
print(shape.bounds)

WriteLock

Context manager for write access in standalone scripts. Does not create an undo entry.

from linkcad.v1.db import WriteLock
with WriteLock():
for cell in dwg.cells:
for obj in list(cell.cell_objects):
if obj.layer_name == "SCRATCH":
obj.destroy()

Transaction

Context manager for write access that creates a single undoable entry in the undo history. Use it in tool plugins. It takes either a description string, shown in the Edit menu, or a numeric tag.

from linkcad.v1.db import Transaction
with Transaction(drawing, "Create Panel"):
panel = drawing.add_cell("PANEL")
panel.add_ref(drawing.cell("UNIT"))

The transaction commits when the block exits normally and rolls back if an exception escapes it. transaction.commit() commits early.