"""Generate detailed Blender GLB design references for the 13 LinuxCNC cases. The meshes are procedural design references. Dimensions are plausible layout values in metres and are carried as editable GLB geometry, while kinematics and calibration remain the responsibility of the machine manager metadata. """ import bpy, json, math, os, hashlib, sys from mathutils import Vector ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) OUT = os.path.join(ROOT, "web", "react", "public", "machine-cases", "physical") os.makedirs(OUT, exist_ok=True) COLORS = { "cast": (0.16, 0.22, 0.25, 1), "paint": (0.08, 0.35, 0.42, 1), "light": (0.48, 0.58, 0.61, 1), "steel": (0.20, 0.27, 0.30, 1), "rail": (0.68, 0.76, 0.78, 1), "black": (0.015, 0.022, 0.025, 1), "yellow": (0.86, 0.48, 0.08, 1), "orange": (0.75, 0.23, 0.05, 1), "glass": (0.03, 0.18, 0.21, 1), "rubber": (0.02, 0.025, 0.027, 1), "joint": (0.78, 0.28, 0.05, 1), } MATS = {} def mat(name, metallic=0.35, rough=0.32): if name in MATS: return MATS[name] m = bpy.data.materials.new("M_" + name) m.diffuse_color = COLORS[name] m.use_nodes = True bsdf = m.node_tree.nodes.get("Principled BSDF") bsdf.inputs["Base Color"].default_value = COLORS[name] bsdf.inputs["Metallic"].default_value = metallic bsdf.inputs["Roughness"].default_value = rough MATS[name] = m return m def collection(name): c = bpy.data.collections.new(name); bpy.context.scene.collection.children.link(c); return c def link(obj, parent): for c in list(obj.users_collection): c.objects.unlink(obj) parent.objects.link(obj); return obj def group(parent, name, position=(0, 0, 0)): g = bpy.data.objects.new(name, None) if hasattr(parent, "objects"): parent.objects.link(g) else: g.parent = parent g.location = position return g def root_object(scene): root = bpy.data.objects.new("machine_root", None) scene.collection.objects.link(root) return root def box(name, size, loc, material="steel", parent=None, bevel=0.012): bpy.ops.mesh.primitive_cube_add(location=loc) o=bpy.context.object; o.name=name; o.dimensions=size; bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) o.data.materials.append(mat(material)) if bevel: mod=o.modifiers.new("edge_radius", "BEVEL"); mod.width=bevel; mod.segments=3 if parent: o.parent=parent return o def cyl(name, radius, depth, loc, material="steel", parent=None, axis="z", vertices=32): bpy.ops.mesh.primitive_cylinder_add(vertices=vertices, radius=radius, depth=depth, location=loc) o=bpy.context.object; o.name=name; o.data.materials.append(mat(material)) if axis=="x": o.rotation_euler[1]=math.pi/2 elif axis=="y": o.rotation_euler[0]=math.pi/2 if parent: o.parent=parent return o def beam(name, a, b, radius, material="steel", parent=None): a,b=Vector(a),Vector(b); d=b-a o=cyl(name,radius,d.length/1.0,a+d/2,material,parent,"z",24) o.rotation_mode="QUATERNION"; o.rotation_quaternion=Vector((0,0,1)).rotation_difference(d.normalized()); return o def bolt_row(parent, start, count, step, axis="x", radius=0.006): for i in range(count): p=list(start); p["xyz".index(axis)] += i*step; cyl(f"bolt_{axis}_{i+1}",radius,0.012,p,"black",parent,axis) def rail(parent, name, loc, length, axis="x"): box(name+"_guide", (length,.026,.026) if axis=="x" else (.026,length,.026) if axis=="y" else (.026,.026,length), loc, "rail", parent, .004) bolt_row(parent, [loc[0]-length/2+0.04,loc[1],loc[2]], max(2,int(length/.16)), .16, axis, .004) def rotary_group(parent,name,axis,loc,radius=0.22,thickness=.08): g=group(parent,name,loc) cyl(name+"_bearing",radius*.38,thickness*1.8,(0,0,0),"black",g,axis) cyl(name+"_axis",radius*.25,thickness*2.1,(0,0,0),"yellow",g,axis) return g def industrial_mill(id, coords, title, rotary): scene=base_scene(id,coords,"design-reference" if id not in ("xyzac-trt","xyzbc-trt") else "real-reference",title) root=root_object(scene) base=group(root,"base"); box("bed_casting",(1.45,.95,.18),(0,0,.09),"cast",base); box("chip_tray",(1.38,.82,.035),(0,0,.20),"black",base) for x in (-.62,.62): box("column",(.14,.20,1.15),(x,.22,.78),"cast",base); rail(base,"vertical_rail",(x, .105, .78),.86,"z") box("bridge_crossbeam",(1.28,.20,.16),(0,.22,1.27),"paint",base) x=group(root,"x_slide"); box("x_carriage",(.30,.22,.18),(0,.10,1.15),"paint",x); rail(x,"x_linear_rail",(0,.04,1.26),1.05,"x") y=group(x,"y_saddle"); box("y_saddle_casting",(.30,.33,.14),(0,0,1.12),"cast",y); rail(y,"y_linear_rail",(0,-.13,1.02),.62,"y") z=group(y,"z_slide"); box("z_ram",(.22,.22,.56),(0,.0,.80),"paint",z); rail(z,"z_linear_rail",(.12,0,.80),.48,"z") tool=group(z,"tool"); cyl("spindle_motor",.075,.24,(0,0,.45),"black",tool); cyl("spindle_quill",.048,.23,(0,0,.22),"rail",tool); cyl("tool_holder",.024,.11,(0,0,.04),"yellow",tool) tilt=group(base, "a_tilt_frame" if rotary=="AC" else "b_trunnion" if rotary=="BC" else "a_outer_table", [0,-.03,.27]) axis="x" if rotary in ("AC","AB") else "y"; rotary_group(tilt,"tilt_axis",axis,(0,0,0),.32,.10) work=group(tilt,"c_table" if rotary in ("AC","BC") else "b_inner_table") rotary_group(work,"rotary_axis","z",(0,0,.08),.27,.10); cyl("work_platter",.24,.06,(0,0,.15),"rail",work); box("fixture_plate",(.28,.18,.04),(0,0,.20),"yellow",work) scene["linuxcnc.parts"]=["base","x_slide","y_saddle","z_slide","tool",tilt.name,work.name] return scene def bridge_scene(): scene=base_scene("bridge-mill-5axiskins","XYZBCWY","real-reference","Bridge Mill bi-rotary head") root=root_object(scene); base=group(root,"base") box("foundation",(2.4,1.4,.22),(0,0,.11),"cast",base); box("moving_x_table",(2.05,1.05,.11),(0,0,.28),"steel",base) for x in (-.95,.95): box("portal_column",(.20,.42,1.75),(x,.18,1.0),"cast",base) box("portal_beam",(2.15,.42,.22),(0,.18,1.82),"paint",base); x=group(root,"x_slide"); box("bridge_carriage",(.38,.36,.24),(0,.18,1.63),"steel",x); y=group(x,"y_slide"); box("cross_carriage",(.38,.50,.20),(0,0,1.62),"paint",y); z=group(y,"z_slide"); box("ram",(.22,.22,.65),(0,0,1.32),"steel",z) b=group(z,"b_head"); rotary_group(b,"b_axis","y",(0,0,.95),.23,.10); c=group(b,"c_head"); rotary_group(c,"c_axis","z",(0,0,.8),.17,.10); cyl("spindle",.055,.25,(0,0,.58),"black",c); cyl("tool",.022,.18,(0,0,.36),"yellow",c); box("y_left_drive",(.10,.20,.18),(-1.04,.18,1.72),"orange",base); box("y_right_drive",(.10,.20,.18),(1.04,.18,1.72),"orange",base); scene["linuxcnc.parts"]=["base","x_slide","y_slide","z_slide","b_head","c_head","y_left_drive","y_right_drive"]; return scene def parallel_scene(id,count,title): scene=base_scene(id,"XYZABC" if count==6 else "XYZAB","real-reference",title); root=root_object(scene); base=group(root,"base"); box("base_plate",(1.15,.90,.10),(0,0,.05),"cast",base); plat=group(root,"moving_platform",[0,0,.75]); box("platform_plate",(.46,.46,.07),(0,0,0),"paint",plat) for i in range(count): a=2*math.pi*i/count; b=2*math.pi*i/count+.4; beam(f"strut_{i+1}_lower",[.43*math.cos(a),.43*math.sin(a),.13],[.20*math.cos(b),.20*math.sin(b),.67],.027,"steel",base); beam(f"strut_{i+1}_upper",[.20*math.cos(b),.20*math.sin(b),.67],[.0,.0,.75],.014,"yellow",plat) cyl("spindle",.07,.22,(0,0,-.16),"black",plat); cyl("tool",.025,.18,(0,0,-.36),"yellow",plat); scene["linuxcnc.parts"]=["base"]+[f"strut_{i+1}" for i in range(count)]+["moving_platform","spindle","tool"]; return scene def robot_scene(id, five=False): coords="XYZAB" if five else "XYZABC"; scene=base_scene(id,coords,"real-reference","SCORBOT ER-4U" if five else "PUMA serial robot"); root=root_object(scene); base=group(root,"base"); box("robot_pedestal",(.55,.55,.18),(0,0,.09),"cast",base) parents=[base]; names=["j1_base","j2_shoulder","j3_elbow","j4_wrist","j5_pitch"]+([] if five else ["j6_wrist"]) heights=[.25,.32,.27,.14,.10,.08] for i,n in enumerate(names): g=group(parents[-1],n,[0,0,heights[i] if i