lib.printing

  1import drawBot
  2from typing import overload
  3
  4from lib import layout, easing
  5from datatypes import DLineStyle
  6
  7
  8def drawEdges(
  9    frame: tuple,
 10    edges: list[layout.BoxEdge],
 11    lineStyle: DLineStyle = DLineStyle(),
 12    gapXY: float | tuple[float, float] = (0, 0),
 13):
 14    """Draw selected frame edges with optional corner gaps.
 15
 16    Args:
 17        frame: Rectangle as (x, y, w, h).
 18        edges: List of edges to draw ("top", "right", "bottom", "left").
 19        lineStyle: Line style to apply before drawing.
 20        gapXY: (gapX, gapY) in mm. The value is split equally per side, so
 21            gapX=2 shortens horizontal edges by 1 mm on both left/right ends,
 22            and gapY=2 shortens vertical edges by 1 mm on both bottom/top ends.
 23    """
 24    frameX, frameY, frameW, frameH = frame
 25    gapX, gapY = layout.unpackTwo(gapXY)
 26
 27    gapHalfX = layout.mm(gapX) / 2
 28    gapHalfY = layout.mm(gapY) / 2
 29
 30    topY = frameY + frameH
 31    rightX = frameX + frameW
 32
 33    edgePositions = {
 34        "top": ((frameX + gapHalfX, topY), (rightX - gapHalfX, topY)),
 35        "bottom": ((frameX + gapHalfX, frameY), (rightX - gapHalfX, frameY)),
 36        "left": ((frameX, frameY + gapHalfY), (frameX, topY - gapHalfY)),
 37        "right": ((rightX, frameY + gapHalfY), (rightX, topY - gapHalfY)),
 38    }
 39
 40    with drawBot.savedState():
 41        lineStyle.apply()
 42        for edge in edges:
 43            if edge in edgePositions:
 44                drawBot.line(*edgePositions[edge])
 45
 46
 47def drawPerforationCircles(
 48    container: tuple,
 49    radius: float = 2,
 50    spacing: float = 80,
 51    count: int = 2,
 52    align: tuple[layout.AlignX, layout.AlignY] = ("left", "center"),
 53) -> tuple:
 54    """
 55    Draw a series of perforation circles within a container.
 56
 57    Args:
 58        container: The container coordinates (x, y, w, h).
 59        radius: The diameter of each circle in mm. Defaults to 2.
 60        spacing: The spacing between circles (center to center) in mm. Defaults to 80.
 61        count: The number of circles to draw. Defaults to 2.
 62        align: The alignment of the series within the container. Defaults to ("left", "center").
 63
 64    Returns:
 65        The coordinates of the area occupied by the perforation circles.
 66    """
 67    radius = layout.mm(radius)
 68    spacing = layout.mm(spacing)
 69    spacing -= radius  # adjust spacing to be center to center
 70
 71    # Infer direction: X-anchored (left/right) → vertical stack; X-centred → horizontal spread.
 72    isVertical = align[0] != "center"
 73
 74    span = radius * count + spacing * (count - 1)
 75    perfDimensions = (radius, span) if isVertical else (span, radius)
 76    perfCoords = layout.align(container, perfDimensions, align)
 77    x, y, w, h = perfCoords
 78
 79    startPos = (x, y)
 80    endPos = (x, y + h - radius) if isVertical else (x + w - radius, y)
 81    positions = easing.interpolate(start=startPos, stop=endPos, steps=count)
 82
 83    for [posX, posY] in positions:
 84        drawBot.oval(posX, posY, radius, radius)
 85
 86    return perfCoords
 87
 88
 89@overload
 90def drawGraphPaper(
 91    container: layout.Coordinates,
 92    *,
 93    rows: int,
 94    cols: int,
 95    lineStyle: DLineStyle = DLineStyle(),
 96    drawFrame: bool = True,
 97) -> None: ...
 98
 99
