"""Quady3D AI original CC0 Blender example. No external model service is called. Run: blender --background --python parametric-organizer.py -- --output ./organizer Edit dimensions below in millimeters, then rerun. Blender 4.5 LTS tested. """ import argparse import json import math from pathlib import Path import sys import bpy import bmesh from mathutils import Vector WIDTH_MM = 80 DEPTH_MM = 50 HEIGHT_MM = 48 WALL_MM = 3 def main(): parser = argparse.ArgumentParser() parser.add_argument("--output", default="./organizer") args = parser.parse_args(sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []) dest = Path(args.output).resolve(); dest.mkdir(parents=True, exist_ok=True) if not 60 <= WIDTH_MM <= 160 or not 40 <= DEPTH_MM <= 100 or not 30 <= HEIGHT_MM <= 100 or not 2 <= WALL_MM <= 5: raise ValueError("Use width 60–160, depth 40–100, height 30–100 and wall 2–5 mm.") bpy.ops.object.select_all(action="SELECT"); bpy.ops.object.delete(use_global=False) scene = bpy.context.scene; scene.unit_settings.system = "METRIC"; scene.unit_settings.scale_length = 1 def material(name, color): m = bpy.data.materials.new(name); m.diffuse_color = (*color, 1); return m teal = material("Teal base", (.035, .46, .39)); orange = material("Orange cup", (.86, .31, .095)) bpy.ops.mesh.primitive_cube_add(size=1, location=(0, 0, WALL_MM / 2000)) base = bpy.context.object; base.name = "Base"; base.dimensions = (WIDTH_MM / 1000, DEPTH_MM / 1000, WALL_MM / 1000) bpy.ops.object.transform_apply(location=False, rotation=False, scale=True); base.data.materials.append(teal) # A closed cup shell with an open cavity; the rim joins inner and outer walls. radius = min(DEPTH_MM / 2 - 5, 22); inner = radius - WALL_MM cx = -WIDTH_MM / 2 + radius + 5 rings = [(0, WALL_MM), (radius, WALL_MM), (radius, HEIGHT_MM), (inner, HEIGHT_MM), (inner, WALL_MM * 2), (0, WALL_MM * 2)] vertices = []; segments = 64 for r,z in rings: for i in range(segments): angle = 2 * math.pi * i / segments vertices.append(((cx + r * math.cos(angle)) / 1000, r * math.sin(angle) / 1000, z / 1000)) faces = [] for j in range(len(rings) - 1): for i in range(segments): k = (i + 1) % segments if rings[j][0] == 0: faces.append((j * segments, (j+1) * segments+k, (j+1) * segments+i)) elif rings[j+1][0] == 0: faces.append((j*segments+i,j*segments+k,(j+1)*segments)) else: faces.append((j*segments+i,j*segments+k,(j+1)*segments+k,(j+1)*segments+i)) mesh = bpy.data.meshes.new("Cup shell"); mesh.from_pydata(vertices, [], faces); mesh.update() cup = bpy.data.objects.new("Cup", mesh); scene.collection.objects.link(cup); cup.data.materials.append(orange) # Remove unused center-ring vertices and orient each closed shell consistently. bm = bmesh.new(); bm.from_mesh(mesh); bmesh.ops.remove_doubles(bm, verts=list(bm.verts), dist=1e-8) bmesh.ops.recalc_face_normals(bm, faces=list(bm.faces)); bm.to_mesh(mesh); bm.free() report = {"creator":"Quady3D AI", "license":"CC0-1.0", "blender":bpy.app.version_string, "dimensionsMm":[WIDTH_MM, DEPTH_MM, HEIGHT_MM], "parts":[]} for obj in [base,cup]: bm=bmesh.new(); bm.from_mesh(obj.data) report["parts"].append({"name":obj.name,"boundaryEdges":sum(e.is_boundary for e in bm.edges),"nonManifoldEdges":sum(not e.is_manifold for e in bm.edges),"volumeMm3":abs(bm.calc_volume())*1e9}) bm.free(); obj.select_set(True) bpy.context.view_layer.objects.active=base bpy.ops.export_scene.gltf(filepath=str(dest/"organizer.glb"),export_format="GLB",use_selection=True) bpy.ops.wm.stl_export(filepath=str(dest/"organizer.stl"),export_selected_objects=True,global_scale=1000) # Presentation objects are excluded from GLB/STL and remain editable in the blend. bpy.ops.object.camera_add(location=(.15,-.18,.14)); camera=bpy.context.object direction=Vector((0,0,.02))-camera.location; camera.rotation_euler=direction.to_track_quat("-Z","Y").to_euler() camera.data.type="ORTHO"; camera.data.ortho_scale=.18; scene.camera=camera scene.render.engine="BLENDER_WORKBENCH"; scene.display.shading.light="STUDIO"; scene.display.shading.color_type="MATERIAL" scene.display.shading.show_shadows=True; scene.display.shading.show_cavity=True; scene.display.shading.background_type="WORLD"; scene.world.color=(.85,.86,.88) scene.render.resolution_x=1200; scene.render.resolution_y=900; scene.render.resolution_percentage=100 scene.render.image_settings.file_format="PNG"; scene.render.filepath=str(dest/"organizer.png") bpy.ops.wm.save_as_mainfile(filepath=str(dest/"organizer.blend")); bpy.ops.render.render(write_still=True) (dest/"inspection.json").write_text(json.dumps(report,indent=2)+"\n") print("QUADY_INSPECTION", json.dumps(report)) if __name__ == "__main__": main()