Layout Knowledge Base

Shapes · Files

Polygons in a layout: vertices, holes, and why a box is not enough

A layout polygon is a closed list of vertices on one layer: how GDSII and OASIS store it, vertex order and area, holes as cut lines, broken polygons, vertex limits, and how to check them in LayoutEditor.

Last edited: October 26, 2026

LayoutEditor view of four met1 polygons with a red marker on every stored vertex: a pad with a square hole and a horizontal cut line, an octagon, a comb, and an L

A polygon in a chip or MEMS layout is a closed list of vertices on one layer, joined by straight edges, with the enclosed area filled. It is the general shape type of the layout formats: a rectangle, an L, a comb, a pad with a hole, and an approximated circle are all stored as one vertex list. GDSII calls the record a BOUNDARY, OASIS calls it a POLYGON, and LayoutEditor calls it a polygon.

The image at the top shows four metal 1 (met1) polygons as LayoutEditor holds them, with a red square on every stored vertex. The comb at the bottom left is one polygon with 12 vertices, not a bar plus three teeth. The L at the bottom right has 6 vertices. The octagon at the top right has 8 vertices and four 45° edges. The pad at the top left has 12 vertices and a square hole: the thin horizontal line on its left side is the cut line that connects the outer contour to the hole, explained below.

How GDSII and OASIS store a polygon

GDSII writes a polygon as a sequence of records. The record codes below are from the Calma Stream format description (Holwerda transcription); coordinates are 4-byte signed integers in database units.

Record Code (hex) Content
BOUNDARY 0800 start of a polygon element
LAYER 0D02 layer number, 0–255 in the original specification
DATATYPE 0E02 datatype number, 0–255 in the original specification
XY 1003 vertex list; the first coordinate is repeated as the last
ENDEL 1100 end of the element

The specification text sets a boundary to at least 4 and at most 200 coordinates, and requires that the first and last coordinates coincide. A triangle therefore takes 4 XY pairs and the L above takes 7. A rectangle can also be written as a BOX element, which has exactly five coordinates. Each element stands alone: GDSII has no record that makes one boundary a hole in another.

OASIS (SEMI P39) stores a polygon in a POLYGON record, code 21, with an initial vertex and a point list of displacements. The draft specification (SEMI document 3626, 2003) defines six point-list types:

Type Stored per vertex Valid for
0 one signed length, horizontal first, then alternating Manhattan polygons
1 one signed length, vertical first, then alternating Manhattan polygons
2 one length with a direction (east, north, west, south) Manhattan polygons
3 one length with one of eight directions horizontal, vertical, 45° edges
4 an x and y displacement any angle
5 the change of the previous displacement any angle

The closing edge is never stored, because it is implied by the last vertex and the start point. For types 0 and 1 the last two displacements are implied as well. The draft treats a polygon with fewer than three vertices as a fatal error. Choosing the cheapest point-list type for each shape is one of the reasons an OASIS file is smaller than the same data in GDSII; see OASIS and GDSII.

Vertex order and area

GDSII assigns no meaning to the direction of the vertex list. The Artwork Conversion description of the format states that clockwise and counterclockwise lists are equivalent. Software that offsets edges (sizing, bias, DRC width checks) needs to know which side of each edge is inside, so most tools normalize the order. The LayoutEditor documentation states that it sorts polygon points counterclockwise. The test for this page confirms it: an L written clockwise into a GDS file is reported counterclockwise after loading, also with both polygon clean-up options switched off.

The signed area of a simple polygon follows from its vertex list with the shoelace formula: A = ½ Σ (xᵢ·yᵢ₊₁ − xᵢ₊₁·yᵢ), summed over all edges. A positive result means counterclockwise order. For the L with vertices (0, 0), (3, 0), (3, 1), (1, 1), (1, 3), (0, 3) in µm, the sum gives 5.000 µm², which equals a 3 × 1 µm foot plus a 1 × 2 µm stem. The formula is only valid for polygons whose edges do not cross; for a crossing contour, parts of the area cancel.

