diff --git a/example.svg b/example.svg new file mode 100644 index 0000000..c60d49b --- /dev/null +++ b/example.svg @@ -0,0 +1,45 @@ + + + + + + + + + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b8a635b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +gmsh==4.9.1 +numpy==1.19.5 +svg.path==5.0.0a1 diff --git a/svg2stl.py b/svg2stl.py new file mode 100644 index 0000000..0b25ef4 --- /dev/null +++ b/svg2stl.py @@ -0,0 +1,150 @@ +import argparse +from xml.dom import minidom + +import gmsh +import numpy as np +from svg.path import parse_path +from svg.path import Close, CubicBezier, Line, Move + + +def parse_svg_into_steps(path: str) -> list: + path_str = minidom.parse(path).getElementsByTagName("path")[0].getAttribute("d") + return parse_path(path_str) + + +if __name__ == "__main__": + + parser = argparse.ArgumentParser(description="Convert an SVG into an STL.") + parser.add_argument("svg_path", type=str, help="path towards an SVG file") + parser.add_argument("--thickness", default=1, type=float) + args = parser.parse_args() + + steps = parse_svg_into_steps(args.svg_path) + + # Interpolate points + + shapes = [] + shape = [] + + for step in steps: + + if isinstance(step, Line): + shape.append([step.start.real, step.start.imag]) + + elif isinstance(step, CubicBezier): + + x0 = step.start.real + y0 = step.start.imag + x1 = step.control1.real + y1 = step.control1.imag + x2 = step.control2.real + y2 = step.control2.imag + x3 = step.end.real + y3 = step.end.imag + + t = np.linspace(0, 1, 5, endpoint=False) + x = ( + x0 * (1 - t) ** 3 + + x1 * (1 - t) ** 2 * 3 * t + + x2 * (1 - t) * 3 * t ** 2 + + x3 * t ** 3 + ) + y = ( + y0 * (1 - t) ** 3 + + y1 * (1 - t) ** 2 * 3 * t + + y2 * (1 - t) * 3 * t ** 2 + + y3 * t ** 3 + ) + + shape.extend(list(zip(x, y))) + + elif isinstance(step, Close): + shapes.append(shape) + shape = [] + + shapes = shapes[1:] + + x_min, y_min = np.vstack(shapes).min(axis=0) + x_max, y_max = np.vstack(shapes).max(axis=0) + x_pad = 0.1 * (x_max - x_min) + y_pad = 0.1 * (y_max - y_min) + corners = [ + [x_min - x_pad, y_min - y_pad], + [x_min - x_pad, y_max + y_pad], + [x_max + x_pad, y_max + y_pad], + [x_max + x_pad, y_min - y_pad], + ] + shapes.append(corners) + + # Build walls + + gmsh.initialize() + gmsh.model.add("test") + + z_floor = 0 + z_ceiling = args.thickness + + factory = gmsh.model.geo + floor_lines = [] + ceiling_lines = [] + wall_lines = [] + + for shape in shapes: + + floor_lines.append([]) + floor_points = [factory.addPoint(*shape[0], z_floor)] + for vertex in shape[1:]: + floor_points.append(factory.addPoint(*vertex, z_floor)) + floor_lines[-1].append(factory.addLine(floor_points[-2], floor_points[-1])) + floor_lines[-1].append(factory.addLine(floor_points[-1], floor_points[0])) + + ceiling_lines.append([]) + ceiling_points = [factory.addPoint(*shape[0], z_ceiling)] + for vertex in shape[1:]: + ceiling_points.append(factory.addPoint(*vertex, z_ceiling)) + ceiling_lines[-1].append( + factory.addLine(ceiling_points[-2], ceiling_points[-1]) + ) + ceiling_lines[-1].append(factory.addLine(ceiling_points[-1], ceiling_points[0])) + + wall_lines.append([]) + for floor_point, ceiling_point in zip(floor_points, ceiling_points): + wall_line = factory.addLine(floor_point, ceiling_point) + wall_lines[-1].append(wall_line) + + for i in range(1, len(floor_lines[-1])): + wall = factory.addCurveLoop( + [ + floor_lines[-1][i - 1], + wall_lines[-1][i], + -ceiling_lines[-1][i - 1], + -wall_lines[-1][i - 1], + ] + ) + factory.addPlaneSurface([wall]) + wall = factory.addCurveLoop( + [ + floor_lines[-1][-1], + wall_lines[-1][0], + -ceiling_lines[-1][-1], + -wall_lines[-1][-1], + ] + ) + factory.addPlaneSurface([wall]) + + floor = [] + for lines in floor_lines: + hole = factory.addCurveLoop(lines) + floor.append(hole) + factory.addPlaneSurface(floor) + + ceiling = [] + for lines in ceiling_lines: + hole = factory.addCurveLoop(lines) + ceiling.append(hole) + factory.addPlaneSurface(ceiling) + + gmsh.model.geo.synchronize() + gmsh.model.mesh.generate() + + gmsh.fltk.run()