Save a camera's full settings bundle — resolution, exposure, world, lens/DOF, background image config (everything pie_menu.draw_camera_settings() already exposes) — as a named, reusable preset that can be applied to any camera.
This is a natural extension of the addon's existing per-camera-property model rather than a new paradigm, and directly serves the "quickly present your work" / product-design use cases called out in the README (recurring hero-shot / turntable-angle setups reused across projects).
Resolved design
Storage: Blender's native preset system (AddPresetBase + a Menu with preset_subdir / draw = Menu.draw_preset), matching the same "+" preset dropdown UX every other Blender panel already uses — not a custom JSON file. Verified AddPresetBase's actual serialization against the Blender source (bl_operators/presets.py): each preset_values entry is written as path = repr(value) and re-executed via bpy.utils.execfile() on apply.
Apply semantics: overwrite outright. Matches the existing CAM_MANAGER_OT_apply_resolution_preset / ..._focal_length_preset operators, which already unconditionally set their target fields. No merge/dirty-tracking logic — there's no reliable way to define "already customized" for fields like lens/clip/exposure that legitimately vary by default.
Field scope — background images excluded from v1: cam.background_images entries reference an Image/MovieClip datablock per entry (same ID-resolution risk as world, but compounded), and AddPresetBase's automatic collection handling calls a bare .add() which background_images doesn't support (only .new(image=..., clip=...)) — likely to error rather than degrade. Background images are also more "per-camera reference photo" than "reusable compositional setup." Revisit as a separate follow-up if it turns out to matter.
ID-pointer fields (world, dof.focus_object) ordered last in preset_values: execfile runs top-to-bottom, so if a referenced datablock doesn't exist in the target file, that line raises and aborts the rest of the script — but only the fields after it are lost, since everything before already executed as a normal sequential assignment. Putting the two risky ID-pointer fields last means a missing-datablock failure only drops those two fields instead of the whole preset.
preset_defines deviates from Blender's own convention, deliberately: native camera presets use cam = bpy.context.camera, reliable only from the Properties Editor's Camera Data tab. This addon's draw_camera_settings() is called from the 3D viewport N-panel and pie menu instead, where context.camera isn't guaranteed to match. Every call site here (ui.py, pie_menu.py) always passes context.scene.camera, so use cam = bpy.context.scene.camera.data instead.
Implementation plan
New camera_presets.py (same classes tuple + register()/unregister() shape as every other module here):
class CAM_MANAGER_OT_camera_preset_add(AddPresetBase, Operator):
bl_idname = "cam_manager.camera_preset_add"
preset_menu = "CAM_MANAGER_MT_camera_presets"
preset_subdir = "simple_camera_manager/camera"
preset_defines = ["cam = bpy.context.scene.camera.data"]
preset_values = [
"cam.type", "cam.resolution_overwrite", "cam.resolution",
"cam.lens", "cam.ortho_scale", "cam.clip_start", "cam.clip_end",
"cam.exposure", "cam.dof.use_dof", "cam.dof.focus_distance",
"cam.dof.focus_subtarget", "cam.show_rotation_gizmo",
"cam.dolly_zoom_link_focus",
"cam.dof.focus_object", "cam.world", # ID pointers last, see above
]
class CAM_MANAGER_MT_camera_presets(Menu):
bl_label = "Camera Presets"
preset_subdir = "simple_camera_manager/camera"
preset_operator = "script.execute_preset"
draw = Menu.draw_preset
Wire into pie_menu.py's draw_camera_settings() with the standard inline add/remove-button row (layout.menu(...), +/- operator buttons), register the new module in __init__.py.
Tests: add both classes to tests/test_addon_registration.py's _REPRESENTATIVE_CLASSES; new tests/test_camera_presets.py doing a save → mutate → apply → assert-restored round trip. Must clean up the preset file it writes in tearDown() — AddPresetBase writes to the real bpy.utils.user_resource('SCRIPTS', ...) path, not a sandboxed temp dir.
Not yet implemented — this is the design, ready to build.
Save a camera's full settings bundle — resolution, exposure, world, lens/DOF, background image config (everything
pie_menu.draw_camera_settings()already exposes) — as a named, reusable preset that can be applied to any camera.This is a natural extension of the addon's existing per-camera-property model rather than a new paradigm, and directly serves the "quickly present your work" / product-design use cases called out in the README (recurring hero-shot / turntable-angle setups reused across projects).
Resolved design
Storage: Blender's native preset system (
AddPresetBase+ aMenuwithpreset_subdir/draw = Menu.draw_preset), matching the same "+" preset dropdown UX every other Blender panel already uses — not a custom JSON file. VerifiedAddPresetBase's actual serialization against the Blender source (bl_operators/presets.py): eachpreset_valuesentry is written aspath = repr(value)and re-executed viabpy.utils.execfile()on apply.Apply semantics: overwrite outright. Matches the existing
CAM_MANAGER_OT_apply_resolution_preset/..._focal_length_presetoperators, which already unconditionally set their target fields. No merge/dirty-tracking logic — there's no reliable way to define "already customized" for fields like lens/clip/exposure that legitimately vary by default.Field scope — background images excluded from v1:
cam.background_imagesentries reference anImage/MovieClipdatablock per entry (same ID-resolution risk asworld, but compounded), andAddPresetBase's automatic collection handling calls a bare.add()whichbackground_imagesdoesn't support (only.new(image=..., clip=...)) — likely to error rather than degrade. Background images are also more "per-camera reference photo" than "reusable compositional setup." Revisit as a separate follow-up if it turns out to matter.ID-pointer fields (
world,dof.focus_object) ordered last inpreset_values:execfileruns top-to-bottom, so if a referenced datablock doesn't exist in the target file, that line raises and aborts the rest of the script — but only the fields after it are lost, since everything before already executed as a normal sequential assignment. Putting the two risky ID-pointer fields last means a missing-datablock failure only drops those two fields instead of the whole preset.preset_definesdeviates from Blender's own convention, deliberately: native camera presets usecam = bpy.context.camera, reliable only from the Properties Editor's Camera Data tab. This addon'sdraw_camera_settings()is called from the 3D viewport N-panel and pie menu instead, wherecontext.cameraisn't guaranteed to match. Every call site here (ui.py,pie_menu.py) always passescontext.scene.camera, so usecam = bpy.context.scene.camera.datainstead.Implementation plan
New
camera_presets.py(sameclassestuple +register()/unregister()shape as every other module here):Wire into
pie_menu.py'sdraw_camera_settings()with the standard inline add/remove-button row (layout.menu(...),+/-operator buttons), register the new module in__init__.py.Tests: add both classes to
tests/test_addon_registration.py's_REPRESENTATIVE_CLASSES; newtests/test_camera_presets.pydoing a save → mutate → apply → assert-restored round trip. Must clean up the preset file it writes intearDown()—AddPresetBasewrites to the realbpy.utils.user_resource('SCRIPTS', ...)path, not a sandboxed temp dir.Not yet implemented — this is the design, ready to build.