M0TH

Building a small DSL for 3D with python.

I've been working on a project called MODELITO for procedural soft robots. It started with a pretty ambitious goal: a general-purpose DSL (Domain-Specific Language) for procedural generation in engineering. Turns out, that was too broad no specific strength, just vague promises. So I narrowed it down to something actually unique: a DSL for procedural soft robots.

I'm writing about this for a few reasons. First, it helps me organize my thoughts and actually finish the damn thing. Second, the project is open source anyway, so why not bring you along? I think there's something valuable in learning how to build your own tool for 3D instead of just conforming to whatever tools already exist.

Why Design Rules Instead of Models?

Here's what I realized: designing rules of motion is way more powerful than modeling a new robot from scratch every single time. As a programmer, I believe it's better to design the system that designs the robot, call it "laziness" if you want.

Once you have a language like this, you unlock some cool possibilities:

For now, let's start with the basics.

Hello World: A Simple Arithmetic DSL with Geometry

This first example combines three powerful Python libraries to turn a simple arithmetic expression into actual 3D geometry:

Here's the complete working code:

# By Daniel Motilla (M0TH)
# The best system is the one you designed.

from lark import Lark, Transformer
from build123d import *
from ocp_vscode import show, show_all

grammar = """
start: add_expr
     | sub_expr
add_expr: NUMBER "+" NUMBER -> add_expr
sub_expr: NUMBER "-" NUMBER -> sub_expr
%import common.NUMBER
%ignore " "
"""

# Global variable to store the result
result_part = None

class CalcTransformer(Transformer):
    def add_expr(self, args):
        global result_part
        
        x = int(args[0])
        y = int(args[1])
        spacing = 3.0  # More space needed for varying sizes
        
        # Create boxes side by side with increasing sizes
        with BuildPart() as calc_result:
            # Create x boxes starting at position 0 with increasing sizes
            for i in range(x):
                size = 1.0 + i * 0.3  # Start at 1.0, increase by 0.3 each time
                with Locations((i * spacing, 0, 0)):
                    Box(size, size, size)
            
            # Create y boxes continuing the size increase
            for i in range(y):
                size = 1.0 + (x + i) * 0.3  # Continue size progression
                with Locations((x * spacing + i * spacing, 0, 0)):
                    Box(size, size, size)
        
        # Store the result globally so show_all() can find it
        result_part = calc_result.part
        result_part.label = f"addition_{x}_plus_{y}"
        
        return f"Created {x + y} boxes: {x} + {y} with increasing sizes"
    
    def sub_expr(self, args):
        global result_part
        
        x = int(args[0])
        y = int(args[1])
        spacing = 3.0  # More space needed for varying sizes
        
        # Create boxes with decreasing sizes
        with BuildPart() as calc_result:
            # Create x boxes starting at position 0 with decreasing sizes
            total_boxes = x + y
            for i in range(x):
                size = 2.0 - i * 0.2  # Start larger, decrease by 0.2 each time
                size = max(size, 0.3)  # Don't let it get too small
                with Locations((i * spacing, 0, 0)):
                    Box(size, size, size)
            
            # Create y boxes to the left continuing the size decrease
            for i in range(y):
                size = 2.0 - (x + i) * 0.2  # Continue size progression
                size = max(size, 0.3)  # Don't let it get too small
                with Locations((-spacing - i * spacing, 0, 0)):
                    Box(size, size, size)
        
        # Store the result globally so show_all() can find it
        result_part = calc_result.part
        result_part.label = f"subtraction_{x}_minus_{y}"
        
        return f"Created {x + y} boxes: {x} with {y} to the left, decreasing sizes"

parser = Lark(grammar, parser='lalr', transformer=CalcTransformer())

def main():
    # Parse and create geometry
    result = parser.parse("1 - 10")
    print(result)
    
    # Show the result
    if result_part is not None:
        print(f"Result part created: {result_part}")
        print(f"Part volume: {result_part.volume}")
        print(f"Part bounding box: {result_part.bounding_box()}")
        
        # Try different display methods
        try:
            show_all()
        except NameError:
            print("show_all not available")
        
        try:
            show(result_part)
        except NameError:
            print("show not available")
    else:
        print("No result_part created!")
    
    # Just to verify - let's also assign to a local variable
    boxes = result_part
    print(f"Local variable 'boxes' assigned: {boxes}")

if __name__ == '__main__':
    main()

How It Works: A Closer Look

The Grammar: The grammar variable uses EBNF syntax that Lark understands. It defines two operations: add_expr and sub_expr, each made of two numbers and an operator. The -> add_expr part tells Lark which method to call in our transformer class.

The Transformer: This is where things get interesting. The CalcTransformer processes the syntax tree that Lark generates:

The Geometry: build123d uses a "builder" paradigm with context managers (with BuildPart()) that makes generating complex geometry way cleaner than traditional CAD scripting. We use Locations() to position boxes in 3D space.

The Visualization: ocp_vscode lets us see the results immediately, which is essential when debugging 3D geometry. No export-import cycles, no switching between applications.

What's Next for MODELITO?

This arithmetic example is just the foundation. The real power comes from building on top of it to create a proper language for procedural soft robots.

Some directions I'm exploring:

Stay tuned.


This is a work in progress. If you have thoughts or questions, reach me at danielmotilla@proton.me