"""
Celtic Trinity Knot
name: celtic_knot.py
by: Gumyr
date: December 19th 2021
desc: This python/cadquery code generates a celtic trinity knot.
license:
Copyright 2021 Gumyr
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from math import sin, cos, pi, atan2, sqrt
import cadquery as cq
MM = 1
class CelticTrinityKnot:
"""
A Celtic Trinity Knot
Parameters:
radius: float = 1.0 - radius of the center of the outside arc
thickness: float = 0.05 - vertical displacement of the arc centers
cross_section_radius: float = 0.2 - size of the cross section
cross_section_side_count: int = 3 - number of sides in the cross section polygon
Properties:
cq_object: cq.Solid - the celtic knot object
"""
center_fraction = 0.5 # Control the shape of the knot - range 0.0-1.0
def arc_path(self, t, radius) -> cq.Vector:
"""Define points in the path of the arcs"""
start_angle = atan2(-self.arc_center[0], radius - self.arc_center[1])
arc_angle = 4 * pi / 3 - 2 * start_angle
arc_radius = sqrt(self.arc_center[0] ** 2 + (radius - self.arc_center[1]) ** 2)
angle = pi / 2 - start_angle - t * arc_angle
return cq.Vector(
self.arc_center[0] + arc_radius * cos(angle),
self.arc_center[1] + arc_radius * sin(angle),
self.thickness * sin(2 * t * 2 * pi),
)
def make_knot_outside(self) -> cq.Solid:
"""Create the outside arcs and fuse them into a single object"""
arc_edges = [
cq.Edge.makeSplineApprox(
[self.arc_path(t / 40, self.radius + r) for t in range(41)]
).translate(cq.Vector(0, 0, h))
for r, h in self.cross_section_pts
]
arc_faces = [
cq.Face.makeRuledSurface(arc_edges[i], arc_edges[(i + 1) % len(arc_edges)])
for i in range(len(arc_edges))
]
knot_outer_faces = [
f.rotate(cq.Vector(0, 0, 0), cq.Vector(0, 0, 1), a)
for f in arc_faces
for a in range(0, 360, 120)
]
return cq.Solid.makeSolid(cq.Shell.makeShell(knot_outer_faces))
def circle_path(self, t, radius) -> cq.Vector:
"""Define points in the path of the central circle"""
angle = t * 2 * pi
return cq.Vector(
radius * cos(angle),
radius * sin(angle),
1.5 * self.thickness * sin(3 * angle + pi / 2),
)
def make_knot_inside(self) -> cq.Solid:
"""Create the inside circle as a single object"""
circle_edges = [
cq.Edge.makeSplineApprox(
[
self.circle_path(t / 40, self.radius * sin(pi / 4) + r)
for t in range(41)
]
).translate(cq.Vector(0, 0, h))
for r, h in self.cross_section_pts
]
circle_faces = [
cq.Face.makeRuledSurface(
circle_edges[i], circle_edges[(i + 1) % len(circle_edges)]
)
for i in range(len(circle_edges))
]
return cq.Solid.makeSolid(cq.Shell.makeShell(circle_faces))
@property
def cq_object(self) -> cq.Solid:
"""The celtic knot object"""
return self.make_knot_outside().fuse(self.make_knot_inside())
def __init__(
self,
radius: float = 1.0,
thickness: float = 0.05,
cross_section_radius: float = 0.2,
cross_section_side_count: int = 3,
):
"""Store inputs and calculate some key values"""
self.radius = radius
self.thickness = thickness
self.cross_section_radius = cross_section_radius
self.cross_section_side_count = cross_section_side_count
center_radius = self.radius * CelticTrinityKnot.center_fraction
self.arc_center = (
center_radius * cos(5 * pi / 6),
center_radius * sin(5 * pi / 6),
0,
)
self.cross_section_pts = [
(v.X, v.Y)
for v in cq.Workplane("XY")
.polygon(self.cross_section_side_count, self.cross_section_radius)
.vertices()
.vals()
]
if __name__ == "main" or "show_object" in locals():
celtic_knot = CelticTrinityKnot(
radius=20 * MM,
thickness=1 * MM,
cross_section_radius=4 * MM,
cross_section_side_count=3,
).cq_object
cq.exporters.export(celtic_knot, "celtic_knot.step")
cq.exporters.export(celtic_knot, "celtic_knot.stl")
show_object(celtic_knot, name="celtic_knot")