"""Create the atlas's own solid mesh. Run with Blender --background --python. Coordinates below are metres in Enfusion's X/right, Y/front, Z/up frame. The printable face retains the existing 744x1024 layout and its left gutter. The rigid book and separate turning sheet use the native atlas hand graph. """ from pathlib import Path import math import bpy ROOT = Path(__file__).resolve().parents[1] OUT = ROOT / 'addons/core/Assets/Items/ERR_Atlas' OUT.mkdir(parents=True, exist_ok=True) bpy.ops.object.select_all(action='SELECT') bpy.ops.object.delete(use_global=False) def xyz(p): # Place the rigid paper just in front of the stock folded-map grip. return (p[0] + .015, -p[2], p[1] + .018) materials = {} for name, color, metal in [('AtlasPrint', (.85,.8,.7,1), 0), ('AtlasPaper', (.73,.70,.62,1), 0), ('AtlasCover', (.48,.018,.012,1), 0), ('AtlasWire', (.8,.8,.78,1), .65)]: mat = bpy.data.materials.new(name) mat.diffuse_color = color mat.use_nodes = True bsdf = mat.node_tree.nodes.get('Principled BSDF') bsdf.inputs['Base Color'].default_value = color bsdf.inputs['Metallic'].default_value = metal bsdf.inputs['Roughness'].default_value = .4 if metal else .8 materials[name] = mat def mesh(name, vertices, faces, material, uvs=None): data = bpy.data.meshes.new(name) data.from_pydata([xyz(v) for v in vertices], [], faces) data.update() obj = bpy.data.objects.new(name, data) bpy.context.collection.objects.link(obj) data.materials.append(materials[material]) uv = data.uv_layers.new(name='UVMap') for polygon in data.polygons: for loop in polygon.loop_indices: v = data.loops[loop].vertex_index uv.data[loop].uv = uvs[v] if uvs else (0,0) return obj def box(name, lo, hi, material): x,y,z = lo; X,Y,Z = hi return mesh(name, [(x,y,z),(X,y,z),(X,Y,z),(x,Y,z), (x,y,Z),(X,y,Z),(X,Y,Z),(x,Y,Z)], [(0,3,2,1),(4,5,6,7),(0,1,5,4),(3,7,6,2), (0,4,7,3),(1,2,6,5)], material) # The paper mesh used a folded sheet. This replacement has separate covers, # a 10 mm block of leaves and actual wire geometry with depth and silhouette. left, right, bottom, top = -.083, .108, -.1485, .1485 box('BackCover', (left-.002,-.012,bottom-.002), (right+.002,-.010,top+.002), 'AtlasCover') box('PageBlock', (left,-.010,bottom), (right,-.0005,top), 'AtlasPaper') # Small real edge seams catch light at grazing angles without one mesh per leaf. for n in range(12): y = -.0096+n*.00075 box('PageEdge_%02d'%n, (left,y,bottom-.00025), (right+.0003,y+.00016,top+.00025), 'AtlasPaper') box('FrontCover', (left-.002,-.0005,bottom-.002), (right+.002,.0005,top+.002), 'AtlasCover') mesh('PrintedSheet', [(left,.0006,bottom),(right,.0006,bottom), (right,.0006,top),(left,.0006,top)], [(0,3,2,1)], 'AtlasPrint', [(80/744,1),(1,1),(1,0),(80/744,0)]) # Continuous helix around the spine; the upper arc passes through the gutter # and the lower arc wraps behind the page block. No painted fake rings. vertices=[]; faces=[] turns=14; steps=turns*32; sides=6 for i in range(steps+1): angle=i/32*2*math.pi z=bottom+.009+(top-bottom-.018)*i/steps for j in range(sides): tube=j/sides*2*math.pi radius=.0088+.0009*math.cos(tube) vertices.append((left+.002+radius*math.cos(angle), -.005+radius*math.sin(angle), z+.0009*math.sin(tube))) for i in range(steps): for j in range(sides): a=i*sides+j; b=i*sides+(j+1)%sides faces.append((a,b,b+sides,a+sides)) wire=mesh('SpiralWire',vertices,faces,'AtlasWire') for poly in wire.data.polygons: poly.use_smooth=True # A simple inventory collider; no dynamic page physics or runtime allocations. collider=box('UBX_Atlas', (left,-.013,bottom), (right,.004,top), 'AtlasPaper') collider['usage']='Geometry' collider.hide_render=True bpy.context.scene.unit_settings.system='METRIC' bpy.context.scene.unit_settings.scale_length=1 bpy.ops.object.select_all(action='SELECT') source = ROOT / 'source/atlas' source.mkdir(parents=True, exist_ok=True) bpy.ops.wm.save_as_mainfile(filepath=str(source/'ERR_Atlas.blend')) bpy.ops.export_scene.fbx(filepath=str(OUT/'ERR_Atlas.fbx'), use_selection=True, object_types={'MESH'}, apply_unit_scale=True, apply_scale_options='FBX_SCALE_ALL', axis_forward='-Z', axis_up='Y', bake_space_transform=True, use_mesh_modifiers=True, use_custom_props=True, add_leaf_bones=False, bake_anim=False) print('ERR ATLAS MODEL EXPORTED', OUT) # Separate lightweight turning leaf. Its origin is the spine hinge; runtime # rotates this visual only, keeping the held book and gripping hand steady. bpy.ops.object.select_all(action='SELECT') bpy.ops.object.delete(use_global=False) verts=[]; faces=[]; uvs=[] for i in range(17): u=i/16 for z,v in [(bottom,1),(top,0)]: # Subtract the book placement offset: this mesh is hinge-local. verts.append((.191*u-.015, .004*math.sin(math.pi*u)-.018, z)) uvs.append((80/744+(1-80/744)*u,v)) for i in range(16): faces.append((2*i,2*i+1,2*i+3,2*i+2)) leaf=mesh('TurningLeaf',verts,faces,'AtlasPrint',uvs) back=mesh('LeafBack',verts,[tuple(reversed(f)) for f in faces],'AtlasPaper',uvs) bpy.ops.object.select_all(action='SELECT') bpy.ops.wm.save_as_mainfile(filepath=str(source/'ERR_AtlasLeaf.blend')) bpy.ops.export_scene.fbx(filepath=str(OUT/'ERR_AtlasLeaf.fbx'),use_selection=True, object_types={'MESH'},apply_unit_scale=True,apply_scale_options='FBX_SCALE_ALL', axis_forward='-Z',axis_up='Y',bake_space_transform=True, use_mesh_modifiers=True,use_custom_props=True,add_leaf_bones=False,bake_anim=False)