--- /dev/null
+import bpy, math, gpu, blf
+from mathutils import *
+from gpu_extras.batch import batch_for_shader
+
+bl_info = {
+ "name":"Graphics Cropper",
+ "author": "Harry Godden (hgn)",
+ "version": (0,1),
+ "blender":(3,1,0),
+ "location":"",
+ "descriptin":"",
+ "warning":"",
+ "wiki_url":"",
+ "category":"",
+}
+
+# Clicky clicky GUI
+# ------------------------------------------------------------------------------
+
+cropper_view_draw_handler = None
+cropper_ui_draw_handler = None
+cropper_view_shader = gpu.shader.from_builtin('3D_SMOOTH_COLOR')
+
+def cropper_draw_ui():
+ mtx = bpy.context.region_data.perspective_matrix
+ w = bpy.context.region.width
+ h = bpy.context.region.height
+
+ for obj in bpy.context.scene.objects:
+ if obj.cropper_data.enabled:
+ x = obj.cropper_data.resolution[0]
+ y = obj.cropper_data.resolution[1]
+ c = Vector((1,1,0,1)) if obj.select_get() else Vector((0.6,0.4,0,1))
+ p0 = obj.matrix_world @ Vector((0,0,0))
+ p1 = obj.matrix_world @ Vector((x,0,0))
+ p2 = obj.matrix_world @ Vector((x,y,0))
+ p3 = obj.matrix_world @ Vector((0,y,0))
+
+ co = mtx @ Vector((p3[0],p3[1],p3[2],1.0))
+
+ co[0] /= co[3]
+ co[1] /= co[3]
+ co[0] *= 0.5
+ co[1] *= 0.5
+ co[0] += 0.5
+ co[1] += 0.5
+ co[0] *= w
+ co[1] *= h
+
+ blf.position(0,co[0],co[1]+8,0)
+ blf.size(0,20,48)
+ blf.color(0,c[0],c[1],c[2],c[3])
+ blf.draw(0,obj.cropper_data.filename)
+
+def cropper_draw():
+ global cropper_view_shader
+
+ verts = []
+ colours = []
+
+ for obj in bpy.context.scene.objects:
+ if obj.cropper_data.enabled:
+ x = obj.cropper_data.resolution[0]
+ y = obj.cropper_data.resolution[1]
+ c = Vector((1,1,0,1)) if obj.select_get() else Vector((0.6,0.4,0,1))
+ p0 = obj.matrix_world @ Vector((0,0,0))
+ p1 = obj.matrix_world @ Vector((x,0,0))
+ p2 = obj.matrix_world @ Vector((x,y,0))
+ p3 = obj.matrix_world @ Vector((0,y,0))
+
+ verts += [p0,p1,p1,p2,p2,p3,p3,p0]
+ colours += [c,c,c,c,c,c,c,c]
+
+ cropper_view_shader.bind()
+ gpu.state.depth_mask_set(False)
+ gpu.state.line_width_set(2.0)
+ gpu.state.face_culling_set('BACK')
+ gpu.state.depth_test_set('NONE')
+ gpu.state.blend_set('NONE')
+ lines = batch_for_shader( cropper_view_shader, 'LINES', \
+ { "pos":verts, "color":colours })
+ lines.draw( cropper_view_shader )
+
+# Blender
+# ------------------------------------------------------------------------------
+
+def cropper_render_item(obj,context):
+ cam = context.scene.camera
+ original_pos_x = cam.location[0]
+ original_pos_y = cam.location[1]
+ original_res_x = context.scene.render.resolution_x
+ original_res_y = context.scene.render.resolution_y
+ original_file = context.scene.render.filepath
+ original_scale = cam.data.ortho_scale
+
+ x = obj.cropper_data.resolution[0]
+ y = obj.cropper_data.resolution[1]
+ fmt = context.scene.render.image_settings.file_format.lower()
+ fn = F"{obj.cropper_data.filename}.{fmt}"
+
+ p0 = obj.matrix_world @ Vector((0,0,0))
+ p1 = obj.matrix_world @ Vector((x,0,0))
+ p2 = obj.matrix_world @ Vector((x,y,0))
+ p3 = obj.matrix_world @ Vector((0,y,0))
+ c = obj.matrix_world @ Vector((x*0.5,y*0.5,0))
+
+ if x > y: s = p2[0]-p0[0]
+ else: s = p2[1]-p0[1]
+
+ cam.location[0] = c[0]
+ cam.location[1] = c[1]
+
+ cam.data.ortho_scale = s
+ context.scene.render.resolution_x = x
+ context.scene.render.resolution_y = y
+ context.scene.render.filepath = original_file + fn
+ print( F"render to: {context.scene.render.filepath}" )
+ bpy.ops.render.render(write_still=True)
+
+ # Reset
+ context.scene.render.resolution_x = original_res_x
+ context.scene.render.resolution_y = original_res_y
+ context.scene.render.filepath = original_file
+
+ cam.data.ortho_scale = original_scale
+ cam.location[0] = original_pos_x
+ cam.location[1] = original_pos_y
+
+class CROPPER_RENDER_ALL(bpy.types.Operator):
+ bl_idname="cropper.render_all"
+ bl_label="Render all"
+ def execute(_,context):
+ for obj in context.scene.objects:
+ if obj.cropper_data.enabled:
+ cropper_render_item(obj,context)
+
+ return {'FINISHED'}
+
+class CROPPER_RENDER(bpy.types.Operator):
+ bl_idname="cropper.render"
+ bl_label="Render"
+ def execute(_,context):
+ obj = context.active_object
+ if obj != None and obj.cropper_data.enabled:
+ cropper_render_item(obj,context)
+
+ return {'FINISHED'}
+
+class CROPPER_OBJ_SETTINGS(bpy.types.PropertyGroup):
+ enabled: bpy.props.BoolProperty( name="enabled" )
+ filename: bpy.props.StringProperty( name="filename" )
+ resolution: bpy.props.IntVectorProperty( name="resolution",size=2 )
+
+class CROPPER_OBJ_PANEL(bpy.types.Panel):
+ bl_label="Cropper Settings"
+ bl_idname="SCENE_PT_cropper"
+ bl_space_type='PROPERTIES'
+ bl_region_type='WINDOW'
+ bl_context="object"
+
+ def draw(_,context):
+ active_object = context.active_object
+ if active_object == None: return
+ _.layout.prop( active_object.cropper_data, "enabled" )
+
+ box = _.layout.box()
+ if not active_object.cropper_data.enabled:
+ box.enabled = False
+
+ box.prop( active_object.cropper_data, "filename" )
+ box.prop( active_object.cropper_data, "resolution" )
+ box.operator( "cropper.render" )
+ _.layout.operator( "cropper.render_all" )
+
+classes = [ CROPPER_OBJ_SETTINGS, CROPPER_OBJ_PANEL, \
+ CROPPER_RENDER, CROPPER_RENDER_ALL ]
+
+def register():
+ global cropper_view_draw_handler, cropper_ui_draw_handler
+
+ for c in classes:
+ bpy.utils.register_class(c)
+
+ bpy.types.Object.cropper_data = \
+ bpy.props.PointerProperty(type=CROPPER_OBJ_SETTINGS)
+
+ cropper_view_draw_handler = bpy.types.SpaceView3D.draw_handler_add(\
+ cropper_draw,(),'WINDOW','POST_VIEW')
+ cropper_ui_draw_handler = bpy.types.SpaceView3D.draw_handler_add(\
+ cropper_draw_ui,(),'WINDOW','POST_PIXEL')
+
+def unregister():
+ global cropper_view_draw_handler, cropper_ui_draw_handler
+
+ for c in classes:
+ bpy.utils.unregister_class(c)
+
+ bpy.types.SpaceView3D.draw_handler_remove(cropper_view_draw_handler,'WINDOW')
+ bpy.types.SpaceView3D.draw_handler_remove(cropper_ui_draw_handler,'WINDOW')