Polygon, box, or path

A box is the special case of four axis-parallel corners, stored as two corners in LayoutEditor and as a BOX or a five-point BOUNDARY in GDSII. A path is a centerline with a width; its outline is computed from the centerline, the width, and the end type. Operations that cannot keep those forms produce polygons: a rotation by an angle other than a multiple of 90°, a chamfer, a boolean operation, or a merge. LayoutEditor has two save options for receivers that accept only polygons: gdsBoxToPolygon and gdsPathToPolygon write boxes and paths as BOUNDARY records.

Shapes that touch are not one polygon

The comb in the image at the top can also be drawn as four rectangles: a 6 × 1 µm bar and three 1 × 3 µm teeth that overlap the bar. On screen both versions look the same. In the file they differ:

Version Elements Vertices Sum of element areas Covered area
one polygon 1 12 12.000 µm² 12.000 µm²
bar plus three teeth 4 16 15.000 µm² 12.000 µm²

The four-rectangle version counts the three overlaps twice. Tools that sum element areas, such as a quick density estimate, report the larger number. A merge computes the union: in the test, LayoutEditor's merge of the four rectangles returned one polygon with 12 vertices and 12.000 µm². Whether to keep separate shapes or merge them is covered in merging polygons and overlapping is not merging.

Holes without a hole record

Neither GDSII nor OASIS has a separate record for a hole. A pad with a window is stored as one contour that runs around the outside, follows a cut line to the hole, runs around the hole in the opposite direction, and returns along the same cut line. The Artwork description calls this a self-touching, re-entrant, or keyhole polygon and states that it is allowed in GDSII, whereas a contour that crosses itself is not.

The pad in the image at the top is such a polygon. Its outer contour is 4 × 4 µm, its hole is 2 × 2 µm, and the cut line runs horizontally from the left edge to the hole. The stored list has 12 vertices, because the two ends of the cut line appear twice, and the area is 16 − 4 = 12 µm². The LayoutEditor documentation recommends horizontal or vertical cut lines, which keep later polygon operations simple. How holes behave in merges and booleans, and why a hole drawn as a second shape on top is not a hole, is the subject of holes in polygons.

Broken polygons

A vertex list can describe geometry that readers interpret differently. The OASIS draft states that the interpretation of self-intersecting polygons, re-entrant polygons, and polygons with zero-area regions is application-dependent. Mask data preparation treats several of these as errors.

Defect Example Status What LayoutEditor does on load
self-intersection bowtie: four vertices whose edges cross GDSII: "illegal" per Artwork; signed area of a symmetric bowtie is 0 rewritten as two triangles that touch at the crossing point
zero-width spur an edge that runs out and back on itself legal coordinates, no area spur removed
duplicate vertex the same point twice in a row legal, redundant point removed
unclosed boundary last XY differs from the first violates the GDSII rule above written back closed

The repairs in the right column depend on two setup options, defaultPolygonFixSelfintersectingPolygons and defaultPolygonRemoveNotNeededPoints, which are on in the tested installation. With both options off, the bowtie keeps its four vertices and a signed area of 0, and the spur keeps all seven vertices. The duplicate vertex is removed in both cases. A repaired bowtie is still two triangles that meet in one point, which a spacing rule can flag.

SKY130 met1 polygons drawn to scale in three stacked panels: A, a bowtie whose edges cross, fails the self-intersection and zero-area checks; B, a square with a 1 µm edge running out and back, fails the zero-width check; C, a 4 µm pad with a 2 µm hole stored as one contour with a cut line, passes

The diagram shows three polygons at 1 µm = 50 px, with every vertex from the file as a black square. A is a bowtie whose four vertices are the corners of a square; its edges cross at (1.000, 1.000) µm, circled. B is a square of the same size with a 1.000 µm spur, drawn as the red line. C is a keyhole pad; the green line is its cut line, where the contour touches itself without crossing. The same coordinates were written to a GDS file without any editor clean-up and checked with the deck below: LayoutEditor reported a self-intersection and a zero-area marker for A, a zero-width marker for B, and nothing for C.

