In [1]:
import numpy as np
from numpy import radians as rad
import k3d
In [2]:
FIL_D = 1.75
NOZ_D = 0.4
RATE = NOZ_D**2 / FIL_D**2
WIDTH = DEPTH = HEIGHT = 300
In [3]:
class PrintTurtle:
    def __init__(self):
        self.moves = []
        self.lines = []
        self.blobs = []

        self.gcode = []
        self.extrude = False

        self.position = np.array([0, 0, 0])
        self.rotation = np.identity(4)
        self.rot_unit = np.array([0, 1, 0, 0])

        self.rate = 1.0

    def note(self, msg):
        self.gcode.append(f'; {msg}')

    def blob(self, amount=5.0, speed=50):
        self.gcode.append(f'G1 E{amount * RATE:.2f} F{speed}')
        self.blobs.append(self.position.copy())

    def goto(self, point, speed):
        move = point - self.position
        dist = np.linalg.norm(move)
        
        loc = 'X{:.2f} Y{:.2f} Z{:.2f}'.format(*move)
        line = [self.position.tolist(), point.tolist(), (np.nan, np.nan, np.nan)]

        if self.extrude:
            self.gcode.append(f'G1 {loc} E{self.rate * RATE * dist:.2f} F{speed}')
            self.lines.extend(line)
        else:
            self.gcode.append(f'G0 {loc} F{speed}')
            self.moves.extend(line)

        self.position = point

    def forward(self, dist, speed=1500):
        point = self.position + dist * (self.rotation @ self.rot_unit)[:3]
        self.goto(point, speed)

    def turn(self, x=0.0, y=0.0, z=0.0):
        rot_x = np.array([[1, 0, 0, 0],
                          [0, np.cos(x), -np.sin(x), 0],
                          [0, np.sin(x), np.cos(x), 0],
                          [0, 0, 0, 1]])

        rot_y = np.array([[np.cos(y), 0, np.sin(y), 0],
                          [0, 1, 0, 0],
                          [-np.sin(y), 0, np.cos(y), 0],
                          [0, 0, 0, 1]])

        rot_z = np.array([[np.cos(z), -np.sin(z), 0, 0],
                          [np.sin(z), np.cos(z), 0, 0],
                          [0, 0, 1, 0],
                          [0, 0, 0, 1]])

        self.rotation = self.rotation @ rot_x @ rot_y @ rot_z

    def pendown(self, rate=None):
        self.extrude = True
        if rate:
            self.rate = rate

    def penup(self):
        self.extrude = False

    def bbox(self):
        points = np.vstack([*self.lines, *self.moves, *self.blobs])
        low = [np.nanmin(points[:,0]), np.nanmin(points[:,1]), np.nanmin(points[:,2])]
        high = [np.nanmax(points[:,0]), np.nanmax(points[:,1]), np.nanmax(points[:,2])]
        return (low, high)
    
    def save(self, path):
        bbox = self.bbox()
        width = bbox[1][0] - bbox[0][0]
        depth = bbox[1][1] - bbox[0][1]
        x0 = WIDTH/2. - bbox[0][0] - width/2.
        y0 = DEPTH/2. - bbox[0][1] - depth/2.
        
        head = ('; start_print BED_TEMP=60.0 EXTRUDER_TEMP=215.0\n'
                'M106 S255 ; turn on part fan\n'
                'M140 S50 ; Start bed heating\n'
                'M109 S215 ; Set and wait for nozzle to reach temperature\n'
                'M190 S50 ; Wait for bed to reach temperature\n'
                'G90 ; absolute xyz positioning\n'
                'G28 ; home\n'
                f'G0 X{x0:.2f} Y{y0:.2f} Z0.3 F1500 ; move to center of print area\n'
                'G91 ; relative xyz positioning\n')
        
        tail = 'end_print'

        with open(path, 'wt+') as file:
            file.write(f'{head}\n{'\n'.join(self.gcode)}\n{tail}')
    
    def show(self):
        plot = k3d.plot()

        if self.lines:
            lines = np.vstack(self.lines)
            indices = list((x, x+1) for x in range(0, len(lines), 2))
            plot += k3d.lines(lines, indices, width=NOZ_D/2, color=0xff0000)

        if self.moves:
            moves = np.vstack(self.moves)
            indices = list((x, x+1) for x in range(0, len(moves), 2))
            plot += k3d.lines(moves, indices, width=NOZ_D/4, color=0x00ff00)

        if self.blobs:
            blobs = np.vstack(self.blobs)
            plot += k3d.points(blobs, point_size=4 * NOZ_D, color=0x0000ff)

        plot.display()

Bumpy Vase¶

In [4]:
rng = np.random.default_rng()

turtle = PrintTurtle()

base = 15
for i in range(150):
    turtle.note(f'LEVEL {i}')

    coeff = 10 + 20 * np.sin(np.pi*i/150 + np.pi/8)
    xp = [coeff*np.sin(2*np.pi*x/360) for x in range(360)]
    yp = [coeff*np.cos(2*np.pi*x/360) for x in range(360)]

    turtle.pendown(3)
    for (x, y) in zip(xp, yp):
        turtle.goto(np.array([x, y, turtle.position[-1]]), 1500)
        if rng.random() > 0.90:
            size = rng.random() * coeff
            turtle.blob(size)

    turtle.penup()
    # offset = 2 * (rng.random(2) - 0.5)
    turtle.goto(turtle.position + [0, 0, 1.5*NOZ_D], 1500)

turtle.show()
turtle.save('vase.gcode')
/home/louis/.local/lib/python3.12/site-packages/traittypes/traittypes.py:97: UserWarning: Given trait value dtype "float64" does not match required type "float32". A coerced copy has been created.
  warnings.warn(
Output()

Tube¶

In [5]:
turtle = PrintTurtle()

for z in range(100):
    for i in range(60):
        if (z + i) % 2 == 0:
            turtle.blob(10) 
        turtle.forward(2)
        turtle.turn(0, 0, rad(6))
    turtle.goto(turtle.position + [0, 0, 2*NOZ_D], 1500)
            
turtle.show()
turtle.save('tube.gcode')
Output()

Cone¶

In [6]:
turtle = PrintTurtle()

turtle.pendown(3)

height = 150
for i in range(height):
    turtle.note(f'LEVEL {i}')

    for w in range(3):
        turtle.forward(30 - ((i + (w/3)) / (height / 30)), 5000)
        turtle.turn(0, 0, rad(120 + 2*i/height))
        turtle.blob(15)

    turtle.turn(rad(90), 0, 0)
    turtle.forward(2*NOZ_D, 3000)
    turtle.turn(-rad(90), 0, 0)

    #turtle.turn(0, 0, rad(0.5))
    turtle.pendown(1.5)

turtle.show()
turtle.save('cone.gcode')
Output()
In [ ]: