Script-driven modeling, with visual checks.
The ABENE was constructed through Blender’s Python API, rendered in Blender, inspected as images, and revised through further scripts. Computer Use provided a short desktop interaction layer. MCP carried some tool calls, but no Blender-specific MCP server created or edited the model.
Computer Use, Python and MCP are different layers.
| Layer | Actual tools | What it did |
|---|---|---|
| Native desktop control | @oai/sky through mcp__node_repl__js | Listed applications, selected Blender’s returned window, captured its state, and sent a few keyboard commands. It did not manually model the machine. |
| Model authoring | Python, bpy, mathutils | Created and modified meshes, curves, text, materials, lighting and cameras inside Blender 5.2. |
| Execution | exec_command, PowerShell, Blender CLI | Ran existing Blender with background Python scripts; also launched saved files in a Blender process. |
| Script editing | apply_patch and file operations | Wrote builders and small repair scripts in work/. |
| Research | web__run | Read the user-provided Yumpu brochure as reference information. |
| Visual inspection | view_image; local thumbnail conversion when needed | Reviewed saved renders and the supplied reference. Large images were downscaled for inspection without changing the source render. |
No Blender-specific MCP server, remote modeling service, image-generation tool, or manual vertex-editing workflow was used in the recorded ABENE construction.
A known table size, a photo-estimated exterior.
The supplied photograph established the asymmetric enclosure, left and right safety doors, central milling head, table, lamp, and separate operator console. The linked ABENE VHF-680 CNC brochure supplied the 900 × 500 mm table, seven 14 mm T-slot openings, and X/Y/Z travel of 800/510/475 mm. The table was used as a scale anchor.
Overall dimensions, casting profiles, enclosure details, console proportions and hidden surfaces were inferred from the photograph. Travel values were reference specifications; they were not implemented as functioning motion constraints. The work produced a visual reconstruction, not measured manufacturer CAD.
A brief UI check—not the modeling method.
- Initialized the Computer Use runtime. Read the Computer Use skill and its guidance/API/confirmation files, imported
@oai/sky, and enumerated applications. - Selected the existing Blender window. The returned app was Blender 5.2 with an unsaved scene. The initial state capture showed the startup scene.
- Attempted a console shortcut. A
Shift+F4input was sent, but the following capture showed a File menu rather than a Python console. The recorded evidence does not support claiming that Python code was entered through the GUI. - Checked opening a saved model. After Escape,
Ctrl+Oproduced a “Save changes before closing?” prompt. Escape cancelled the operation, preserving the unsaved scene. - Used the CLI for the saved asset. Blender was then invoked directly with the generated
.blendpath; its output confirmed that it read the file. Later window-list checks did not consistently expose the running Blender window, so those checks were not proof of a visible, interactive session.
Representative recorded Computer Use calls
// Called through the MCP Node REPL tool.
const { sky } = await import("@oai/sky");
const apps = await sky.list_apps();
// Select the unique returned Blender window, then:
const w = await sky.get_window(returnedWindow);
await sky.get_window_state({window: w});
await sky.press_key({window: w, key: "Control_L+o"});
// Following the observed save prompt:
await sky.press_key({window: w, key: "Escape"});Condensed illustration of the recorded calls; not a standalone automation script.
Establishing the machine’s major assemblies.
build.py started a new Blender scene and supplied helper functions for boxes, cylinders, rods, tubes and text. It assembled the base, enclosure, table, head, handwheel, lamp, console and coiled cable as separate objects. Bevels and weighted normals softened manufactured edges. A studio floor, area lights and camera produced the first render.
This first pass was recognizable but too approximate for the user’s intended visualization quality. The window surrounds were simple bars, the enclosure was too boxy, and the camera/console proportions did not match the reference closely enough. The materials had basic Principled values but lacked the richer surface finish introduced in the second pass.
ABENE_VHF680.png. The user requested greater accuracy and PBR refinement after this version.How Blender Python was executed
& 'C:/Program Files/Blender Foundation/Blender 5.2/blender.exe' `
-b -t 12 --python '.../work/rebuild_v2.py'
# -b: background Blender; --python: execute the modeling script.
# bpy creates and saves real editable scene geometry.The first launch attempts resolved relative paths under C:\, so the commands were corrected to absolute paths with shell login behavior disabled. The geometry was created by Blender’s API, not by drawing a rendered image and converting it to a mesh.
A second builder replaced the first approximation.
rebuild_v2.py reconstructed the scene in named assembly collections. Rounded window cutters were subtracted from sheet-metal panels. Solid glazing, tapered lower aprons, table channels, stepped spindle pieces, selector markings, handwheel spokes, fittings and control keys added detail. The monitor and keyboard used separate local transforms to reproduce their different slopes.
Geometry choices
- Boxes and extruded profiles for castings and panels.
- Cylinders and rods for spindle steps, shafts and hardware.
- Curves for flexible hoses, handles and the coiled cable.
- Text objects for model labels and control legends.
- Small bevels and weighted normals for edge highlights.
Material choices
- Principled BSDF shaders with distinct base color, metallic and roughness settings.
- Procedural noise driving microscopic bump and roughness variation.
- Anisotropy on selected metal finishes.
- Transmission and IOR 1.49 for safety glazing.
- Separate paint, casting enamel, aluminium, steel, chrome, rubber, ABS and display glass.
These are authored procedural PBR materials, not scanned surface measurements or downloaded texture sets. A saved intermediate inventory recorded 19 materials and no external image dependencies. Its 823-object count preceded later mounting-part additions and should not be read as a final object census.
ABENE_VHF680_v2_hero.png; the original render is 2600 × 2600 pixels.The render checks changed the model.
| Observed problem | Correction | Evidence / script |
|---|---|---|
| PBR builder failed on a shader socket name. | Queried the installed Blender shader inputs and used Anisotropic. | rebuild_v2.py; the earlier Anisotropic IOR Level name was unavailable in this installation. |
| The first refined render showed solid doors instead of openings. | Corrected reversed profile-mesh face winding before evaluating boolean cuts. | Preview was inspected and the builder rerun. |
| The camera angle and floor horizon distracted from comparison. | Adjusted the camera and added a curved studio cyclorama. | finalize_v2.py. |
| A conduit and later a head hose stopped short of their housings. | Moved curve endpoints into the corresponding covers. | finalize_v2.py, connection_fix.py. |
| Close-up showed a gap between monitor and keyboard. | Added a connecting housing and pedestal mounting head. | console_mount_fix.py. |
| The added mounting head protruded through the keyboard deck. | Lowered it and reduced its height; re-rendered the affected views. | console_mount_trim.py. |
Rendering path
The final overall view used Cycles, 128 maximum samples and denoising. Close-ups used 1600 × 1600 pixels and 96 samples. Rendering began on the CPU; a later script detected an available OptiX device and used GPU rendering for corrections. The saved scene retained portable settings rather than saving the user’s global GPU preferences.
Representative logs recorded the overall image being saved at process elapsed 05:51 on the CPU run and 00:34 on a later GPU repair run. These are observed run timestamps, not a controlled hardware benchmark. Permission warnings for preferences, thumbnails and the OptiX cache appeared, but the model saves and image renders completed. No security settings were changed to bypass them.
What is supported by the record.
| Deliverable | Recorded role |
|---|---|
ABENE_VHF680.blend | Initial editable reconstruction. |
ABENE_VHF680_v2_PBR.blend | Refined editable scene with procedural materials and cameras. |
ABENE_VHF680_v2_hero.png…_spindle.png…_controls.png | Actual Blender render outputs used for visual review. |
Current artifact files may also contain later changes; this is a process reconstruction, not an immutable forensic archive.
Verified
- Editable Blender scenes were saved and renders were produced.
- Overall, spindle and console views were inspected; visible defects prompted repairs.
Not established
- Survey-grade dimensions or manufacturer CAD equivalence.
- Functional CNC behavior, machining accuracy or a rigged machine.
- Cross-renderer parity for procedural PBR shaders without conversion or baking.
Inspect the scripts without leaving this file.
These are the surviving working files, embedded in full. Some builders were edited after initial runs. They preserve the implementation but are not a pristine, one-command replay package: paths are machine-specific, several repair scripts are stateful, and rerunning all of them blindly can duplicate parts. The report does not execute them.
build.pyInitial blockout builder
import bpy, math, os
from mathutils import Vector
OUT=r'C:/Users/mtw/Documents/Codex/2026-09-09/x20/outputs'
bpy.ops.object.select_all(action='SELECT'); bpy.ops.object.delete(use_global=False)
def mat(n,c,metal=0,rough=.35):
m=bpy.data.materials.new(n); m.diffuse_color=(*c,1); m.use_nodes=True; p=m.node_tree.nodes.get('Principled BSDF'); p.inputs['Base Color'].default_value=(*c,1); p.inputs['Metallic'].default_value=metal; p.inputs['Roughness'].default_value=rough; return m
white=mat('Warm powder coated enclosure',(.72,.75,.73)); grey=mat('Cast iron grey',(.29,.33,.32),.5); steel=mat('Machined steel',(.48,.53,.55),.85,.23); black=mat('Graphite rubber',(.023,.03,.034)); blue=mat('Abene blue',(.015,.26,.47),.35); red=mat('Emergency red',(.7,.025,.018)); yellow=mat('Safety yellow',(.95,.64,.015)); screen=mat('LCD glass',(.012,.036,.047),.25,.16); ink=mat('Lettering',(.19,.23,.23)); light=mat('Display graphics',(.3,.75,.7))
def finish(o,n,m):
o.name=n; o.data.materials.append(m); return o
def box(n,p,d,m=white,b=.012):
bpy.ops.mesh.primitive_cube_add(size=1,location=p); o=bpy.context.object; o.dimensions=d; bpy.ops.object.transform_apply(location=False,rotation=False,scale=True); finish(o,n,m)
if b: mod=o.modifiers.new('Soft manufactured edges','BEVEL'); mod.width=b; mod.segments=3; o.modifiers.new('Weighted corner normals','WEIGHTED_NORMAL')
return o
def cyl(n,p,r,h,m=steel,axis=(0,0,1),verts=48):
bpy.ops.mesh.primitive_cylinder_add(vertices=verts,radius=r,depth=h,location=p); o=bpy.context.object; o.rotation_euler=Vector(axis).to_track_quat('Z','Y').to_euler(); finish(o,n,m); mod=o.modifiers.new('Edge bevel','BEVEL'); mod.width=.003; mod.segments=2; o.modifiers.new('Normals','WEIGHTED_NORMAL'); return o
def tube(n,pts,r,m=black):
cu=bpy.data.curves.new(n,'CURVE'); cu.dimensions='3D'; cu.bevel_depth=r; cu.bevel_resolution=3; s=cu.splines.new('POLY'); s.points.add(len(pts)-1)
for p,co in zip(s.points,pts): p.co=(*co,1)
o=bpy.data.objects.new(n,cu); bpy.context.collection.objects.link(o); finish(o,n,m); return o
def rod(n,a,b,r,m=steel): return cyl(n,(Vector(a)+Vector(b))/2,r,(Vector(b)-Vector(a)).length,m,Vector(b)-Vector(a))
def text(n,t,p,size,m=ink):
cu=bpy.data.curves.new(n,'FONT'); cu.body=t; cu.size=size; cu.extrude=.0005; o=bpy.data.objects.new(n,cu); bpy.context.collection.objects.link(o); o.location=p; o.rotation_euler=(math.pi/2,0,0); finish(o,n,m); return o
# front faces negative Y; scale in metres
box('Lower machine plinth',(0,.16,.26),(1.65,1.14,.27),grey)
box('Coolant catch basin',(0,-.05,.43),(1.75,1.30,.12),steel)
box('Front sill',(0,-.69,.38),(1.86,.09,.17))
box('Rear splash wall',(0,.69,1.35),(1.9,.09,1.86))
box('Heavy inclined column',(0,.42,1.22),(.50,.47,1.59),grey,.055)
box('Column vertical guide',(0,.155,1.5),(.35,.055,1.25),steel)
for x in [-.88,.88]:
box('Enclosure side',(x,.13,1.32),(.085,1.12,1.93))
box('Lower enclosure apron',(x,-.38,.55),(.24,.60,.45))
for y in [-.52,.61]:
cyl('Levelling screw',(x,y,.16),.018,.20); cyl('Foot pad',(x,y,.065),.05,.035,black); cyl('Caster wheel',(x,y-.035,.045),.045,.035,grey,(1,0,0))
# framed protective windows with open centre
for x,w in [(-.66,.48),(.75,.33)]:
y=-.50
for xx in [x-w/2,x+w/2]: box('Safety door upright',(xx,y,1.49),(.066,.08,1.57))
for z in [.71,2.26]: box('Safety door crossbar',(x,y,z),(w+.065,.08,.085))
box('Door lower label panel',(x,y,.63),(w+.065,.08,.23))
for z in [.94,1.96]: box('Door hinge',(x-w/2-.02,y-.05,z),(.025,.025,.10),steel,.004)
rod('Door handle',(x+w/2-.035,y-.10,1.12),(x+w/2-.035,y-.10,1.38),.014)
text('Model label','VHF-680',(-.89,-.546,.61),.081)
text('Brand enclosure','ABENE',(.58,-.547,2.235),.052)
# accordion way covers
for x in [-.62,.62]:
for i in range(19): box('Concertina fold',(x,.06,.73+i*.016),(.32,.53,.012),grey,.002)
box('Knee casting',(0,.04,.78),(.62,.59,.53),grey,.05)
box('Cross slide',(0,-.09,1.035),(1.10,.44,.10),grey)
box('900 x 500 mm table',(0,-.15,1.13),(.9,.5,.11),steel,.006)
for i in range(7): box('14mm T-slot',(0,-.36+i*.07,1.187),(.887,.014,.006),black,.001)
for x in [-.57,.57]: box('Table end guard',(x,-.11,1.19),(.22,.46,.25),white,.035)
box('Rotary fixture base',(0,-.14,1.215),(.28,.27,.045),grey)
cyl('Rotary fixture',(0,-.14,1.26),.125,.055)
cyl('Workpiece',(0,-.14,1.31),.067,.052,grey)
for a in range(0,360,45):
q=math.radians(a); cyl('Fixture bolts',(.102*math.cos(q),-.14+.102*math.sin(q),1.295),.008,.008,black,verts=6)
# spindle and side-mounted milling head
box('Head carriage',(0,.10,1.89),(.42,.34,.32),grey)
box('Side gearbox',(.19,-.08,1.91),(.34,.37,.34),grey,.03)
cyl('Head swivel axis',(-.02,-.13,1.86),.145,.30,steel,(0,1,0))
box('Vertical head',(-.08,-.23,1.78),(.22,.23,.29),grey,.035)
for z,r,h in [(1.60,.09,.09),(1.53,.063,.065),(1.48,.039,.05),(1.42,.017,.072)]: cyl('Spindle and tool',(-.08,-.23,z),r,h)
cyl('Spindle motor',(0,.14,2.17),.072,.29,black)
for i in range(12):
a=i*math.tau/12; rod('Motor cooling fin',(.078*math.cos(a),.14+.078*math.sin(a),2.05),(.078*math.cos(a),.14+.078*math.sin(a),2.29),.004,grey)
cyl('Blue gearbox dial',(.20,-.273,1.98),.074,.017,blue,(0,1,0))
for i in range(24):
a=i*math.tau/24; cyl('Dial tick',(.20+.061*math.cos(a),-.285,1.98+.061*math.sin(a)),.002,.003,white,(0,1,0),12)
text('Head brand','ABENE',(.105,-.277,1.865),.042,white)
cyl('Handwheel hub',(-.09,-.37,1.82),.025,.09,steel,(0,1,0))
pts=[(-.09+.085*math.cos(i*math.tau/64),-.413,1.82+.085*math.sin(i*math.tau/64)) for i in range(65)]; tube('Handwheel rim',pts,.009,steel)
for a in [0,math.tau/3,2*math.tau/3]: rod('Handwheel spoke',(-.09,-.413,1.82),(-.09+.084*math.cos(a),-.413,1.82+.084*math.sin(a)),.006)
rod('Handwheel grip',(-.09,-.41,1.905),(-.09,-.47,1.905),.013,black)
tube('Coolant hose',[(-.20,.0,2.08),(-.30,-.15,2.10),(-.30,-.33,1.93),(-.22,-.35,1.58),(-.14,-.25,1.48)],.012,black)
tube('Rear service hose',[(-.32,.5,2.19),(-.6,.35,1.94),(-.69,.26,1.55),(-.6,.18,1.24),(-.42,.08,1.19)],.029)
# articulated task lamp
for a,b in [((.30,.30,2.10),(.12,.27,2.61)),((.12,.27,2.61),(-.40,.08,2.40))]:
rod('Lamp arm',a,b,.018,grey); cyl('Lamp pivot',a,.032,.04,black,(0,1,0))
box('Work lamp',(-.43,.055,2.37),(.17,.12,.16),grey,.025)
box('Lamp lens',(-.43,-.009,2.37),(.14,.007,.125),white)
# console pedestal and angled panel; local panel coordinates
box('Console foot',(1.04,-.92,.07),(.13,.84,.12),black)
box('Console pedestal',(1.04,-.98,.56),(.14,.15,1.04),black)
rod('Console support',(.79,-.28,.40),(1.04,-1.0,.90),.045,grey)
root=bpy.data.objects.new('Heidenhain console assembly',None); bpy.context.collection.objects.link(root); root.location=(1.03,-.96,1.35); root.rotation_euler[0]=math.radians(-18)
def cb(n,p,d,m=white,b=.006):
o=box(n,p,d,m,b); o.parent=root; return o
cb('Console housing',(0,0,0),(.58,.14,.51))
cb('Display bezel',(-.045,-.076,.06),(.43,.015,.31),grey)
cb('LCD screen',(-.045,-.086,.07),(.365,.008,.24),screen)
for i in range(7): cb('Display status line',(-.085,-.092,.15-i*.025),(.23-i%3*.045,.002,.003),light,.0)
for i in range(9):
cb('Display softkey',(-.225+i*.045,-.09,-.113),(.028,.017,.016),black,.003)
for j in range(7):
for i in range(3): cb('Numeric keypad',(.202+i*.023,-.083,.12-j*.035),(.018,.02,.021),yellow if j==2 else black,.002)
deck=cb('Sloping keyboard desk',(0,-.18,-.31),(.59,.40,.065));
for j in range(5):
for i in range(14): cb('Keyboard key',(-.24+i*.028,-.31+j*.032,-.272),(.021,.022,.012),grey,.002)
for i in range(6):
o=cyl('Override knob',(-.11+i*.055,-.345,-.245),.016,.025,black); o.parent=root
for p,r,m in [((-.24,-.335,-.242),.032,yellow),((-.24,-.335,-.222),.021,red),((.27,-.035,.25),.023,yellow),((.27,-.05,.263),.017,red)]:
o=cyl('Emergency stop',p,r,.025,m); o.parent=root
o=tube('Console hand rest',[(-.27,-.405,-.32),(-.23,-.43,-.33),(.23,-.43,-.33),(.27,-.405,-.32)],.018); o.parent=root
pts=[]
for i in range(650):
t=i/649; pts.append((1.14+.027*math.cos(t*math.tau*42),-1.0+.027*math.sin(t*math.tau*42),.94-.66*t))
tube('Coiled pendant cable',pts,.006)
for x in [-.9,.9]:
for z in [.43,.73,2.20]: cyl('Enclosure fastener',(x,-.548,z),.005,.006,steel,(0,1,0),6)
scene=bpy.context.scene; scene.unit_settings.system='METRIC'; scene['Reference']='ABENE VHF-680 CNC photo and Yumpu brochure'; scene['Accuracy']='Visual reconstruction. Table 900x500 mm; exterior estimated from photo.'
# studio
floor=mat('Studio floor',(.16,.19,.22),0,.65); box('Studio floor',(0,0,-.045),(200,200,.04),floor,0)
world=scene.world; world.use_nodes=True; world.node_tree.nodes['Background'].inputs[0].default_value=(.22,.26,.31,1); world.node_tree.nodes['Background'].inputs[1].default_value=.45
def aim(o,p): o.rotation_euler=(Vector(p)-o.location).to_track_quat('-Z','Y').to_euler()
for n,p,power,size in [('Key',(-3,-4,6),1700,4),('Fill',(4,-2,4),1200,3),('Rim',(1,3,5),1900,3)]:
bpy.ops.object.light_add(type='AREA',location=p); o=bpy.context.object; o.name=n; o.data.energy=power; o.data.shape='DISK'; o.data.size=size; aim(o,(0,0,1))
bpy.ops.object.camera_add(location=(3.7,-6.6,3.35)); cam=bpy.context.object; aim(cam,(.15,-.04,1.3)); cam.data.type='ORTHO'; cam.data.ortho_scale=3.85; scene.camera=cam
scene.render.engine='CYCLES'; scene.cycles.samples=32; scene.cycles.use_denoising=True; scene.render.resolution_x=1400; scene.render.resolution_y=1400; scene.render.resolution_percentage=100
for area in bpy.context.screen.areas:
if area.type=='VIEW_3D':
area.spaces.active.region_3d.view_perspective='CAMERA'; area.spaces.active.shading.type='MATERIAL'
bpy.ops.object.select_all(action='DESELECT')
bpy.ops.wm.save_as_mainfile(filepath=OUT+'/ABENE_VHF680.blend')
scene.render.filepath=OUT+'/ABENE_VHF680.png'; bpy.ops.render.render(write_still=True)
rebuild_v2.pyDetailed procedural rebuild
import bpy, math, os, json
from mathutils import Vector
from math import sin,cos,pi
OUT=r'C:/Users/mtw/Documents/Codex/2026-09-09/x20/outputs'
bpy.ops.object.select_all(action='SELECT'); bpy.ops.object.delete(use_global=False)
for c in list(bpy.data.collections):
if c.name!='Collection': bpy.data.collections.remove(c)
scene=bpy.context.scene
scene.unit_settings.system='METRIC'
scene.unit_settings.length_unit='MILLIMETERS'
CURRENT=None
def collection(name):
global CURRENT
CURRENT=bpy.data.collections.new(name); scene.collection.children.link(CURRENT)
def put(o,name,mat=None):
o.name=name
for c in list(o.users_collection): c.objects.unlink(o)
CURRENT.objects.link(o)
if mat: o.data.materials.clear(); o.data.materials.append(mat)
return o
def pbr(name,col,metal=0,rough=.35,micro=0,scale=900,anis=0):
m=bpy.data.materials.new(name); m.use_nodes=True; m.diffuse_color=(*col,1)
nt=m.node_tree; p=nt.nodes.get('Principled BSDF'); p.inputs['Base Color'].default_value=(*col,1); p.inputs['Metallic'].default_value=metal; p.inputs['Roughness'].default_value=rough
p.inputs['Anisotropic'].default_value=anis
if micro:
tex=nt.nodes.new('ShaderNodeTexNoise'); tex.name='Microscopic surface finish'; tex.inputs['Scale'].default_value=scale; tex.inputs['Detail'].default_value=2
coord=nt.nodes.new('ShaderNodeTexCoord'); nt.links.new(coord.outputs['Object'],tex.inputs['Vector'])
bump=nt.nodes.new('ShaderNodeBump'); bump.inputs['Strength'].default_value=.22; bump.inputs['Distance'].default_value=micro; nt.links.new(tex.outputs['Fac'],bump.inputs['Height']); nt.links.new(bump.outputs['Normal'],p.inputs['Normal'])
ramp=nt.nodes.new('ShaderNodeMapRange'); ramp.inputs['To Min'].default_value=rough*.82; ramp.inputs['To Max'].default_value=min(.95,rough*1.18); nt.links.new(tex.outputs['Fac'],ramp.inputs['Value']); nt.links.new(ramp.outputs['Result'],p.inputs['Roughness'])
return m
ivory=pbr('PBR | RAL 7035 light-grey powder coat',(.57,.59,.57),0,.31,.00007,1600)
apron=pbr('PBR | lower grey powder coat',(.33,.36,.35),0,.38,.00008,1500)
cast=pbr('PBR | grey-green enamel on cast iron',(.145,.18,.165),0,.32,.00022,580)
alloy=pbr('PBR | satin aluminium',(.5,.53,.55),1,.28,.000018,2400,.55)
steel=pbr('PBR | ground steel',(.35,.39,.42),1,.23,.00001,2800,.65)
chrome=pbr('PBR | hard chrome',(.64,.67,.69),1,.135)
darkmetal=pbr('PBR | black oxide steel',(.018,.023,.028),.8,.29,.000025,1700)
rubber=pbr('PBR | EPDM rubber',(.009,.012,.014),0,.57,.00009,1700)
plastic=pbr('PBR | ABS charcoal',(.018,.022,.027),0,.36,.000045,2400)
blue=pbr('PBR | blue gear-selector ring',(.014,.16,.28),.45,.27)
yellow=pbr('PBR | safety yellow plastic',(.82,.52,.012),0,.3)
red=pbr('PBR | emergency-stop red',(.55,.014,.01),0,.28)
green=pbr('PBR | start key green',(.025,.35,.13),0,.28)
printmat=pbr('PBR | dark screen printed legends',(.07,.085,.08),0,.5)
whiteprint=pbr('PBR | white legends',(.72,.76,.74),0,.42)
glass=pbr('PBR | clear safety glazing',(.95,.98,.97),0,.055)
glass.node_tree.nodes.get('Principled BSDF').inputs['Transmission Weight'].default_value=1
glass.node_tree.nodes.get('Principled BSDF').inputs['IOR'].default_value=1.49
lcd=pbr('PBR | powered-off LCD',(.003,.005,.007),0,.18)
def bevel(o,w=.002,seg=3):
if w:
m=o.modifiers.new('Manufactured edge radius','BEVEL'); m.width=w; m.segments=seg
m=o.modifiers.new('Face weighted normals','WEIGHTED_NORMAL'); m.keep_sharp=True
return o
def box(n,p,d,m=ivory,b=.002):
bpy.ops.mesh.primitive_cube_add(size=1,location=p); o=put(bpy.context.object,n,m); o.dimensions=d; bpy.ops.object.transform_apply(location=False,rotation=False,scale=True); return bevel(o,b)
def cyl(n,p,r,h,m=steel,axis=(0,0,1),v=64,b=.001):
bpy.ops.mesh.primitive_cylinder_add(vertices=v,radius=r,depth=h,location=p); o=put(bpy.context.object,n,m); o.rotation_euler=Vector(axis).to_track_quat('Z','Y').to_euler()
for f in o.data.polygons: f.use_smooth=len(f.vertices)==4
return bevel(o,b)
def rod(n,a,b,r,m=chrome): return cyl(n,(Vector(a)+Vector(b))/2,r,(Vector(b)-Vector(a)).length,m,Vector(b)-Vector(a))
def ball(n,p,r,m=plastic):
bpy.ops.mesh.primitive_uv_sphere_add(segments=24,ring_count=12,radius=r,location=p); o=put(bpy.context.object,n,m)
for f in o.data.polygons:f.use_smooth=True
return o
def path(n,pts,r,m=rubber,smooth=False):
cu=bpy.data.curves.new(n,'CURVE'); cu.dimensions='3D'; cu.bevel_depth=r; cu.bevel_resolution=4; cu.resolution_u=20
if smooth:
s=cu.splines.new('BEZIER'); s.bezier_points.add(len(pts)-1)
for p,co in zip(s.bezier_points,pts): p.co=co; p.handle_left_type='AUTO'; p.handle_right_type='AUTO'
else:
s=cu.splines.new('POLY'); s.points.add(len(pts)-1)
for p,co in zip(s.points,pts): p.co=(*co,1)
o=bpy.data.objects.new(n,cu); return put(o,n,m)
def label(n,t,p,size,m=printmat,rot=(pi/2,0,0),align='LEFT'):
cu=bpy.data.curves.new(n,'FONT'); cu.body=t; cu.size=size; cu.align_x=align; cu.extrude=.00012; cu.resolution_u=12
o=bpy.data.objects.new(n,cu); put(o,n,m); o.location=p; o.rotation_euler=rot; return o
def prism(n,outline,y0,y1,m=ivory,b=.002):
# profile in XZ
verts=[(x,y,z) for y in [y0,y1] for x,z in outline]; l=len(outline); faces=[tuple(range(l-1,-1,-1)),tuple(range(l,2*l))]+[(i,(i+1)%l,(i+1)%l+l,i+l) for i in range(l)]
faces=[tuple(reversed(f)) for f in faces]
me=bpy.data.meshes.new(n); me.from_pydata(verts,[],faces); me.update(); o=bpy.data.objects.new(n,me); put(o,n,m); return bevel(o,b)
def rounded(cx,cz,w,h,r,steps=12):
pts=[]
for x,z,a in [(cx+w/2-r,cz+h/2-r,0),(cx-w/2+r,cz+h/2-r,90),(cx-w/2+r,cz-h/2+r,180),(cx+w/2-r,cz-h/2+r,270)]:
for i in range(steps+1):
q=math.radians(a+i*90/steps); pts.append((x+r*cos(q),z+r*sin(q)))
return pts
def cut(o,c):
bpy.context.view_layer.objects.active=o; mod=o.modifiers.new('True window aperture','BOOLEAN'); mod.operation='DIFFERENCE'; mod.object=c; bpy.ops.object.modifier_move_up(modifier=mod.name)
# apply every modifier to retain stable opening geometry
for mm in list(o.modifiers):
try:bpy.ops.object.modifier_apply(modifier=mm.name)
except:pass
bpy.data.objects.remove(c,do_unlink=True)
def screw(p,axis=(0,-1,0),r=.005):
cyl('M6 washer',p,r*1.45,.0015,steel,axis,32,.0003); q=Vector(p)+Vector(axis)*.0018; cyl('Socket cap screw',q,r,.003,darkmetal,axis,6,.0004); cyl('Recess',q+Vector(axis)*.002,r*.45,.0004,rubber,axis,6,0)
collection('01 | Enclosure and safety glazing')
# lower sheet metal and apron, open access channel under machine
box('Front crossmember',(0,-.57,.36),(1.87,.105,.145),ivory)
box('Rear crossmember',(0,.58,.33),(1.83,.11,.16),apron)
for x in [-.89,.89]:box('Side base rail',(x,.02,.285),(.14,1.18,.13),ivory)
box('Rear splash panel',(0,.625,1.22),(1.83,.035,1.80),ivory)
for x in [-.9,.9]:
side=box('Side enclosure shell',(x,.02,1.25),(.045,1.20,1.84),ivory)
cutter=prism('Side window cutter',rounded(0,1.43,.72,1.27,.055),-.13,.13)
# rotate profile to side plane: original X maps Y
cutter.rotation_euler[2]=pi/2; cutter.location.x=x
cut(side,cutter)
pane=prism('Side safety glass',rounded(0,1.43,.724,1.274,.055),-.002,.002,glass,.0006); pane.rotation_euler[2]=pi/2; pane.location.x=x
for y in [-.55,.55]:
for z in [.57,1.07,1.86,2.10]:screw((x-.025 if x<0 else x+.025,y,z),(-1,0,0) if x<0 else (1,0,0),.0035)
# asymmetric sliding doors with large rounded rectangular windows
for cx,w in [(-.62,.66),(.75,.42)]:
outline=[(cx-w/2,.35),(cx+w/2-.11,.35),(cx+w/2-.11,.58),(cx+w/2,.80),(cx+w/2,2.16),(cx-w/2,2.16)]
panel=prism('Sliding safety door',outline,-.612,-.565,ivory)
hole=rounded(cx,1.47,w-.15,1.14,.055)
cut(panel,prism('Window cutting tool',hole,-.67,-.51))
pane=prism('Clear door glazing',rounded(cx,1.47,w-.143,1.147,.055),-.582,-.578,glass,.0005)
path('Window rubber edge',[(x,-.584,z) for x,z in hole+[hole[0]]],.0027,apron)
prism('Lower contrasting folded apron',[(cx-w/2,.35),(cx+w/2-.11,.35),(cx+w/2-.11,.58),(cx+w/2-.05,.70),(cx-w/2,.70)],-.614,-.612,apron,.0007)
hx=cx-w/2+.045
for z in [.86,1.28,1.92]:box('Door hinge leaf',(hx,-.641,z),(.028,.014,.065),alloy,.001); cyl('Hinge pin',(hx+.009,-.648,z),.004,.07,darkmetal)
hy=-.672; hz=1.19
for zz in [hz-.08,hz+.08]:rod('Handle standoff',(hx,-.627,zz),(hx,hy,zz),.008,chrome)
rod('Vertical door pull',(hx,hy,hz-.08),(hx,hy,hz+.08),.010,chrome)
for xx in [cx-w/2+.023,cx+w/2-.023]:
for zz in [.76,2.125]:screw((xx,-.616,zz),r=.0033)
o=label('VHF-680 italic decal','VHF-680',(-.86,-.618,.78),.089,apron); o.data.shear=.18
label('ABENE door logo','ABENE',(.61,-.618,2.087),.049,printmat)
for z in [.39,.47]:
for x in [-.47,0,.47]:screw((x,-.626,z),r=.003)
collection('02 | Base, knee and slide castings')
box('Machine base',(0,.24,.31),(.76,.75,.30),cast,.018)
box('Chip tray',(.0,-.01,.48),(1.55,1.09,.034),darkmetal,.006)
for x in [-.66,.66]:
o=box('Sloping swarf tray',(x,.06,.60),(.29,.97,.015),steel,.001); o.rotation_euler[1]=(-1 if x<0 else 1)*.42
prism('Angled bed casting',[(-.30,.40),(.30,.40),(.26,1.90),(-.22,1.94)],.21,.58,cast,.019)
box('Slanted column face',(0,.174,1.47),(.36,.06,1.08),steel,.002)
box('Knee casting',(0,.01,.82),(.49,.55,.55),cast,.02)
prism('Knee front faceted cover',[(-.27,1.05),(.27,1.05),(.20,.70),(-.14,.66),(-.27,.76)],-.285,-.267,alloy,.003)
box('Y axis saddle',(0,-.025,1.055),(.72,.53,.08),cast,.004)
box('X axis guide',(0,-.04,1.115),(1.19,.39,.042),darkmetal,.002)
for x in [-.89,.89]:
for y in [-.51,.53]:
cyl('Levelling stem',(x,y,.175),.013,.19,chrome)
for z in [.19,.24]:cyl('Levelling locknut',(x,y,z),.022,.013,steel,v=6)
box('Caster yoke',(x,y,.094),(.055,.062,.05),alloy,.004)
for xx in [x-.027,x+.027]:box('Caster fork',(xx,y,.07),(.009,.061,.065),alloy,.002)
cyl('Caster tyre',(x,y,.049),.045,.036,rubber,(1,0,0))
cyl('Wheel hub',(x-.021,y,.049),.028,.009,alloy,(1,0,0)); cyl('Axle',(x-.027,y,.049),.009,.009,steel,(1,0,0),6)
collection('03 | Table, bellows and workholding')
# corrugated guards run vertically behind the moving table
for x in [-.57,.57]:
for i in range(22):
xx=x-.19+i*.018
prism('Vertical stainless way-guard fold',[(xx,.55),(xx+.008,.55),(xx+.008,1.15),(xx,1.15)],.24,.272,alloy,.0007)
box('Table underside',(0,-.065,1.165),(1.1,.47,.055),cast,.005)
# seven real slots: top land strips above a continuous lower plate
box('900 x 500 mm angle table',(0,-.08,1.204),(.900,.500,.054),steel,.0015)
for i in range(8):
yl=-.33 if i==0 else -.33+(i-.5)*(.5/7)+.007
yr=.17 if i==7 else -.33+(i+.5)*(.5/7)-.007
box('Ground table land',(0,(yl+yr)/2,1.24),(.9,yr-yl,.018),steel,.00065)
for i in range(7):box('T-slot recessed channel',(0,-.33+(i+.5)*(.5/7),1.226),(.893,.020,.004),darkmetal,.0005)
for x in [-.57,.57]:
box('White table end housing',(x,-.10,1.285),(.23,.40,.27),ivory,.012)
box('End guard rear step',(x,.1,1.31),(.19,.09,.19),ivory,.005)
for y in [-.24,.05]:screw((x+(-.12 if x<0 else .12),y,1.30),(-1,0,0) if x<0 else (1,0,0),.004)
box('Fixture cube',(0,-.11,1.292),(.275,.255,.08),alloy,.005)
for x in [-.105,-.06,0,.06,.105]:
for z in [1.273,1.308]:cyl('Fixture threaded hole',(x,-.241,z),.006,.002,darkmetal,(0,1,0),32,0)
cyl('Rotary-table body',(0,-.11,1.365),.116,.06,cast)
cyl('Rotary ground face',(0,-.11,1.40),.119,.016,steel)
for i in range(72):
a=i*2*pi/72; rod('Index graduation',(.112*cos(a),-.11+.112*sin(a),1.410),(.118*cos(a),-.11+.118*sin(a),1.410),.00045,printmat)
cyl('Workpiece base',(0,-.11,1.433),.050,.047,steel)
cyl('Stepped sample workpiece',(0,-.11,1.47),.032,.033,chrome)
collection('04 | Milling head and spindle')
box('Rear black vertical cover',(-.08,.30,1.98),(.27,.19,.48),darkmetal,.004)
box('Head attachment casting',(.05,.08,1.90),(.34,.38,.31),cast,.012)
box('Right gearbox casing',(.235,-.065,1.91),(.27,.30,.33),cast,.010)
box('Gearbox front cover',(.235,-.222,1.915),(.253,.013,.304),cast,.004)
for x in [.125,.345]:
for z in [1.784,2.043]:screw((x,-.232,z),r=.005)
cyl('Head pivot flange',(-.057,-.103,1.90),.135,.21,alloy,(0,1,0))
cyl('Pivot flange step',(-.057,-.222,1.90),.12,.03,cast,(0,1,0))
prism('Vertical spindle casting',[(-.19,1.72),(-.01,1.72),(.025,1.84),(.025,2.04),(-.17,2.04),(-.20,1.98)],-.306,-.09,cast,.009)
box('Head front machined plate',(-.09,-.316,1.91),(.17,.015,.155),alloy,.004)
for x in [-.16,-.025]:
for z in [1.85,1.97]:screw((x,-.328,z),r=.0045)
cyl('Upper quill housing',(-.09,-.18,2.063),.086,.06,alloy)
box('Top rectangular bearing block',(-.09,-.18,2.107),(.17,.17,.04),cast,.004)
cyl('Spindle drive motor',(-.03,.135,2.205),.061,.30,plastic)
cyl('Motor top cap',(-.03,.135,2.36),.066,.023,plastic)
for i in range(20):
a=i*pi/10; rod('Motor fin',(-.03+.063*cos(a),.135+.063*sin(a),2.07),(-.03+.063*cos(a),.135+.063*sin(a),2.34),.0025,darkmetal)
for z,r,h,m in [(1.729,.079,.065,cast),(1.685,.071,.023,alloy),(1.651,.059,.045,chrome),(1.623,.056,.01,darkmetal),(1.603,.045,.029,steel),(1.578,.036,.024,chrome),(1.557,.025,.019,darkmetal),(1.529,.012,.038,steel)]:cyl('Stepped spindle and SK40 tool',(-.09,-.20,z),r,h,m)
for i in range(12):
a=i*pi/6; box('Collet flute',(-.09+.037*cos(a),-.20+.037*sin(a),1.594),(.004,.004,.018),darkmetal,.0004)
# selector ring with engraved numerals
cyl('Selector chrome bezel',(.215,-.239,1.977),.065,.008,chrome,(0,1,0))
cyl('Blue selector ring',(.215,-.245,1.977),.058,.007,blue,(0,1,0))
cyl('Selector central metal dial',(.215,-.251,1.977),.037,.008,alloy,(0,1,0))
for i in range(20):
a=i*2*pi/20; ball('Selector serration',(.215+.059*cos(a),-.247,1.977+.059*sin(a)),.0037,chrome)
for i,t in enumerate(['55','125','280','630','1400','3150']):
a=i*pi/3; label('Spindle-speed legend',t,(.215+.046*cos(a),-.252,1.974+.046*sin(a)),.0075,whiteprint,align='CENTER')
rod('Selector lever',(.215,-.256,1.977),(.235,-.279,2.012),.005,darkmetal); ball('Selector knob',(.235,-.279,2.012),.01,plastic)
cyl('Brand medallion',(.215,-.242,1.866),.047,.004,alloy,(0,1,0))
cyl('Brand badge inset',(.215,-.245,1.866),.043,.003,cast,(0,1,0))
label('ABENE head medallion','ABENE',(.174,-.249,1.859),.022,whiteprint)
def wheel(cx,cy,cz,r):
cyl('Handwheel shaft',(cx,cy+.033,cz),r*.22,.10,chrome,(0,1,0))
path('Polished handwheel rim',[(cx+r*cos(i*2*pi/96),cy,cz+r*sin(i*2*pi/96)) for i in range(97)],.0065,chrome)
for a in [0,2*pi/3,4*pi/3]:rod('Handwheel spoke',(cx,cy,cz),(cx+r*cos(a),cy,cz+r*sin(a)),.0045,chrome)
rod('Crank handle',(cx+r,cy,cz),(cx+r,cy-.044,cz),.009,plastic)
wheel(-.07,-.368,1.87,.067)
wheel(-.255,-.21,1.934,.048)
rod('Quill lock lever',(-.04,-.333,1.984),(-.002,-.347,2.045),.006,chrome); ball('Quill lock ball',(-.002,-.347,2.045),.013,plastic)
cyl('Head stop yellow collar',(.356,-.24,2.018),.021,.011,yellow,(0,1,0)); cyl('Head stop red',(.356,-.25,2.018),.013,.011,red,(0,1,0))
collection('05 | Hoses, cable chain and task light')
path('Large flexible service conduit',[(-.33,.39,2.12),(-.48,.31,1.86),(-.68,.16,1.44),(-.63,-.01,1.27),(-.35,-.10,1.21)],.027,rubber,True)
path('Head electrical loop',[(.30,.04,2.064),(.25,.16,2.23),(.11,.04,2.24),(.13,-.08,2.064)],.010,rubber,True)
cool=[(-.22,-.1,1.94),(-.26,-.17,1.85),(-.25,-.26,1.72),(-.20,-.29,1.61),(-.13,-.24,1.56)]
path('Coolant feed',cool,.011,rubber,True)
for i in range(13):
t=i/12; p=Vector(cool[2]).lerp(Vector(cool[4]),t); ball('Segmented coolant hose',p,.013,blue)
rod('Brass coolant nozzle',(-.13,-.24,1.56),(-.10,-.21,1.55),.007,pbr('PBR | brass',(.52,.30,.09),1,.27))
for a,b in [((.36,.28,2.095),(.16,.24,2.61)),((.16,.24,2.61),(-.39,.03,2.40))]:
rod('Articulated lamp rectangular arm',a,b,.012,alloy)
cyl('Lamp pivot bolt',a,.022,.035,darkmetal,(0,1,0))
rod('Lamp pivot cross pin',(.16,.22,2.61),(.16,.26,2.61),.019,chrome)
lamp=box('Halogen worklamp housing',(-.40,.015,2.38),(.15,.13,.14),alloy,.012); lamp.rotation_euler[1]=-.18
box('Worklamp black gasket',(-.40,-.054,2.38),(.126,.006,.118),rubber,.006)
box('Worklamp lens',(-.40,-.06,2.38),(.112,.005,.102),glass,.006)
for i in range(7):box('Lamp cooling groove',(-.46+i*.02,.083,2.38),(.004,.003,.11),darkmetal,.001)
collection('06 | Heidenhain iTNC 530 control console')
cx=1.08
box('Black console pedestal',(cx,-.83,.59),(.115,.15,.94),plastic,.006)
box('Console floor rail',(cx,-.69,.12),(.115,.97,.105),plastic,.003)
box('Console transverse foot',(.80,-.27,.12),(.60,.11,.09),plastic,.003)
box('Console mounting head',(cx,-.88,1.115),(.15,.22,.15),plastic,.004)
box('Monitor to keyboard housing',(cx,-.70,1.265),(.49,.135,.15),ivory,.004)
rod('Console diagonal support',(.67,-.28,.47),(cx,-.81,1.09),.031,alloy)
def parent(o,root):o.parent=root;return o
deckroot=bpy.data.objects.new('Keyboard deck | 12 degree slope',None); put(deckroot,deckroot.name); deckroot.location=(cx,-.95,1.18); deckroot.rotation_euler[0]=math.radians(12)
def db(n,p,d,m=ivory,b=.001):return parent(box(n,p,d,m,b),deckroot)
db('Keyboard deck enclosure',(0,0,-.035),(.57,.43,.07),ivory,.006)
db('Inset keyboard plate',(0,.038,.002),(.515,.305,.006),alloy,.001)
for row in range(5):
for i in range(17):
x=-.235+i*.0215; y=.14-row*.024
db('Alphanumeric key',(x,y,.010),(.0175,.018,.01),blue if row==4 else plastic,.002)
o=label('Key legend',('QWERTYUIOPASDFGHJK'[i] if row==0 else str((i+row)%10)),(x,y-.004,.016),.0065,whiteprint,rot=(0,0,0),align='CENTER');parent(o,deckroot)
for row in range(5):
for i in range(4):db('Axis and function key',(.145+i*.023,.14-row*.026,.011),(.019,.021,.012),yellow if i==3 else plastic,.002)
for i in range(7):
x=-.15+i*.055
parent(cyl('Override knob bezel',(x,-.145,.013),.019,.006,alloy),deckroot)
parent(cyl('Feed override knob',(x,-.145,.026),.011,.021,plastic),deckroot)
db('Knob index',(x,-.146,.038),(.002,.013,.001),whiteprint,.0)
parent(label('Override legend',['X','Y','Z','F','S','%','MODE'][i],(x,-.111,.009),.008,printmat,rot=(0,0,0),align='CENTER'),deckroot)
parent(cyl('Emergency stop collar',(-.237,-.147,.015),.025,.012,yellow),deckroot)
parent(cyl('Emergency mushroom',(-.237,-.147,.034),.017,.025,red),deckroot)
parent(path('Front black grab handle',[(-.255,-.213,-.008),(-.237,-.242,-.012),(.237,-.242,-.012),(.255,-.213,-.008)],.014,plastic,True),deckroot)
# monitor tilted back independently of keyboard
mon=bpy.data.objects.new('Monitor | backward tilt 20 degrees',None);put(mon,mon.name);mon.location=(cx,-.72,1.50);mon.rotation_euler[0]=math.radians(-20)
def mb(n,p,d,m=ivory,b=.002):return parent(box(n,p,d,m,b),mon)
mb('Monitor casing',(0,0,0),(.51,.115,.43),ivory,.008)
mb('Dark display bezel',(-.017,-.061,.032),(.421,.012,.302),plastic,.003)
mb('Black LCD',(-.017,-.069,.032),(.365,.003,.253),lcd,.001)
for i in range(8):
parent(cyl('Monitor bezel fastener',(-.229 if i<4 else .229,-.06,-.18+(i%4)*.12),.003,.003,darkmetal,(0,1,0),16,.0002),mon)
parent(label('Control model name','HEIDENHAIN iTNC 530',(-.197,-.070,-.137),.009,printmat),mon)
for i in range(7):
x=-.195+i*.063
parent(cyl('Round machine pushbutton bezel',(x,-.067,-.181),.016,.007,chrome,(0,1,0)),mon)
parent(cyl('Machine pushbutton',(x,-.073,-.181),.010,.009,green if i==5 else red if i==6 else plastic,(0,1,0)),mon)
mb('Right handheld pendant',(.304,0,-.009),(.082,.10,.385),plastic,.012)
parent(cyl('Pendant emergency collar',(.304,-.058,.17),.022,.012,yellow,(0,1,0)),mon)
parent(cyl('Pendant red mushroom',(.304,-.069,.17),.015,.016,red,(0,1,0)),mon)
parent(cyl('Pendant jog dial',(.304,-.057,.09),.028,.012,darkmetal,(0,1,0)),mon)
for j in range(6):
for i in range(2):mb('Pendant function key',(.288+i*.030,-.054,.027-j*.026),(.019,.009,.018),yellow if j==0 else alloy,.001)
path('Pendant cable lead',[(1.39,-.73,1.34),(1.43,-.84,1.08),(1.4,-.90,.97)],.007,rubber,True)
# hanging U-shaped spiral cable
pts=[]
for i in range(1501):
t=i/1500; a=pi*t; base=Vector((1.28-.22*cos(a),-.96,.94-.56*sin(a))); pts.append((base.x+.018*cos(t*2*pi*60),base.y+.018*sin(t*2*pi*60),base.z))
path('Coiled pendant cable',pts,.0045,rubber)
for x in [cx-.255,cx+.255]:
for y in [-1.10,-.82]:screw((x,y,1.17),(0,0,1),.003)
collection('07 | Fine hardware and identification')
box('Rear electrical cabinet',(0,.74,1.19),(1.30,.20,1.51),ivory,.004)
for i in range(13):box('Cabinet ventilation slot',(.46,.848,.87+i*.016),(.25,.003,.005),darkmetal,.001)
box('Serial plate',(-.72,.85,.88),(.18,.004,.10),alloy,.001)
label('Serial plate engraving','ABENE\nVHF-680 CNC\nREFERENCE RECONSTRUCTION',(-.798,.853,.91),.008,printmat,rot=(pi/2,0,pi))
for x in [-.30,.30]:
for z in [.52,.67,.87]:screw((x,-.299,z),r=.004)
collection('08 | Studio and cameras')
floor=pbr('PBR | studio floor',(.18,.20,.22),0,.5)
box('Studio ground',(0,0,-.018),(2000,2000,.02),floor,0)
def aim(o,p):o.rotation_euler=(Vector(p)-o.location).to_track_quat('-Z','Y').to_euler()
scene.world.use_nodes=True; bg=scene.world.node_tree.nodes.get('Background'); bg.inputs[0].default_value=(.32,.37,.43,1);bg.inputs[1].default_value=.22
for n,p,power,size in [('Large softbox left',(-3.5,-4,5.5),750,4),('Tall strip right',(3,-1.5,3.4),500,2.5),('Rear rim',(0,3,4.8),950,3)]:
bpy.ops.object.light_add(type='AREA',location=p);o=put(bpy.context.object,n);o.data.energy=power;o.data.shape='DISK';o.data.size=size;aim(o,(0,0,1.15))
def camera(n,p,target,lens):
bpy.ops.object.camera_add(location=p);o=put(bpy.context.object,n);aim(o,target);o.data.lens=lens;o.data.clip_end=2000;return o
cam=camera('Camera | reference three-quarter',(-2.2,-7.5,3.0),(.16,-.02,1.25),65)
camera('Camera | spindle close-up',(-1.20,-2.3,2.12),(.02,-.08,1.86),70)
camera('Camera | operator controls',(2.4,-3.3,2.3),(1.09,-.85,1.36),72)
scene.camera=cam;scene.render.engine='CYCLES';scene.cycles.samples=48;scene.cycles.use_denoising=True
scene.cycles.max_bounces=12;scene.cycles.transmission_bounces=8
scene.render.resolution_x=1500;scene.render.resolution_y=1500;scene.render.resolution_percentage=100
scene.view_settings.view_transform='AgX';scene.view_settings.look='AgX - Medium High Contrast';scene.view_settings.exposure=-.25
scene.render.image_settings.file_format='PNG';scene.render.image_settings.color_mode='RGBA'
scene['Reference']='https://www.yumpu.com/de/document/view/1352354/abene-vhf-680-cnc and user supplied photo'
scene['Dimensional accuracy']='Table 900 x 500 mm, 7 slots at 14 mm; remaining geometry is photo-estimated. Not measured CAD.'
scene['Materials']='Self-contained procedural Principled BSDF PBR; micro-normal and roughness detail; no external texture dependencies.'
scene['Version']='2.0 | refined visual reconstruction'
bpy.ops.object.select_all(action='DESELECT')
for ar in bpy.context.screen.areas:
if ar.type=='VIEW_3D':
ar.spaces.active.region_3d.view_perspective='CAMERA';ar.spaces.active.shading.type='MATERIAL'
scene.render.filepath=OUT+'/ABENE_VHF680_v2_preview.png'
bpy.ops.wm.save_as_mainfile(filepath=OUT+'/ABENE_VHF680_v2_PBR.blend')
bpy.ops.render.render(write_still=True)
finalize_v2.pyStudio, cameras and production renders
import bpy, os, json
from mathutils import Vector
OUT=r'C:/Users/mtw/Documents/Codex/2026-09-09/x20/outputs'
bpy.ops.wm.open_mainfile(filepath=OUT+'/ABENE_VHF680_v2_PBR.blend')
s=bpy.context.scene
studio=bpy.data.collections['08 | Studio and cameras']
# Seamless curved studio sweep eliminates the visible floor horizon.
cross=[(-100,-.007),(2,-.007)]
import math
for i in range(1,65):
a=i*math.pi/128; cross.append((2+3*math.sin(a),3-3*math.cos(a)-.007))
cross.append((5,100))
verts=[(x,y,z) for x in [-100,100] for y,z in cross];n=len(cross)
faces=[(i,i+n,i+1+n,i+1) for i in range(n-1)]
me=bpy.data.meshes.new('Cyclorama');me.from_pydata(verts,[],faces);me.update()
o=bpy.data.objects.new('Seamless studio cyclorama',me);studio.objects.link(o);me.materials.append(bpy.data.materials['PBR | studio floor'])
for p in me.polygons:p.use_smooth=True
# Terminate the flexible conduit inside its cover rather than leaving a loose end.
bpy.data.objects['Large flexible service conduit'].data.splines[0].bezier_points[0].co=(-.16,.35,2.15)
cu=bpy.data.curves.new('Pendant cable connection','CURVE');cu.dimensions='3D';cu.bevel_depth=.007;cu.bevel_resolution=4
sp=cu.splines.new('BEZIER');sp.bezier_points.add(2)
for p,co in zip(sp.bezier_points,[(1.4,-.9,.97),(1.47,-.95,.99),(1.5,-.96,.94)]):p.co=co;p.handle_left_type='AUTO';p.handle_right_type='AUTO'
o=bpy.data.objects.new('Pendant cable connection',cu);bpy.data.collections['06 | Heidenhain iTNC 530 control console'].objects.link(o);cu.materials.append(bpy.data.materials['PBR | EPDM rubber'])
def aim(o,p):o.rotation_euler=(Vector(p)-o.location).to_track_quat('-Z','Y').to_euler()
head=bpy.data.objects['Camera | spindle close-up']; head.location=(-.50,-2.20,2.02); aim(head,(.065,-.16,1.88)); head.data.lens=78
s.cycles.samples=128;s.cycles.use_denoising=True
s.render.resolution_x=2600;s.render.resolution_y=2600
s.render.filepath=OUT+'/ABENE_VHF680_v2_hero.png'
bpy.context.preferences.filepaths.save_version=0
bpy.ops.wm.save_as_mainfile(filepath=OUT+'/ABENE_VHF680_v2_PBR.blend')
stats={'objects':len(s.objects),'mesh_objects':sum(o.type=='MESH' for o in s.objects),'materials':len([m for m in bpy.data.materials if m.users]),'collections':[c.name for c in s.collection.children],'external_images':[im.filepath for im in bpy.data.images if im.source=='FILE' and not im.packed_file]}
open(OUT+'/ABENE_VHF680_v2_scene_info.json','w').write(json.dumps(stats,indent=2))
print('SCENE_VALIDATED',json.dumps(stats),flush=True)
bpy.ops.render.render(write_still=True)
s.camera=head;s.render.resolution_x=1600;s.render.resolution_y=1600;s.cycles.samples=96;s.render.filepath=OUT+'/ABENE_VHF680_v2_spindle.png';bpy.ops.render.render(write_still=True)
s.camera=bpy.data.objects['Camera | operator controls'];s.render.filepath=OUT+'/ABENE_VHF680_v2_controls.png';bpy.ops.render.render(write_still=True)
connection_fix.pyHead hose repair and GPU selection
import bpy
OUT=r'C:/Users/mtw/Documents/Codex/2026-09-09/x20/outputs'
bpy.ops.wm.open_mainfile(filepath=OUT+'/ABENE_VHF680_v2_PBR.blend')
s=bpy.context.scene
p=bpy.data.objects['Head electrical loop'].data.splines[0].bezier_points
p[0].co=(.30,.04,2.064);p[-1].co=(.13,-.08,2.064)
bpy.context.preferences.filepaths.save_version=0
s.render.filepath=OUT+'/ABENE_VHF680_v2_hero.png'
bpy.ops.wm.save_as_mainfile(filepath=OUT+'/ABENE_VHF680_v2_PBR.blend')
# Use an available native Cycles accelerator, falling back to CPU.
prefs=bpy.context.preferences.addons['cycles'].preferences
for backend in ['OPTIX','CUDA','HIP','ONEAPI','METAL']:
try:
prefs.compute_device_type=backend;prefs.get_devices()
accelerators=[d for d in prefs.devices if d.type!='CPU']
if accelerators:
for d in prefs.devices:d.use=d.type!='CPU'
s.cycles.device='GPU';print('CYCLES_ACCELERATOR',backend,flush=True);break
except:pass
print('RENDERING_CORRECTED_VIEWS',flush=True)
bpy.ops.render.render(write_still=True)
s.camera=bpy.data.objects['Camera | spindle close-up'];s.render.resolution_x=1600;s.render.resolution_y=1600;s.cycles.samples=96;s.render.filepath=OUT+'/ABENE_VHF680_v2_spindle.png';bpy.ops.render.render(write_still=True)
console_mount_fix.pyConsole connecting structure
import bpy
OUT=r'C:/Users/mtw/Documents/Codex/2026-09-09/x20/outputs'
bpy.ops.wm.open_mainfile(filepath=OUT+'/ABENE_VHF680_v2_PBR.blend');s=bpy.context.scene
col=bpy.data.collections['06 | Heidenhain iTNC 530 control console']
for name,loc,dim,mat in [('Monitor to keyboard housing',(1.08,-.70,1.265),(.49,.135,.15),'PBR | RAL 7035 light-grey powder coat'),('Console mounting head',(1.08,-.88,1.115),(.15,.22,.15),'PBR | ABS charcoal')]:
bpy.ops.mesh.primitive_cube_add(size=1,location=loc);o=bpy.context.object;o.name=name;o.dimensions=dim;bpy.ops.object.transform_apply(location=False,rotation=False,scale=True)
for c in list(o.users_collection):c.objects.unlink(o)
col.objects.link(o);o.data.materials.append(bpy.data.materials[mat]);m=o.modifiers.new('Edge radius','BEVEL');m.width=.004;m.segments=3;o.modifiers.new('Weighted normals','WEIGHTED_NORMAL')
bpy.ops.object.select_all(action='DESELECT');bpy.context.preferences.filepaths.save_version=0;bpy.ops.wm.save_as_mainfile(filepath=OUT+'/ABENE_VHF680_v2_PBR.blend')
prefs=bpy.context.preferences.addons['cycles'].preferences;prefs.compute_device_type='OPTIX';prefs.get_devices()
for d in prefs.devices:d.use=d.type!='CPU'
s.cycles.device='GPU';s.render.filepath=OUT+'/ABENE_VHF680_v2_hero.png';bpy.ops.render.render(write_still=True)
s.camera=bpy.data.objects['Camera | operator controls'];s.render.resolution_x=1600;s.render.resolution_y=1600;s.cycles.samples=96;s.render.filepath=OUT+'/ABENE_VHF680_v2_controls.png';bpy.ops.render.render(write_still=True)
console_mount_trim.pyFinal mount height correction
import bpy
OUT=r'C:/Users/mtw/Documents/Codex/2026-09-09/x20/outputs'
bpy.ops.wm.open_mainfile(filepath=OUT+'/ABENE_VHF680_v2_PBR.blend');s=bpy.context.scene
col=bpy.data.collections['06 | Heidenhain iTNC 530 control console']
o=bpy.data.objects['Console mounting head'];o.location.z=1.075;o.dimensions.z=.14
bpy.ops.object.select_all(action='DESELECT');bpy.context.preferences.filepaths.save_version=0;bpy.ops.wm.save_as_mainfile(filepath=OUT+'/ABENE_VHF680_v2_PBR.blend')
prefs=bpy.context.preferences.addons['cycles'].preferences;prefs.compute_device_type='OPTIX';prefs.get_devices()
for d in prefs.devices:d.use=d.type!='CPU'
s.cycles.device='GPU';s.render.filepath=OUT+'/ABENE_VHF680_v2_hero.png';bpy.ops.render.render(write_still=True)
s.camera=bpy.data.objects['Camera | operator controls'];s.render.resolution_x=1600;s.render.resolution_y=1600;s.cycles.samples=96;s.render.filepath=OUT+'/ABENE_VHF680_v2_controls.png';bpy.ops.render.render(write_still=True)