# RACI-Konsolen-Board (rund) — FUNKTIONS-BLANK · Blender-Generator (bpy) # SLC-Workshop Tabletop · 1 Blender-Unit = 1 mm # Ziel: korrekt & schlicht (Maße/Passungen stimmen). Der edle Look wird SEPARAT # interaktiv draufmodelliert. Hier nur: runde Platte, Chip-Mulde, 10 Sockel in # 4 klar getrennten RACI-Sektoren (R3 A1 C4 I2), Kartenschlitz, lesbare Labels. import bpy, math, os # ----------------------------- Parameter (mm) ----------------------------- R_BOARD, BASE_H = 130.0, 14.0 EDGE_BEVEL, EDGE_SEG = 1.6, 3 CHIP_D, CHIP_DEP = 50.0, 2.2 # Mittelmulde Ø50 (Chip) NOTCH_D = 14.0 SOCK_D, SOCK_DEP = 32.8, 2.0 # Figuren-Sockel Ø32 + 0,8 Spiel RING_R = 88.0 PHASE_NAME, PHASE_COLOR = "DESIGN", (0.184, 0.502, 0.788, 1) # #2f80c9 # Sektor: Name, Label-Mittenwinkel, Sockel-Winkel (Grad; 90=oben, Top frei fuer Karte). # Lücken zwischen den Sektoren (34-36°) > Lücken innerhalb (28°) -> Gruppen klar sichtbar. # B+ : symmetrisches 90°-Kreuz (Trennlinien bei 0/90/180/270), Sektoren in den # Diagonalen, Sockel je Sektor gleichmaessig ueber den Sektor verteilt. Karte oben. SECTORS = [ ("RESPONSIBLE", 135, [98, 135, 172]), ("ACCOUNTABLE", 45, [45]), ("CONSULTED", -45, [-8, -33, -57, -82]), ("INFORMED", -135, [-98, -172]), ] DIVIDERS = [0, 90, 180, 270] WORD_R = RING_R + SOCK_D/2 + 9 # Labels ausserhalb der Sockel WORD_SIZE, WORD_DEP = 6.0, 0.9 DESIGN_SIZE, DESIGN_DEP, DESIGN_POS = 9.0, 1.0, (0, -48) CARD_CY, CARD_BW, CARD_BD, CARD_BH = 116.0, 84.0, 22.0, 16.0 SLOT_W, SLOT_T, SLOT_DEPTH, SLOT_TILT = 70.0, 5.0, 27.0, 12.0 # tief bis ~3 mm vorm Boden TOP = BASE_H def _outdir(): d = os.path.dirname(bpy.data.filepath) if d: return d try: return os.path.dirname(os.path.abspath(__file__)) except NameError: return os.path.expanduser("~") HERE = _outdir() STL_OUT, PNG_OUT = os.path.join(HERE, "raci-board.stl"), os.path.join(HERE, "raci_preview.png") print("Ausgabe-Ordner:", HERE) # ----------------------------- 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 engrave(board, body, x, y, rotz=0, size=WORD_SIZE, dep=WORD_DEP, ztop=None): z0 = (TOP if ztop is None else ztop) - dep try: bpy.ops.object.text_add(location=(x, y, z0)) 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() base = cyl(R_BOARD*2, BASE_H, (0, 0, BASE_H/2), verts=160) 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') # Chip-Mulde + Greifkerbe boolean(base, cyl(CHIP_D, 6, (0, 0, TOP - CHIP_DEP + 3)), 'DIFFERENCE') boolean(base, cyl(NOTCH_D, 6, (0, -CHIP_D/2, TOP - CHIP_DEP + 3)), 'DIFFERENCE') # Sockelmulden (4 Sektoren, ueber Luecken getrennt) for _, _, angles in SECTORS: for a in angles: boolean(base, cyl(SOCK_D, 6, (RING_R*math.cos(math.radians(a)), RING_R*math.sin(math.radians(a)), TOP - SOCK_DEP + 3)), 'DIFFERENCE') # Sektor-Trennlinien — symmetrisches Kreuz (0/90/180/270), flach graviert _di, _do = CHIP_D/2 + 5, RING_R + SOCK_D/2 + 6 for a in DIVIDERS: g = cube(_do - _di, 2.6, 1.4, (0, 0, 0)) g.rotation_euler = (0, 0, math.radians(a)) g.location = (((_di+_do)/2)*math.cos(math.radians(a)), ((_di+_do)/2)*math.sin(math.radians(a)), TOP - 0.7) boolean(base, g, 'DIFFERENCE') # Action-Card-Schlitz (oben offen, 70x5 mm, tief bis ~3 mm vorm Boden, leicht geneigt) _sb = BASE_H + CARD_BH - SLOT_DEPTH; _sh = SLOT_DEPTH + 20 slot = cube(SLOT_W, SLOT_T, _sh, (0, CARD_CY, _sb + _sh/2)) slot.rotation_euler = (math.radians(-SLOT_TILT), 0, 0); bpy.ops.object.transform_apply(rotation=True) boolean(base, slot, 'DIFFERENCE') # dezente Rand-Linie rim = cyl((R_BOARD-7)*2, 1.4, (0, 0, TOP-0.7)) boolean(rim, cyl((R_BOARD-8.6)*2, 2.0, (0, 0, TOP-0.7)), 'DIFFERENCE') boolean(base, rim, 'DIFFERENCE') # Sektor-Labels (gleich gross): links/rechts vertikal, oben/unten waagerecht -> lesbar & passt for name, wc, _ in SECTORS: rot = (wc - 90) if math.sin(math.radians(wc)) >= 0 else (wc + 90) # tangential, lesbar engrave(base, name, WORD_R*math.cos(math.radians(wc)), WORD_R*math.sin(math.radians(wc)), rot) # Phasenname in die Chip-Mulde (Boden) graviert — durch den klaren Chip sichtbar engrave(base, PHASE_NAME, 0, 0, 0, DESIGN_SIZE, DESIGN_DEP, ztop=TOP - CHIP_DEP) base.name = "RACI-Board" try: bpy.ops.object.shade_auto_smooth(angle=math.radians(30)) except Exception: try: bpy.ops.object.shade_flat() except Exception: pass # Material (Kontext-Farbe; fuer den Blank zweitrangig) mat = bpy.data.materials.new("Phase"); mat.use_nodes = True bsdf = next((n for n in mat.node_tree.nodes if n.type == 'BSDF_PRINCIPLED'), None) if bsdf: bsdf.inputs["Base Color"].default_value = PHASE_COLOR try: bsdf.inputs["Roughness"].default_value = 0.5 except Exception: pass base.data.materials.clear(); base.data.materials.append(mat) # ----------------------------- Vorschau-Render ----------------------------- try: sc = bpy.context.scene # Meshy-tauglich: heller, neutraler Hintergrund + gleichmaessiges Licht try: bg = next((n for n in sc.world.node_tree.nodes if n.type == 'BACKGROUND'), None) if bg: bg.inputs[0].default_value = (0.92, 0.92, 0.92, 1) bg.inputs[1].default_value = 1.6 except Exception: pass bpy.ops.object.light_add(type='SUN', location=(140, -180, 260)); bpy.context.object.data.energy = 2.2 bpy.ops.object.light_add(type='AREA', location=(-140, -60, 180)) bpy.context.object.data.energy = 9000; bpy.context.object.data.size = 320 bpy.ops.object.empty_add(location=(0, 0, 5)); tgt = bpy.context.object bpy.ops.object.camera_add(location=(330, -410, 370)); cam = bpy.context.object cam.data.lens = 50 con = cam.constraints.new('TRACK_TO'); con.target = tgt con.track_axis = 'TRACK_NEGATIVE_Z'; con.up_axis = 'UP_Y' sc.camera = cam sc.render.resolution_x, sc.render.resolution_y = 1500, 1100; 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 ----------------------------- 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) except Exception: try: bpy.ops.export_mesh.stl(filepath=STL_OUT, use_selection=True) except Exception as e: print("STL-Export manuell noetig:", e) print("STL:", STL_OUT)