100@overload
101def drawGraphPaper(
102    container: layout.Coordinates,
103    *,
104    cellSize: float,
105    lineStyle: DLineStyle = DLineStyle(),
106    drawFrame: bool = True,
107) -> None: ...
108
109
110def drawGraphPaper(
111    container: layout.Coordinates,
112    *,
113    rows: int | None = None,
114    cols: int | None = None,
115    cellSize: float | None = None,
116    lineStyle: DLineStyle = DLineStyle(),
117    drawFrame: bool = True,
118):
119    """Draw a graph paper grid within a container. Accepts either (rows and cols) or cellSize to determine the grid.
120
121    Args:
122        container: Rectangle as (x, y, w, h).
123        rows: Number of horizontal rows.
124        cols: Number of vertical columns.
125        cellSize: Target size for each cell in container units.
126        lineStyle: Line style to apply before drawing.
127        drawFrame: Whether to draw the outer frame of the container. Defaults to True.
128    """
129    hasGridShape = rows is not None or cols is not None
130    hasSizeShape = cellSize is not None
131
132    if hasGridShape and hasSizeShape:
133        raise TypeError("Use either (rows and cols) or (cellSize), not both")
134
135    if hasGridShape:
136        if rows is None or cols is None:
137            raise TypeError("When using grid shape, both rows and cols are required")
138        if rows <= 0 or cols <= 0:
139            raise ValueError("rows and cols must be greater than 0")
140    elif hasSizeShape:
141        if cellSize <= 0:
142            raise ValueError("cellSize must be greater than 0")
143    else:
144        raise TypeError("Use either (rows and cols) or (cellSize)")
145
146    x, y, w, h = container
147    if cellSize is not None:
148        # Derive count from target cell size.
149        rows = max(1, round(h / cellSize))
150        cols = max(1, round(w / cellSize))
151
152    rowHeight = h / rows
153    colWidth = w / cols
154
155    with drawBot.savedState():
156        lineStyle.apply()
157        if drawFrame:
158            drawBot.rect(x, y, w, h)
159
160        for i in range(1, rows):
161            drawBot.line((x, y + i * rowHeight), (x + w, y + i * rowHeight))
162        for j in range(1, cols):
163            drawBot.line((x + j * colWidth, y), (x + j * colWidth, y + h))
def drawEdges( frame: tuple, edges: list[typing.Literal['top', 'right', 'bottom', 'left']], lineStyle: datatypes.data_linestyle.DLineStyle = DLineStyle(strokeWidth=1, strokeColor=(0,), lineDash=None, lineCap='butt', clearFill=True), gapXY: float | tuple[float, float] = (0, 0)):
 9def drawEdges(
10    frame: tuple,
11    edges: list[layout.BoxEdge],
12    lineStyle: DLineStyle = DLineStyle(),
13    gapXY: float | tuple[float, float] = (0, 0),
14):
15    """Draw selected frame edges with optional corner gaps.
16
17    Args:
18        frame: Rectangle as (x, y, w, h).
19        edges: List of edges to draw ("top", "right", "bottom", "left").
20        lineStyle: Line style to apply before drawing.
21        gapXY: (gapX, gapY) in mm. The value is split equally per side, so
22            gapX=2 shortens horizontal edges by 1 mm on both left/right ends,
23            and gapY=2 shortens vertical edges by 1 mm on both bottom/top ends.
24    """
25    frameX, frameY, frameW, frameH = frame
26    gapX, gapY = layout.unpackTwo(gapXY)
27
28    gapHalfX = layout.mm(gapX) / 2
29    gapHalfY = layout.mm(gapY) / 2
30
31    topY = frameY + frameH
32    rightX = frameX + frameW
33
34    edgePositions = {
35        "top": ((frameX + gapHalfX, topY), (rightX - gapHalfX, topY)),
36        "bottom": ((frameX + gapHalfX, frameY), (rightX - gapHalfX, frameY)),
37        "left": ((frameX, frameY + gapHalfY), (frameX, topY - gapHalfY)),
38        "right": ((rightX, frameY + gapHalfY), (rightX, topY - gapHalfY)),
39    }
40
41    with drawBot.savedState():
42        lineStyle.apply()
43        for edge in edges:
44            if edge in edgePositions:
45                drawBot.line(*edgePositions[edge])

Draw selected frame edges with optional corner gaps.

Arguments:
  • frame: Rectangle as (x, y, w, h).
  • edges: List of edges to draw ("top", "right", "bottom", "left").
  • lineStyle: Line style to apply before drawing.
  • gapXY: (gapX, gapY) in mm. The value is split equally per side, so gapX=2 shortens horizontal edges by 1 mm on both left/right ends, and gapY=2 shortens vertical edges by 1 mm on both bottom/top ends.
