Created
August 6, 2025 23:07
-
-
Save ednisley/e662201392ae0e4be0847f4fdb538189 to your computer and use it in GitHub Desktop.
Python source code: SVG generator for laser-cut layered paper
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Generator for rainbow block layered paper | |
# Ed Nisley - KE4ZNU | |
# 2025-08-03 cargo-culted from svg library examples | |
import svg | |
import math | |
from argparse import ArgumentParser | |
from random import randint, choice, seed | |
from itertools import chain | |
from pprint import pprint | |
INCH = 25.4 | |
X = 0 | |
Y = 1 | |
def as_mm(number): | |
return repr(number) + "mm" | |
parser = ArgumentParser() | |
parser.add_argument('--layernum', type=int, default=0) | |
parser.add_argument('--colors', type=int, default=16) | |
parser.add_argument('--seed', type=int, default=1) | |
parser.add_argument('--width', type=int, default=16) | |
parser.add_argument('--height', type=int, default=16) | |
parser.add_argument('--debug', default=False) | |
args = parser.parse_args() | |
PageSize = (round(8.5*INCH,3), round(11.0*INCH,3)) | |
SheetCenter = (PageSize[X]/2,PageSize[X]/2) # symmetric on Y! | |
SheetSize = (200,200) # overall sheet | |
AlignOC = (180,180) # alignment pins in corners | |
AlignOD = 5.0 # … pin diameter | |
MatrixOA = (170,170) # outer limit of cell matrix | |
CellCut = "black" # C00 Black | |
SheetCut = "red" # C02 Red | |
HeavyCut = "rgb(255,128,0)" # C05 Orange black mask paper is harder | |
HeavyCellCut = "rgb(0,0,160)" # C09 Dark Blue ditto | |
Tooling = "rgb(12,150,217)" # T2 Tool | |
DefStroke = "0.2mm" | |
DefFill = "none" | |
ThisLayer = args.layernum # determines which cells get cut | |
Layers = args.colors # black mask = 0, color n = not perforated | |
SashWidth = 1.5 # between adjacent cells | |
CellSize = ((MatrixOA[X] - (args.width - 1)*SashWidth)/args.width, | |
(MatrixOA[Y] - (args.height - 1)*SashWidth)/args.height) | |
CellOC = (CellSize[X] + SashWidth,CellSize[Y] + SashWidth) | |
if args.seed: | |
seed(args.seed) | |
#--- accumulate tooling layout | |
ToolEls = [] | |
# mark center of sheet for drag-n-drop location | |
ToolEls.append( | |
svg.Circle( | |
cx=SheetCenter[X], | |
cy=SheetCenter[Y], | |
r="2mm", | |
stroke=Tooling, | |
stroke_width=DefStroke, | |
fill="none", | |
) | |
) | |
# mark page perimeter for alignment check | |
if False: | |
ToolEls.append( | |
svg.Rect( | |
x=0, | |
y=0, | |
width=as_mm(PageSize[X]), | |
height=as_mm(PageSize[Y]), | |
stroke=Tooling, | |
stroke_width=DefStroke, | |
fill="none", | |
) | |
) | |
# center huge box on matrix center | |
if False: | |
ToolEls.append( | |
svg.Rect( | |
x=as_mm(SheetCenter[X] - 2*SheetSize[X]/2), | |
y=as_mm(SheetCenter[Y] - 2*SheetSize[Y]/2), | |
width=as_mm(2*SheetSize[X]), | |
height=as_mm(2*SheetSize[Y]), | |
stroke=Tooling, | |
stroke_width=DefStroke, | |
fill="none", | |
) | |
) | |
#--- accumulate sheet cuts | |
SheetEls = [] | |
# cut perimeter | |
SheetEls.append( | |
svg.Rect( | |
x=as_mm(SheetCenter[X] - SheetSize[X]/2), | |
y=as_mm(SheetCenter[Y] - SheetSize[Y]/2), | |
width=as_mm(SheetSize[X]), | |
height=as_mm(SheetSize[Y]), | |
stroke=SheetCut if ThisLayer > 0 else HeavyCut, | |
stroke_width=DefStroke, | |
fill="none", | |
), | |
) | |
# cut layer ID holes except on mask layer | |
if ThisLayer > 0: | |
c = ((1,1)) | |
h = f'{ThisLayer:0{Layers.bit_length()}b}' | |
for i in range(Layers.bit_length()): | |
SheetEls.append( | |
svg.Circle( | |
cx=as_mm(SheetCenter[X] + c[X]*AlignOC[X]/2 - (i + 2)*AlignOD), | |
cy=as_mm(SheetCenter[Y] + c[Y]*AlignOC[Y]/2), | |
r=AlignOD/4 if h[-(i + 1)] == '1' else AlignOD/8, | |
stroke=SheetCut, | |
stroke_width=DefStroke, | |
fill="none", | |
) | |
) | |
# cut alignment pin holes except on mask layer | |
if ThisLayer > 0: | |
for c in ((1,1),(-1,1),(-1,-1),(1,-1)): | |
SheetEls.append( | |
svg.Circle( | |
cx=as_mm(SheetCenter[X] + c[X]*AlignOC[X]/2), | |
cy=as_mm(SheetCenter[Y] + c[Y]*AlignOC[Y]/2), | |
r=as_mm(AlignOD/2), | |
stroke=SheetCut, | |
stroke_width=DefStroke, | |
fill="none", | |
) | |
) | |
#--- calculate matrix contents | |
CenterPoint = (choice(range(args.width)),choice(range(args.height))) | |
CellMatrix = [[math.hypot(x - CenterPoint[X],y - CenterPoint[Y]) | |
for y in range(args.height)] | |
for x in range(args.width)] | |
dmax = max(list(chain.from_iterable(CellMatrix))) | |
if args.debug: | |
print(CenterPoint) | |
print(dmax) | |
pprint(CellMatrix) | |
print() | |
#--- accumulate matrix cuts | |
LayerThreshold = (ThisLayer/Layers)*dmax | |
if args.debug: | |
print(LayerThreshold) | |
MatrixEls = [] | |
for i in range(args.width): | |
x =i*CellOC[X] | |
for j in range(args.height): | |
y = j*CellOC[Y] | |
if args.debug: | |
print(i) | |
print(j) | |
print(CellMatrix[i][j]) | |
if ThisLayer == 0: # black mask | |
s = HeavyCellCut | |
elif LayerThreshold < CellMatrix[i][j]: # rest of sheets above color layer | |
s = CellCut | |
else: | |
s = Tooling # at or below color layer | |
MatrixEls.append( | |
svg.Rect( | |
x=as_mm(SheetCenter[X] - MatrixOA[X]/2 + x), | |
y=as_mm(SheetCenter[Y] - MatrixOA[Y]/2 + y), | |
width=as_mm(CellSize[X]), | |
height=as_mm(CellSize[Y]), | |
stroke=s, | |
stroke_width=DefStroke, | |
fill="none", | |
) | |
) | |
#--- assemble and blurt out the SVG file | |
if not args.debug: | |
canvas = svg.SVG( | |
width=as_mm(PageSize[X]), | |
height=as_mm(PageSize[Y]), | |
elements=[ | |
ToolEls, | |
SheetEls, | |
MatrixEls | |
], | |
) | |
print(canvas) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
More details on my blog at https://softsolder.com/2025/08/07/layered-paper-svg-generator/