174 lines
7.4 KiB
Python
174 lines
7.4 KiB
Python
# 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 = 90.0, 12.0
|
|
EDGE_BEVEL, EDGE_SEG = 1.4, 3
|
|
|
|
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 = 62.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.
|
|
SECTORS = [
|
|
("RESPONSIBLE", 174, [146, 174, 202]),
|
|
("ACCOUNTABLE", 58, [58]),
|
|
("CONSULTED", -20, [22, -6, -34, -62]),
|
|
("INFORMED", -110, [-96, -124]),
|
|
]
|
|
WORD_R = RING_R + SOCK_D/2 + 8 # Labels ausserhalb der Sockel
|
|
WORD_SIZE, WORD_DEP = 6.0, 0.9
|
|
DESIGN_SIZE, DESIGN_DEP, DESIGN_POS = 9.0, 1.0, (0, -32)
|
|
|
|
CARD_CY, CARD_BW, CARD_BD, CARD_BH = 72.0, 76.0, 18.0, 14.0
|
|
SLOT_W, SLOT_T, SLOT_TILT = 63.0, 4.0, 12.0
|
|
|
|
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):
|
|
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()
|
|
|
|
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')
|
|
|
|
# Action-Card-Schlitz (oben offen, leicht nach hinten geneigt)
|
|
slot = cube(SLOT_W, SLOT_T, 40, (0, CARD_CY, 22))
|
|
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:
|
|
c = math.cos(math.radians(wc))
|
|
rot = -90 if c > 0.7 else (90 if c < -0.7 else 0)
|
|
engrave(base, name, WORD_R*math.cos(math.radians(wc)), WORD_R*math.sin(math.radians(wc)), rot)
|
|
|
|
# Phasenname unten im Innenkreis
|
|
engrave(base, PHASE_NAME, DESIGN_POS[0], DESIGN_POS[1], 0, DESIGN_SIZE, DESIGN_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=(235, -295, 270)); 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)
|