Hi community!
Wanted to share a test file for a geometry workflow I’ve been thinking about. The process is designed to read a pattern from an image, have an AI analyze and lock in the logic, and then generate it as a fractal.
It’s definitely a work-in-progress, but I’m sharing it now because I’d love to see what kind of ideas and feedback this community has. I’m open to all suggestions!
(FYI - I’m already planning the next step to move this to a local AI, mainly because of the token usage costs with the Gemini connection.)
This is Python, which combines the processes of the three steps below into one.
Don’t raise the number slide by more than 5. The rules have not been organized yet, so unnecessary loading and shaping work will stop the work. Use the api key individually.
Stage 1: The AI Analyst
*Role: A ‘Computational Geometry Expert’ that analyzes an image and provides a detailed, human-readable description of its geometric patterns, rules, and relationships.
*Inputs: Image file path, API Key, and the AI model to be used (Pro or Flash).
*Process: It utilizes the advanced reasoning of the Gemini Pro model to analyze the visual information and generate text that explains the essence of the pattern.
*Output: A detailed text description, such as the recursive construction method for a Sierpinski triangle.
Stage 2: The AI Translator
*Role: A ‘Data Structuring Expert’ that translates the complex natural language text from Stage 1 into a strictly formatted JSON that can be machine-processed by Stage 3.
*Inputs: The text output from Node 1, API Key, and the AI model name.
*Process: It uses a highly constrained prompt with strong role assignment and explicit prohibitions (e.g., “Do not ask for the image”). This forces the AI to avoid the temptation of content analysis and focus solely on the ‘Text-to-JSON’ conversion task.
*Output: A clean JSON-formatted text with keys such as id, type, and description.
Stage 3: The Dynamo Constructor
*Role: A ‘Geometry Expert’ that receives the perfectly structured JSON ‘blueprint’ from Node 2, interprets and extracts the data within, and ‘constructs’ the actual Dynamo geometry.
*Inputs: The JSON text output from Node 2.
*Process: The node parses the text using json.loads(). It then uses Regular Expressions on the description field of each element to find patterns like Start-(x,y,z) and extract precise numerical coordinates. These coordinates are then used to generate the final geometry objects (Points, Lines, NurbsCurves, etc.).
*Output: A list of the final geometry objects, visible in the Dynamo viewport.
I expect that some of you have already experienced trial and error in various ways. The end goal is to automate pattern generation (2D) and create 3D shapes using that pattern. Please give us your various opinions. Please share the contents of the document organization of the process to the linked in letter address below. (Link later)
I tested with dynamosandbox 3.6.
image to geometry v19 share.dyn (32.5 KB)
7z images.7z (122.8 KB)
# --- 1. Required Library Imports ---
import sys
import clr
import json
import math
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *
from Autodesk.DesignScript.Geometry import Vector as DSVector
try:
from PIL import Image
import google.generativeai as genai
except ImportError as e:
OUT = "CRITICAL ERROR: Failed to import required libraries. Please ensure google-generativeai and Pillow are installed in your Python environment. Error: " + str(e)
sys.exit()
# --- 2. Input Variables ---
# IN[0]: Path to the base unit image
# IN[1]: API Key
# IN[2]: Model Name (e.g., "gemini-1.5-pro", "gemini-1.5-flash")
# IN[3]: Expansion Level (Integer)
if not (isinstance(IN, list) and len(IN) >= 2 and IN[0] and IN[1]):
OUT = "CRITICAL ERROR: Please connect at least 2 inputs (Image Path, API Key)."
sys.exit()
image_path = IN[0]
api_key = IN[1]
# Default is set to 'flash' for easier testing by the community
model_name = IN[2] if len(IN) > 2 and IN[2] else 'gemini-1.5-flash'
recursion_level = IN[3] if len(IN) > 3 and IN[3] is not None else 0
# --- 3. Core Function Definitions ---
def setup_gemini_model(key, name):
"""Sets up the Gemini model using the API key and model name."""
try:
genai.configure(api_key=key)
# Use the model name received from the Dynamo input
model = genai.GenerativeModel('models/' + name)
return model
except Exception as e:
return {"error": "Error: Failed to set up Gemini model - " + str(e)}
def get_fractal_dna_from_ai(model, img):
"""Requests the fractal generation rule (DNA) from the AI in a structured JSON format."""
prompt = """
You are a computational geometry expert specializing in L-systems and fractal geometry. Analyze the provided image of a fractal's base unit.
**Task:**
1. Identify the base polygon (initiator). Provide its corner coordinates.
2. Identify the recursive rule (generator). This rule is a list of transformations (scale, translation, rotation) to create the next generation.
**Strict Output Rules:**
1. Your ENTIRE response MUST be ONLY a JSON object.
2. The JSON must contain: "description", "base_shape_points", and "recursive_rule".
3. Each object in "recursive_rule" must have "scale" (number), "translation_vector" [tx, ty], and "rotation_angle" (degrees).
**Example for a Sierpinski Triangle:**
{
"description": "A Sierpinski triangle...",
"base_shape_points": [[0, 0], [100, 0], [50, 86.6]],
"recursive_rule": [
{"scale": 0.5, "translation_vector": [0, 0], "rotation_angle": 0},
{"scale": 0.5, "translation_vector": [50, 0], "rotation_angle": 0},
{"scale": 0.5, "translation_vector": [25, 43.3], "rotation_angle": 0}
]
}
"""
try:
generation_config = {"temperature": 0.0}
response = model.generate_content([prompt, img], generation_config=generation_config)
if not response.candidates: return {"error": "AI failed to analyze the rule."}
json_string = response.text.strip().replace("```json", "").replace("```", "").strip()
return json.loads(json_string)
except Exception as e:
return {"error": "Error: AI analysis API call failed - " + str(e)}
def generate_pattern(base_points, rule, level):
"""Generates the pattern through a recursive function."""
if level <= 0:
return [base_points]
next_generation_shapes = []
if not base_points: return []
# Use the first point of the shape as the origin for transformations
origin = base_points[0]
origin_x, origin_y = origin[0], origin[1]
for transform in rule:
scale = transform.get("scale", 1.0)
translation = transform.get("translation_vector", [0, 0])
angle_deg = transform.get("rotation_angle", 0)
angle_rad = math.radians(angle_deg)
new_shape = []
for point in base_points:
# 1. Translate point to origin (0,0) for scaling and rotation
px, py = point[0] - origin_x, point[1] - origin_y
# The translation vector must also be scaled and rotated to maintain relative position
scaled_tx = translation[0] * scale
scaled_ty = translation[1] * scale
rotated_tx = scaled_tx * math.cos(angle_rad) - scaled_ty * math.sin(angle_rad)
rotated_ty = scaled_tx * math.sin(angle_rad) + scaled_ty * math.cos(angle_rad)
# 2. Scale and rotate the shape's points
scaled_px, scaled_py = px * scale, py * scale
rotated_px = scaled_px * math.cos(angle_rad) - scaled_py * math.sin(angle_rad)
rotated_py = scaled_px * math.sin(angle_rad) + scaled_py * math.cos(angle_rad)
# 3. Translate back to original position + apply the transformed translation vector
final_x = rotated_px + origin_x + rotated_tx
final_y = rotated_py + origin_y + rotated_ty
new_shape.append([final_x, final_y])
next_generation_shapes.extend(generate_pattern(new_shape, rule, level - 1))
return next_generation_shapes
def create_dynamo_geometry(all_shapes):
"""Creates Dynamo geometry from all the generated shape data."""
all_points, all_lines = [], []
for shape_points in all_shapes:
dynamo_points = [Point.ByCoordinates(p[0], p[1], 0) for p in shape_points]
if len(dynamo_points) >= 2:
for i in range(len(dynamo_points)):
start_point = dynamo_points[i]
# Connect the last point back to the first to close the shape
end_point = dynamo_points[(i + 1) % len(dynamo_points)]
if not start_point.Equals(end_point):
all_lines.append(Line.ByStartPointEndPoint(start_point, end_point))
all_points.extend(dynamo_points)
return {"points": all_points, "lines": all_lines}
# --- 4. Main Workflow Execution ---
def main():
model = setup_gemini_model(api_key, model_name)
if isinstance(model, dict) and "error" in model: return model
try:
img = Image.open(image_path)
except Exception as e:
return "Error: Failed to open the image file - " + str(e)
fractal_dna = get_fractal_dna_from_ai(model, img)
if isinstance(fractal_dna, dict) and "error" in fractal_dna: return fractal_dna
base_shape = fractal_dna.get("base_shape_points")
recursive_rule = fractal_dna.get("recursive_rule")
if not base_shape or not recursive_rule:
return "Error: AI did not return a valid fractal DNA (rule)."
final_shapes_data = generate_pattern(base_shape, recursive_rule, int(recursion_level))
final_geometry = create_dynamo_geometry(final_shapes_data)
return {
"AI_Analysis": fractal_dna.get("description"),
"Generated_Geometry": final_geometry,
"Expansion_Level": recursion_level,
"Model_Used": model_name
}
# Assign the final result to the OUT variable
OUT = main()