# RACI-Konsolen-Board — Blender-Generator (bpy) # SLC-Workshop Tabletop · Einheiten: 1 Blender-Unit = 1 mm # Baut das Board parametrisch mit gefasten Kanten (Bevel), Mulden, Sektoren, # gravierten Labels, Action-Card-Steckschlitz; rendert eine Vorschau und # exportiert eine STL. Startgeruest v1 — in Blender 4.x getestet gegen die API, # einzelne Schritte sind per try/except abgesichert (Labels optional). # # Lauf: Blender -> Scripting -> Open -> Run ODER blender -b -P raci-board.py import bpy, bmesh, math, os from mathutils import Vector # ----------------------------- Parameter (mm) ----------------------------- BOARD_W, BOARD_D, BASE_H = 210.0, 210.0, 10.0 EDGE_BEVEL, EDGE_SEG = 1.4, 4 DIAL_CX, DIAL_CY = 0.0, -15.0 CHIP_D, CHIP_DEP = 40.6, 1.8 # Acryl-Chip Ø40 x 2 mm NOTCH_D = 12.0 SOCK_D, SOCK_DEP = 25.3, 1.5 # Figuren-Sockel Ø24,5 RING_R = 48.0 N_SOCK = 10 # Winkel ab oben (90°), 36°-Teilung RIDGE_H, RIDGE_W = 2.6, 3.2 DIVIDERS = [72, -72, -144, 108] # Sektor-Mitten (fuer grosse Buchstaben) R3 A1 C4 I2 LETTERS = [(90, "A"), (0, "C"), (-108, "I"), (162, "R")] CARD_CY = 78.0 CARD_BW, CARD_BD, CARD_BH = 84.0, 22.0, 16.0 SLOT_W, SLOT_T, SLOT_TILT = 63.0, 4.0, 12.0 LET_SIZE, LET_DEP = 11.0, 1.0 WORD_SIZE, WORD_DEP = 5.0, 0.8 TOP = BASE_H HERE = os.path.dirname(bpy.data.filepath) or os.path.dirname(os.path.abspath(__file__)) STL_OUT = os.path.join(HERE, "raci-board.stl") PNG_OUT = os.path.join(HERE, "raci_preview.png") # ----------------------------- Helfer ----------------------------- def clear_scene(): bpy.ops.object.select_all(action='SELECT') bpy.ops.object.delete(use_global=False) for blk in (bpy.data.meshes, bpy.data.materials, bpy.data.curves): for d in list(blk): if d.users == 0: blk.remove(d) def cube(sx, sy, sz, loc): bpy.ops.mesh.primitive_cube_add(size=1, location=loc) o = bpy.context.object; o.scale = (sx, sy, sz) bpy.ops.object.transform_apply(scale=True) return o def cyl(d, h, loc, verts=96): bpy.ops.mesh.primitive_cylinder_add(radius=d/2.0, depth=h, location=loc, vertices=verts) return bpy.context.object def boolean(obj, tool, op='DIFFERENCE'): m = obj.modifiers.new("bool", 'BOOLEAN'); m.operation = op m.object = tool try: m.solver = 'EXACT' except Exception: pass bpy.context.view_layer.objects.active = obj bpy.ops.object.modifier_apply(modifier=m.name) bpy.data.objects.remove(tool, do_unlink=True) def apply_bevel(obj, width=EDGE_BEVEL, seg=EDGE_SEG, ang=30): m = obj.modifiers.new("bevel", 'BEVEL') m.width = width; m.segments = seg m.limit_method = 'ANGLE'; m.angle_limit = math.radians(ang) bpy.context.view_layer.objects.active = obj bpy.ops.object.modifier_apply(modifier=m.name) def cut_text(board, body, x, y, rotz=0, size=LET_SIZE, dep=LET_DEP): try: bpy.ops.object.text_add(location=(x, y, TOP - dep)) t = bpy.context.object; t.data.body = body t.data.size = size; t.data.extrude = dep + 1.5 t.data.align_x = 'CENTER'; t.data.align_y = 'CENTER' t.rotation_euler = (0, 0, math.radians(rotz)) bpy.ops.object.convert(target='MESH') boolean(board, t, 'DIFFERENCE') except Exception as e: print("Label uebersprungen (%s): %s" % (body, e)) # ----------------------------- Aufbau ----------------------------- clear_scene() # Basisplatte + Kartenblock, beide gefast, dann vereinen base = cube(BOARD_W, BOARD_D, BASE_H, (0, 0, BASE_H/2)) apply_bevel(base) block = cube(CARD_BW, CARD_BD, BASE_H + CARD_BH, (0, CARD_CY, (BASE_H + CARD_BH)/2)) apply_bevel(block, width=1.0) boolean(base, block, 'UNION') # Gefaste Ecke vorne rechts (Optik) cc = cube(28, 28, BASE_H + 4, (BOARD_W/2, -BOARD_D/2, BASE_H/2)) cc.rotation_euler = (0, 0, math.radians(45)); bpy.ops.object.transform_apply(rotation=True) boolean(base, cc, 'DIFFERENCE') # Chip-Mulde + Greifkerbe boolean(base, cyl(CHIP_D, 6, (DIAL_CX, DIAL_CY, TOP - CHIP_DEP + 3)), 'DIFFERENCE') boolean(base, cyl(NOTCH_D, 6, (DIAL_CX, DIAL_CY - CHIP_D/2, TOP - CHIP_DEP + 3)), 'DIFFERENCE') # 10 Sockelmulden im Ring for i in range(N_SOCK): a = math.radians(90 - i * 36) x = DIAL_CX + RING_R * math.cos(a); y = DIAL_CY + RING_R * math.sin(a) boolean(base, cyl(SOCK_D, 6, (x, y, TOP - SOCK_DEP + 3)), 'DIFFERENCE') # Deko-Rillen im Mittelfeld for rr in [24, 27, 30, 33]: ring = cyl(rr*2, 0.8, (DIAL_CX, DIAL_CY, TOP - 0.25)) inner = cyl((rr-0.8)*2, 1.2, (DIAL_CX, DIAL_CY, TOP - 0.25)) boolean(ring, inner, 'DIFFERENCE') boolean(base, ring, 'DIFFERENCE') # Sektor-Trennstege (erhaben) ri, ro = CHIP_D/2 + 3, RING_R + SOCK_D/2 + 4 for a in DIVIDERS: r = cube(ro - ri, RIDGE_W, RIDGE_H, ((ri+ro)/2, 0, TOP + RIDGE_H/2)) r.rotation_euler = (0, 0, math.radians(a)) # um Dial-Zentrum rotieren: erst an Zentrum verschieben r.location = (DIAL_CX + ((ri+ro)/2)*math.cos(math.radians(a)), DIAL_CY + ((ri+ro)/2)*math.sin(math.radians(a)), TOP + RIDGE_H/2) bpy.ops.object.transform_apply(rotation=False) boolean(base, r, 'UNION') # Action-Card-Steckschlitz (oben offen, nach hinten geneigt) slot = cube(SLOT_W, SLOT_T, 38, (0, CARD_CY, 22)) slot.rotation_euler = (math.radians(-SLOT_TILT), 0, 0) bpy.ops.object.transform_apply(rotation=True) boolean(base, slot, 'DIFFERENCE') # Gravierte Rand-Linie outer = cube(BOARD_W-14, BOARD_D-14, 1.4, (0, 0, TOP-0.7)) inner = cube(BOARD_W-17, BOARD_D-17, 2.0, (0, 0, TOP-0.7)) boolean(outer, inner, 'DIFFERENCE'); boolean(base, outer, 'DIFFERENCE') # Labels: grosse R/A/C/I + Woerter for ang, ch in LETTERS: rl = RING_R + 13 cut_text(base, ch, DIAL_CX + rl*math.cos(math.radians(ang)), DIAL_CY + rl*math.sin(math.radians(ang))) cut_text(base, "RESPONSIBLE", -(RING_R+29), DIAL_CY+6, 90, WORD_SIZE, WORD_DEP) cut_text(base, "CONSULTED", (RING_R+29), DIAL_CY+6, -90, WORD_SIZE, WORD_DEP) cut_text(base, "INFORMED", DIAL_CX, DIAL_CY-(RING_R+30), 0, WORD_SIZE, WORD_DEP) cut_text(base, "ACCOUNTABLE", DIAL_CX, CARD_CY - CARD_BD/2 - 8, 0, WORD_SIZE, WORD_DEP) base.name = "RACI-Board" bpy.ops.object.shade_smooth() # ----------------------------- Material ----------------------------- mat = bpy.data.materials.new("BoardBlue"); mat.use_nodes = True bsdf = mat.node_tree.nodes.get("Principled BSDF") if bsdf: bsdf.inputs["Base Color"].default_value = (0.10, 0.16, 0.30, 1) try: bsdf.inputs["Roughness"].default_value = 0.55 except Exception: pass base.data.materials.append(mat) # ----------------------------- Vorschau-Render (guarded) ----------------------------- try: bpy.ops.object.light_add(type='SUN', location=(120, -160, 220)) bpy.context.object.data.energy = 3.0 bpy.ops.object.camera_add(location=(150, -190, 210)) cam = bpy.context.object cam.rotation_euler = (math.radians(58), 0, math.radians(38)) bpy.context.scene.camera = cam sc = bpy.context.scene sc.render.resolution_x, sc.render.resolution_y = 1400, 1000 sc.render.filepath = PNG_OUT try: sc.render.engine = 'BLENDER_EEVEE_NEXT' except Exception: try: sc.render.engine = 'BLENDER_EEVEE' except Exception: pass bpy.ops.render.render(write_still=True) print("Vorschau:", PNG_OUT) except Exception as e: print("Render uebersprungen:", e) # ----------------------------- STL-Export (versionstolerant) ----------------------------- bpy.ops.object.select_all(action='DESELECT') base.select_set(True); bpy.context.view_layer.objects.active = base try: bpy.ops.wm.stl_export(filepath=STL_OUT, export_selected_objects=True) # Blender >= 4.1 except Exception: try: bpy.ops.export_mesh.stl(filepath=STL_OUT, use_selection=True) # Blender <= 4.0 except Exception as e: print("STL-Export fehlgeschlagen — bitte manuell File>Export>STL:", e) print("STL:", STL_OUT)