CNC first try with rotary module

Hi all!

Today I started to create a model I want to carve out of wood, which I need 24 parts from.

The model had originally 1.2 million faces so I reduced it to around 64,000 faces and corrected the reversed faces the auhtor used to create “2 colors”. Then I exported it from Sketchup to STL and imported the result into Luban. Everything OK until here.

It tells me of course about some errors in the model which doesn’t surprise me as I could not fix each and everthing.

Nevertheless it created a nice depthmap image and also a correct 3D preview as you can see here:
https://www.ccedv.de/download/photos/cnc/Column8472.jpg

Looks similar to the demo Lion from the Luban example for me.

Now when I click preview and choose the V-carving tool I get this as result:
https://www.ccedv.de/download/photos/cnc/Column8472_Preview.jpg

And the estimated time is 355h?? The lion takes around 2h…

What am I missing here? Why is there only a big blue column and why does it take that long time? I mean, on my lathe I can create a straight column in 5 minutes… :smiley:

Cheers,

Christian

Un-tick the Toolpath or simulation boxes.

Thanks, that helps to now get the 3D model displayed (sometimes it can be so easy…).

The other problem still remains - 355h, that can’t be a realistic value vor a 20cm. column.

Depending on your material you could dig deeper than a 1mm step down.
The V-Carving bit has a step over of 0.1mm this causes the high carving time.
Do you need this high detail of 0.1mm stepover?
Maybe a other carving bit would also work?

Thanks for your answer!
Oh,I should really have tested to change these values before asking :grimacing:

I just used the default values and thought, before I do anything wrong, let them be as they are… now, after increasing the stepover now I know of course why I got a “blue solid column”… :slight_smile:

To answer your question: No, this is a miniature column which should be a nice feature for a bird feeding house. So I don’t need to have so many details.

The material will be yaw wood, I think, so should be easy to carve.
I changed the values to stepdown of 3mm and Stepover 1mm. With that the time is now 4h27m, which is still a bit slow. When I change it to SD4 and SO1.5 the result in the simulation still looks OK and the time is 2h28m.

But I have not done any CNC carving before, I know when I use my standard mill (which of course has at least 6 or more mm width) I usually use not more than 2mm stepdown for each step. Of course it removes a lot more material in one step.
This one here is extremely small (the original V carving bit which was delivered together with the rotary module) so maybe going deeper in one step would be OK?

The question is, which SD/SO values could be used as maximum for the bit? Simulation looks OK, but with a needle drilling into the material - will it not result in some carved discs instead of a model?
By the way, this is also the original milling module, not the 50W model.

I want to avoid that the first try just breaks the bit… :slight_smile:

The “problem” is that Luban does not really support multiple toolpaths, i.e. you cannot easily do a roughing pass with a larger bit to get away a lot of material quickly and then a second pass with the tiny V-carve bit to work out the details. The lion you refer to earlier is supposed to be milled into the epoxy material, and there the V-carve bit is able to eat away the soft material in one go, which makes the lion relatively fast. In wood, I’d doubt that the V-carve bit will be able to do that.

There is a guide by @Skreelink that coaxes a multi-path approach from Luban, but you need to do a few tricks. One problem is that Luban does not know rest machining, i.e. it does not remember what was already milled away in an earlier step.

Snapmaker themselves confirmed some limitations of Luban, i.e. not taking the milling bit dimensions fully info account (see this thread).

Long story short: The rotary module can do a lot (it will be slow, after all it is only 50W), but the software limits you. Unfortunately good 4-axis-CAM software is prohibitively expensive for hobbyists - DeskProto has a hobbyist’s license that is affordable, but has its own limitations, and with Fusion360 free edition, there at least seems to be a way to do indexed machining (never tried myself). If anyone has a good recommendation for affordable 4-axis CAM please let us know!

If you go deeper with the v-carving bit with a greater stepover you will get carving lines with not carved paths in between…

You should definitely try a bigger carving bit and follow the guide which @hauke posted.

Thanks for the links, really helpful to see what you get out from the dwarf experiment. But as we see in the Lion example (which I know definitely will test before I start my own) the result should be similar detailed and the dwarf should be possible. Maybe with a thicker than 5mm column to hold better.
The tutorial of Skreelink was also very interesting.
I’ve also found this one which looks promising:

I tried today to create the GCode with Luban just to simulate it using CAMotics. This has a nice simulation mode, although not for 4axis movements, but at least it shows the 2D movements of the GCode.
Looks like that:


The green horizontal lines shows that it first trims down the material until it reaches the area where to start the real carving. So the material could be prepared to be much smaller in diameter which is quickly made with my lathe. (I have created an addon for my lathe to be able to use a milling machine to get exact cylindric columns, photos can be seen here: Frächselvorrichtung - Drechsler-Forum and the construction can be downloaded here: 3D Warehouse, maybe useful for some of you.)

The small green vertical lines shows where the head goes down and up again while cutting where the red lines are only movements of the head. I can only guess why there are so many vertical red lines at only some specific places - maybe because of the pattern of the column or the rotation which CAMotics can’t show.
But at least it should be helpful to choose the right stepover?

I’ve also tried to find a way to create these “unfolded” heightmaps from a STL model, seems to be hard to find. There are tutorials for Blender for example - Blender would create great results because it doesn’t try to repair a model, it simply displays it as it should and can create heightmaps - but unfortunately only from one side of the model. Luban does the unfolding correct - but unfortunately it tries to repair the model and creates some ugly holes although there are none in the model (not that the model does not have some problems). Makes no difference if I repair or not.
That’s surprising as the 3D preview in the mini window does everything right, model looks OK - but the height map creates several black spots as you can see in my first picture above. And as far as I saw there is no option to export the heightmap Luban creates to fix the problems and reinsert it.

I think it’s a long way until I get all that running… :slight_smile:

Yep, I agree, it is in the tool to do so. But the linkage mode seems only to really work with the V-bit, and that rules out Wood (unless you are willing to let the machine run for days). With the epoxy material it should work (but see the limitations in my dwarf post). If Luban was better, it would work!

This may work in your case, since it uses rotary mode (not linkage) - and as I understand, this is better, but only works for things that are rotationally “simple” - like your column.

Good luck with your experiments - let us know how it goes!

Btw., in this video Thomas Sanladerer mentions how to use the Snapmaker rotary with Fusion 360 in indexed mode - perhaps a way forward for you?

Thanks, I will have a look at this video!