def drawPerforationCircles( container: tuple, radius: float = 2, spacing: float = 80, count: int = 2, align: tuple[typing.Literal['left', 'center', 'right', 'stretch', None], typing.Literal['top', 'center', 'bottom', 'stretch', None]] = ('left', 'center')) -> tuple:
48def drawPerforationCircles(
49    container: tuple,
50    radius: float = 2,
51    spacing: float = 80,
52    count: int = 2,
53    align: tuple[layout.AlignX, layout.AlignY] = ("left", "center"),
54) -> tuple:
55    """
56    Draw a series of perforation circles within a container.
57
58    Args:
59        container: The container coordinates (x, y, w, h).
60        radius: The diameter of each circle in mm. Defaults to 2.
61        spacing: The spacing between circles (center to center) in mm. Defaults to 80.
62        count: The number of circles to draw. Defaults to 2.
63        align: The alignment of the series within the container. Defaults to ("left", "center").
64
65    Returns:
66        The coordinates of the area occupied by the perforation circles.
67    """
68    radius = layout.mm(radius)
69    spacing = layout.mm(spacing)
70    spacing -= radius  # adjust spacing to be center to center
71
72    # Infer direction: X-anchored (left/right) → vertical stack; X-centred → horizontal spread.
73    isVertical = align[0] != "center"
74
75    span = radius * count + spacing * (count - 1)
76    perfDimensions = (radius, span) if isVertical else (span, radius)
77    perfCoords = layout.align(container, perfDimensions, align)
78    x, y, w, h = perfCoords
79
80    startPos = (x, y)
81    endPos = (x, y + h - radius) if isVertical else (x + w - radius, y)
82    positions = easing.interpolate(start=startPos, stop=endPos, steps=count)
83
84    for [posX, posY] in positions:
85        drawBot.oval(posX, posY, radius, radius)
86
87    return perfCoords

Draw a series of perforation circles within a container.

Arguments:
  • container: The container coordinates (x, y, w, h).
  • radius: The diameter of each circle in mm. Defaults to 2.
  • spacing: The spacing between circles (center to center) in mm. Defaults to 80.
  • count: The number of circles to draw. Defaults to 2.
  • align: The alignment of the series within the container. Defaults to ("left", "center").
Returns:

The coordinates of the area occupied by the perforation circles.

def drawGraphPaper( container: tuple[float, float, float, float], *, rows: int | None = None, cols: int | None = None, cellSize: float | None = None, lineStyle: datatypes.data_linestyle.DLineStyle = DLineStyle(strokeWidth=1, strokeColor=(0,), lineDash=None, lineCap='butt', clearFill=True), drawFrame: bool = True):
111def drawGraphPaper(
112    container: layout.Coordinates,
113    *,
114    rows: int | None = None,
115    cols: int | None = None,
116    cellSize: float | None = None,
117    lineStyle: DLineStyle = DLineStyle(),
118    drawFrame: bool = True,
119):
120    """Draw a graph paper grid within a container. Accepts either (rows and cols) or cellSize to determine the grid.
121
122    Args:
123        container: Rectangle as (x, y, w, h).
124        rows: Number of horizontal rows.
125        cols: Number of vertical columns.
126        cellSize: Target size for each cell in container units.
127        lineStyle: Line style to apply before drawing.
128        drawFrame: Whether to draw the outer frame of the container. Defaults to True.
129    """
130    hasGridShape = rows is not None or cols is not None
131    hasSizeShape = cellSize is not None
132
133    if hasGridShape and hasSizeShape:
134        raise TypeError("Use either (rows and cols) or (cellSize), not both")
135
136    if hasGridShape:
137        if rows is None or cols is None:
138            raise TypeError("When using grid shape, both rows and cols are required")
139        if rows <= 0 or cols <= 0:
140            raise ValueError("rows and cols must be greater than 0")
141    elif hasSizeShape:
142        if cellSize <= 0:
143            raise ValueError("cellSize must be greater than 0")
144    else:
145        raise TypeError("Use either (rows and cols) or (cellSize)")
146
147    x, y, w, h = container
148    if cellSize is not None:
149        # Derive count from target cell size.
150        rows = max(1, round(h / cellSize))
151        cols = max(1, round(w / cellSize))
152
153    rowHeight = h / rows
154    colWidth = w / cols
155
156    with drawBot.savedState():
157        lineStyle.apply()
158        if drawFrame:
159            drawBot.rect(x, y, w, h)
160
161        for i in range(1, rows):
162            drawBot.line((x, y + i * rowHeight), (x + w, y + i * rowHeight))
163        for j in range(1, cols):
164            drawBot.line((x + j * colWidth, y), (x + j * colWidth, y + h))

Draw a graph paper grid within a container. Accepts either (rows and cols) or cellSize to determine the grid.

Arguments:
  • container: Rectangle as (x, y, w, h).
  • rows: Number of horizontal rows.
  • cols: Number of vertical columns.
  • cellSize: Target size for each cell in container units.
  • lineStyle: Line style to apply before drawing.
  • drawFrame: Whether to draw the outer frame of the container. Defaults to True.