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:
A core system for your own 3D tool: It becomes the engine powering something bigger and more complex.
A node-based visual system: The simple syntax translates naturally into visual nodes.
Training an LLM: You could train an AI on this simple syntax. Which raises an interesting question: how do you design a syntax that prevents the spatial hallucinations AIs are notorious for? I'll dig into that later.
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:
- Lark: A parsing toolkit that builds a syntax tree from your text input.
- build123d: A parametric CAD library for creating 2D and 3D geometry.
- ocp_vscode: Visualizes the 3D results directly in Visual Studio Code.
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:
add_expr
takes two numbers and creates a series of boxes with increasing sizes using build123d.sub_expr
does the opposite—decreasing sizes, with some boxes placed on the left side of the origin.
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:
- Beyond arithmetic: How can we define more complex instructions, like motion primitives or variables?
- Hierarchy and nesting: How do we structure commands to operate on nested assemblies—like building an arm with fingers that can grip?
- Core components: Defining the fundamental building blocks of soft robots: actuators, segments, joints, and how they connect.
- Motion rules: Starting to describe how parts should move or deform in response to commands—the part that makes this about robots and not just CAD.
Stay tuned.
This is a work in progress. If you have thoughts or questions, reach me at danielmotilla@proton.me