From a7f9a87cf5375d23f34be9c67c77f92ff50162ba Mon Sep 17 00:00:00 2001 From: wes Date: Thu, 25 Apr 2024 11:01:12 -0400 Subject: [PATCH] add tree --- planter.py | 31 +++++++++++--- svg_path.py | 118 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 6 deletions(-) create mode 100644 svg_path.py diff --git a/planter.py b/planter.py index 50c0370..2ade952 100644 --- a/planter.py +++ b/planter.py @@ -1,16 +1,18 @@ +from cadquery import exporters from cq_warehouse.bearing import SingleRowDeepGrooveBallBearing +from cq_warehouse.extensions import Workplane from cq_warehouse.fastener import * from cq_warehouse.thread import * from cqmore.curve import archimedeanSpiral, circle from cqmore.polygon import regularPolygon, star from cqmore.polyhedron import polarZonohedra, Polyhedron, superellipsoid -from cq_warehouse.extensions import Workplane -import functools as fnc -import itertools as it +from svg_path import addSvgPath +from svgpathtools import svg2paths import cadquery as cq import cqmore Workplane = cqmore.extend(Workplane) +Workplane.addSvgPath = addSvgPath planter_height = 210 planter_radius = 129 @@ -29,8 +31,7 @@ result = Workplane().polyhedron(*outer_poly) holder_angle_offset = 20 -result = ( - result.workplane(offset=-36) +planter_polygon = (result.workplane(offset=-36) .makePolygon( regularPolygon( nSides=4, @@ -39,11 +40,29 @@ result = ( thetaEnd=360 + holder_angle_offset, ), forConstruction=True, - ) + )) + +result = ( + planter_polygon .vertices() .sphere(8, combine="cut") ) +paths, attributes = svg2paths("/home/deck/cad_files/tree_of_gondor_small.svg") + +#print(dir(result.addSvgPath(paths[0]))) + +tree = (Workplane("YZ") + .center(0, 75) + .addSvgPath(paths[0]) + .extrude(-4) + .translate((94.8, -100, 0)) + ) + +print(tree) + +result = result.cut(tree, clean=False) + cone = cq.Solid.makeCone(planter_radius, 33, planter_height + 95) result = result.workplane().cut(cone) diff --git a/svg_path.py b/svg_path.py new file mode 100644 index 0000000..d379dba --- /dev/null +++ b/svg_path.py @@ -0,0 +1,118 @@ +import svgpathtools +import numpy as np +from math import sin, cos, sqrt, pi, acos, fmod, degrees + + +# See https://gist.github.com/dov/8d9b0304ba85e3229aabccac3c6468ef/d007910a9b68201a6c95852a220f96711ba3faa1 +def tpl(cplx): + """Convert a complex number to a tuple""" + return (cplx.real, cplx.imag) + + +def angle_between(u, v): + """Find the angle between the vectors u an v""" + ux, uy = u + vx, vy = v + sign = 1 if ux * vy - uy * vx > 0 else -1 + arg = (ux * vx + uy * vy) / (sqrt(ux * ux + uy * uy) * sqrt(vx * vx + vy * vy)) + return sign * acos(arg) + + +# Implementation of https://www.w3.org/TR/SVG/implnote.html#ArcConversionCenterToEndpoint +def arc_endpoint_to_center(start, end, flag_a, flag_s, radius, phi): + """Convert a endpoint elliptical arc description to a center description""" + rx, ry = radius.real, radius.imag + x1, y1 = start.real, start.imag + x2, y2 = end.real, end.imag + + cosphi = cos(phi) + sinphi = sin(phi) + rx2 = rx * rx + ry2 = ry * ry + + # Step 1. Compute x1p,y1p + x1p, y1p = ( + np.array([[cosphi, sinphi], [-sinphi, cosphi]]) + @ np.array([x1 - x2, y1 - y2]) + * 0.5 + ).flatten() + x1p2 = x1p * x1p + y1p2 = y1p * y1p + + # Step 2: Compute (cx', cy') + cxyp = sqrt( + (rx2 * ry2 - rx2 * y1p2 - ry2 * x1p2) / (rx2 * y1p2 + ry2 * x1p2) + ) * np.array([rx * y1p / ry, -ry * x1p / rx]) + + if flag_a == flag_s: + cxyp = -cxyp + + cxp, cyp = cxyp.flatten() + + # Step 3: compute (cx,cy) from (cx',cy') + cx, cy = ( + cosphi * cxp - sinphi * cyp + 0.5 * (x1 + x2), + sinphi * cxp + cosphi * cyp + 0.5 * (y1 + y2), + ) + + # Step 4: compute theta1 and deltatheta + theta1 = angle_between((1, 0), ((x1p - cxp) / rx, (y1p - cyp) / ry)) + delta_theta = fmod( + angle_between( + ((x1p - cxp) / rx, (y1p - cyp) / ry), ((-x1p - cxp) / rx, (-y1p - cyp) / ry) + ), + 2 * pi, + ) + + # Choose the right edge according to the flags + if not flag_s and delta_theta > 0: + delta_theta -= 2 * pi + elif flag_s and delta_theta < 0: + delta_theta += 2 * pi + + return (cx, cy), theta1, delta_theta + + +def addSvgPath(self, path): + """Add the svg path object to the current workspace""" + res = self + path_start = None + arc_id = 0 + for p in path: + if path_start is None: + path_start = p.start + res = res.moveTo(*tpl(p.start)) + + # Support the four svgpathtools different objects + if isinstance(p, svgpathtools.CubicBezier): + coords = (tpl(p.start), tpl(p.control1), tpl(p.control2), tpl(p.end)) + res = res.bezier(coords) + print("cubic bezier") + elif isinstance(p, svgpathtools.QuadraticBezier): + coords = (tpl(p.start), tpl(p.control), tpl(p.end)) + res = res.bezier(coords) + print("quadratic bezier") + pass + elif isinstance(p, svgpathtools.Arc): + arc_id += 1 + center, theta1, delta_theta = arc_endpoint_to_center( + p.start, p.end, p.large_arc, p.sweep, p.radius, p.rotation + ) + + res = res.ellipseArc( + x_radius=p.radius.real, + y_radius=p.radius.imag, + rotation_angle=degrees(p.rotation), + angle1=degrees(theta1), + angle2=degrees(theta1 + delta_theta), + ) + print("ellipse arc") + elif isinstance(p, svgpathtools.Line): + res = res.lineTo(p.end.real, p.end.imag) + print("line") + + if path_start == p.end: + path_start = None + res = res.close() + + return res