The check runs on the file as exported, because the .ledrc engine reads the raw coordinates. The deck uses the shape checks from the LayoutEditor .ledrc reference and the SKY130 grid rule x.1b:

units 1e-6
layer m1 = input(68, 20)
check "SI"   "self-intersecting polygon"           : self_intersecting(m1)
check "ZW"   "zero-width out-and-back edge"        : zero_width(m1)
check "SP"   "spike, interior angle < 1 deg"       : spikes(m1, 1)
check "ZA"   "zero-area shape"                     : area(m1) < 0.000001 um2
check "x.1b" "vertex off the 0.005 um grid"        : ongrid(m1) != 0.005 um
layout --drc polygon_hygiene.ledrc polygon_cases.gds --outdir=out

The test file polygon_cases.gds holds nine cases on met1 at a pitch of 10 µm: the L, the same L clockwise, the comb as one polygon, the comb as four rectangles, the keyhole pad, the bowtie at x = 50 µm, the spur at x = 60 µm, a duplicate vertex, and a rectangle whose right edge sits at x = 81.002 µm. The report lists five markers; the six clean cases produce none:

SI  self-intersecting polygon   error   50000,0 51000,0 | 50000,1000 51000,1000 0
ZW  zero-width out-and-back edge    error   60000,0 61000,0 | 60000,1000 61000,1000 0
ZA  zero-area shape error   50000,0 51000,0 | 50000,1000 51000,1000 0
x.1b    vertex off the 0.005 um grid    error   81002,0 81002,0 | 81002,0 81002,0   0
x.1b    vertex off the 0.005 um grid    error   81002,1000 81002,1000 | 81002,1000 81002,1000   0

Precheck tools differ in coverage. According to the source of the open ChipFoundry precheck (cf-precheck v1.3.7), its default run has no general self-intersection check; a symmetric bowtie fails only its zero-area check, because the two lobes cancel. A hygiene deck of your own covers the cases above independently of the receiver.

Angles and the vertex grid

Each edge direction and each vertex coordinate is subject to process rules. In the SkyWater SKY130 periphery rules, x.2 allows only multiples of 90° on diffusion, tap, poly, li1 in the periphery, licon1, mcon, and the via layers, and x.3a allows multiples of 45° on the other layers. Rule x.1b requires every vertex to lie on a 0.005 µm grid; the case at x = 81.002 µm above violates it at both vertices of its right edge. An octagon such as the one in the image at the top meets x.3a and x.1b on met1 only if its diagonal edges run at exactly 45° and all eight vertices are on the grid. Angle classes and their cost are compared in Manhattan versus all-angle; grid snapping is covered in snapping and off-grid.

How many vertices a polygon may have

The GDSII limit of 200 coordinates dates from the original specification. The record header stores the record length in 2 bytes, so one XY record holds at most 65,535 − 4 bytes, or 8,191 vertex pairs of 8 bytes; the Artwork description recommends 8,191 as the reader maximum and a configurable writer limit, because receivers differ. LayoutEditor's GDSII options follow this: the setting gdsMaxPoints defaults to 8191, a polygon with more vertices is split into several polygons on save, and gdsMultiXYRecord writes one polygon across several XY records instead, an extension that is not part of the GDSII standard.

LayoutEditor window showing a 300-vertex met1 polygon of 20 µm radius after saving with a limit of 200 points: two half-discs that share one horizontal seam

The second image shows the result of a lower limit. A 300-vertex polygon approximating a circle of 20 µm radius was saved with gdsMaxPoints = 200 and reopened. The file holds two BOUNDARY records of 152 XY pairs each; the seam across the middle is the shared edge of the two halves. With the default limit, the same polygon is one BOUNDARY with 301 XY pairs. The split keeps the covered area, but a downstream tool that does not merge the halves treats them as two shapes.

