BLENDER SCREEN VİEWS TO LAYOUT
-
Copy the following code and paste it into Notepad, saving it with the .py extension.
Save the example.py file as a zip file and install it as a plugin in Blender. -
Layout Export Preview
Task:
To extract layout output (PNG) from camera views of a model using the Blender 3D application.
Usage:
-
Install the plugin.
-
Open the side panel.
-
First click the camera option button, then the screenshot button on the left. Repeat this for each view.
-
Once all view saves are complete, select the folder where the output will be saved using the “Create Layout” button and save.
Future:
- Layout Options
- Fine-Tuning
- Page Selection
- Render Settings
- Simple / Advanced Render Mode Output Option
- PDF Output Option
- One-touch automatic output (manual version preferred for now due to faulty camera area selection).
This application was developed with QWEN3-Max AI. I would be very grateful if you could share more advanced interfaces and features here. Thank you.
CODE:
import bpy
from mathutils import Vector
import os
import shutil
from PIL import Image, ImageDraw, ImageFont, ImageEnhance
from datetime import datetime
import platform
bl_info = {
“name”: “Multi-View Layout Renderer”,
“blender”: (4, 5, 0),
“category”: “Render”,
}
-------------------------------------------------------------
FONT YÜKLEYİCİ
-------------------------------------------------------------
def load_font(size):
addon_dir = os.path.dirname(file)
font_path = os.path.join(addon_dir, “fonts”, “arial.ttf”)
try:
return ImageFont.truetype(font_path, size)
except:
return ImageFont.load_default()
-------------------------------------------------------------
GEÇİCİ KLÖSÖR (MASAÜSTÜ / .multiview_cache)
-------------------------------------------------------------
def get_cache_dir():
desktop = os.path.join(os.path.expanduser(“~”), “Desktop”)
cache_dir = os.path.join(desktop, “.multiview_cache”)
os.makedirs(cache_dir, exist_ok=True)
if platform.system() == “Windows”:
import subprocess
subprocess.check_call([“attrib”, “+H”, cache_dir], shell=True)
return cache_dir
-------------------------------------------------------------
PROPERTY GROUP – SADE
-------------------------------------------------------------
class MultiViewProperties(bpy.types.PropertyGroup):
company_name: bpy.props.StringProperty(name=“Company”, default=“AURACAD”)
project_name: bpy.props.StringProperty(name=“Project”, default=“GENERAL MESH TRANSFORM”)
drawing_code: bpy.props.StringProperty(name=“Drawing Code”, default=“DWG-001”)
version: bpy.props.StringProperty(name=“Version”, default=“1.0”)
date: bpy.props.StringProperty(name=“Date”, default=datetime.now().strftime(“%d.%m.%Y”))
notes: bpy.props.StringProperty(name=“Extra Notes”, default=“”)
light_color: bpy.props.FloatVectorProperty(
name="Light Color",
subtype='COLOR',
default=(1.0, 1.0, 1.0),
min=0.0,
max=1.0
)
light_energy: bpy.props.FloatProperty(
name="Light Energy",
default=2.5,
min=0.5,
max=10.0
)
front_img: bpy.props.StringProperty()
back_img: bpy.props.StringProperty()
left_img: bpy.props.StringProperty()
right_img: bpy.props.StringProperty()
top_img: bpy.props.StringProperty()
bottom_img: bpy.props.StringProperty()
perspective_img: bpy.props.StringProperty()
-------------------------------------------------------------
GEOMETRY UTILS
-------------------------------------------------------------
def get_object_bounds(obj):
if obj.type == ‘MESH’:
points = [obj.matrix_world @ Vector(corner) for corner in obj.bound_box]
x = [p.x for p in points]
y = [p.y for p in points]
z = [p.z for p in points]
min_b = Vector((min(x), min(y), min(z)))
max_b = Vector((max(x), max(y), max(z)))
center = (min_b + max_b) / 2
return center, min_b, max_b
return Vector((0,0,0)), Vector((0,0,0)), Vector((0,0,0))
-------------------------------------------------------------
KAMERA AYARLAMA – BASİT VE SABİT
-------------------------------------------------------------
def set_camera_view(name, props):
obj = bpy.context.selected_objects[0] if bpy.context.selected_objects else None
if not obj:
return
center, min_b, max_b = get_object_bounds(obj)
base_size = (max_b - min_b).length
views = {
"front": Vector((0, 0, 1)),
"back": Vector((0, 0, -1)),
"left": Vector((-1, 0, 0)),
"right": Vector((1, 0, 0)),
"top": Vector((0, 1, 0)),
"bottom": Vector((0, -1, 0)),
"perspective": Vector((1, 1, 1)).normalized(),
}
rot = views.get(name, Vector((0, 0, 1)))
cam_name = "ActiveCamera"
if cam_name not in bpy.data.objects:
cam_data = bpy.data.cameras.new(cam_name)
cam_obj = bpy.data.objects.new(cam_name, cam_data)
bpy.context.collection.objects.link(cam_obj)
else:
cam_obj = bpy.data.objects[cam_name]
if name == "perspective":
cam_obj.data.type = 'PERSP'
cam_obj.data.lens = 50
else:
cam_obj.data.type = 'ORTHO'
cam_obj.data.ortho_scale = base_size * 1.2
cam_obj.location = center + rot * (base_size * 1.8)
direction = center - cam_obj.location
rot_q = direction.to_track_quat('-Z', 'Y')
cam_obj.rotation_euler = rot_q.to_euler()
bpy.context.scene.camera = cam_obj
for area in bpy.context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.region_3d.view_rotation = rot_q
space.region_3d.view_location = center
space.region_3d.view_distance = base_size * 1.8
break
-------------------------------------------------------------
GÖRÜNTÜ ÇEKME
-------------------------------------------------------------
def capture_view(name, props):
scene = bpy.context.scene
if not scene.camera:
return None
cache_dir = get_cache_dir()
file = os.path.join(cache_dir, f"{name.lower()}_temp.png")
final_file = os.path.join(cache_dir, f"{name.lower()}.png")
scene.render.image_settings.file_format = 'PNG'
scene.render.image_settings.color_mode = 'RGBA'
scene.render.filepath = file
scene.render.resolution_x = 512
scene.render.resolution_y = 512
bpy.context.view_layer.update()
bpy.ops.render.render(write_still=True)
img = Image.open(file).convert("RGB")
img = ImageEnhance.Contrast(img).enhance(1.2)
img = ImageEnhance.Brightness(img).enhance(0.9)
target_size = (600, 600) if name == "perspective" else (300, 300)
w, h = img.size
ratio = min(target_size[0]/w, target_size[1]/h)
img = img.resize((int(w*ratio), int(h*ratio)), Image.LANCZOS)
canvas = Image.new("RGB", target_size, "gray")
x = (target_size[0] - img.width) // 2
y = (target_size[1] - img.height) // 2
canvas.paste(img, (x, y))
canvas.save(final_file)
return final_file
-------------------------------------------------------------
LAYOUT OLUŞTURMA
-------------------------------------------------------------
def create_professional_layout(images, out_path, obj_name, info):
addon_dir = os.path.dirname(file)
small = 300
large = 600
spacing = 20
padding = 40
right_w = 350
title_h = 90
canvas_w = large + small * 2 + spacing * 4 + padding * 2 + right_w
canvas_h = large + small + spacing * 3 + padding * 2 + title_h
canvas = Image.new(“RGB”, (canvas_w, canvas_h), “white”)
draw = ImageDraw.Draw(canvas)
f_title = load_font(32)
f_normal = load_font(22)
f_small = load_font(18)
right_x = canvas_w - right_w
draw.rectangle([right_x, 0, canvas_w, canvas_h], fill=(240,240,240))
logo_path = os.path.join(addon_dir, "logo.png")
if os.path.exists(logo_path):
try:
logo = Image.open(logo_path).convert("RGBA")
logo.thumbnail((300, 120))
canvas.paste(logo, (right_x + 25, 20), logo)
except:
pass
yt = 170
tx = right_x + 25
draw.text((tx, yt), "PROJECT INFO", fill=(0,0,0), font=f_title)
yt += 50
for label, key in [
("Company:", "company"),
("Project:", "project"),
("Code:", "code"),
("Version:", "version"),
("Date:", "date"),
]:
draw.text((tx, yt), label, fill=(80,80,80), font=f_normal)
draw.text((tx, yt + 25), info.get(key, ""), fill=(0,0,0), font=f_small)
yt += 60
draw.text((tx, yt), "Notes:", fill=(80,80,80), font=f_normal)
draw.text((tx, yt + 25), info.get("notes", ""), fill=(0,0,0), font=f_small)
px = padding
py = padding + title_h
# Perspective
if images.get("perspective") and os.path.exists(images["perspective"]):
img = Image.open(images["perspective"])
draw.rectangle([px, py, px + large, py + large], outline=(200,200,200), width=3)
draw.text((px + 5, py - 20), "Perspective", fill=(0,0,0), font=f_small)
img.thumbnail((large-15, large-15))
canvas.paste(img, (px + (large - img.width) // 2, py + (large - img.height) // 2))
# Top & Bottom
tx1 = px + large + spacing
ty1 = py
for label, key, xx in [
("Top", "top", tx1),
("Bottom", "bottom", tx1 + small + spacing),
]:
draw.text((xx, ty1 - 30), label, fill=(0,0,0), font=f_small)
draw.rectangle([xx, ty1, xx + small, ty1 + small], outline=(200,200,200), width=3)
if images.get(key) and os.path.exists(images[key]):
img = Image.open(images[key])
img.thumbnail((small-15, small-15))
canvas.paste(img, (xx + (small - img.width) // 2, ty1 + (small - img.height) // 2))
# Front, Back, Left, Right
oy = py + large + spacing
ortho_s = 200
os_gap = 15
views = [
("front", "Front"),
("back", "Back"),
("left", "Left"),
("right", "Right"),
]
ox = padding
for key, label in views:
draw.text((ox, oy - 25), label, fill=(0,0,0), font=f_small)
draw.rectangle([ox, oy, ox + ortho_s, oy + ortho_s], outline=(200,200,200), width=3)
if images.get(key) and os.path.exists(images[key]):
img = Image.open(images[key])
img.thumbnail((ortho_s-15, ortho_s-15))
canvas.paste(img, (ox + (ortho_s - img.width) // 2, oy + (ortho_s - img.height) // 2))
ox += ortho_s + os_gap
final_path = os.path.join(out_path, "LAYOUT_FINAL.png")
canvas.save(final_path)
-------------------------------------------------------------
OPERATORS
-------------------------------------------------------------
class RENDER_OT_set_view(bpy.types.Operator):
bl_idname = “render.set_view”
bl_label = “Set View”
view_name: bpy.props.StringProperty()
def execute(self, context):
set_camera_view(self.view_name, context.scene.multiview_props)
return {‘FINISHED’}
class RENDER_OT_capture_view(bpy.types.Operator):
bl_idname = “render.capture_view”
bl_label = “Capture View”
view_name: bpy.props.StringProperty()
def execute(self, context):
props = context.scene.multiview_props
obj = bpy.context.selected_objects[0] if bpy.context.selected_objects else None
if not obj:
self.report({‘ERROR’}, “Please select an object”)
return {‘CANCELLED’}
img_path = capture_view(self.view_name, props)
if img_path:
setattr(props, f"{self.view_name}_img", img_path)
self.report({‘INFO’}, f"{self.view_name} captured!")
else:
self.report({‘ERROR’}, “Failed to capture view”)
return {‘FINISHED’}
class RENDER_OT_create_layout(bpy.types.Operator):
bl_idname = “render.create_layout”
bl_label = “Create Layout”
filepath: bpy.props.StringProperty(subtype=‘DIR_PATH’)
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {‘RUNNING_MODAL’}
def execute(self, context):
if not self.filepath:
self.report({‘ERROR’}, “Please select output directory”)
return {‘CANCELLED’}
props = context.scene.multiview_props
obj = bpy.context.selected_objects[0] if bpy.context.selected_objects else None
if not obj:
self.report({‘ERROR’}, “Please select an object”)
return {‘CANCELLED’}
images = {
"front": props.front_img,
"back": props.back_img,
"left": props.left_img,
"right": props.right_img,
"top": props.top_img,
"bottom": props.bottom_img,
"perspective": props.perspective_img,
}
create_professional_layout(images, self.filepath, obj.name, {
'company': props.company_name,
'project': props.project_name,
'code': props.drawing_code,
'version': props.version,
'date': props.date,
'notes': props.notes,
})
# ✅ GEÇİCİ KLÖSÖRÜ SİL
cache_dir = get_cache_dir()
if os.path.exists(cache_dir):
shutil.rmtree(cache_dir)
self.report({'INFO'}, f"Layout saved to: {self.filepath}")
return {'FINISHED'}
-------------------------------------------------------------
PANEL – SON DÜZEN
-------------------------------------------------------------
class RENDER_PT_multiview_panel(bpy.types.Panel):
bl_label = “Multi-View Layout”
bl_category = “Multi-View”
bl_space_type = ‘VIEW_3D’
bl_region_type = ‘UI’
def draw(self, context):
layout = self.layout
props = context.scene.multiview_props
layout.label(text="Project Information", icon='INFO')
box = layout.box()
box.prop(props, "company_name")
box.prop(props, "project_name")
box.prop(props, "drawing_code")
box.prop(props, "version")
box.prop(props, "date")
layout.separator()
layout.prop(props, "notes")
layout.separator()
layout.label(text="Render Settings", icon='LIGHT')
box = layout.box()
box.prop(props, "light_color")
box.prop(props, "light_energy")
layout.separator()
layout.label(text="📌 Kontroller", icon='VIEW3D')
# 2x2 Grid
row = layout.row()
col1 = row.column(align=True)
col2 = row.column(align=True)
# Front / Back
r1 = col1.row(align=True)
r1.operator("render.capture_view", text="", icon='CAMERA_DATA').view_name = "front"
r1.operator("render.set_view", text="→ Front", icon='FORWARD').view_name = "front"
r1b = col2.row(align=True)
r1b.operator("render.capture_view", text="", icon='CAMERA_DATA').view_name = "back"
r1b.operator("render.set_view", text="← Back", icon='BACK').view_name = "back"
# Left / Right
r2 = col1.row(align=True)
r2.operator("render.capture_view", text="", icon='CAMERA_DATA').view_name = "left"
r2.operator("render.set_view", text="← Left", icon='BACK').view_name = "left"
r2b = col2.row(align=True)
r2b.operator("render.capture_view", text="", icon='CAMERA_DATA').view_name = "right"
r2b.operator("render.set_view", text="→ Right", icon='FORWARD').view_name = "right"
# Top / Bottom
r3 = col1.row(align=True)
r3.operator("render.capture_view", text="", icon='CAMERA_DATA').view_name = "top"
r3.operator("render.set_view", text="↑ Top", icon='TRIA_UP').view_name = "top"
r3b = col2.row(align=True)
r3b.operator("render.capture_view", text="", icon='CAMERA_DATA').view_name = "bottom"
r3b.operator("render.set_view", text="↓ Bottom", icon='TRIA_DOWN').view_name = "bottom"
# Perspective
r4 = layout.row(align=True)
r4.operator("render.capture_view", text="", icon='CAMERA_DATA').view_name = "perspective"
r4.operator("render.set_view", text="👁️ Perspective", icon='VIEW_PERSPECTIVE').view_name = "perspective"
layout.separator()
layout.operator("render.create_layout", icon='RENDER_STILL')
-------------------------------------------------------------
REGISTER / UNREGISTER
-------------------------------------------------------------
def register():
bpy.utils.register_class(MultiViewProperties)
bpy.utils.register_class(RENDER_OT_set_view)
bpy.utils.register_class(RENDER_OT_capture_view)
bpy.utils.register_class(RENDER_OT_create_layout)
bpy.utils.register_class(RENDER_PT_multiview_panel)
bpy.types.Scene.multiview_props = bpy.props.PointerProperty(type=MultiViewProperties)
def unregister():
bpy.utils.unregister_class(MultiViewProperties)
bpy.utils.unregister_class(RENDER_OT_set_view)
bpy.utils.unregister_class(RENDER_OT_capture_view)
bpy.utils.unregister_class(RENDER_OT_create_layout)
bpy.utils.unregister_class(RENDER_PT_multiview_panel)
if “multiview_props” in bpy.types.Scene.dict:
del bpy.types.Scene.multiview_props
if name == “main”:
register()