In the meantime I found another interesting Addon: BlenderCAM, which is an Addon for the well-known 3D program Blender. This was made to generate GCode. Unfortunately it is full of parameters and I’m lost what I need to setup for my Snapmaker and for the object to get a result - but at least BlenderCAM supports 4 and even 5 axis (which of course Snapmaker does not have). So rotary should theoretically work with that.
If I ever find out how, the documentation of the author is not very helpful.
(If anybody wants to try it: https://blendercam.com/
After several fails installing it: You need to use the newest 4.2 Blender, then the installation works.

But as I was already in Blender I had another idea: Luban can also use a heightmap picture, not only a STL model. And Blender is much better in working with 3D objects and can also use UV-Mapping to unfold an object. This is of course complicate with an object like this as there must be a set of faces select as seam. The original STL has 1.1million faces, which I could break down to at least 60,000 but too much to select all that manually to unfold.

So my first idea was now, to calculate from the center of my column (from a vertical axe in the middle) the distance to the farest point of the model in all directions. This is a bit complicate, as “the world” is behind the model and then it’s difficult to find the farest point. So I made the opposite and created a simple cylinder around the column to calculate the distance from the column to the cylinder. Then map that to greyscale values as bitmap and use the Shader/UV-Mapping of Blender to create a flat unfolded Greyscale image. If I’m not wrong this should be a negative of the model. So when the resulting Greyscale bitmap will be simply inverted - should be the model, if I’m not wrong.

I found out together with ChatGPT that Blender has a Python API and such calculations could be done with this. I’m not deep enough in Python programming and Blender, but at least the current script creates an image. Currently black, but seems to be a scaling problem of the values only. Need to further discuss that with the AI. :smiley:
If anyone is Python pro and interested I can of course paste the script here. Completely done by ChatGPT… :slight_smile:

In the end the heightmap should be able to place in Luban so that this can create the GCode.

1 Like

After some programming sessions with Blender and Python I have now a first result, so if anyone is interested, here it is.

This is a Python script which needs Blender (currently 4.2). In Blender there is a top menu “Scripting”, there a new Text file can be created, this script inserted and then the script can generate hulls on 3D objects.

This is the script:

import bpy
import os
from mathutils import Vector
import bmesh
import math

# --------------------------------------------------------------------------------------
class Settings:
    def __init__(self):
        self.OFFSET_MM = 6  # Abstand in mm
        self.TOOL_DIAMETER_MM = 3  # Fräserdurchmesser in mm    
        self.TOLERANCE = 0.0001
        self.NUMBER_OF_RINGS = 50
        self.SHORTEN_CYLINDER_MM = 0.1

# --------------------------------------------------------------------------------------
def show_message_box(message="", title="Message Box", icon='INFO'):
    # draw ist eine Callback-Funktion, die Blender aufruft
    def draw(self, context):
        self.layout.label(text=message)

    print(message)
    bpy.context.window_manager.popup_menu(draw, title=title, icon=icon)

# --------------------------------------------------------------------------------------
def delete_all_except_camera_lights():
    # Löscht alle Objekte außer Kamera und Lichtquellen
    for obj in bpy.context.scene.objects:
        if obj.type not in ['CAMERA', 'LIGHT']:
            bpy.data.objects.remove(obj, do_unlink=True)

# --------------------------------------------------------------------------------------
def get_max_distance_from_origin(obj):
    max_distance = 0
    for vertex in obj.data.vertices:
        co = obj.matrix_world @ vertex.co
        distance = math.sqrt(co.x**2 + co.z**2)
        if distance > max_distance:
            max_distance = distance
    return max_distance

# --------------------------------------------------------------------------------------
def get_object_center(obj):
    min_x = min_y = min_z = float('inf')
    max_x = max_y = max_z = float('-inf')

    for vertex in obj.data.vertices:
        co = obj.matrix_world @ vertex.co
        min_x = min(min_x, co.x)
        min_y = min(min_y, co.y)
        min_z = min(min_z, co.z)
        max_x = max(max_x, co.x)
        max_y = max(max_y, co.y)
        max_z = max(max_z, co.z)

    center_x = (min_x + max_x) / 2
    center_y = (min_y + max_y) / 2
    center_z = (min_z + max_z) / 2
    return (center_x, center_y, center_z)

# --------------------------------------------------------------------------------------
def center_object_to_origin(obj):
    center_x, center_y, center_z = get_object_center(obj)
    obj.location.x -= center_x
    obj.location.y -= center_y
    obj.location.z -= center_z
    bpy.ops.object.transform_apply(location=True)

# --------------------------------------------------------------------------------------
# Äußeren Zylinder erstellen
def create_outer_cylinder(obj, offset, side_length):
    # Berechne die maximale Entfernung eines Punktes vom Ursprung in X-Z-Ebene
    max_distance = get_max_distance_from_origin(obj)

    # Berechne den neuen Radius mit dem zusätzlichen Abstand
    new_radius = max_distance + offset

    # Berechne die Anzahl der Seiten
    circumference = 2 * math.pi * new_radius
    num_sides = round(circumference / (side_length * 0.9)) # 90% der Breite des Fräsers

    # Länge des Objekts in der Y-Richtung übernehmen
    height = obj.dimensions.y - 2 * stng.SHORTEN_CYLINDER_MM

    # Zylinder erstellen und ausrichten
    bpy.ops.mesh.primitive_cylinder_add(vertices=num_sides, radius=new_radius, depth=height, location=(0, 0, 0))

    # Zylinder um 90 Grad um die X-Achse rotieren, damit er entlang der Y-Achse ausgerichtet ist
    cylinder = bpy.context.object
    cylinder.rotation_euler[0] = math.radians(90)

    bpy.ops.object.transform_apply(location=True, rotation=True)

    return cylinder

# --------------------------------------------------------------------------------------
def get_relevant_edges(obj):
    """Holt vom erstellten Zylinder nur die
    Kanten, die den Zylinder darstellen,
    nicht die, die Boden und Deckel beschreiben

    Args:
        obj (Mesh-Objekt von Blender): Ein Standard-Zylinder-Objekt von Blender
    
    Returns:
        list: Eine Liste der relevanten Kanten
    """
    relevant_edges = []
    for edge in obj.data.edges:
        start_point = obj.data.vertices[edge.vertices[0]].co
        end_point = obj.data.vertices[edge.vertices[1]].co

        # Nur Kanten mit Y-Ausdehnung ausgeben
        if abs(start_point.y - end_point.y) > stng.TOLERANCE:
            relevant_edges.append((edge, start_point, end_point))

    # Sortiere die Punkte in jedem Edge-Tupel basierend auf ihrem Y-Wert
    sorted_edges = []
    for edge, start_point, end_point in relevant_edges:
        sorted_points = sorted([start_point, end_point], key=lambda point: point.y)
        sorted_edges.append((edge, sorted_points[0], sorted_points[1]))

    print(sorted_edges)
    print(len(sorted_edges))

    return sorted_edges

# --------------------------------------------------------------------------------------
# Funktion zur Ermittlung der Verteilungspunkte
def get_distribution_points(relevant_edges, num_distribution_points):
    """_summary_

     Args:
         relevant_edges (_type_): _description_
         num_distribution_points (_type_): _description_

     Returns:
         _type_: _description_
    """
    distribution_points = []
    for edge, start_point, end_point in relevant_edges:
        points = []
        for i in range(num_distribution_points):
            factor = i / (num_distribution_points - 1)
            point = start_point + (end_point - start_point) * factor
            points.append(point)
        distribution_points.append((edge, points))

    return distribution_points


# --------------------------------------------------------------------------------------
# Funktion zur Ermittlung der Schnittpunkte
def get_intersection_points_all(distribution_points, inner_obj):
    """_summary_

     Args:
         distribution_points (_type_): _description_
         inner_obj (_type_): _description_
    """
    intersection_points = []
    for edge, points in distribution_points:
        for point in points:
            origin = point + Vector((0, stng.TOLERANCE, 0))
            direction = (Vector((0, point.y, 0)) - origin).normalized()
            result, location, normal, index = inner_obj.ray_cast(origin, direction)
            if result:
                intersection_points.append((edge, Vector(location), point))
            else:
                print(f"Ray casting missed at point: {point} on edge: {edge.index}")

    return intersection_points

# --------------------------------------------------------------------------------------
# Kürzen der Linien aktualisieren
def shorten_lines_all(intersection_points, cylinder_obj):
    # Neues bmesh für den Zylinder erstellen
    bm = bmesh.new()
    bm.from_mesh(cylinder_obj.data)
    short_points_all = []  # Liste zum Speichern aller "short points"

    for edge, location, point in intersection_points:
        # Berechne die Weltkoordinaten der Schnittpunkte und Startpunkte
        world_location = cylinder_obj.matrix_world @ location
        world_point = cylinder_obj.matrix_world @ point

        # Richtung vom Schnittpunkt zum Punkt berechnen (Vektor in Weltkoordinaten)
        direction = (world_location - world_point).normalized()

        # Endpunkt 2 mm vom Schnittpunkt nach innen verschieben
        short_point_world = world_location - direction *  stng.OFFSET_MM  # Abstand zu Modell, Linien kürzen

        # Konvertiere den gekürzten Punkt in die lokalen Koordinaten des Zylinders
        short_point = cylinder_obj.matrix_world.inverted() @ short_point_world

        # Erstelle die Kante mit dem Startpunkt und dem gekürzten Endpunkt im bmesh
        start_vert = bm.verts.new(point)
        end_vert = bm.verts.new(short_point)
        bm.edges.new((start_vert, end_vert))

        short_points_all.append((edge.index, short_point))

    # Aktualisiere das Mesh mit den neuen Kanten
    bm.to_mesh(cylinder_obj.data)
    bm.free()

    return short_points_all

# --------------------------------------------------------------------------------------
def create_edges_from_points(short_points, num_distribution_points):
    bm = bmesh.new()

    # Gruppiere die Punkte nach Edge-Index
    points_by_edge = {}
    for edge_index, point in short_points:
        print(edge_index)
        if edge_index not in points_by_edge:
            points_by_edge[edge_index] = []
        points_by_edge[edge_index].append(point)

    # Edge-Indices sortieren, um Konsistenz sicherzustellen
    edge_indices = sorted(points_by_edge.keys())
    num_edges = len(edge_indices)

    for i in range(num_edges):
        edge1_points = points_by_edge[edge_indices[i]]
        # % num_edges: Um beim letzten Edge wieder beim ersten Edge zu verbinden
        edge2_points = points_by_edge[edge_indices[(i + 1) % num_edges]]

        for j in range(len(edge1_points) - 1):
            v1 = edge1_points[j]
            v2 = edge1_points[j + 1]
            v3 = edge2_points[j + 1]
            v4 = edge2_points[j]

            vert1 = bm.verts.new(v1)
            vert2 = bm.verts.new(v2)
            vert3 = bm.verts.new(v3)
            vert4 = bm.verts.new(v4)


            bm.faces.new((vert1, vert2, vert3, vert4))
#            # Linien statt Faces erzeugen:
#            bm.edges.new((vert1, vert2))
#            bm.edges.new((vert2, vert3))
#            bm.edges.new((vert3, vert4))
#            bm.edges.new((vert4, vert1))

    # Neues Mesh erstellen
    mesh = bpy.data.meshes.new("HullMesh")
    bm.to_mesh(mesh)
    bm.free()

    # Neues Objekt für das neue Mesh erstellen und zur Szene hinzufügen
    obj = bpy.data.objects.new("HullObject", mesh)
    bpy.context.collection.objects.link(obj)
    
    return obj

    
# --------------------------------------------------------------------------------------
# --------------------------------------------------------------------------------------
# --------------------------------------------------------------------------------------
def main():

    # Aktives Objekt abrufen und zentrieren
    obj = bpy.context.active_object
    center_object_to_origin(obj)

    # Äußeren Zylinder erstellen    cylinder_obj = create_outer_cylinder(obj, stng.OFFSET_MM, stng.TOOL_DIAMETER_MM)

    # Aufruf der Funktionen und Debug-Ausgabe
    relevant_edges = get_relevant_edges(cylinder_obj)

    distribution_points = get_distribution_points(relevant_edges, stng.NUMBER_OF_RINGS)

    intersection_points = get_intersection_points_all(distribution_points, obj)

    short_points = shorten_lines_all(intersection_points, cylinder_obj)

    create_edges_from_points(short_points, stng.NUMBER_OF_RINGS)

    # Lösche das Objekt
    bpy.data.objects.remove(cylinder_obj, do_unlink=True)


# --------------------------------------------------------------------------------------
if __name__ == "__main__":
    # Systemkonsole leeren
    os.system('cls')  # Konsolenausgabe löschen

    stng = Settings()  # Instanziierung der Konstanten

    # Überprüfen, ob mindestens ein Objekt ausgewählt ist
    if bpy.context.selected_objects:
        main()  # Hauptfunktion aufrufen
    else:        
        show_message_box("Kein Objekt ausgewählt. Bitte wähle ein Objekt aus und versuche es erneut.", title = "Fehler", icon="ERROR")

At the beginning in the class Settings the parameters can be changed.
OFFSET_MM is the distance between the hull and the STL model.
TOOL_DIAMETER_MM don’t need an explanation :slight_smile: The hull is created with a cylinder around the object and 90% of width of the diameter is used for the number of cylinder faces.
NUMBER_OF_RINGS: The number of positions on the cylinder used to test the height of the model at this place. With 10 it would be a rough hull, with 200 a finer. The more rings, the longer the calculation.
TOLERANCE should not be changed.
SHORTEN_CYLINDER_MM: The cylinder around the model is shortened at both ends by this value. This makes sure that the first and last ring lands on the STL model.

After setting the parameters this is the way it works:
Load a STL model and align it to the world’s Y-axis (as this is the axis you also use later in snapmaker). The position doesn’t matter, it will be aligned to 0/0/0 later.
Set the size in a way that fits to the final size of the model when using it in the rotary module. That can be done by clicking on the object properties (orange square with orange brackets in the vertical icon row on the right side) and use “Scale X/Y/Z” to scale it until it fits.
Important step: After doing that, make sure the object is selected, click CTRL-A and use “Scale”. This resets the Scale to 1/1/1 and saves the current model size in the szene to the new scale. Otherwise it cannot be measured with Python.

When importing the STL-object, look in the file dialog at the right side, there also should always be a scale value of 1.

And then - just select the object and in the Scripting at the top of the text editor there is a “play” button. Click it and you’re done. You see the new hull object around the model, depending on your setting more or less detailed.

Check the hull against the model being inside if nothing goes through the hull object. That can happen if you choose too less rings and the model has a lot height differences. Increase it until the hull does not touch the model. So this is now your roughing model which you can make with a bigger tool instead of the tiny tools of Snapmaker. This can be setup with rough stepping and going deeper at the first try, depending on your tool. But you can get 6mm diameter tools into the machine, so for this size very much bigger heads exist which will not break.

The hull generator works also on the hull object. So if you select the generated hull you can generate the next around this with another distance and detail grade. So it’s also possible to work on material which is bigger to take away the rough parts without waiting years to let the V bit doing it…:slight_smile:

The generated hull can be exported as STL and imported to Luban.
Using a heightmap is in my eyes way to inaccurate to use it as there is no exact positioning possibility in Luban and in my tests I often see seams in the preview results in Luban - that’s not good enough. So I’m afraid, only Luban itself can generate exact height maps when using an STL file. I’ve tried several heightmaps importing but Luban then also wants to have the depth and so on - maybe OK for 2D reliefs, but not for rotary models with exact output.

Next I’m trying to generate GCode from Blender with Python. The first fast shot was not bad, but inserted to much model details. But the result in Luban was surprisingly very exact the representation of what was generated. So it’s possible and I’ll try to get it work.

Result should be to have a method to first remove the undetailed material and then be faster and more secure for the fine tool to do the detailed work. I also found a GCode which makes it possible to wait on a click on the screen so maybe a tool change between this and the detail run should also be possible.

Maybe it’s useful for someone!

2 Likes