Skip to content

Instantly share code, notes, and snippets.

@Mockapapella
Created March 16, 2025 19:19
Show Gist options
  • Save Mockapapella/ca9c591acdf57cf69268ad274cb374fd to your computer and use it in GitHub Desktop.
Save Mockapapella/ca9c591acdf57cf69268ad274cb374fd to your computer and use it in GitHub Desktop.
import bpy, random, math, time
from functools import partial
from collections import defaultdict
def create_circuit_pattern(w=30, h=160, trace_t=0.05, via_r=0.075, min_len=6, max_len=20, min_s=1, board_t=0.2, timeout=600,
chip_w=8, chip_h=8, chip_height=0.125, max_traces=None, density_factor=0.25, pin_spacing=1,
grid_spacing=0.25, color_transition_margin=0.4, emission_intensity=3.0, render_samples=128,
use_sector_based_filling=True, chip_emanating_trace_percentage=0.9, y_direction_bias=3.0):
# Initialize core variables and clear scene
start, count = time.time(), 0
max_traces = max_traces or int((w * h - chip_w * chip_h) * density_factor)
[obj.select_set(True) for obj in bpy.context.scene.objects]; bpy.ops.object.delete()
# Core utility functions with one-line definitions
grid_to_world, world_to_grid = lambda x, y, z=0: (x * grid_spacing, y * grid_spacing, z), lambda x, y: (int(x / grid_spacing), int(y / grid_spacing))
is_valid_pos, get_distance = lambda x, y: 0 <= x < w and 0 <= y < h and grid.get((x, y), -3) == -1, lambda x1, y1, x2, y2: abs(x2-x1) + abs(y2-y1)
# Material creation with node setup - no fallbacks
def create_mat(name, base_color, emission_color=None, emission_strength=0, metallic=0, roughness=1, mix_factor=0):
mat = bpy.data.materials.new(name=name); mat.use_nodes = True
nodes, links = mat.node_tree.nodes, mat.node_tree.links; [nodes.remove(node) for node in list(nodes)]
output, principled = nodes.new('ShaderNodeOutputMaterial'), nodes.new('ShaderNodeBsdfPrincipled')
principled.inputs[0].default_value, principled.inputs.get('Metallic').default_value, principled.inputs.get('Roughness').default_value = base_color, metallic, roughness
if emission_color and emission_strength > 0:
emission, mix = nodes.new('ShaderNodeEmission'), nodes.new('ShaderNodeMixShader')
emission.inputs[0].default_value, emission.inputs[1].default_value, mix.inputs[0].default_value = emission_color, emission_strength, mix_factor
links.new(principled.outputs[0], mix.inputs[1]); links.new(emission.outputs[0], mix.inputs[2]); links.new(mix.outputs[0], output.inputs[0])
else: links.new(principled.outputs[0], output.inputs[0])
return mat
# Get color based on position from chip for traces and vias
def get_trace_color(y_pos):
blue, red = (0.0, 0.1, 0.9, 1.0), (0.95, 0.0, 0.0, 1.0)
margin = chip_h * color_transition_margin
transition_bottom, transition_top = max(0, chip_y - margin), min(h, chip_y + chip_h + margin)
factor = 0 if y_pos <= transition_bottom else 1 if y_pos >= transition_top else (y_pos - transition_bottom) / (transition_top - transition_bottom)
rgb = [blue[i] + (red[i] - blue[i]) * factor for i in range(3)]
intensity = 1.0 + 0.3 * (1.0 - sum(rgb[:3])/3) if sum(rgb[:3]) > 0 else 1.0
return tuple([min(1.0, c * intensity) for c in rgb[:3]] + [1.0])
# Create board and chip materials
board_mat = create_mat("BoardMaterial", (0.02, 0.05, 0.02, 1), (0.02, 0.05, 0.02, 1), 0.05, 0.1, 0.8, 0.05)
# Create true black chip material with edge highlights
chip_mat = bpy.data.materials.new(name="TrueBlackChip"); chip_mat.use_nodes = True
nodes, links = chip_mat.node_tree.nodes, chip_mat.node_tree.links; [nodes.remove(node) for node in list(nodes)]
output, principled, emission, mix_shader, fresnel = [nodes.new(t) for t in
['ShaderNodeOutputMaterial', 'ShaderNodeBsdfPrincipled', 'ShaderNodeEmission', 'ShaderNodeMixShader', 'ShaderNodeFresnel']]
principled.inputs['Base Color'].default_value, principled.inputs['Metallic'].default_value, principled.inputs['Roughness'].default_value = (0.0, 0.0, 0.0, 1.0), 0.2, 0.2
emission.inputs['Color'].default_value, emission.inputs['Strength'].default_value, fresnel.inputs['IOR'].default_value = (0.05, 0.1, 0.3, 1.0), 1.5, 1.7
[links.new(*ln) for ln in [(fresnel.outputs['Fac'], mix_shader.inputs[0]), (principled.outputs[0], mix_shader.inputs[1]),
(emission.outputs[0], mix_shader.inputs[2]), (mix_shader.outputs[0], output.inputs[0])]]
# Create board and chip in one block
board_width, board_height = w * grid_spacing, h * grid_spacing
bpy.ops.mesh.primitive_cube_add(size=1, location=(board_width/2, board_height/2, -board_t/2))
board = bpy.context.active_object; board.name, board.scale = "CircuitBoard", (board_width+grid_spacing, board_height+grid_spacing, board_t)
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True); board.data.materials.append(board_mat)
# Setup grid and create chip
grid = {(x, y): -1 for x in range(w) for y in range(h)}
chip_x, chip_y = w // 2 - chip_w // 2, h // 2 - chip_h // 2
# Create chip with beveled edges
bpy.ops.mesh.primitive_cube_add(size=1, location=(chip_x * grid_spacing + chip_w * grid_spacing/2, chip_y * grid_spacing + chip_h * grid_spacing/2, chip_height/2))
chip = bpy.context.active_object; chip.name, chip.scale = "Chip", (chip_w * grid_spacing, chip_h * grid_spacing, chip_height * 1.2)
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
bpy.context.active_object.modifiers.new(name="Bevel", type='BEVEL').width, bpy.context.active_object.modifiers["Bevel"].segments = 0.02, 3
chip.data.materials.append(chip_mat)
# Create hourglass symbol with one function call and condensed code
def create_hourglass_symbol():
symbol_location = (chip_x * grid_spacing + chip_w * grid_spacing/2, chip_y * grid_spacing + chip_h * grid_spacing/2, chip_height * 1.2 + 0.002)
h_width, h_height = min(chip_w, chip_h) * grid_spacing * 0.3, min(chip_w, chip_h) * grid_spacing * 0.48
verts = [(h_height/2, -h_width, 0), (h_height/2, h_width, 0), (0, 0, 0), (-h_height/2, -h_width, 0), (-h_height/2, h_width, 0)]
faces = [[0, 1, 2], [2, 3, 4]]
hourglass_mesh = bpy.data.meshes.new("Hourglass_Mesh"); hourglass_mesh.from_pydata(verts, [], faces); hourglass_mesh.update()
symbol = bpy.data.objects.new("Hourglass_Symbol", hourglass_mesh); bpy.context.collection.objects.link(symbol); symbol.location = symbol_location
# Steel material for symbol - one-shot node creation
steel_mat = bpy.data.materials.new(name="GlowingSteel"); steel_mat.use_nodes = True
nodes, links = steel_mat.node_tree.nodes, steel_mat.node_tree.links; [nodes.remove(node) for node in list(nodes)]
output, principled, emission, mix = [nodes.new(t) for t in ['ShaderNodeOutputMaterial', 'ShaderNodeBsdfPrincipled', 'ShaderNodeEmission', 'ShaderNodeMixShader']]
principled.inputs['Base Color'].default_value, principled.inputs['Metallic'].default_value, principled.inputs['Roughness'].default_value = (0.95, 0.95, 0.98, 1.0), 1.0, 0.15
emission.inputs['Color'].default_value, emission.inputs['Strength'].default_value, mix.inputs[0].default_value = (1.0, 1.0, 1.0, 1.0), 2.0, 0.3
[links.new(*ln) for ln in [(principled.outputs[0], mix.inputs[1]), (emission.outputs[0], mix.inputs[2]), (mix.outputs[0], output.inputs[0])]]
symbol.data.materials.append(steel_mat)
symbol.modifiers.new(name="Bevel", type='BEVEL').width, symbol.modifiers["Bevel"].segments = 0.003, 2
symbol.modifiers.new(name="Solidify", type='SOLIDIFY').thickness = 0.004
return symbol
# Create hourglass symbol and mark chip area in grid
hourglass = create_hourglass_symbol()
for x in range(chip_x, chip_x+chip_w+1):
for y in range(chip_y, chip_y+chip_h+1):
if 0 <= x < w and 0 <= y < h: grid[(x, y)] = -3
# Direction vectors and movement validation (0=right, 1=right-up, 2=up, 3=left-up, 4=left, 5=left-down, 6=down, 7=right-down)
dirs = [(1,0), (1,1), (0,1), (-1,1), (-1,0), (-1,-1), (0,-1), (1,-1)]
# Direction bias calculation in one line
dir_bias = lambda i: 1.0 + (abs(y_direction_bias) * (2.0 if i in [2,6] else 1.0 if i in [1,3,5,7] else 0.0)
* (1.0 if (i == 2 and y_direction_bias > 0) or (i == 6 and y_direction_bias < 0) else
0.7 if (i in [1,3] and y_direction_bias > 0) or (i in [5,7] and y_direction_bias < 0) else 0.3))
# Path validation functions with self-crossing prevention
is_valid_move = lambda x1, y1, x2, y2: is_valid_pos(x2, y2) and not (abs(x2-x1) == 1 and abs(y2-y1) == 1 and (grid.get((x1, y2), -3) != -1 or grid.get((x2, y1), -3) != -1))
# Only allow 45-degree turns (not 135-degree sharp turns)
turns = lambda i: [i, (i-1)%8, (i+1)%8] # Current direction and 45-degree turns only
# Prevent backwards or sharp turns by checking the opposite of current direction
is_sharp_turn = lambda current, new: abs(current - new) > 2 and abs(current - new) < 6
can_go = lambda x, y, i, steps=1: all(is_valid_move(x+dirs[i][0]*(j-1), y+dirs[i][1]*(j-1), x+dirs[i][0]*j, y+dirs[i][1]*j) for j in range(1, steps+1))
# Optimized via creation
def via(x, y):
if not (0 <= x < w and 0 <= y < h): return False
world_x, world_y, world_z = grid_to_world(x, y)
via_color = get_trace_color(y)
via_material = create_mat(f"ViaMaterial_{x}_{y}", via_color, via_color, emission_intensity, 0, 0, 1.0)
# Create torus for via - no fallback needed
bpy.ops.mesh.primitive_torus_add(major_segments=32, minor_segments=16, major_radius=via_r, minor_radius=via_r/2, location=(world_x, world_y, world_z))
v = bpy.context.active_object; v.scale = (1, 1, board_t*0.85)
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
v.name, grid[(x, y)] = f"Via_{x}_{y}", -2
v.data.materials.append(via_material)
return True
# Trace creation with gradient materials and precise endpoints
def trace(path, tid, start_via=True, end_via=True):
if len(path) < min_len or any(not is_valid_move(path[i-1][0], path[i-1][1], path[i][0], path[i][1]) for i in range(1, len(path))):
return False
# Create vias at endpoints
if start_via: via(path[0][0], path[0][1])
if end_via: via(path[-1][0], path[-1][1])
# Create trace curve with gradient material
c = bpy.data.curves.new(f'Trace_{tid}', 'CURVE')
c.dimensions, c.resolution_u, c.bevel_depth, c.bevel_resolution, c.use_fill_caps = '3D', 12, trace_t/2, 6, True
obj = bpy.data.objects.new(f'Trace_{tid}', c); bpy.context.collection.objects.link(obj)
# Create gradient material - optimized node setup
mat = bpy.data.materials.new(name=f"TraceMaterial_{tid}"); mat.use_nodes = True
nodes, links = mat.node_tree.nodes, mat.node_tree.links; [nodes.remove(node) for node in nodes]
output = nodes.new('ShaderNodeOutputMaterial')
# Create color gradient for trace with increased emission
color_ramp, tex_coord, emission = [nodes.new(t) for t in ['ShaderNodeValToRGB', 'ShaderNodeTexCoord', 'ShaderNodeEmission']]
color_ramp.color_ramp.elements[0].color, color_ramp.color_ramp.elements[1].color = get_trace_color(path[0][1]), get_trace_color(path[-1][1])
emission.inputs[1].default_value = emission_intensity * 1.5
[links.new(*ln) for ln in [(tex_coord.outputs['Object'], color_ramp.inputs[0]), (color_ramp.outputs['Color'], emission.inputs[0]),
(emission.outputs[0], output.inputs['Surface'])]]
obj.data.materials.append(mat)
# Create spline with adjusted endpoints
spline = c.splines.new('POLY'); spline.points.add(len(path) - 1)
for i, (x, y) in enumerate(path):
grid[(x, y)] = tid # Mark grid
world_x, world_y, world_z = grid_to_world(x, y)
adjusted_pos = [world_x, world_y, world_z]
# Adjust endpoints for smooth via connections
if (i == 0 and start_via) or (i == len(path)-1 and end_via) and len(path) >= 2:
next_x, next_y = path[1] if i == 0 else path[-2]
next_world_x, next_world_y, _ = grid_to_world(next_x, next_y)
dir_x, dir_y = world_x - next_world_x, world_y - next_world_y
dist = math.sqrt(dir_x**2 + dir_y**2)
if dist > 0:
dir_x, dir_y = dir_x/dist, dir_y/dist
adjusted_pos[0], adjusted_pos[1] = world_x - dir_x * via_r * 1.05, world_y - dir_y * via_r * 1.05
spline.points[i].co = (*adjusted_pos, 1)
return True
# Path generation with target-based and random routing - optimized for density
def generate_path(start_x, start_y, target_x=None, target_y=None, max_length=max_len, min_length=min_len):
path, path_set = [(start_x, start_y)], {(start_x, start_y)}
# Direction selection - target-based or random with y-direction bias
if target_x is None:
options = [d for d in range(8) if can_go(start_x, start_y, d, min_s)]
if not options: return []
# Apply direction bias with weighted random selection
if abs(y_direction_bias) > 0.001:
weights = [dir_bias(d) for d in options]
r, cumulative_weight = random.random() * sum(weights), 0
for i, w in enumerate(weights):
if (cumulative_weight := cumulative_weight + w) >= r:
dir_idx = options[i]
break
else:
dir_idx = random.choice(options)
else:
# Target-based direction selection
dx, dy = (1 if target_x > start_x else -1 if target_x < start_x else 0), (1 if target_y > start_y else -1 if target_y < start_y else 0)
dir_match = next((i for i, d in enumerate(dirs) if d == (dx, dy)), None)
options = [d for d in range(8) if can_go(start_x, start_y, d, 1)]
dir_idx = dir_match if dir_match is not None and dir_match in options else (random.choice(options) if options else None)
if dir_idx is None: return []
# Path growth with obstacle avoidance and target seeking
d, x, y, straight = dirs[dir_idx], start_x, start_y, 0
target_length = random.randint(min_length, max_length) if target_x is None else 100
# Path extension loop - generates path with turning
while len(path) < target_length:
if target_x is not None and x == target_x and y == target_y: break
nx, ny = x + d[0], y + d[1]
# Handle obstacles with weighted direction change
if (nx, ny) in path_set or not is_valid_move(x, y, nx, ny):
options = [(i, abs(x+dirs[i][0] - (target_x or 0)) + abs(y+dirs[i][1] - (target_y or 0)) if target_x is not None else 0)
for i in turns(dir_idx) if is_valid_move(x, y, x+dirs[i][0], y+dirs[i][1]) and (x+dirs[i][0], y+dirs[i][1]) not in path_set]
if not options: return path if len(path) >= min_length else []
# Apply direction bias for non-targeted paths
if target_x is None and abs(y_direction_bias) > 0.001:
weighted_options = [(i, dir_bias(i), s) for i, s in options]
r, cumulative_weight = random.random() * sum(w for _, w, _ in weighted_options), 0
for i, w, _ in weighted_options:
if (cumulative_weight := cumulative_weight + w) >= r:
dir_idx = i
break
else:
dir_idx = min(options, key=lambda o: o[1])[0] if target_x is not None else random.choice([o[0] for o in options])
d, straight = dirs[dir_idx], 0
continue
# Add point to path and consider random turns
x, y = nx, ny
path.append((x, y))
path_set.add((x, y))
straight += 1
if straight >= min_s and random.random() < 0.2 and len(path) < target_length - min_s:
turn_options = [(i, abs(x+dirs[i][0] - (target_x or 0)) + abs(y+dirs[i][1] - (target_y or 0)) if target_x is not None else 0)
for i in turns(dir_idx) if i != dir_idx and is_valid_move(x, y, x+dirs[i][0], y+dirs[i][1])
and (x+dirs[i][0], y+dirs[i][1]) not in path_set]
if turn_options:
dir_idx = min(turn_options, key=lambda o: o[1])[0] if target_x is not None else random.choice([o[0] for o in turn_options])
d, straight = dirs[dir_idx], 0
return path if len(path) >= min_length else []
# Three-phase trace generation with timing and statistics
def generate_traces():
nonlocal count
gen_stats = {'phase0_traces': 0, 'phase0_time': 0, 'phase1_traces': 0, 'phase1_time': 0, 'phase2_traces': 0, 'phase2_time': 0, 'sectors_filled': 0}
# Phase 0: Chip-emanating traces
phase0_start = time.time()
chip_points = [(x, y) for x in range(chip_x, chip_x+chip_w+1) for y in range(chip_y, chip_y+chip_h+1)
if 0 <= x < w and 0 <= y < h and grid.get((x, y), 0) == -3 and
any(0 <= nx < w and 0 <= ny < h and grid.get((nx, ny), 0) != -3 for dx, dy in dirs if (nx := x + dx) and (ny := y + dy))]
# Calculate chip trace allocation and shuffle starting points
desired_chip_traces = int(max_traces * chip_emanating_trace_percentage)
traces_per_point = max(1, desired_chip_traces // (len(chip_points) or 1))
chip_traces_created = 0
expanded_chip_points = [point for point in chip_points for _ in range(traces_per_point)]
random.shuffle(expanded_chip_points)
# Generate chip-emanating traces
for attempt in range(min(len(expanded_chip_points), desired_chip_traces * 2)):
if chip_traces_created >= desired_chip_traces or time.time() - start >= timeout: break
start_x, start_y = expanded_chip_points[attempt % len(expanded_chip_points)]
# Find valid directions away from chip
valid_dirs = []
for i, (dx, dy) in enumerate(dirs):
nx, ny = start_x + dx, start_y + dy
if not (0 <= nx < w and 0 <= ny < h) or grid.get((nx, ny), 0) != -1: continue
path_away, can_extend = [(start_x, start_y), (nx, ny)], True
current_x, current_y = nx, ny
# Try to extend path away from chip
for _ in range(2):
found_next = False
for next_dir in turns(i):
next_x, next_y = current_x + dirs[next_dir][0], current_y + dirs[next_dir][1]
if is_valid_move(current_x, current_y, next_x, next_y):
path_away.append((next_x, next_y)); current_x, current_y, i, found_next = next_x, next_y, next_dir, True
break
if not found_next: can_extend = False; break
if can_extend: valid_dirs.append((i, path_away))
if not valid_dirs: continue
# Choose direction and extend path
chosen_dir, initial_path = random.choice(valid_dirs)
x, y = initial_path[-1]
extended_path, path_set, d, straight = initial_path.copy(), set(initial_path), dirs[chosen_dir], 0
# Extend path with random turning
remaining_length = random.randint(2, max_len - len(extended_path))
for _ in range(remaining_length):
nx, ny = x + d[0], y + d[1]
# Handle obstacles with direction change
if (nx, ny) in path_set or not is_valid_move(x, y, nx, ny):
options = [(i, dirs[i]) for i in turns(chosen_dir)
if is_valid_move(x, y, x+dirs[i][0], y+dirs[i][1]) and (x+dirs[i][0], y+dirs[i][1]) not in path_set]
if not options: break
chosen_dir, d, straight = random.choice(options)[0], random.choice(options)[1], 0
continue
# Add point and consider turning
x, y = nx, ny
extended_path.append((x, y))
path_set.add((x, y))
straight += 1
if straight >= min_s and random.random() < 0.2:
options = [(i, dirs[i]) for i in turns(chosen_dir)
if i != chosen_dir and is_valid_move(x, y, x+dirs[i][0], y+dirs[i][1])
and (x+dirs[i][0], y+dirs[i][1]) not in path_set]
if options: chosen_dir, d, straight = random.choice(options)[0], random.choice(options)[1], 0
# Create trace without starting via (from chip)
if len(extended_path) >= min_len and trace(extended_path, count, start_via=False, end_via=True):
count, chip_traces_created, gen_stats['phase0_traces'] = count + 1, chip_traces_created + 1, gen_stats['phase0_traces'] + 1
if count % 5 == 0: print(f"Created {count} traces (chip-emanating: {chip_traces_created}/{desired_chip_traces})..."); bpy.context.view_layer.update()
gen_stats['phase0_time'] = time.time() - phase0_start
# Phase 1: Main random traces
phase1_start = time.time()
main_target, tries = max(count, int(max_traces * (1 - chip_emanating_trace_percentage) * 0.7)), 0
# Generate random traces until target reached
while count < main_target and tries < 2000 and time.time() - start < timeout:
tries += 1
empty = [(x, y) for (x, y), val in grid.items() if val == -1]
if not empty: break
# Generate path from random empty cell
start_pos = random.choice(empty)
adjusted_max = min(max_len, max(min_len + 2, int(min(w, h) * 0.2)))
path = generate_path(*start_pos, max_length=adjusted_max)
if path and trace(path, count):
count, gen_stats['phase1_traces'] = count + 1, gen_stats['phase1_traces'] + 1
if count % 10 == 0: print(f"Created {count} traces..."); bpy.context.view_layer.update()
gen_stats['phase1_time'] = time.time() - phase1_start
# Phase 2: Gap filling with sectors
phase2_start = time.time()
if time.time() - start < timeout and count < max_traces and use_sector_based_filling:
sector_size = max(3, min(w, h) // 5)
# Create and shuffle sectors
sectors = [[(x, y) for x in range(sx, min(sx + sector_size, w)) for y in range(sy, min(sy + sector_size, h))
if (x, y) in grid and grid[(x, y)] == -1]
for sx in range(0, w, sector_size) for sy in range(0, h, sector_size)
if any(grid.get((x, y), -999) == -1 for x in range(sx, min(sx + sector_size, w))
for y in range(sy, min(sy + sector_size, h)))]
random.shuffle(sectors)
# Process each sector
for sector in sectors:
if time.time() - start > timeout or count >= max_traces: break
tries, fills = 0, 0
random.shuffle(sector)
# Fill each sector with traces
while sector and tries < 50 and count < max_traces and time.time() - start < timeout:
tries += 1
start_pos = sector.pop(0)
if grid.get(start_pos, -999) != -1: continue
# Find candidates and create paths
sx, sy, radius = start_pos[0], start_pos[1], min(sector_size, 4)
candidates = [((rx, ry), abs(rx - sx) + abs(ry - sy))
for rx in range(max(0, sx - radius), min(w, sx + radius + 1))
for ry in range(max(0, sy - radius), min(h, sy + radius + 1))
if grid.get((rx, ry), -999) == -1 and (rx, ry) != (sx, sy)
and min_len - 1 <= abs(rx - sx) + abs(ry - sy) <= max_len - 1]
if not candidates: continue
candidates.sort(key=lambda x: x[1])
# Try top candidates
for target_pos, _ in candidates[:3]:
path = generate_path(sx, sy, *target_pos, max_length=min(max_len, max(min_len, int(min(w, h) * 0.15))))
if path and trace(path, count):
count, gen_stats['phase2_traces'], fills = count + 1, gen_stats['phase2_traces'] + 1, fills + 1
sector = [pos for pos in sector if pos not in path]
break
if fills >= 3: gen_stats['sectors_filled'] += 1; break
gen_stats['phase2_time'] = time.time() - phase2_start
return gen_stats
# Create traces with all phases
trace_stats = generate_traces()
# Add light with one compact function
def add_light(type, location, energy, color, size=None, shadow_soft_size=None):
bpy.ops.object.light_add(type=type, radius=1, location=location)
light = bpy.context.active_object.data
light.energy, light.color = energy, color
if size is not None: light.size = size
if shadow_soft_size is not None: light.shadow_soft_size = shadow_soft_size
return bpy.context.active_object
# Setup camera and lighting in one block
bpy.ops.object.camera_add(location=(board_width/2, -board_height/2, board_height*1.1), rotation=(math.radians(40), 0, 0))
camera = bpy.context.active_object; bpy.context.scene.camera = camera
# Create strategic lighting rig
main_light = add_light('AREA', (board_width/2, board_height/2, board_height*1.5), 0.6, (0.15, 0.2, 0.25), size=max(board_width, board_height) * 3)
rim_light = add_light('POINT', (board_width*0.3, board_height*0.3, board_height*0.8), 3.0, (0.1, 0.2, 0.4), shadow_soft_size=3.0)
key_light = add_light('SPOT', (board_width*0.6, board_height*0.3, board_height*1.3), 5.0, (1.0, 1.0, 1.0), shadow_soft_size=1.0)
key_light.data.spot_size, key_light.data.spot_blend = math.radians(30), 0.3
edge_light = add_light('POINT', (board_width*0.5, board_height*0.5, -board_height*0.2), 0.8, (0.05, 0.1, 0.3), shadow_soft_size=10.0)
# Configure render settings
world = bpy.context.scene.world or bpy.data.worlds.new("World")
bpy.context.scene.world, world.use_nodes, world.color = world, False, (0, 0, 0)
scene = bpy.context.scene; scene.render.engine = 'CYCLES'
scene.cycles.samples, scene.cycles.film_exposure = render_samples, 1.2
# Calculate statistics and return
occupied, coverage, trace_density = sum(1 for val in grid.values() if val != -1), (sum(1 for val in grid.values() if val != -1) / (w * h)) * 100, count / (w * h - chip_w * chip_h) * 100
# Return comprehensive statistics
stats = {
'traces': count, 'time': time.time() - start, 'coverage': coverage, 'density': trace_density,
'phase0_traces': trace_stats['phase0_traces'], 'phase1_traces': trace_stats['phase1_traces'], 'phase2_traces': trace_stats['phase2_traces'],
'phase0_time': trace_stats['phase0_time'], 'phase1_time': trace_stats['phase1_time'], 'phase2_time': trace_stats['phase2_time'],
'sectors_filled': trace_stats['sectors_filled'], 'chip_connected': trace_stats['phase0_traces'] > 0
}
return f"Circuit: {count} traces in {stats['time']:.1f}s. Coverage: {coverage:.1f}%. Density: {trace_density:.1f}%. " \
f"Chip Traces: {stats['phase0_traces']} in {stats['phase0_time']:.1f}s. " \
f"Phase 1: {stats['phase1_traces']} traces in {stats['phase1_time']:.1f}s. " \
f"Phase 2: {stats['phase2_traces']} traces in {stats['phase2_time']:.1f}s."
# Execute with default parameters
create_circuit_pattern()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment