JustinSDK/cqMore

[feature request] Add Rounded Star

Opened this issue · 5 comments

Rounded stars can be useful shapes for e.g. generating knurled screwdriver handles. Inkscape has the ability to round stars that is based on splines. See below psuedocode adapted to the cqMore style and example output. I can also provide a working example that uses a mixture of the fluent API and direct API in cadquery.

image

def roundedStar(outerRadius: float = 1, innerRadius: float = 0.381966, n: int = 5, starRounding: float = 0.5) -> Polygon:
    """
    Create a rounded star. Inspired by the same in Inkscape.
    
    ## Parameters
    - `outerRadius`: the outer radius of the star.
    - `innerRadius`: The inner radius of the star.
    - `n`: the burst number.
    - `starRounding`: the amount of rounding to apply. =0 breaks this function, but could be handled by cqMore star
    
    ## Examples 
    
        from cqmore import Workplane
        from cqmore.polygon import roundedStar
        
        polygon = (Workplane()
                        .makeSplinePolygon(
                            roundedStar(
                                outerRadius = 10,
                                innerRadius = 5,
                                n = 8,
                                starRounding = 0.6)
                        )
                        .extrude(1)
                    )
    """
    
    starSpoke = innerRadius/outerRadius # spoke ratio, 0 being an infinitely pointy star, and 1 being not a star, but a regular polygon*2
    
    majang = 360/n #angle between maj pts, degrees
    minang = majang/2 #angle between maj and min
    majpts = [(outerRadius*sin(radians(majang*i)),outerRadius*cos(radians(majang*i))) for i in range(0,n)]
    minpts = [(innerRadius*sin(radians(minang+majang*j)),innerRadius*cos(radians(minang+majang*j))) for j in range(0,n)]
    allpts = [item for sublist in zip(majpts,minpts) for item in sublist]
    ts = starRounding/outerRadius*3 #tangent scale
    
    majtans = [(ts*t[1], -ts*t[0]) for t in majpts]
    mintans = [(ts*t[1]/starSpoke, -ts*t[0]/starSpoke) for t in minpts]
    alltans = [item for sublist in zip(majtans,mintans) for item in sublist]
    return allpts, alltans

def makeSplinePolygon(points: Iterable[VectorLike], tangents: Iterable[VectorLike]) -> Edge:
    edgs = Edge.makeSpline(listOfVector = allpts, tangents = alltans, periodic = True, scale = False)
    #need to assemble to wire, e.g. Wire.assembleEdges(edgs)?, this function should return a Wire to be consistent
    return edgs

If you only add a starRounding parameter and use makeSpline, it seems that a smooth tool is enough. For example, bezier_smooth. I might add it in the future.
image

I didn't use Inkscape before. According to the picture you provided, if you want something that Inkscape can do, I think more parameters are required.

Thank you for your interest. Here is a fully working example where I fixed makeSplinePolygon():

import cadquery as cq
from cadquery import Workplane, Edge, Wire
from math import sin,cos,radians

def roundedStar(outerRadius: float = 1, innerRadius: float = 0.381966, n: int = 5, starRounding: float = 0.5):
    """
    Create a rounded star. Inspired by the same in Inkscape.
    
    ## Parameters
    - `outerRadius`: the outer radius of the star.
    - `innerRadius`: The inner radius of the star.
    - `n`: the burst number.
    - `starRounding`: the amount of rounding to apply. =0 breaks this function, but could be handled by cqMore star
    """
    
    starSpoke = innerRadius/outerRadius # spoke ratio, 0 being an infinitely pointy star, and 1 being not a star, but a regular polygon*2
    
    majang = 360/n #angle between maj pts, degrees
    minang = majang/2 #angle between maj and min
    majpts = [(outerRadius*sin(radians(majang*i)),outerRadius*cos(radians(majang*i)),0) for i in range(0,n)]
    minpts = [(innerRadius*sin(radians(minang+majang*j)),innerRadius*cos(radians(minang+majang*j)),0) for j in range(0,n)]
    allpts = [item for sublist in zip(majpts,minpts) for item in sublist]
    ts = starRounding/outerRadius*3 #tangent scale
    
    majtans = [(ts*t[1], -ts*t[0], 0) for t in majpts]
    mintans = [(ts*t[1]/starSpoke, -ts*t[0]/starSpoke, 0) for t in minpts]
    alltans = [item for sublist in zip(majtans,mintans) for item in sublist]
    return allpts, alltans

def makeSplinePolygon(outerRadius: float = 1, innerRadius: float = 0.381966, n: int = 5, starRounding: float = 0.5) -> Wire:
    allpts, alltans = roundedStar(outerRadius,innerRadius,n,starRounding)
    allpts2 = Workplane()._toVectors(allpts,includeCurrent=False)
    alltans2 = Workplane()._toVectors(alltans,includeCurrent=False)
    edgs = Edge.makeSpline(listOfVector = allpts2, tangents = alltans2, periodic = True, scale = False)
    wirs = Wire.assembleEdges([edgs])
    return wirs

f1 = (Workplane()
  .add(makeSplinePolygon())
  )

if "show_object" in locals():
    show_object(f1,options={"alpha":0.10, "color": (65, 94, 55)})

with example output:
image

Here is a screenshot from inkscape, it has similar inputs:
image

I think that cqMore might provide a wire module for collecting Wire functions.

from cqmore import Workplane
from cqmore.polygon import star
from cadquery import Edge, Vector, Wire

def roundedStar(outerRadius: float = 1, innerRadius: float = 0.381966, n: int = 5, rounded: float = 0.5, spoke: float = 0.381966) -> Wire:
    vts = [Vector(*p) for p in star(outerRadius, innerRadius, n)]
    ts = rounded / outerRadius * 3 # tangent scale
    spokes = [1, spoke]
    tangents = [
        Vector(-ts * vts[i].y / spokes[i % 2], ts * vts[i].x / spokes[i % 2], 0)
        for i in range(len(vts))
    ]

    return Wire.assembleEdges(
        [Edge.makeSpline(listOfVector = vts, tangents = tangents, periodic = True, scale = False)]
    )

outerRadius = 1
innerRadius = 0.381966
n = 5
rounded = 0.5
spoke = 0.381966

w = (Workplane()
        .add(roundedStar(outerRadius, innerRadius, n, rounded, spoke))
    )

image

I add it to examples first.
https://github.com/JustinSDK/cqMore/blob/main/examples/rounded_star.py

Looks great, only thing is that I wanted to make sure to mention that spoke = innerRadius/outerRadius, so having inputs for both spoke and innerRadius is redundant.

fixed.