Each vertex also costs time downstream. Mask writers with variable-shaped beams expose rectangles and 45° triangles, so the fracture step splits every polygon into such shots; a curve approximated with many short edges produces many small shots. How the number of sides of a circle is chosen is covered in circles and curves; shot counts per shape in e-beam direct write vs. mask writing.

Polygons in LayoutEditor

In a LayoutScript program, cell.addPolygon(points, layer) adds a polygon from a pointArray; the documentation states that the last point is connected back to the first. In the test, a polygon created from 6 vertices was saved as a BOUNDARY with 7 XY pairs, the first equal to the last. The script below reports every polygon on one layer as LayoutEditor holds it after loading: start column, vertex count without the closing point, area, and orientation.

import sys
from LayoutScript import *

gds, layer = sys.argv[1], int(sys.argv[2])
d = project.newLayout().drawing        # 1 dbu = 1 nm
d.openFile(gds)
cell = d.findTopCell()

def signed_area(pts):                  # nm2
    n = len(pts)
    return sum(pts[i][0] * pts[(i + 1) % n][1] - pts[(i + 1) % n][0] * pts[i][1]
               for i in range(n)) / 2

rows = []
e = cell.firstElement
while e is not None:
    s = e.thisElement
    if s is not None and s.layerNum == layer and s.isPolygon():
        pa = s.getPoints()
        pts = [(pa.point(i).x(), pa.point(i).y()) for i in range(pa.size())]
        if pts[0] == pts[-1]:
            pts = pts[:-1]             # the closing point repeats the first vertex
        rows.append((min(x for x, y in pts), len(pts), signed_area(pts)))
    e = e.nextElement
for x, n, a in sorted(rows):
    turn = "counterclockwise" if a > 0 else ("clockwise" if a < 0 else "zero area")
    print(f"x = {x / 1000:4.1f} um  {n:2d} vertices  {abs(a) / 1e6:6.3f} um2  {turn}")

Run with PYTHONPATH=/opt/layout/python python3 polygon_report.py polygon_cases.gds 68 on the test file, it prints one line per polygon. The four lines at x = 30 to 35 µm are the comb drawn as four rectangles; the clockwise L at x = 10 µm is already counterclockwise; the bowtie at x = 50 µm has become 6 vertices with 0.500 µm²; the spur at x = 60 µm is gone:

x =  0.0 um   6 vertices   5.000 um2  counterclockwise
x = 10.0 um   6 vertices   5.000 um2  counterclockwise
x = 20.0 um  12 vertices  12.000 um2  counterclockwise
x = 30.0 um   4 vertices   3.000 um2  counterclockwise
x = 30.0 um   4 vertices   6.000 um2  counterclockwise
x = 32.5 um   4 vertices   3.000 um2  counterclockwise
x = 35.0 um   4 vertices   3.000 um2  counterclockwise
x = 40.0 um  12 vertices  12.000 um2  counterclockwise
x = 50.0 um   6 vertices   0.500 um2  counterclockwise
x = 60.0 um   4 vertices   1.000 um2  counterclockwise
x = 70.0 um   4 vertices   1.000 um2  counterclockwise
x = 80.0 um   4 vertices   1.002 um2  counterclockwise

The difference between this list and the deck output is the point of running both: the report shows what the editor made of the file, the deck shows what the file contains.

Before you export a polygon layer

  • Run a hygiene deck on the exported file, not on the open design: self-intersection, zero width, spikes, zero area, and the process grid.
  • Merge shapes that must count as one region before area or density calculations.
  • Store holes as one contour with a horizontal or vertical cut line.
  • Set the vertex limit to what the receiver reads; 200 for strict GDSII readers, 8191 otherwise.
  • Check edge angles against the layer's rule (x.2 or x.3a in SKY130).

Continue exploring