work: save startup because I'm sick of the arnoldesque materials

This commit is contained in:
2026-04-24 14:39:38 -06:00
parent b401eb4bcf
commit 2f8e5f472f
57 changed files with 2257 additions and 1643 deletions
+8 -8
View File
@@ -10,13 +10,13 @@ D:\Work\9 iClone\Amazon\
D:\Amazon\00_external-files\
N:\1. CHARACTERS\remapping\
[Recent]
A:\1 Amazon_Active_Projects\260421_Pallet_Loading_and_Wrapping\Blends\stills\
A:\1 Amazon_Active_Projects\260421_Pallet_Loading_and_Wrapping\Assets\Blends\
A:\1 Amazon_Active_Projects\260421_Pallet_Loading_and_Wrapping\Blends\stills\img-BG\
A:\1 Amazon_Active_Projects\260421_Pallet_Loading_and_Wrapping\Deliverable\still\
P:\260423_MukBuddy\Blends\animations\
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\animations\2025_old\
A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\USD\dust\
C:\Users\Nathan\Downloads\
D:\2026-04-14 BCONNA2026_MARI\
G:\Flamenco\renders\
D:\portfolio\
P:\250827_FestivalTurf\Blends\animations\02 Preparing the Base\Watering Ground\
P:\250827_FestivalTurf\Assets\Blends\
T:\250827_FestivalTurf\Blends\animations\02 Preparing the Base\
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\stills\2025_old\img-BG\
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\stills\2025_old\
+9 -224
View File
@@ -1,15 +1,15 @@
{
"flamenco_version": {
"version": "3.8.2",
"shortversion": "3.8.2",
"version": "3.8.5",
"shortversion": "3.8.5",
"name": "Flamenco",
"git": "51a41a19"
"git": "690bf6ce"
},
"shared_storage": {
"location": "R:\\mnt\\user\\Cerberus Max\\Flamenco",
"location": "F:\\jobs",
"audience": "users",
"platform": "windows",
"shaman_enabled": false
"shaman_enabled": true
},
"job_types": {
"job_types": [
@@ -283,197 +283,6 @@
"etag": "fe38e5ae9b3e640119d0d55fc7cbf4f8f15f2982",
"description": "Create a viewport preview/playblast using Cycles OPTIX GPU rendering and generate a video file"
},
{
"name": "HoloMARI",
"label": "HoloMARI (MARI render_one)",
"settings": [
{
"key": "mari_jobs_json",
"type": "string",
"description": "JSON object with a \"jobs\" array: {\"jobs\":[{\"cam_name\":...,\"H\":...,\"V\":...}, ...]}. Optional \"frame\" per job for ANIM.",
"eval": "__import__('json').dumps({'jobs':[{'cam_name':o.name,'H':int(o['H']),'V':int(o['V'])} for o in C.scene.objects if getattr(o,'type',None)=='CAMERA' and o.name.startswith('MARI_CAMERA') and 'H' in o.keys() and 'V' in o.keys()]})",
"eval_info": {
"show_link_button": true,
"description": "Auto-collect MARI cameras (name MARI_CAMERA*, custom props H,V). Matches MARI Advanced / HoloMARI job JSON."
},
"required": true,
"visible": "submission"
},
{
"key": "mari_action",
"type": "string",
"choices": [
"STILL",
"ANIM"
],
"default": "STILL",
"description": "MARI action passed to render_one / export_mari: STILL or ANIM",
"visible": "submission"
},
{
"key": "mari_mode",
"type": "string",
"choices": [
"CIRCLE",
"FRAME"
],
"default": "CIRCLE",
"description": "MARI layout mode for export_mari type=FRAME|CIRCLE",
"eval": "(lambda m: 'FRAME' if str(m) == 'FRAME' else 'CIRCLE')(str(getattr(getattr(C.scene, 'mari_props', None), 'frame', 'CIRCLE') or 'CIRCLE'))",
"eval_info": {
"show_link_button": true,
"description": "FRAME vs CIRCLE from scene mari_props.frame"
},
"required": true,
"visible": "submission"
},
{
"key": "chunk_size",
"type": "int32",
"default": 1.0,
"description": "Number of MARI render_one units (cameras/frames) per Blender task",
"propargs": {
"max": 4096.0,
"min": 1.0
},
"visible": "submission"
},
{
"key": "frames",
"type": "string",
"description": "Frame range for ANIM expansion. Examples: '47', '1-30'",
"eval": "f'{C.scene.frame_start}-{C.scene.frame_end}'",
"eval_info": {
"show_link_button": true,
"description": "Scene frame range (used when expanding ANIM image sequences)"
},
"required": true
},
{
"key": "frame_start",
"type": "int32",
"eval": "C.scene.frame_start",
"visible": "hidden"
},
{
"key": "frame_end",
"type": "int32",
"eval": "C.scene.frame_end",
"visible": "hidden"
},
{
"key": "frame_step",
"type": "int32",
"eval": "max(1, C.scene.frame_step)",
"visible": "hidden"
},
{
"key": "render_output_root",
"type": "string",
"description": "Root folder for MARI output; images go under <root>/<mari_props.render_settings_name> (name comes from the scene, not this field).",
"required": true,
"subtype": "dir_path",
"visible": "submission"
},
{
"key": "add_path_components",
"type": "int32",
"default": 0.0,
"description": "Number of path components of the current blend file to append under the render root",
"propargs": {
"max": 32.0,
"min": 0.0
},
"required": true,
"visible": "submission"
},
{
"key": "render_output_path",
"type": "string",
"description": "Job artifact path (mari_job.txt). MARI JPEG/output folder = render_output_root + MARI folder name.",
"editable": false,
"eval": "str(Path(abspath(settings.render_output_root), last_n_dir_parts(settings.add_path_components), jobname, '{timestamp}', 'mari_job.txt'))",
"subtype": "file_path"
},
{
"key": "mari_output_dir_abs",
"type": "string",
"description": "Absolute MARI output folder (render root + scene folder name); used by the zip step.",
"editable": false,
"eval": "str(Path(abspath(settings.render_output_root)) / str(getattr(getattr(C.scene, 'mari_props', None), 'render_settings_name', '') or '').strip())",
"visible": "hidden"
},
{
"key": "mari_output_zip_abs",
"type": "string",
"description": "Absolute path for the final .zip next to the output folder.",
"editable": false,
"eval": "str(Path(abspath(settings.render_output_root)) / (str(getattr(getattr(C.scene, 'mari_props', None), 'render_settings_name', '') or '').strip() + '.zip'))",
"visible": "hidden"
},
{
"key": "zip_render_output",
"type": "bool",
"default": true,
"description": "After all MARI chunk tasks finish, create a .zip of the output folder (requires a worker that runs misc tasks; uses tar).",
"required": false,
"visible": "submission"
},
{
"key": "use_cycles_optix",
"type": "bool",
"default": false,
"description": "When enabled, sets Cycles preferences to OPTIX and scene Cycles device to GPU (same idea as cycles_optix_gpu.js).",
"label": "Cycles: OPTIX + GPU device",
"required": false,
"visible": "submission"
},
{
"key": "experimental_gp3",
"type": "bool",
"description": "Experimental Flag: Grease Pencil 3",
"label": "Experimental: GPv3",
"required": false
},
{
"key": "experimental_new_anim",
"type": "bool",
"description": "Experimental Flag: New Animation Data-block",
"label": "Experimental: Baklava",
"required": false
},
{
"key": "blender_args_before",
"type": "string",
"description": "CLI arguments for Blender, placed before the .blend filename",
"label": "Blender CLI args: Before",
"required": false
},
{
"key": "blender_args_after",
"type": "string",
"description": "CLI arguments for Blender, placed after the .blend filename",
"label": "After",
"required": false
},
{
"key": "blendfile",
"type": "string",
"description": "Path of the Blend file",
"required": true,
"visible": "web"
},
{
"key": "format",
"type": "string",
"eval": "C.scene.render.image_settings.file_format",
"required": true,
"visible": "web"
}
],
"etag": "d19240e637ce81fab40f8e4218ce0249db67d95e",
"description": "MARI Advanced render_one batches (export .mari3d prep + per-camera jobs). Optional Cycles OPTIX+GPU setup (off by default)."
},
{
"name": "TalkingHeads Custom Render",
"label": "TalkingHeads Custom Render",
@@ -517,18 +326,6 @@
"subtype": "dir_path",
"visible": "submission"
},
{
"key": "daily_path",
"type": "string",
"description": "Daily folder name under the render root, e.g. 'daily_250813'. If empty, auto-fills to today's date.",
"eval": "__import__('datetime').datetime.now().strftime('daily_%y%m%d')",
"eval_info": {
"show_link_button": true,
"description": "Auto-fill with today's daily folder name"
},
"required": false,
"visible": "submission"
},
{
"key": "use_submodule",
"type": "bool",
@@ -555,7 +352,7 @@
"type": "string",
"description": "Final file path of where render output will be saved",
"editable": false,
"eval": "str(Path(abspath(settings.render_output_root or '//'), ((str(settings.submodule or '').strip()) if (settings.use_submodule and str(settings.submodule or '').strip()) else ((__import__('os').path.basename(__import__('os').path.dirname(bpy.data.filepath))) if settings.use_submodule else '')), (settings.daily_path or __import__('datetime').datetime.now().strftime('daily_%y%m%d')), jobname, jobname + '_######'))",
"eval": "((lambda Path, abspath, os_path, settings_obj, jobname: str(Path(abspath(settings_obj.render_output_root or '//')) / (((str(settings_obj.submodule or '').strip()) if (settings_obj.use_submodule and str(settings_obj.submodule or '').strip()) else ((os_path.basename(os_path.dirname(bpy.data.filepath))) if settings_obj.use_submodule else ''))) / jobname / (jobname + '_######')))(__import__('pathlib').Path, __import__('os').path.abspath, __import__('os').path, settings, (job.name if 'job' in dir() and job and job.name else __import__('os').path.splitext(__import__('os').path.basename(bpy.data.filepath))[0])))",
"subtype": "file_path"
},
{
@@ -596,7 +393,7 @@
"visible": "web"
}
],
"etag": "7773b03fd0d493d6d823d7d45bb3ef7f987384f3",
"etag": "5f716e7195e55712daee20c237d95f748e2c3d1e",
"description": "Render a sequence of frames, and create a preview video file"
},
{
@@ -634,18 +431,6 @@
"subtype": "dir_path",
"visible": "submission"
},
{
"key": "daily_path",
"type": "string",
"description": "Daily folder name under the render root, e.g. 'daily_250813'. If empty, auto-fills to today's date.",
"eval": "__import__('datetime').datetime.now().strftime('daily_%y%m%d')",
"eval_info": {
"show_link_button": true,
"description": "Auto-fill with today's daily folder name"
},
"required": false,
"visible": "submission"
},
{
"key": "use_submodule",
"type": "bool",
@@ -672,7 +457,7 @@
"type": "string",
"description": "Final file path of where render output will be saved",
"editable": false,
"eval": "str(Path(abspath(settings.render_output_root or '//'), ((str(settings.submodule or '').strip()) if (settings.use_submodule and str(settings.submodule or '').strip()) else ((__import__('os').path.basename(__import__('os').path.dirname(bpy.data.filepath))) if settings.use_submodule else '')), (settings.daily_path or __import__('datetime').datetime.now().strftime('daily_%y%m%d')), jobname, jobname + '_######'))",
"eval": "((lambda Path, abspath, os_path, settings_obj, jobname: str(Path(abspath(settings_obj.render_output_root or '//')) / (((str(settings_obj.submodule or '').strip()) if (settings_obj.use_submodule and str(settings_obj.submodule or '').strip()) else ((os_path.basename(os_path.dirname(bpy.data.filepath))) if settings_obj.use_submodule else ''))) / jobname / (jobname + '_######')))(__import__('pathlib').Path, __import__('os').path.abspath, __import__('os').path, settings, (job.name if 'job' in dir() and job and job.name else __import__('os').path.splitext(__import__('os').path.basename(bpy.data.filepath))[0])))",
"subtype": "file_path"
},
{
@@ -741,7 +526,7 @@
"visible": "hidden"
}
],
"etag": "5e49db0874b8ee1211f6dbc0a59d66137b6996b2",
"etag": "758c56b672b0970896f30b85ee5165a5c096aec4",
"description": "OPTIX GPU rendering + extra checkboxes for some experimental features + extra CLI args for Blender"
},
{
+43 -43
View File
@@ -1,3 +1,46 @@
A:\1 Amazon_Active_Projects\260421_Pallet_Loading_and_Wrapping\Blends\stills\iPalletization_11a.blend
A:\1 Amazon_Active_Projects\260421_Pallet_Loading_and_Wrapping\Blends\stills\iPalletization_11b.blend
A:\1 Amazon_Active_Projects\260421_Pallet_Loading_and_Wrapping\Blends\stills\iPalletization_11c.blend
A:\1 Amazon_Active_Projects\260421_Pallet_Loading_and_Wrapping\Assets\Blends\Pallet_Label.blend
A:\1 Amazon_Active_Projects\260421_Pallet_Loading_and_Wrapping\Blends\stills\iPalletization_9.blend
A:\1 Amazon_Active_Projects\260421_Pallet_Loading_and_Wrapping\Blends\stills\iPalletization_10.blend
A:\1 Amazon_Active_Projects\260421_Pallet_Loading_and_Wrapping\Blends\stills\iPalletization_2a.blend
A:\1 Amazon_Active_Projects\260421_Pallet_Loading_and_Wrapping\Blends\animations\Pallet building_animation 2a.blend
A:\1 Amazon_Active_Projects\260421_Pallet_Loading_and_Wrapping\Blends\animations\Pallet building_animation 2b.blend
A:\1 Amazon_Active_Projects\260421_Pallet_Loading_and_Wrapping\Blends\stills\iPalletization_6c.blend
A:\1 Amazon_Active_Projects\260421_Pallet_Loading_and_Wrapping\Blends\stills\iPalletization_6b.blend
A:\1 Amazon_Active_Projects\260421_Pallet_Loading_and_Wrapping\Blends\stills\iPalletization_6a.blend
A:\1 Amazon_Active_Projects\260421_Pallet_Loading_and_Wrapping\Blends\stills\iPalletization_8.blend
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\animations\2026_new\P&S_2026_animation short 2a.blend
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\animations\2025_old\Ambassador_M3_A4_picker_s2.blend
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\animations\2025_old\P&S2025_anim_3b.blend
P:\260423_MukBuddy\Blends\animations\Muk Buddy Animation_gif.blend
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\animations\2025_old\P&S2025_anim_2a.blend
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\animations\2025_old\P&S2025_anim_2c.blend
P:\260423_MukBuddy\Blends\animations\Muk Buddy Animation.blend
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\animations\2025_old\P&S_safety_refresher_1b-1.blend
P:\260423_MukBuddy\Blends\animations\Pak Buddy Animation.blend
P:\260423_MukBuddy\Assets\Blends\Spray Paint.blend
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\animations\2025_old\P&S_safety_refresher_1c_rev2.blend
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\animations\2025_old\P&S_safety_refresher_1b-2_v3.blend
G:\Amazon\2024\240906_amazon-safety-refreshers\blends\P&S\P&S_safety_refresher_1b-2_v3.blend
C:\Users\Nathan\Downloads\P&S_safety_refresher_1d_rev2.blend
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 3B_SPA.blend
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 2B_SPA.blend
F:\jobs\Visual 2B_SPA-29zr\Visual 2B_SPA.flamenco.blend
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 6_SPA.blend
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 5B_alt_vo_SPA.blend
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 4A_SPA.blend
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\01_opening_SPA.blend
A:\1 Amazon_Active_Projects\260420_FLOW_2026\Blends\animations\FlowCSA_360_1aA.blend
A:\1 Amazon_Active_Projects\260420_FLOW_2026\Blends\animations\FlowCSA_360_1a.blend
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_animation 1_CYCLES.blend
A:\1 Amazon_Active_Projects\260421_Pallet_Loading_and_Wrapping\Blends\animations\Pallet wrapping_animation 1aB.blend
F:\jobs\Pallet wrapping_animation 1bC\Pallet wrapping_animation 1bC.flamenco.blend
F:\jobs\Pallet wrapping_animation 1bA-lyma\Pallet wrapping_animation 1bA.flamenco.blend
C:\Users\Nathan\Downloads\Visual 6_SPA.blend
C:\Users\Nathan\Downloads\Visual 8_SPA.blend
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\Visual 6_SPA.blend
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\Visual 8_SPA.blend
D:\2026-04-14 BCONNA2026_MARI\Sample Stanford Bunny2.blend
D:\2026-04-14 BCONNA2026_MARI\Sample Stanford Bunny1.blend
@@ -7,7 +50,6 @@ D:\portfolio\demo.blend
D:\2025-12-28 ytshorts gigaproj\Blends\animations\FreddyEpstein\FreddyEpstein.blend
P:\250827_FestivalTurf\Blends\animations\02 Preparing the Base\06_Watering.blend
T:\250827_FestivalTurf\Blends\animations\02 Preparing the Base\06_Watering.blend
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 2B_SPA.blend
P:\250827_FestivalTurf\Blends\animations\FT-lipsync_SPA.blend
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 2D_SPA.blend
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 2B.blend
@@ -95,7 +137,6 @@ A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\stills\iRSR_fluid_unl
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\stills\iRSR_fluid_unload_25a.blend
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\stills\iRSR_fluid_unload_24d.blend
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\stills\iRSR_fluid_unload_24e.blend
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_animation 1_CYCLES.blend
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\stills\iRSR_fluid_unload_16e.blend
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\stills\iRSR_fluid_unload_24a.blend
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\stills\iRSR_fluid_unload_4b.blend
@@ -132,7 +173,6 @@ P:\250827_FestivalTurf\Blends\animations\07 Final Touches And Maintenance\Visual
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 5B_insert.blend
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 5.blend
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\animations\2025_old\P&S_safety_refresher_1d_rev2.blend
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\animations\2025_old\P&S_safety_refresher_1c_rev2.blend
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\animations\2025_old\P&S_safety_refresher_1c.blend
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\SPA_noncon_OOG_short_animation 9a.blend
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_short_animation 9a_v3.blend
@@ -141,9 +181,7 @@ A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_shor
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Assets\Blends\lipsync.blend
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_short_animation 1b.blend
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\animations\2026_new\P&S_2026_animation short 1c.blend
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\animations\2025_old\P&S2025_anim_2a.blend
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\animations\2025_old\P&S2025_anim_2b.blend
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\animations\2025_old\P&S2025_anim_2c.blend
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\stills\iP&S_2026_2b.blend
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\stills\iP&S_2026_2a.blend
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\stills\iP&S_2026_1b.blend
@@ -155,46 +193,8 @@ C:\Users\Nathan\AppData\Local\Temp\2026-03-27_15-13_P&S2025_anim_2a.blend
C:\Users\Nathan\AppData\Local\Temp\2026-03-27_15-19_P&S2025_anim_2a.blend
C:\Users\Nathan\AppData\Local\Temp\2026-03-27_15-21_P&S2025_anim_2a.blend
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\animations\2025_old\P&S2025_anim_3a.blend
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\animations\2025_old\P&S2025_anim_3b.blend
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\animations\2025_old\P&S2025_anim_1a.blend
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 6.blend
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\Visual 6.blend
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 5B_alt_vo.blend
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 5B.blend
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 4B.blend
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 4A.blend
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 4A_2.blend
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 3A.blend
E:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 3A.blend
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 3A_.blend
T:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 3A.blend
T:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 3B.blend
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\01_opening.blend
T:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 2D.blend
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 2A.blend
T:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 2C.blend
T:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 2B.blend
P:\250827_FestivalTurf\Assets\Blends\Char\FT-rig_moustache.blend
P:\250827_FestivalTurf\Assets\Blends\Char\FT-rig_moustache_fixed.blend
T:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\Visual 11.blend
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\Visual 11.blend
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\Visual 10.blend
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\Visual 9.blend
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\Visual 8.blend
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\Visual 8 insert2.blend
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\Visual 8 insert.blend
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\01_intro.blend
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\Visual 7.blend
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\Visual 6_INSERT.blend
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\04_talking.blend
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\04_stretching pattern.blend
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\03_FT Shuffle_Intro.blend
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\03_FT Shuffle.blend
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\02_talking.blend
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\02_kicker insert.blend
P:\260217_Jarvis-Defense\Blends\animations\Shot_Q.blend
F:\jobs\Shot_5c\Shot_Q.flamenco.blend
F:\jobs\Shot5c-e3zp\Shot_Q.flamenco.blend
P:\260217_Jarvis-Defense\Blends\animations\Shot_4.blend
P:\260217_Jarvis-Defense\Blends\animations\comp_RR\Shot_4a_holdout.blend
P:\260217_Jarvis-Defense\Blends\animations\comp_RR\Shot_4_comp.blend
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -58,7 +58,7 @@
"id": "atomic_data_manager",
"name": "Atomic Data Manager",
"tagline": "Smart cleanup and inspection of Blender data-blocks",
"version": "2.6.2",
"version": "2.6.3",
"type": "add-on",
"maintainer": "RaincloudTheDragon",
"license": [
@@ -70,9 +70,9 @@
"management",
"cleanup"
],
"archive_url": "https://github.com/RaincloudTheDragon/atomic-data-manager/releases/download/v2.6.2/Atomic_Data_Manager.v2.6.2.zip",
"archive_size": 121191,
"archive_hash": "sha256:1f4af882cdf73d3bb0b8cf1badc094b179bf9e982486ee516c45a6a2d478c05d"
"archive_url": "https://github.com/RaincloudTheDragon/atomic-data-manager/releases/download/v2.6.3/Atomic_Data_Manager.v2.6.3.zip",
"archive_size": 122700,
"archive_hash": "sha256:b444463a0443864077abcfe97332406e606a697d3567ac32f85843760da11372"
},
{
"schema_version": "1.0.0",
@@ -0,0 +1,100 @@
{
"version": "v1",
"blocklist": [],
"data": [
{
"schema_version": "1.0.0",
"id": "basedplayblast",
"name": "BasedPlayblast",
"tagline": "Easily create playblasts from Blender and Flamenco",
"version": "2.6.3",
"type": "add-on",
"maintainer": "RaincloudTheDragon <raincloudthedragon@gmail.com>",
"license": [
"GPL-3.0-or-later"
],
"blender_version_min": "4.2.0",
"website": "https://github.com/RaincloudTheDragon/BasedPlayblast",
"permissions": {
"files": "Import/export files and data"
},
"tags": [
"Animation",
"Render",
"Workflow",
"Video"
],
"archive_url": "https://github.com/RaincloudTheDragon/BasedPlayblast/releases/download/v2.6.3/BasedPlayblast.v2.6.3.zip",
"archive_size": 49732,
"archive_hash": "sha256:078b406105ce6f4802e75233569841e2f73d082e09cd1d954696681ebf72b627"
},
{
"schema_version": "1.0.0",
"id": "rainclouds_bulk_scene_tools",
"name": "Raincloud's Bulk Scene Tools",
"tagline": "Bulk utilities for optimizing scene data",
"version": "0.17.0",
"type": "add-on",
"maintainer": "RaincloudTheDragon <raincloudthedragon@gmail.com>",
"license": [
"GPL-3.0-or-later"
],
"blender_version_min": "4.2.0",
"website": "https://github.com/RaincloudTheDragon/Rainys-Bulk-Scene-Tools",
"permissions": {
"files": "Read and write external resources referenced by scenes"
},
"tags": [
"Scene",
"Workflow",
"Materials"
],
"archive_url": "https://github.com/RaincloudTheDragon/Rainys-Bulk-Scene-Tools/releases/download/v0.17.0/Rainys_Bulk_Scene_Tools.v0.17.0.zip",
"archive_size": 80981,
"archive_hash": "sha256:419433069465b45ea903bd7bb46d89aa28b9c96c541d587d5f3be651a762811f"
},
{
"schema_version": "1.0.0",
"id": "atomic_data_manager",
"name": "Atomic Data Manager",
"tagline": "Smart cleanup and inspection of Blender data-blocks",
"version": "2.6.3",
"type": "add-on",
"maintainer": "RaincloudTheDragon",
"license": [
"GPL-3.0-or-later"
],
"blender_version_min": "4.2.0",
"tags": [
"utility",
"management",
"cleanup"
],
"archive_url": "https://github.com/RaincloudTheDragon/atomic-data-manager/releases/download/v2.6.3/Atomic_Data_Manager.v2.6.3.zip",
"archive_size": 122700,
"archive_hash": "sha256:b444463a0443864077abcfe97332406e606a697d3567ac32f85843760da11372"
},
{
"schema_version": "1.0.0",
"id": "sheepit_project_submitter",
"name": "SheepIt Project Submitter",
"tagline": "Submit projects to SheepIt render farm",
"version": "0.0.8",
"type": "add-on",
"maintainer": "RaincloudTheDragon",
"license": [
"GPL-3.0-or-later"
],
"blender_version_min": "3.0.0",
"tags": [
"render",
"farm",
"submission",
"utility"
],
"archive_url": "https://github.com/RaincloudTheDragon/sheepit_project_submitter/releases/download/v0.0.8/SheepIt_Project_Submitter.v0.0.8.zip",
"archive_size": 47667,
"archive_hash": "sha256:93cd8f18456079130c48c66cfd40235f7fe6414f929f59f90670e7a864821110"
}
]
}
@@ -0,0 +1,100 @@
{
"version": "v1",
"blocklist": [],
"data": [
{
"schema_version": "1.0.0",
"id": "basedplayblast",
"name": "BasedPlayblast",
"tagline": "Easily create playblasts from Blender and Flamenco",
"version": "2.6.3",
"type": "add-on",
"maintainer": "RaincloudTheDragon <raincloudthedragon@gmail.com>",
"license": [
"GPL-3.0-or-later"
],
"blender_version_min": "4.2.0",
"website": "https://github.com/RaincloudTheDragon/BasedPlayblast",
"permissions": {
"files": "Import/export files and data"
},
"tags": [
"Animation",
"Render",
"Workflow",
"Video"
],
"archive_url": "https://github.com/RaincloudTheDragon/BasedPlayblast/releases/download/v2.6.3/BasedPlayblast.v2.6.3.zip",
"archive_size": 49732,
"archive_hash": "sha256:078b406105ce6f4802e75233569841e2f73d082e09cd1d954696681ebf72b627"
},
{
"schema_version": "1.0.0",
"id": "rainclouds_bulk_scene_tools",
"name": "Raincloud's Bulk Scene Tools",
"tagline": "Bulk utilities for optimizing scene data",
"version": "0.17.0",
"type": "add-on",
"maintainer": "RaincloudTheDragon <raincloudthedragon@gmail.com>",
"license": [
"GPL-3.0-or-later"
],
"blender_version_min": "4.2.0",
"website": "https://github.com/RaincloudTheDragon/Rainys-Bulk-Scene-Tools",
"permissions": {
"files": "Read and write external resources referenced by scenes"
},
"tags": [
"Scene",
"Workflow",
"Materials"
],
"archive_url": "https://github.com/RaincloudTheDragon/Rainys-Bulk-Scene-Tools/releases/download/v0.17.0/Rainys_Bulk_Scene_Tools.v0.17.0.zip",
"archive_size": 80981,
"archive_hash": "sha256:419433069465b45ea903bd7bb46d89aa28b9c96c541d587d5f3be651a762811f"
},
{
"schema_version": "1.0.0",
"id": "atomic_data_manager",
"name": "Atomic Data Manager",
"tagline": "Smart cleanup and inspection of Blender data-blocks",
"version": "2.6.3",
"type": "add-on",
"maintainer": "RaincloudTheDragon",
"license": [
"GPL-3.0-or-later"
],
"blender_version_min": "4.2.0",
"tags": [
"utility",
"management",
"cleanup"
],
"archive_url": "https://github.com/RaincloudTheDragon/atomic-data-manager/releases/download/v2.6.3/Atomic_Data_Manager.v2.6.3.zip",
"archive_size": 122700,
"archive_hash": "sha256:b444463a0443864077abcfe97332406e606a697d3567ac32f85843760da11372"
},
{
"schema_version": "1.0.0",
"id": "sheepit_project_submitter",
"name": "SheepIt Project Submitter",
"tagline": "Submit projects to SheepIt render farm",
"version": "0.0.8",
"type": "add-on",
"maintainer": "RaincloudTheDragon",
"license": [
"GPL-3.0-or-later"
],
"blender_version_min": "3.0.0",
"tags": [
"render",
"farm",
"submission",
"utility"
],
"archive_url": "https://github.com/RaincloudTheDragon/sheepit_project_submitter/releases/download/v0.0.8/SheepIt_Project_Submitter.v0.0.8.zip",
"archive_size": 47667,
"archive_hash": "sha256:93cd8f18456079130c48c66cfd40235f7fe6414f929f59f90670e7a864821110"
}
]
}
@@ -1,3 +1,9 @@
## [v2.6.3] - 2026-04-22
### Fixes
- **CC3 / iClone import caches**: `Scene['CC3ImportProps']` (e.g. `pbr_material_cache`) no longer blocks unused detection—when `material_all()` is empty, cache-only refs (including Blenders 2× `users` per cell) are ignored for clean and RNA material scans (`stats/ghost_users.py`).
## [v2.6.2] - 2026-04-06
### Fixes
@@ -2,7 +2,7 @@ schema_version = "1.0.0"
id = "atomic_data_manager"
name = "Atomic Data Manager"
version = "2.6.2"
version = "2.6.3"
type = "add-on"
author = "RaincloudTheDragon"
maintainer = "RaincloudTheDragon"
@@ -0,0 +1,88 @@
"""
Detect 'ghost' ID references (e.g. Reallusion CC / iClone import caches on Scene)
that keep bpy.users > 0 but are not real scene usage. Used by materials_deep and
any future cleanup paths.
"""
import bpy
from .. import config
def _idprop_count_material(p, mat, seen):
"""Recursively count pointers equal to `mat` under an ID property value."""
if p is None or id(p) in seen:
return 0
if isinstance(p, (str, int, float, bool)):
return 0
seen = seen | {id(p)}
if isinstance(p, bpy.types.Material) and p == mat:
return 1
n = 0
t = type(p).__name__
if isinstance(p, (list, tuple)):
for x in p:
n += _idprop_count_material(x, mat, seen)
elif isinstance(p, dict):
for v in p.values():
n += _idprop_count_material(v, mat, seen)
elif t == "IDPropertyGroup" or (hasattr(p, "keys") and hasattr(p, "values") and t != "dict"):
try:
for k in p.keys():
try:
v = p[k]
except Exception:
continue
n += _idprop_count_material(v, mat, seen)
except Exception:
pass
return n
def count_cc3_import_cache_references(material):
"""
Count Material pointers stored in Scene ID property group CC3ImportProps
(Character Creator 3 / iClone pipeline). These are import-cache ghosts and
are not object/world/brush use.
"""
m = 0
for scene in bpy.data.scenes:
try:
if "CC3ImportProps" not in scene:
continue
except Exception:
continue
try:
cc3 = scene["CC3ImportProps"]
except Exception:
continue
m += _idprop_count_material(cc3, material, set())
return m
def material_blender_users_fully_cc3_ghosts(material):
"""
True if every material.user can be explained by CC3 import_cache pointers.
Blender often reports *two* users per pbr cache slot: the Scene and the
pbr_material_cache cell both contribute to bpy.types.Material.users, while
our walk only counts Material pointers in the idprop tree (one per cell).
So u == 2 * cc3 is normal; u == cc3 can occur when the count already matches
1:1 in some file versions.
"""
try:
u = material.users
except Exception:
return False
if u == 0:
return True
cc3 = count_cc3_import_cache_references(material)
if cc3 == 0:
return False
if cc3 == u or u == 2 * cc3:
return True
if config.enable_debug_prints:
config.debug_print(
f"[Atomic Debug] ghost_users: material '{material.name}' users={u} "
f"cc3_count={cc3} (not fully explained by CC3; keep conservative block)"
)
return False
@@ -28,6 +28,7 @@ import json
import os
from .. import config
from ..utils import compat
from . import ghost_users
# Data-block types we care about for dependency analysis
@@ -1197,7 +1198,9 @@ def analyze_unused_from_graph(graph, category, include_fake_users=None):
if category == 'materials':
try:
if datablock.users > 0 and not datablock.use_fake_user:
continue
if not ghost_users.material_blender_users_fully_cc3_ghosts(
datablock):
continue
except (AttributeError, RuntimeError, ReferenceError):
pass
unused.append(item_name)
@@ -28,6 +28,7 @@ from .. import config
from ..utils import compat
from ..utils import version
from . import users
from . import ghost_users
def shallow(data):
@@ -223,10 +224,10 @@ def materials_deep():
# check if material has a fake user or if ignore fake users
# is enabled
if not material.use_fake_user or config.include_fake_users:
# If Blender still counts users but we found none, don't flag (name collisions
# with linked IDs, drivers, or refs we don't traverse). Fake-user purge unchanged.
if material.users > 0 and not material.use_fake_user:
continue
# CC3 / iClone import_cache ID props keep bpy.users>0 with no object/world use.
if not ghost_users.material_blender_users_fully_cc3_ghosts(material):
continue
unused.append(material.name)
else:
# Second check: material is used, but check if it's ONLY used by unused objects
@@ -1,6 +1,7 @@
import bpy
from ..stats import unused
from ..stats import users
from . import ghost_users
from .. import config
from ..utils import compat
@@ -128,7 +129,8 @@ def _has_any_unused_materials():
if not users.material_all(material.name):
if not material.use_fake_user or config.include_fake_users:
if material.users > 0 and not material.use_fake_user:
continue
if not ghost_users.material_blender_users_fully_cc3_ghosts(material):
continue
return True
else:
# Second check: material is used, but check if it's ONLY used by unused objects
+7 -6
View File
@@ -20,7 +20,7 @@
bl_info = {
"name": "Animation Layers",
"author": "Tal Hershkovich",
"version" : (2, 3, 8),
"version" : (2, 4, 0),
"blender" : (3, 2, 0),
"location": "View3D - Properties - Animation Panel",
"description": "Simplifying the NLA editor into an animation layers UI and workflow",
@@ -153,7 +153,6 @@ def update_panel(self, context):
bpy.utils.unregister_class(panel)
for panel in panels:
#print (panel.bl_category)
panel.bl_category = context.preferences.addons[__name__].preferences.category
bpy.utils.register_class(panel)
@@ -176,8 +175,9 @@ class AnimLayersAddonPreferences(bpy.types.AddonPreferences):
items = [('ANIMLAYERS', 'Anim Layers Settings', 'Use Anim Layers properties to adjust custom frame range'),
('NLA', 'NLA Settings', 'Use the nla properties to adjust custom frame range')])
lock_nlatracks: bpy.props.BoolProperty(name="Automatically lock the nla tracks for safety measures", description="Automatically lock nla tracks when creating layers for safety", default = True)
lock_nlatracks: bpy.props.BoolProperty(name="Automatically lock the NLA tracks", description="Automatically lock nla tracks when creating layers for safety", default = True)
auto_custom_range: bpy.props.BoolProperty(name="Switch automatically to custom frame range when editing NLA Strips", description="Automatically use custom frame range when adjusting NLA Strips manually", default = False)
#Property for ClearActiveAction
proceed: bpy.props.EnumProperty(name="Choose how to proceed", description="Select an option how to proceed with Anim Layers", override = {'LIBRARY_OVERRIDABLE'},
items = [
@@ -260,8 +260,9 @@ class AnimLayersAddonPreferences(bpy.types.AddonPreferences):
row.label(text = "Custom Frame Range Settings")
row.prop(self, "frame_range_settings", text = '')
col.prop(self, "lock_nlatracks")
row = col.row()
row.prop(self, "auto_custom_range")
row.prop(self, "lock_nlatracks")
classes = (AnimLayersSettings, AnimLayersSceneSettings, AnimLayersItems, AnimLayersObjects)
+28 -23
View File
@@ -399,7 +399,7 @@ def get_fcu_layer_keyframes(obj, context, track):
keyframes = []
# fcurves = get_fcurves(track.strips[0].action)
# fcurves = track.strips[0].action.fcurves
fcurves = get_fcurves(obj, track.strips[0].action)
fcurves = get_fcurves(obj, track.strips[0].action, obj.als.data_type)
#store all the keyframe locations from the fcurves of the layer
for fcu in fcurves:
if fcu.group is not None:
@@ -1546,6 +1546,8 @@ def strip_action_recalc(self, strip):
###################################################### HELPER FUNCTIONS ################################################
def redraw_areas(areas):
if not len(bpy.context.window_manager.windows):
return
for area in bpy.context.window_manager.windows[0].screen.areas:
if area.type in areas:
area.tag_redraw()
@@ -1621,7 +1623,7 @@ def select_layer_bones(self, context):
###################################################### CLASSES ###########################################################
class SelectBonesInLayer(bpy.types.Operator):
"""Select bones with keyframes in the current layer"""
"""Select bones with keyframes in the current layer, use shift to add to the current selection"""
bl_idname = "anim.bones_in_layer"
bl_label = "Select layer bones"
bl_icon = "BONE_DATA"
@@ -1864,18 +1866,9 @@ class AutoCustomFrameRange(bpy.types.Operator):
# return {'CANCELLED'}
def restore(self, context):
if hasattr(subscriptions, 'frame_range'):
frame_start, frame_end = subscriptions.frame_range
else:
frame_start, frame_end = subscriptions.get_frame_range(context.scene)
print('restore')
subscriptions.frameend_update_callback()
self.strip.repeat = 1 #change strip repeat but keep self.repeat value stored
self.strip.use_reverse = False
self.strip.frame_start = frame_start
self.strip.scale = self.layer.speed
self.strip.frame_end = frame_end
# update_action_frame_range(frame_start, frame_end, layer, strip)
subscriptions.subscriptions_add(context.scene)
def update_action_list(scene):
@@ -2596,9 +2589,7 @@ class RemoveFcurves(bpy.types.Operator):
if mod.type == 'CYCLES':
fcu.modifiers.remove(mod)
fcu.update()
for area in context.window_manager.windows[0].screen.areas:
if area.type == 'GRAPH_EDITOR' or area.type == 'VIEW_3D':
area.tag_redraw()
redraw_areas(['GRAPH_EDITOR', 'VIEW_3D'])
break
return {'FINISHED'}
@@ -3301,25 +3292,36 @@ def copy_action(action):
return new_action
def get_obj_slot(obj, action, data_type = 'OBJECT'):
def get_obj_slot(obj, action, data_type = None):
'''Get the slot in the action that this object is using either it's object, or shapekeys'''
if data_type is None:
data_type = obj.als.data_type
if not hasattr(action, 'slots'):
return None
if not len(action.slots):
# If no slots exist, create one for the object and return it
slot = add_action_slot(obj, action)
return slot
# data_type = obj.als.data_type
for slot in action.slots:
if slot.target_id_type != data_type:
continue
# if obj.als.data_type == 'OBJECT' and obj in slot.users():
# return slot
if data_type == 'KEY' and obj.data.shape_keys in slot.users():
return slot
elif obj in slot.users():
return slot
return None
return add_action_slot(obj, action)
def get_fcurves(obj: bpy.types.Object, action: bpy.types.Action, data_type = 'OBJECT'):
def get_fcurves(obj: bpy.types.Object, action: bpy.types.Action, data_type = None):
if data_type is None:
data_type = obj.als.data_type
if hasattr(action, 'layers'):
slot = get_obj_slot(obj, action, data_type)
@@ -3335,10 +3337,13 @@ def get_fcurves(obj: bpy.types.Object, action: bpy.types.Action, data_type = 'OB
return action.fcurves
return []
def get_channelbag(obj: bpy.types.Object, action: bpy.types.Action, data_type = 'OBJECT'):
def get_channelbag(obj: bpy.types.Object, action: bpy.types.Action, data_type = None):
'''Getting the container of the fcurves, either the action or channelbag
Using this when adding a new group to the action'''
if data_type is None:
data_type = obj.als.data_type
if hasattr(action, 'layers'):
slot = get_obj_slot(obj, action, data_type)
channelbag = None
@@ -1,16 +1,9 @@
{
"last_check": "2026-04-21 12:45:18.211769",
"backup_date": "March-27-2026",
"update_ready": true,
"last_check": "2026-04-21 12:47:39.126086",
"backup_date": "April-21-2026",
"update_ready": false,
"ignore": false,
"just_restored": false,
"just_updated": false,
"version_text": {
"link": "https://gitlab.com/api/v4/projects/22294607/repository/archive.zip?sha=dddd6932039b8a3e5fae3ce2de957f21a5942c84",
"version": [
2,
4,
0
]
}
"version_text": {}
}
@@ -20,7 +20,7 @@
bl_info = {
"name": "Animation Layers",
"author": "Tal Hershkovich",
"version" : (2, 3, 7),
"version" : (2, 3, 8),
"blender" : (3, 2, 0),
"location": "View3D - Properties - Animation Panel",
"description": "Simplifying the NLA editor into an animation layers UI and workflow",
@@ -271,7 +271,6 @@ def register_layers(obj, nla_tracks):
continue
strip = track.strips[0]
use_animated_influence(strip)
strip.influence = 1
#updating the ui list with the nla track names
def visible_layers(obj, nla_tracks):
@@ -355,9 +354,9 @@ def use_animated_influence(strip):
else:
keyframe = keyframes[0]
keyframes.remove(keyframe)
strip.influence = 1
strip.influence = 1
def check_override_layers(obj):
if obj.override_library is None:
return False
@@ -1298,7 +1297,6 @@ def load_action(self, context):
subscriptions.frameend_update_callback()
strip.use_sync_length = False
use_animated_influence(strip)
strip.influence = 1
return
subscriptions.subscriptions_remove()
strip = track.strips[0]
@@ -1949,7 +1947,6 @@ def add_animlayer(layer_name = 'Anim_Layer' , duplicate = False, index = 1, blen
new_strip.blend_type = blend_type
new_strip.use_sync_length = False
use_animated_influence(new_strip)
new_strip.influence = 1
return new_track
@@ -1,16 +1,16 @@
{
"last_check": "2026-03-27 11:33:07.238724",
"backup_date": "March-20-2026",
"last_check": "2026-04-21 12:47:39.126086",
"backup_date": "March-27-2026",
"update_ready": true,
"ignore": false,
"just_restored": false,
"just_updated": false,
"version_text": {
"link": "https://gitlab.com/api/v4/projects/22294607/repository/archive.zip?sha=eef950c6d5e620a8240db5c2a7b20955fe31df6f",
"link": "https://gitlab.com/api/v4/projects/22294607/repository/archive.zip?sha=dddd6932039b8a3e5fae3ce2de957f21a5942c84",
"version": [
2,
3,
8
4,
0
]
}
}
@@ -645,6 +645,7 @@ def AL_bake(frame_start, frame_end, nla_tracks, fcu_keys, additive, step, action
baked_action = track.strips[0].action
clean_no_user_slots(baked_action)
#create the baked fcurve
# baked_channelbag = anim_layers.get_channelbag(obj, baked_action, obj.als.data_type)
baked_channelbag = anim_layers.get_channelbag(obj, baked_action)
baked_fcurves = baked_channelbag.fcurves
@@ -73,7 +73,6 @@ def animlayers_frame(scene, context):
scene['framerange_preview'] = scene.use_preview_range
frameend_update_callback()
return
frame_start, frame_end = bake_ops.frame_start_end(scene)
# frame_start, frame_end = get_frame_range(scene)
reset_subscription = False
@@ -106,7 +105,6 @@ def animlayers_frame(scene, context):
for i, track in enumerate(nla_tracks):
if len(track.strips) != 1:
continue
#checks if the layer has a custom frame range
layer = obj.Anim_Layers[i]
if layer.custom_frame_range:
@@ -127,6 +125,7 @@ def animlayers_frame(scene, context):
if strip.frame_start < 0:
strip.frame_start = 0
anim_layers.update_action_frame_range(0, frame_end, layer, strip)
return
anim_layers.update_action_frame_range(strip.frame_start, current + 10.0, layer, strip)
strip.frame_end = current + 10.0
@@ -231,6 +230,9 @@ def track_layer_synchronization(obj, nla_tracks):
if obj.als.layer_index > len(obj.Anim_Layers)-1:
obj.als.layer_index = len(obj.Anim_Layers)-1
if not bpy.context.preferences.addons[__package__].preferences.auto_custom_range:
return
#update new layer with strip settings
frame_start, frame_end = get_frame_range(bpy.context.scene)
@@ -243,7 +245,6 @@ def track_layer_synchronization(obj, nla_tracks):
continue
if (strip.frame_start, strip.frame_end) != (frame_start, frame_end):
subscriptions_remove()
# print(f'strip.frame_start {strip.frame_start} strip.frame_end {strip.frame_end} frame_start {frame_start} frame_end {frame_end}')
bpy.ops.anim.custom_frame_range_warning('INVOKE_DEFAULT')
return
@@ -311,11 +312,16 @@ def sync_frame_range(scene, track, layer):
return
#Turn on custom frame range if the current strip is not following the scene frame range
# Should be activated when nla strips are edited manually in the nla editor, only when auto custom range is turned on, otherwise just update the strip frame range to the scene frame range
if (round(strip.frame_start, 2), round(strip.frame_end, 2)) != (round(frame_start, 2), round(frame_end, 2)):
subscriptions_remove()
# print('315 custom frame range')
bpy.ops.anim.custom_frame_range_warning('INVOKE_DEFAULT')
return
if bpy.context.preferences.addons[__package__].preferences.auto_custom_range:
subscriptions_remove()
# print('321 custom frame range')
bpy.ops.anim.custom_frame_range_warning('INVOKE_DEFAULT')
return
else:
frameend_update_callback()
return
def sync_strip_range(scene):
'''Checking all the strips if a value was changed in the nla (not including UI changes)
@@ -356,7 +362,6 @@ def sync_strip_range(scene):
if (strip_frame_start, round(strip_frame_end, 2)) != (frame_start, float(frame_end)):
subscriptions_remove()
# print('357 custom_frame_range_warning ')
# print(f'strip_frame_start {strip_frame_start} strip_frame_end {round(strip_frame_end, 2)} frame_start {frame_start} frame_end {float(frame_end)}')
bpy.ops.anim.custom_frame_range_warning('INVOKE_DEFAULT')
return
@@ -567,7 +572,6 @@ def subscribe_to_preview_frame_end(scene):
# Subscribing to preview frame end since it's not registering in the depsgraph
subscribe_preview_end = scene.path_resolve("frame_preview_end", False)
subscribe_use_preview = scene.path_resolve("use_preview_range", False)
# print('subscribe_to_preview_frame_end')
for subscribe in [subscribe_preview_end, subscribe_use_preview]:
bpy.msgbus.subscribe_rna(
+1 -1
View File
@@ -5,7 +5,7 @@
bl_info = {
"name": "Flamenco",
"author": "Sybren A. Stüvel",
"version": (3, 8, 2),
"version": (3, 8, 5),
"blender": (3, 1, 0),
"description": "Flamenco client for Blender.",
"location": "Output Properties > Flamenco",
+1 -1
View File
@@ -10,7 +10,7 @@
"""
__version__ = "3.8.2"
__version__ = "3.8.5"
# import ApiClient
from flamenco.manager.api_client import ApiClient
@@ -76,7 +76,7 @@ class ApiClient(object):
self.default_headers[header_name] = header_value
self.cookie = cookie
# Set default User-Agent.
self.user_agent = 'Flamenco/3.8.2 (Blender add-on)'
self.user_agent = 'Flamenco/3.8.5 (Blender add-on)'
def __enter__(self):
return self
@@ -404,7 +404,7 @@ conf = flamenco.manager.Configuration(
"OS: {env}\n"\
"Python Version: {pyversion}\n"\
"Version of the API: 1.0.0\n"\
"SDK Package Version: 3.8.2".\
"SDK Package Version: 3.8.5".\
format(env=sys.platform, pyversion=sys.version)
def get_host_settings(self):
+1 -1
View File
@@ -4,7 +4,7 @@ Render Farm manager API
The `flamenco.manager` package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.0.0
- Package version: 3.8.2
- Package version: 3.8.5
- Build package: org.openapitools.codegen.languages.PythonClientCodegen
For more information, please visit [https://flamenco.blender.org/](https://flamenco.blender.org/)
+302 -7
View File
@@ -3,6 +3,7 @@
import datetime
import logging
import os
import time
from pathlib import Path, PurePosixPath
from typing import Optional, TYPE_CHECKING
@@ -363,11 +364,132 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
return filepath
def _bat_project_path(
self, context: bpy.types.Context, blendfile: Path
) -> Path:
"""BAT 'project' directory: preference root, or wider if links lie outside it.
Linked blends outside the configured project force BAT to use KEEP_PATH;
Shaman packing can then fail path rewriting. Using a common ancestor of
the submission blend and all linked library paths keeps assets inside
the project tree for BAT.
"""
prefs = preferences.get(context)
configured = bpathlib.make_absolute(
Path(bpy.path.abspath(str(prefs.project_root())))
)
blend_abs = bpathlib.make_absolute(blendfile)
lib_paths: list[Path] = []
for lib in bpy.data.libraries:
if not lib.filepath:
continue
lib_paths.append(
bpathlib.make_absolute(Path(bpy.path.abspath(lib.filepath)))
)
def is_under(root: Path, path: Path) -> bool:
try:
path.resolve().relative_to(root.resolve())
return True
except ValueError:
return False
need_widen = (not is_under(configured, blend_abs)) or any(
not is_under(configured, lp) for lp in lib_paths
)
if not need_widen:
return configured
all_paths = [blend_abs] + lib_paths
try:
common = Path(os.path.commonpath([str(p) for p in all_paths])).resolve()
except ValueError:
self.log.warning(
"Could not compute common path for BAT project root, using preferences"
)
return configured
self.log.info(
"BAT project root widened from %s to %s (assets outside preference project)",
configured,
common,
)
return common
def _convert_relpaths_to_absolute(self, context: bpy.types.Context) -> None:
"""Convert all relative paths in the blend file to absolute paths.
Covers libraries, images, clips, sounds, fonts, volumes, and cache_files
(point caches / GN caches). Allows the blend to be sent as-is without BAT.
"""
# Convert library paths to absolute
for library in bpy.data.libraries:
if library.filepath:
old_path = library.filepath
abs_path = bpy.path.abspath(library.filepath)
library.filepath = abs_path
self.log.debug("Converted library path: %s -> %s", old_path, abs_path)
# Convert image paths to absolute
for image in bpy.data.images:
if image.filepath and not image.packed_file:
old_path = image.filepath
abs_path = bpy.path.abspath(image.filepath)
image.filepath = abs_path
self.log.debug("Converted image path: %s -> %s", old_path, abs_path)
# Convert movie paths to absolute
for movie in bpy.data.movieclips:
if movie.filepath:
old_path = movie.filepath
abs_path = bpy.path.abspath(movie.filepath)
movie.filepath = abs_path
self.log.debug("Converted movie path: %s -> %s", old_path, abs_path)
# Convert sound paths to absolute
for sound in bpy.data.sounds:
if sound.filepath:
old_path = sound.filepath
abs_path = bpy.path.abspath(sound.filepath)
sound.filepath = abs_path
self.log.debug("Converted sound path: %s -> %s", old_path, abs_path)
# Convert font paths to absolute (skip VectorFont - its filepath is read-only)
for font in bpy.data.fonts:
if font.filepath:
try:
old_path = font.filepath
abs_path = bpy.path.abspath(font.filepath)
font.filepath = abs_path
self.log.debug("Converted font path: %s -> %s", old_path, abs_path)
except (TypeError, AttributeError):
self.log.debug("Skipping font %s (filepath is read-only)", font.name)
# Convert volume paths to absolute
for volume in bpy.data.volumes:
if volume.filepath:
old_path = volume.filepath
abs_path = bpy.path.abspath(volume.filepath)
volume.filepath = abs_path
self.log.debug("Converted volume path: %s -> %s", old_path, abs_path)
# Point / mesh cache files (e.g. .pc2), geometry-nodes caches, etc.
for cache_file in bpy.data.cache_files:
if cache_file.filepath:
old_path = cache_file.filepath
abs_path = bpy.path.abspath(cache_file.filepath)
cache_file.filepath = abs_path
self.log.debug("Converted cache_file path: %s -> %s", old_path, abs_path)
def _submit_files(self, context: bpy.types.Context, blendfile: Path) -> bool:
"""Ensure that the files are somewhere in the shared storage.
Returns True if a packing thread has been started, and False otherwise.
"""
prefs = preferences.get(context)
if prefs.bat_bypass:
return self._submit_files_bat_bypass(context, blendfile)
from .bat import interface as bat_interface
@@ -405,6 +527,179 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
return True
def _submit_files_bat_bypass(self, context: bpy.types.Context, blendfile: Path) -> bool:
"""Bypass BAT: absolute paths, then upload or copy the blend only."""
manager = self._manager_info(context)
if not manager:
return False
self.log.info("Converting all relative paths to absolute")
self._convert_relpaths_to_absolute(context)
self.log.info("Saving blend file with absolute paths")
blendfile = self._save_blendfile(context)
blendfile = bpathlib.make_absolute(blendfile)
if manager.shared_storage.shaman_enabled:
self.log.info("Uploading blend file directly to Shaman (bypassing BAT)")
self._upload_blendfile_to_shaman(context, blendfile)
self._quit(context)
return False
if job_submission.is_file_inside_job_storage(context, blendfile):
self.log.info(
"File is already in job storage location, submitting it as-is"
)
self._use_blendfile_directly(context, blendfile)
return False
self.log.info(
"File is not already in job storage location, copying it there"
)
try:
self._copy_blendfile_to_storage(context, blendfile)
except FileNotFoundError:
self._quit(context)
return False
return False
def _upload_blendfile_to_shaman(
self, context: bpy.types.Context, blendfile: Path
) -> None:
"""Upload blend file directly to Shaman without BAT.
Creates a Shaman checkout with just the blend file, maintaining its
relative path from the project root.
"""
from .bat import cache
from .manager.apis import ShamanApi
from .manager.models import (
ShamanFileSpec,
ShamanCheckout,
)
from .manager.exceptions import ApiException
from . import preferences
api_client = self.get_api_client(context)
shaman_api = ShamanApi(api_client)
# Get project root to calculate relative path
prefs = preferences.get(context)
project_path: Path = prefs.project_root()
project_path = bpathlib.make_absolute(Path(bpy.path.abspath(str(project_path))))
# Calculate relative path from project root
try:
blendfile_rel_path = blendfile.relative_to(project_path)
# Convert to POSIX path for Shaman
blendfile_path_in_checkout = PurePosixPath(blendfile_rel_path.as_posix())
except ValueError:
# Blend file is not under project root, use just the filename
self.log.warning(
"Blend file %s is not under project root %s, using filename only",
blendfile,
project_path,
)
blendfile_path_in_checkout = PurePosixPath(blendfile.name)
# Compute checksum and file size
self.log.info("Computing checksum for %s", blendfile.name)
checksum = cache.compute_cached_checksum(blendfile)
filesize = blendfile.stat().st_size
# Upload the blend file to Shaman
self.log.info("Uploading blend file to Shaman: %s", blendfile.name)
try:
with blendfile.open("rb") as file_reader:
shaman_api.shaman_file_store(
checksum=checksum,
filesize=filesize,
body=file_reader,
x_shaman_can_defer_upload=True,
x_shaman_original_filename=blendfile.name,
)
except ApiException as ex:
if ex.status == 208:
# File already known to Shaman
self.log.info("Blend file already known to Shaman")
elif ex.status == 425:
# Defer upload - someone else is uploading
self.log.info("Blend file is being uploaded by another client, deferring")
# Retry after a short delay
import time
time.sleep(1)
with blendfile.open("rb") as file_reader:
shaman_api.shaman_file_store(
checksum=checksum,
filesize=filesize,
body=file_reader,
x_shaman_can_defer_upload=False,
x_shaman_original_filename=blendfile.name,
)
else:
self.log.error("Error uploading to Shaman: %s", ex)
self.report({"ERROR"}, f"Error uploading to Shaman: {ex}")
return
# Create checkout definition with just the blend file
checkout_path = self._shaman_checkout_path()
filespec = ShamanFileSpec(
sha=checksum,
size=filesize,
path=str(blendfile_path_in_checkout), # Relative path from project root
)
# Create the checkout
self.log.info("Creating Shaman checkout: %s", checkout_path)
self.log.info("Blend file path in checkout: %s", blendfile_path_in_checkout)
checkout = ShamanCheckout(
files=[filespec],
checkout_path=str(checkout_path),
)
try:
result = shaman_api.shaman_checkout(checkout)
self.actual_shaman_checkout_path = PurePosixPath(result.checkout_path)
# The checkout itself is created in a unique subdirectory. The job's
# blendfile must include that checkout path.
self.blendfile_on_farm = (
PurePosixPath("{jobs}")
/ self.actual_shaman_checkout_path
/ blendfile_path_in_checkout
)
self.log.info("Shaman checkout created: %s", self.actual_shaman_checkout_path)
self._submit_job(context)
except ApiException as ex:
self.log.error("Error creating Shaman checkout: %s", ex)
self.report({"ERROR"}, f"Error creating Shaman checkout: {ex}")
return
def _copy_blendfile_to_storage(
self, context: bpy.types.Context, blendfile: Path
) -> None:
"""Copy blend file to job storage without BAT."""
import shutil
manager = self._manager_info(context)
if not manager:
raise FileNotFoundError("Manager info not known")
unique_dir = "%s-%s" % (
datetime.datetime.now().isoformat("-").replace(":", ""),
self.job_name,
)
pack_target_dir = Path(manager.shared_storage.location) / unique_dir
pack_target_dir.mkdir(parents=True, exist_ok=True)
pack_target_file = pack_target_dir / blendfile.name
self.log.info("Copying blend file to %s", pack_target_file)
shutil.copy2(blendfile, pack_target_file)
self.blendfile_on_farm = PurePosixPath(pack_target_file.as_posix())
self.actual_shaman_checkout_path = None
self._submit_job(context)
def _bat_pack_filesystem(
self, context: bpy.types.Context, blendfile: Path
) -> PurePosixPath:
@@ -414,9 +709,7 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
"""
from .bat import interface as bat_interface
# Get project path from addon preferences.
prefs = preferences.get(context)
project_path: Path = prefs.project_root()
project_path = self._bat_project_path(context, blendfile)
project_path = bpathlib.make_absolute(Path(bpy.path.abspath(str(project_path))))
if not project_path.exists():
@@ -442,7 +735,9 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
project=project_path,
target=str(pack_target_dir),
exclusion_filter="", # TODO: get from GUI.
relative_only=True, # TODO: get from GUI.
# False: relative_only=True can leave linked blends on KEEP_PATH and hit BAT
# _rewrite_paths assertions with Shaman (same as stock 3.8.x + BAT 1.x).
relative_only=False,
)
return PurePosixPath(pack_target_file.as_posix())
@@ -473,15 +768,15 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
assert self.job is not None
self.log.info("Sending BAT pack to Shaman")
prefs = preferences.get(context)
project_path: Path = prefs.project_root()
project_path = self._bat_project_path(context, blendfile)
self.packthread = bat_interface.copy(
base_blendfile=blendfile,
project=project_path,
target="/", # Target directory irrelevant for Shaman transfers.
exclusion_filter="", # TODO: get from GUI.
relative_only=True, # TODO: get from GUI.
# See _bat_pack_filesystem: avoid BAT+Shaman KEEP_PATH / _rewrite_paths failure.
relative_only=False,
packer_class=bat_shaman.Packer,
packer_kwargs=dict(
api_client=self.get_api_client(context),
+14
View File
@@ -65,6 +65,16 @@ class FlamencoPreferences(bpy.types.AddonPreferences):
items=_project_finder_enum_items,
)
bat_bypass: bpy.props.BoolProperty( # type: ignore
name="Bypass BAT",
description=(
"When enabled, submission skips Blender Asset Tracer: paths in the blend "
"are written as absolute, then the blend is uploaded or copied without a BAT pack. "
"When disabled, Flamenco uses the normal BAT pack (Shaman or job storage)"
),
default=True,
)
# Property that gets its value from the above _job_storage, and cannot be
# set. This makes it read-only in the GUI.
job_storage_for_gui: bpy.props.StringProperty( # type: ignore
@@ -118,6 +128,10 @@ class FlamencoPreferences(bpy.types.AddonPreferences):
else:
text_row(col, str(project_root))
col = layout.column(align=True)
col.label(text="Submission")
col.prop(self, "bat_bypass")
def project_root(self) -> Path:
"""Use the configured project finder to find the project root directory."""
@@ -0,0 +1,45 @@
# Changelog
All notable changes to **Maya Config Pro** are listed here. Versions match `blender_manifest.toml`.
## [1.8.0] — 2026-04-22
### WIP — Animation shelf (3D View)
- 3D View **tool header** row + **Sidebar (N) Animation** tab (shared operator row with Scene shelf); add-on preference and `SpaceView3D.show_region_tool_header` wiring; import/pref id fixes. **Removed** the earlier **POST_PIXEL** GPU/BLF viewport strip. Still in progress / verify per layout and Blender 5.0.
### From recent commits
- **NLA:** right-click in FA keymap aligned with Blender (tracks/editor/generic).
- **FA hotkeys:** `Home` zoom respects camera-view context; **auto_loader** so FA add-on key layer does not double-load with the default/Industry keyconfig.
- **Input:** `Ctrl+Comma` → preferences (FA layer).
- **Keymaps:** stop shipping local copies of Blenders **official** keysets; key switcher maps to upstream; smaller repo footprint.
## [1.7.1] — 2026-04-12
### Fixed
- **Legacy add-on install:** Added `bl_info` in `__init__.py` so ZIP installs that land under `scripts/addons` (Install from Disk / drag-drop) register correctly instead of empty `Modules Installed ()` with no entry to enable.
- **FA hotkeys preset:** Restored **Node Generic** + **Node Editor** keymaps (shader/node editors: selection, links, `NODE_MT_view_pie`, etc.). Completed **View2D** (shift+MMB pan, trackpad pan, horizontal wheel). Added **`` ` ``** view framing pies for **Dopesheet** and **Graph Editor**. **Outliner:** `.` and numpad `.` for *Show Active* (replaced disabled numpad entry and non-standard `F` binding).
### Added
- **GitHub Actions** `release.yml`: build versioned ZIP from the repo, verify layout, attach to a draft GitHub Release. Release **title** and **ZIP filename** use the version in `blender_manifest.toml` (set the same value as your git tag, e.g. `v1.7.1``1.7.1`).
- **`INSTALL.txt`** and README instructions for **Get Extensions** vs legacy add-ons paths.
---
## [1.7.0] — Extension port (summary)
First **Blender Extension** release (`blender_manifest.toml`): preferences, **deploy keymap presets** / optional **startup.blend**, **activate keymap** presets with **view zoom axis** (horizontal for FA). **ProAni** / shelf / sidebar / animation panels; **5.1** UI refactors and toolbar drawing fixes. **Deploy** operators and preferences wiring. **Marking menu** safer invoke; **D** key → scene-level affect-only-origins toggle; pie menu / FA keymap ID updates toward current Blender menus; FA **view zoom** / axis-related preferences.
---
## Earlier history (legacy MCP → port)
Condensed from git history:
| Theme | Changes |
|--------|---------|
| **Port** | Import MCP 1.7 codebase; extension layout; README; `.gitignore`; remove obsolete scripts |
| **UI** | Pro panel, shelves, camera tools, animation panel; shelf/header drawing; Blender **5.1** refactors |
| **Deploy** | `deploy_ops`, `utils/deploy`, preferences UI for keymap + startup deploy |
| **Ops / prefs** | Marking menu guards; special tools / pie IDs; zoom-axis handling for FA vs default keymaps |
Prior feature-level history (pre-extension numbering) remains in **README → Update History**.
@@ -2,7 +2,7 @@
A Maya-like configuration, shortcuts, and UI elements for Blender.
**Version:** 1.7.0 (Extension Port)
**Version:** 1.8.0 (Extension Port)
**Original Author:** Jesse Doyle | Form Affinity
**License:** GPL-3.0-or-later
@@ -254,6 +254,12 @@ For older Blender versions (pre-4.2) or if you prefer the original mod approach:
## Update History
### Version 1.8.0
- WIP: 3D View animation shelf (tool header + N-panel); NLA/FA keymap and loader fixes. See `CHANGELOG.md`.
### Version 1.7.1
- See **[CHANGELOG.md](CHANGELOG.md)** for full notes. Highlights: `bl_info` for legacy ZIP installs, GitHub release workflow + install docs, FA hotkeys (node editor, View2D, framing pies, Outliner show active).
### Version 1.6 / Extension 1.7.0
- **Extension Format:** Complete port to modern Blender Extension platform
- **Multi-Version Support:** Compatible with Blender 4.2, 4.5 LTS, and 5.0+
@@ -2,7 +2,7 @@
Maya Config Pro - Blender Extension
A Maya-like configuration, shortcuts, and UI elements for Blender.
Version: 1.7.0
Version: 1.8.0
Author: Form Affinity (Jesse Doyle)
"""
@@ -12,7 +12,7 @@ Author: Form Affinity (Jesse Doyle)
bl_info = {
"name": "Maya Config Pro",
"author": "Form Affinity (Jesse Doyle)",
"version": (1, 7, 0),
"version": (1, 8, 0),
"blender": (4, 2, 0),
"location": "View3D > Sidebar > Maya Config",
"description": "Maya-like configuration, shortcuts, UI panels, and keymap presets",
@@ -28,7 +28,6 @@ from .utils import compat
# not only blender_manifest.toml id — otherwise AddonPreferences.draw never appears.
_ADDON_MODULE = __package__ or "form_affinity_maya_config_pro"
# Import all submodules
from .panels import (
panel_pro,
panel_sidebar,
@@ -46,12 +45,34 @@ from .ops import (
object_select,
mesh_select_mode,
delete_ops,
view_frame,
)
from .keyconfigs import fa_hotkeys
from .utils import deploy as deploy_util
from .ops import deploy_ops
def _mcp_on_auto_load_keymap(self, context):
try:
from .keyconfigs import fa_hotkeys
fa_hotkeys.sync_addon_keymaps()
except Exception as e:
print(f"Warning: Maya Config Pro: keymap sync: {e}")
def _mcp_on_viewport_anim_shelf(self, context):
if not self.viewport_anim_shelf:
return
try:
from .panels import panel_animation
panel_animation.view3d_set_show_region_tool_header(True)
except Exception as e:
print(f"Warning: Maya Config Pro: tool header: {e}")
# Addon preferences
class MCP_AddonPreferences(bpy.types.AddonPreferences):
"""Maya Config Pro preferences"""
@@ -59,8 +80,24 @@ class MCP_AddonPreferences(bpy.types.AddonPreferences):
auto_load_keymap: bpy.props.BoolProperty(
name="Auto-load FA Keymap",
description="Automatically load the Maya-like keymap when Blender starts",
default=True
description=(
"When the keyconfig is FA Hotkeys, register MCP's addon keymap layer (Maya-style). "
"Switches to Blender/Industry remove that layer; no restart required"
),
default=True,
update=_mcp_on_auto_load_keymap,
)
viewport_anim_shelf: bpy.props.BoolProperty(
name="3D View: Animation shelf (tool header + sidebar)",
description=(
"Show the Animation shelf row in the 3D View tool header (enables the tool "
"header strip) and in Sidebar (N) under the Animation tab. If you still do "
"not see the tool-header row, use the Animation sidebar tab; the strip can be "
"off in some layouts until View shows the tool header."
),
default=True,
update=_mcp_on_viewport_anim_shelf,
)
def draw(self, context):
@@ -100,13 +137,14 @@ class MCP_AddonPreferences(bpy.types.AddonPreferences):
col.separator()
col.label(
text="Copies Blender's keymaps (and MCP fa_hotkeys) into your Scripts folder, "
"and optional startup.blend into your config. Needed for Pro panel keymap buttons.",
text="Installs MCP fa_hotkeys into your keyconfig folder; default/Industry use "
"this Blender's built-in keymaps. Optional startup.blend deploys to your config.",
icon="INFO",
)
layout.separator()
layout.prop(self, "auto_load_keymap")
layout.prop(self, "viewport_anim_shelf")
# List of all classes in this module
@@ -130,40 +168,32 @@ submodules = [
object_select,
mesh_select_mode,
delete_ops,
view_frame,
fa_hotkeys,
]
def register():
"""Register all classes and submodules"""
# Register main classes
fa_hotkeys.set_addon_id(_ADDON_MODULE)
for cls in classes:
compat.safe_register_class(cls)
# Register all submodules
for module in submodules:
try:
module.register()
except Exception as e:
print(f"Warning: Failed to register {module.__name__}: {e}")
# Register keymaps if auto-load is enabled
prefs = bpy.context.preferences.addons.get(_ADDON_MODULE)
if prefs and prefs.preferences.auto_load_keymap:
try:
fa_hotkeys.register_keymaps()
except Exception as e:
print(f"Warning: Failed to register keymaps: {e}")
try:
fa_hotkeys.sync_addon_keymaps()
except Exception as e:
print(f"Warning: Failed to sync keymaps: {e}")
def unregister():
"""Unregister all classes and submodules"""
# Unregister keymaps first
try:
fa_hotkeys.unregister_keymaps()
except Exception as e:
print(f"Warning: Failed to unregister keymaps: {e}")
# Unregister all submodules (in reverse order)
# Unregister all submodules (in reverse order; fa_hotkeys clears addon keymaps + listeners)
for module in reversed(submodules):
try:
module.unregister()
@@ -3,7 +3,7 @@ schema_version = "1.0.0"
id = "form_affinity_maya_config_pro"
name = "Maya Config Pro"
tagline = "Maya-like configuration, shortcuts, and UI elements for Blender"
version = "1.7.0"
version = "1.8.0"
type = "add-on"
maintainer = "Form Affinity"
@@ -2,7 +2,10 @@
Maya Config Pro - Keymap Configuration
Maya-like keymap preset for Blender.
This module loads and registers the FA Hotkeys keymap configuration.
This module loads and registers the FA Hotkeys keymap configuration into
`wm.keyconfigs.addon` only when the user keyconfig preset is the FA one and
"Auto-load FA Keymap" is enabled. Switching to Blender/Industry unregisters
that layer without restarting.
"""
import bpy
@@ -11,27 +14,59 @@ from ..utils import compat
# Import the keyconfig data
from . import fa_hotkeys_data
# Addon keymaps
addon_keymaps = []
# Addon keymaps: (keymap, keymap_item) and created keymaps to remove for clean sync
addon_keymaps: list = []
addon_keymap_objects: list = []
# Set from __init__.register to match this add-on's module id
_addon_id: str = "form_affinity_maya_config_pro"
# Message bus: react to keyconfig changes from Preferences
_msgbus_owner = object()
def set_addon_id(addon_id: str) -> None:
global _addon_id
_addon_id = addon_id
def _get_prefs():
a = bpy.context.preferences.addons.get(_addon_id)
return a.preferences if a else None
def is_fa_keyconfig_active() -> bool:
"""True when the active user keyconfig is the `fa_hotkeys` preset (file stem)."""
try:
s = str(bpy.context.preferences.keymap.active_keyconfig or "").lower()
if s == "fa_hotkeys":
return True
except Exception:
pass
try:
kc = bpy.context.window_manager.keyconfigs.active
if kc and kc.name and kc.name.lower() == "fa_hotkeys":
return True
except Exception:
pass
return False
def register_keymaps():
"""Register FA Hotkeys as addon keymaps"""
"""Register FA Hotkeys as addon keymaps (call unregister_keymaps first for a clean set)."""
global addon_keymap_objects
wm = bpy.context.window_manager
kc = wm.keyconfigs.addon
if not kc:
return
# Try to load the keyconfig data
try:
# The keyconfig_data is defined in fa_hotkeys_data module
if hasattr(fa_hotkeys_data, 'keyconfig_data'):
if hasattr(fa_hotkeys_data, "keyconfig_data"):
for km_name, km_args, km_content in fa_hotkeys_data.keyconfig_data:
# Create or get keymap
km = kc.keymaps.new(name=km_name, **km_args)
addon_keymap_objects.append(km)
# Add keymap items
if "items" in km_content:
for item in km_content["items"]:
if len(item) >= 2:
@@ -39,28 +74,25 @@ def register_keymaps():
keymap_item = item[1]
props = item[2] if len(item) > 2 else None
# Create keymap item
kmi = km.keymap_items.new(
idname,
type=keymap_item.get("type", 'A'),
value=keymap_item.get("value", 'PRESS'),
type=keymap_item.get("type", "A"),
value=keymap_item.get("value", "PRESS"),
any=keymap_item.get("any", False),
shift=keymap_item.get("shift", 0),
ctrl=keymap_item.get("ctrl", 0),
alt=keymap_item.get("alt", 0),
oskey=keymap_item.get("oskey", False),
key_modifier=keymap_item.get("key_modifier", 'NONE'),
key_modifier=keymap_item.get("key_modifier", "NONE"),
repeat=keymap_item.get("repeat", False),
head=keymap_item.get("head", False)
head=keymap_item.get("head", False),
)
# Set properties if provided
if props and "properties" in props:
for prop_name, prop_value in props["properties"]:
if hasattr(kmi.properties, prop_name):
setattr(kmi.properties, prop_name, prop_value)
# Handle active state
if props and "active" in props:
kmi.active = props["active"]
@@ -70,15 +102,15 @@ def register_keymaps():
def unregister_keymaps():
"""Unregister FA Hotkeys addon keymaps"""
"""Remove all MCP addon keymap items and the keymap objects we created."""
global addon_keymap_objects
wm = bpy.context.window_manager
kc = wm.keyconfigs.addon
if not kc:
return
# Remove all registered keymap items
for km, kmi in addon_keymaps:
for km, kmi in list(addon_keymaps):
try:
km.keymap_items.remove(kmi)
except Exception:
@@ -86,23 +118,85 @@ def unregister_keymaps():
addon_keymaps.clear()
for km in reversed(addon_keymap_objects):
try:
kc.keymaps.remove(km)
except Exception:
pass
addon_keymap_objects.clear()
def sync_addon_keymaps() -> None:
"""
If "Auto-load FA Keymap" is on and the active preset is `fa_hotkeys`, register
the addon keymap layer; otherwise remove it. Safe to call often (switcher, prefs, msgbus).
"""
prefs = _get_prefs()
if not prefs or not getattr(prefs, "auto_load_keymap", False):
unregister_keymaps()
return
if is_fa_keyconfig_active():
unregister_keymaps()
register_keymaps()
else:
unregister_keymaps()
@bpy.app.handlers.persistent
def _mcp_load_post_sync(_a, _b) -> None:
try:
sync_addon_keymaps()
except Exception as e:
print(f"Warning: Maya Config Pro: keymap sync after load: {e}")
def _msgbus_keyconfig(_):
try:
sync_addon_keymaps()
except Exception as e:
print(f"Warning: Maya Config Pro: keymap sync: {e}")
def register_keymap_listeners() -> None:
if _mcp_load_post_sync not in bpy.app.handlers.load_post:
bpy.app.handlers.load_post.append(_mcp_load_post_sync)
try:
rna_t = type(bpy.context.preferences.keymap)
bpy.msgbus.subscribe_rna(
key=(rna_t, "active_keyconfig"),
owner=_msgbus_owner,
args=(),
notify=_msgbus_keyconfig,
options={"PERSISTENT"},
)
except Exception as e:
print(f"Warning: Maya Config Pro: keyconfig msgbus: {e}")
def unregister_keymap_listeners() -> None:
try:
bpy.msgbus.clear_by_owner(_msgbus_owner)
except Exception:
pass
try:
while _mcp_load_post_sync in bpy.app.handlers.load_post:
bpy.app.handlers.load_post.remove(_mcp_load_post_sync)
except Exception:
pass
# Classes to register (none needed for keyconfig)
_classes = []
def register():
"""Register keymap module"""
for cls in _classes:
compat.safe_register_class(cls)
# Don't auto-register keymaps here - let preferences control this
# register_keymaps()
register_keymap_listeners()
def unregister():
"""Unregister keymap module"""
unregister_keymap_listeners()
unregister_keymaps()
for cls in reversed(_classes):
compat.safe_unregister_class(cls)
@@ -1,4 +1,4 @@
keyconfig_version = (5, 0, 120)
keyconfig_version = (5, 0, 121)
keyconfig_data = \
[("3D View",
{"space_type": 'VIEW_3D', "region_type": 'WINDOW'},
@@ -107,15 +107,7 @@ keyconfig_data = \
],
},
),
("view3d.view_center_camera", {"type": 'HOME', "value": 'PRESS', "repeat": True}, None),
("view3d.view_center_lock", {"type": 'HOME', "value": 'PRESS', "repeat": True}, None),
("view3d.view_all",
{"type": 'HOME', "value": 'PRESS', "repeat": True},
{"properties":
[("center", False),
],
},
),
("mcp.view_frame_home", {"type": 'HOME', "value": 'PRESS', "repeat": True}, None),
("view3d.view_all",
{"type": 'HOME', "value": 'PRESS', "ctrl": True, "repeat": True},
{"properties":
@@ -1534,6 +1526,211 @@ keyconfig_data = \
],
},
),
("NLA Generic",
{"space_type": 'NLA_EDITOR', "region_type": 'WINDOW'},
{"items":
[("wm.context_toggle",
{"type": 'N', "value": 'PRESS'},
{"properties": [('data_path', 'space_data.show_region_ui')]}),
("nla.tweakmode_enter",
{"type": 'TAB', "value": 'PRESS'},
{"properties": [('use_upper_stack_evaluation', True)]}),
("nla.tweakmode_exit",
{"type": 'TAB', "value": 'PRESS'},
None),
("nla.tweakmode_enter",
{"type": 'TAB', "value": 'PRESS', "shift": True},
{"properties": [('isolate_action', True)]}),
("nla.tweakmode_exit",
{"type": 'TAB', "value": 'PRESS', "shift": True},
{"properties": [('isolate_action', True)]}),
("anim.channels_select_filter",
{"type": 'F', "value": 'PRESS', "ctrl": True},
None),
],
},
),
("NLA Tracks",
{"space_type": 'NLA_EDITOR', "region_type": 'WINDOW'},
{"items":
[("nla.channels_click",
{"type": 'LEFTMOUSE', "value": 'PRESS'},
None),
("nla.channels_click",
{"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True},
{"properties": [('extend', True)]}),
("nla.tracks_add",
{"type": 'A', "value": 'PRESS', "shift": True},
{"properties": [('above_selected', False)]}),
("nla.tracks_add",
{"type": 'A', "value": 'PRESS', "shift": True, "ctrl": True},
{"properties": [('above_selected', True)]}),
("nla.tracks_delete",
{"type": 'X', "value": 'PRESS'},
None),
("nla.tracks_delete",
{"type": 'DEL', "value": 'PRESS'},
None),
("wm.call_menu",
{"type": 'RIGHTMOUSE', "value": 'PRESS'},
{"properties": [('name', 'NLA_MT_channel_context_menu')]}),
("wm.call_menu",
{"type": 'APP', "value": 'PRESS'},
{"properties": [('name', 'NLA_MT_channel_context_menu')]}),
],
},
),
("NLA Editor",
{"space_type": 'NLA_EDITOR', "region_type": 'WINDOW'},
{"items":
[("nla.click_select",
{"type": 'LEFTMOUSE', "value": 'PRESS'},
{"properties": [('deselect_all', True)]}),
("nla.click_select",
{"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True},
{"properties": [('extend', True)]}),
("nla.select_leftright",
{"type": 'LEFTMOUSE', "value": 'CLICK', "ctrl": True},
{"properties": [('mode', 'CHECK')]}),
("nla.select_leftright",
{"type": 'LEFTMOUSE', "value": 'CLICK', "ctrl": True, "shift": True},
{"properties": [('mode', 'CHECK'), ('extend', True)]}),
("nla.select_leftright",
{"type": 'LEFT_BRACKET', "value": 'PRESS'},
{"properties": [('mode', 'LEFT')]}),
("nla.select_leftright",
{"type": 'RIGHT_BRACKET', "value": 'PRESS'},
{"properties": [('mode', 'RIGHT')]}),
("nla.select_all",
{"type": 'A', "value": 'PRESS'},
{"properties": [('action', 'SELECT')]}),
("nla.select_all",
{"type": 'A', "value": 'PRESS', "alt": True},
{"properties": [('action', 'DESELECT')]}),
("nla.select_all",
{"type": 'I', "value": 'PRESS', "ctrl": True},
{"properties": [('action', 'INVERT')]}),
("nla.select_all",
{"type": 'A', "value": 'DOUBLE_CLICK'},
{"properties": [('action', 'DESELECT')]}),
("nla.select_box",
{"type": 'B', "value": 'PRESS'},
{"properties": [('axis_range', False)]}),
("nla.select_box",
{"type": 'B', "value": 'PRESS', "alt": True},
{"properties": [('axis_range', True)]}),
("nla.select_box",
{"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'},
{"properties": [('tweak', True), ('mode', 'SET')]}),
("nla.select_box",
{"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True},
{"properties": [('tweak', True), ('mode', 'ADD')]}),
("nla.select_box",
{"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True},
{"properties": [('tweak', True), ('mode', 'SUB')]}),
("nla.previewrange_set",
{"type": 'P', "value": 'PRESS', "ctrl": True, "alt": True},
None),
("nla.view_all",
{"type": 'HOME', "value": 'PRESS'},
None),
("nla.view_all",
{"type": 'NDOF_BUTTON_FIT', "value": 'PRESS'},
None),
("nla.view_selected",
{"type": 'NUMPAD_PERIOD', "value": 'PRESS'},
None),
("nla.view_frame",
{"type": 'NUMPAD_0', "value": 'PRESS'},
None),
("wm.call_menu_pie",
{"type": 'ACCENT_GRAVE', "value": 'PRESS'},
{"properties": [('name', 'NLA_MT_view_pie')]}),
("nla.actionclip_add",
{"type": 'A', "value": 'PRESS', "shift": True},
None),
("nla.transition_add",
{"type": 'T', "value": 'PRESS', "shift": True},
None),
("nla.soundclip_add",
{"type": 'K', "value": 'PRESS', "shift": True},
None),
("nla.meta_add",
{"type": 'G', "value": 'PRESS', "ctrl": True},
None),
("nla.meta_remove",
{"type": 'G', "value": 'PRESS', "ctrl": True, "alt": True},
None),
("nla.duplicate_linked_move",
{"type": 'D', "value": 'PRESS', "shift": True},
None),
("nla.duplicate_move",
{"type": 'D', "value": 'PRESS', "alt": True},
None),
("nla.make_single_user",
{"type": 'U', "value": 'PRESS'},
None),
("nla.delete",
{"type": 'X', "value": 'PRESS'},
None),
("nla.delete",
{"type": 'DEL', "value": 'PRESS'},
None),
("nla.split",
{"type": 'Y', "value": 'PRESS'},
None),
("nla.mute_toggle",
{"type": 'H', "value": 'PRESS'},
None),
("nla.swap",
{"type": 'F', "value": 'PRESS', "alt": True},
None),
("nla.move_up",
{"type": 'PAGE_UP', "value": 'PRESS', "repeat": True},
None),
("nla.move_down",
{"type": 'PAGE_DOWN', "value": 'PRESS', "repeat": True},
None),
("nla.apply_scale",
{"type": 'A', "value": 'PRESS', "ctrl": True},
None),
("nla.clear_scale",
{"type": 'S', "value": 'PRESS', "alt": True},
None),
("wm.call_menu_pie",
{"type": 'S', "value": 'PRESS', "shift": True},
{"properties": [('name', 'NLA_MT_snap_pie')]}),
("nla.fmodifier_add",
{"type": 'M', "value": 'PRESS', "shift": True, "ctrl": True},
None),
("transform.transform",
{"type": 'G', "value": 'PRESS'},
{"properties": [('mode', 'TRANSLATION')]}),
("transform.transform",
{"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'},
{"properties": [('mode', 'TRANSLATION')]}),
("transform.transform",
{"type": 'E', "value": 'PRESS'},
{"properties": [('mode', 'TIME_EXTEND')]}),
("transform.transform",
{"type": 'S', "value": 'PRESS'},
{"properties": [('mode', 'TIME_SCALE')]}),
("marker.add",
{"type": 'M', "value": 'PRESS'},
None),
("wm.call_menu",
{"type": 'RIGHTMOUSE', "value": 'PRESS'},
{"properties": [('name', 'NLA_MT_context_menu')]}),
("wm.call_menu",
{"type": 'APP', "value": 'PRESS'},
{"properties": [('name', 'NLA_MT_context_menu')]}),
("anim.change_frame",
{"type": 'RIGHTMOUSE', "value": 'PRESS', "shift": True},
{"properties": [('seq_solo_preview', True)]}),
],
},
),
("Frames",
{"space_type": 'EMPTY', "region_type": 'WINDOW'},
{"items":
@@ -4409,6 +4606,7 @@ keyconfig_data = \
("render.view_cancel", {"type": 'ESC', "value": 'PRESS', "repeat": True}, None),
("render.view_show", {"type": 'F11', "value": 'PRESS', "repeat": True}, None),
("render.play_rendered_anim", {"type": 'F11', "value": 'PRESS', "ctrl": True, "repeat": True}, None),
("screen.userpref_show", {"type": 'COMMA', "value": 'PRESS', "ctrl": True, "repeat": True}, None),
],
},
),
@@ -6,6 +6,7 @@ import os
import bpy
from ..keyconfigs import fa_hotkeys
from ..utils import compat
from ..utils import deploy as deploy_util
@@ -14,8 +15,7 @@ class MCP_OT_DeployKeymapPresets(bpy.types.Operator):
bl_idname = "mcp.deploy_keymap_presets"
bl_label = "Deploy Keymap Presets"
bl_description = (
"Copy Blender default keymaps (and MCP fa_hotkeys) into your user scripts folder "
"so keymap switching buttons work"
"Install the MCP fa_hotkeys keymap into your user keyconfig folder so FA Hotkeys works"
)
bl_options = {"REGISTER", "INTERNAL"}
@@ -23,6 +23,10 @@ class MCP_OT_DeployKeymapPresets(bpy.types.Operator):
ok, msg = deploy_util.deploy_keymap_presets()
if ok:
self.report({"INFO"}, msg.replace("\n", " - "))
try:
fa_hotkeys.sync_addon_keymaps()
except Exception:
pass
else:
self.report({"ERROR"}, msg)
return {"FINISHED"} if ok else {"CANCELLED"}
@@ -62,27 +66,30 @@ class MCP_OT_ActivateKeymapPreset(bpy.types.Operator):
)
def execute(self, context):
if not deploy_util.is_keyconfig_deployed():
self.report(
{"ERROR"},
"Keymap presets are not deployed. "
"Edit → Preferences → Add-ons → Maya Config Pro → Deploy keymap presets.",
)
return {"CANCELLED"}
names = {
"BLENDER": "Blender.py",
"FA_HOTKEYS": "fa_hotkeys.py",
"INDUSTRY": "Industry_Compatible.py",
}
filename = names[self.preset]
filepath = os.path.join(deploy_util.user_keyconfig_dir(), filename)
if not os.path.isfile(filepath):
self.report(
{"ERROR"},
f"Missing {filename}. Open Preferences and run Deploy keymap presets again.",
)
return {"CANCELLED"}
if self.preset == "FA_HOTKEYS":
if not deploy_util.is_keyconfig_deployed():
self.report(
{"ERROR"},
"Keymap is not installed. "
"Edit → Preferences → Add-ons → Maya Config Pro → Deploy keymap presets.",
)
return {"CANCELLED"}
filepath = os.path.join(deploy_util.user_keyconfig_dir(), filename)
else:
filepath = deploy_util.official_keyconfig_filepath(filename)
if not filepath:
self.report(
{"ERROR"},
f"Could not find {filename} in this Blender install's keyconfig folder.",
)
return {"CANCELLED"}
try:
bpy.ops.preferences.keyconfig_activate(filepath=filepath)
@@ -97,6 +104,11 @@ class MCP_OT_ActivateKeymapPreset(bpy.types.Operator):
elif self.preset in {"BLENDER", "INDUSTRY"}:
inputs.view_zoom_axis = "VERTICAL"
try:
fa_hotkeys.sync_addon_keymaps()
except Exception:
pass
return {"FINISHED"}
@@ -0,0 +1,45 @@
"""
Home key: frame camera bounds in camera view, frame all in perspective/ortho.
"""
import bpy
from ..utils import compat
def _in_camera_view(context) -> bool:
r = getattr(context, "region_data", None)
if r is None or type(r).__name__ != "RegionView3D":
return False
vp = getattr(r, "view_perspective", None)
if vp is not None:
return str(vp) == "CAMERA"
return False
class MCP_OT_ViewFrameHome(bpy.types.Operator):
bl_idname = "mcp.view_frame_home"
bl_label = "Frame View (FA Home)"
bl_description = "In camera view: frame camera bounds. Otherwise: frame all"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return bool(getattr(getattr(context, "area", None), "type", None) == "VIEW_3D")
def execute(self, context):
if _in_camera_view(context):
return bpy.ops.view3d.view_center_camera()
return bpy.ops.view3d.view_all(center=False)
_classes = (MCP_OT_ViewFrameHome,)
def register():
for c in _classes:
compat.safe_register_class(c)
def unregister():
for c in reversed(_classes):
compat.safe_unregister_class(c)
@@ -1,90 +1,32 @@
"""
Maya Config Pro - Panels Package
UI panels for the extension.
"""
"""Maya Config Pro - panels package."""
import sys
import importlib.util
import os
from . import panel_pro
from . import panel_sidebar
from . import panel_shelf
from . import panel_animation
# Module cache
_module_cache = {}
def _load_module(name):
"""Load a module by executing its file"""
if name in _module_cache:
return _module_cache[name]
# Get the file path
current_dir = os.path.dirname(__file__)
file_path = os.path.join(current_dir, f"{name}.py")
if not os.path.exists(file_path):
raise ImportError(f"Module file not found: {file_path}")
# Load the module
spec = importlib.util.spec_from_file_location(
f"{__name__}.{name}",
file_path
)
module = importlib.util.module_from_spec(spec)
# Add to sys.modules temporarily
sys.modules[f"{__name__}.{name}"] = module
spec.loader.exec_module(module)
_module_cache[name] = module
return module
__all__ = (
"panel_pro",
"panel_sidebar",
"panel_shelf",
"panel_animation",
)
def register():
"""Register all panels"""
# Register shelf header first (replaces PROPERTIES panels)
try:
shelf_header = _load_module("shelf_header")
shelf_header.register()
except Exception as e:
print(f"MCP: Failed to register shelf header: {e}")
# Register other panels
try:
panel_pro = _load_module("panel_pro")
panel_pro.register()
except Exception as e:
print(f"MCP: Failed to register panel_pro: {e}")
try:
panel_sidebar = _load_module("panel_sidebar")
panel_sidebar.register()
except Exception as e:
print(f"MCP: Failed to register panel_sidebar: {e}")
try:
panel_shelf = _load_module("panel_shelf")
panel_shelf.register()
except Exception as e:
print(f"MCP: Failed to register panel_shelf (operators only): {e}")
try:
panel_animation = _load_module("panel_animation")
panel_animation.register()
except Exception as e:
print(f"MCP: Failed to register panel_animation (operators only): {e}")
try:
viewport_shelf = _load_module("viewport_shelf")
viewport_shelf.register()
except Exception as e:
print(f"MCP: Failed to register viewport_shelf: {e}")
"""Register panels (if this package is ``register()``'d standalone). Viewport
anim shelf is registered from the add-on root only, not here (avoid double register).
"""
for mod in (panel_pro, panel_sidebar, panel_shelf, panel_animation):
try:
mod.register()
except Exception as e:
print(f"MCP: Failed to register {mod.__name__}: {e}")
def unregister():
"""Unregister all panels"""
# Unregister in reverse order
for name in ["viewport_shelf", "panel_animation", "panel_shelf", "panel_sidebar", "panel_pro", "shelf_header"]:
for mod in (panel_animation, panel_shelf, panel_sidebar, panel_pro):
try:
if name in _module_cache:
_module_cache[name].unregister()
mod.unregister()
except Exception as e:
print(f"MCP: Failed to unregister {name}: {e}")
print(f"MCP: Failed to unregister {mod.__name__}: {e}")
@@ -7,6 +7,67 @@ import bpy
from ..utils import compat
def _addon_id_from_name() -> str:
n = __name__
if ".panels." in n:
return n.split(".panels.", 1)[0]
return n.rsplit(".", 2)[0] if n.count(".") >= 2 else n.split(".", 1)[0]
def view3d_set_show_region_tool_header(show: bool) -> None:
"""Set ``SpaceView3D.show_region_tool_header`` on every 3D view (Blender 5+)."""
for screen in bpy.data.screens:
for area in screen.areas:
if area.type != "VIEW_3D":
continue
# Use active space; iterating ``area.spaces`` is unreliable in some builds.
sp = getattr(area.spaces, "active", None) or (
area.spaces[0] if len(area.spaces) else None
)
if not sp or sp.type != "VIEW_3D":
continue
if hasattr(sp, "show_region_tool_header"):
sp.show_region_tool_header = show
def draw_animation_shelf_row(
layout: bpy.types.UILayout, scale_x: float = 2.0, scale_y: float = 1.0
) -> None:
"""Same operators as the Scene properties row; larger scale for 3D tool header."""
row = layout.row(align=True)
row.scale_y = scale_y
row.scale_x = scale_x
row.operator("wm.link", text="", icon="LINKED")
row.operator("wm.append", text="", icon="APPEND_BLEND")
row.operator("mcp.pose_mode_ani", text="", icon="POSE_HLT")
row.operator("object.armature_add", text="", icon="OUTLINER_OB_ARMATURE")
row.operator("mcp.drop_to_floor_ani", text="", icon="TRIA_DOWN")
row.label(text="", icon="DOT")
row.operator("mcp.graph_editor_ani", text="", icon="GRAPH")
row.operator("mcp.dope_sheet_ani", text="", icon="ACTION")
row.operator("mcp.drivers_ani", text="", icon="DRIVER")
row.operator("anim.keyframe_insert_menu", text="", icon="DECORATE_ANIMATE")
def _anim_shelf_pref_enabled(context) -> bool:
try:
p = context.preferences.addons[_addon_id_from_name()].preferences
if hasattr(p, "viewport_anim_shelf"):
return bool(p.viewport_anim_shelf)
except (KeyError, TypeError, AttributeError, ReferenceError):
try:
for k in list(context.preferences.addons.keys()):
pr = getattr(context.preferences.addons[k], "preferences", None)
if pr and pr.__class__.__name__ == "MCP_AddonPreferences":
if hasattr(pr, "viewport_anim_shelf"):
return bool(pr.viewport_anim_shelf)
except (AttributeError, TypeError, ReferenceError, KeyError):
pass
return True
# ------------------------------------------------------------------------
# Animation Shelf Panel
# ------------------------------------------------------------------------
@@ -22,24 +83,52 @@ class MCP_PT_AnimationShelf(bpy.types.Panel):
draw_type = "animation"
def draw(self, context):
layout = self.layout
obj = context.object
draw_animation_shelf_row(self.layout, scale_x=2.0, scale_y=1.0)
row = layout.row()
row.scale_y = 1
row.scale_x = 2
row.operator('wm.link', text="", icon="LINKED")
row.operator('wm.append', text="", icon="APPEND_BLEND")
row.operator('mcp.pose_mode_ani', text="", icon="POSE_HLT")
row.operator('object.armature_add', text="", icon="OUTLINER_OB_ARMATURE")
row.operator('mcp.drop_to_floor_ani', text="", icon='TRIA_DOWN')
# ------------------------------------------------------------------------
# 3D View — Tool header (native Layout, not POST_PIXEL)
# ------------------------------------------------------------------------
row.label(text="", icon='DOT')
row.operator('mcp.graph_editor_ani', text="", icon="GRAPH")
row.operator('mcp.dope_sheet_ani', text="", icon="ACTION")
row.operator('mcp.drivers_ani', text="", icon="DRIVER")
row.operator('anim.keyframe_insert_menu', text="", icon="DECORATE_ANIMATE")
class MCP_PT_AnimToolHeader(bpy.types.Panel):
"""Larger icon row in the 3D View tool header (below the main top bar)."""
bl_idname = "MCP_PT_AnimToolHeader"
bl_label = "Animation"
bl_space_type = "VIEW_3D"
bl_region_type = "TOOL_HEADER"
bl_order = 20
@classmethod
def poll(cls, context):
if not _anim_shelf_pref_enabled(context):
return False
if context.space_data is None or context.space_data.type != "VIEW_3D":
return False
return True
def draw(self, context):
draw_animation_shelf_row(self.layout, scale_x=2.4, scale_y=1.35)
class MCP_PT_AnimNPanel(bpy.types.Panel):
"""Same shelf in the 3D View sidebar (N) so it is always available."""
bl_idname = "MCP_PT_AnimNPanel"
bl_label = "Animation shelf"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Animation"
bl_order = 2
@classmethod
def poll(cls, context):
if not _anim_shelf_pref_enabled(context):
return False
if context.space_data is None or context.space_data.type != "VIEW_3D":
return False
return True
def draw(self, context):
draw_animation_shelf_row(self.layout, scale_x=1.0, scale_y=1.1)
# ------------------------------------------------------------------------
@@ -180,6 +269,8 @@ class MCP_OT_DropToFloorAni(bpy.types.Operator):
_classes = [
# Panel
MCP_PT_AnimationShelf,
MCP_PT_AnimToolHeader,
MCP_PT_AnimNPanel,
# Animation Operators
MCP_OT_PropertiesEditorAni,
MCP_OT_LinkRigAni,
@@ -191,11 +282,35 @@ _classes = [
]
_th_once = False
def _deferred_show_tool_header():
global _th_once
if _th_once or bpy.app.background:
return None
_th_once = True
if not _anim_shelf_pref_enabled(bpy.context):
return None
try:
view3d_set_show_region_tool_header(True)
except (ReferenceError, TypeError, AttributeError):
pass
return None
def register():
for cls in _classes:
compat.safe_register_class(cls)
if not bpy.app.background:
try:
bpy.app.timers.register(_deferred_show_tool_header, first_interval=0.15)
except (RuntimeError, TypeError, AttributeError):
pass
def unregister():
global _th_once
_th_once = False
for cls in reversed(_classes):
compat.safe_unregister_class(cls)
@@ -1,384 +0,0 @@
# SPDX-FileCopyrightText: 2018-2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
import os
import bpy
from bpy.props import (
BoolProperty,
EnumProperty,
)
DIRNAME, FILENAME = os.path.split(__file__)
IDNAME = os.path.splitext(FILENAME)[0]
def update_fn(_self, _context):
load()
class Prefs(bpy.types.KeyConfigPreferences):
bl_idname = IDNAME
select_mouse: EnumProperty(
name="Select Mouse",
items=(
('LEFT', "Left",
"Use left mouse button for selection. "
"The standard behavior that works well for mouse, trackpad and tablet devices"),
('RIGHT', "Right",
"Use right mouse button for selection, and left mouse button for actions. "
"This works well primarily for keyboard and mouse devices"),
),
description=(
"Mouse button used for selection"
),
update=update_fn,
)
spacebar_action: EnumProperty(
name="Spacebar Action",
items=(
('PLAY', "Play",
"Toggle animation playback "
"('Shift-Space' for Tools)",
1),
('TOOL', "Tools",
"Open the popup tool-bar\n"
"When 'Space' is held and used as a modifier:\n"
"\u2022 Pressing the tools binding key switches to it immediately.\n"
"\u2022 Dragging the cursor over a tool and releasing activates it (like a pie menu).\n"
"For Play use 'Shift-Space'",
0),
('SEARCH', "Search",
"Open the operator search popup",
2),
),
description=(
"Action when 'Space' is pressed"
),
default='PLAY',
update=update_fn,
)
tool_key_mode: EnumProperty(
name="Tool Keys",
description=(
"The method of keys to activate tools such as move, rotate & scale (G, R, S)"
),
items=(
('IMMEDIATE', "Immediate",
"Activate actions immediately"),
('TOOL', "Active Tool",
"Activate the tool for editors that support tools"),
),
default='IMMEDIATE',
update=update_fn,
)
rmb_action: EnumProperty(
name="Right Mouse Select Action",
items=(
('TWEAK', "Select & Tweak",
"Right mouse always tweaks"),
('FALLBACK_TOOL', "Selection Tool",
"Right mouse uses the selection tool"),
),
description=(
"Default action for the right mouse button"
),
update=update_fn,
)
# Experimental: only show with developer extras, see: #107785.
use_region_toggle_pie: BoolProperty(
name="Region Toggle Pie",
description=(
"N-key opens a pie menu to toggle regions"
),
default=False,
update=update_fn,
)
use_alt_click_leader: BoolProperty(
name="Alt Click Tool Prompt",
description=(
"Tapping Alt (without pressing any other keys) shows a prompt in the status-bar, "
"prompting a second keystroke to activate the tool"
),
default=False,
update=update_fn,
)
# NOTE: expose `use_alt_tool` and `use_alt_cursor` as two options in the UI
# as the tool-tips and titles are different enough depending on RMB/LMB select.
use_alt_tool: BoolProperty(
name="Alt Tool Access",
description=(
"Hold Alt to use the active tool when the gizmo would normally be required\n"
"Incompatible with the input preference \"Emulate 3 Button Mouse\" when the \"Alt\" key is used"
),
default=False,
update=update_fn,
)
use_alt_cursor: BoolProperty(
name="Alt Cursor Access",
description=(
"Hold Alt-LMB to place the Cursor (instead of LMB), allows tools to activate on press instead of drag.\n"
"Incompatible with the input preference \"Emulate 3 Button Mouse\" when the \"Alt\" key is used"
),
default=False,
update=update_fn,
)
# end note.
use_select_all_toggle: BoolProperty(
name="Select All Toggles",
description=(
"Causes select-all ('A' key) to de-select in the case a selection exists"
),
default=False,
update=update_fn,
)
gizmo_action: EnumProperty(
name="Activate Gizmo",
items=(
('PRESS', "Press", "Press causes immediate activation, preventing click being passed to the tool"),
('DRAG', "Drag", "Drag allows click events to pass through to the tool, adding a small delay"),
),
description="Activation event for gizmos that support drag motion",
default='DRAG',
update=update_fn,
)
# 3D View
use_v3d_tab_menu: BoolProperty(
name="Tab for Pie Menu",
description=(
"Causes tab to open pie menu (swaps 'Tab' / 'Ctrl-Tab')"
),
default=False,
update=update_fn,
)
use_v3d_shade_ex_pie: BoolProperty(
name="Extra Shading Pie Menu Items",
description=(
"Show additional options in the shading menu ('Z')"
),
default=False,
update=update_fn,
)
v3d_tilde_action: EnumProperty(
name="Tilde Action",
items=(
('VIEW', "Navigate",
"View operations (useful for keyboards without a numpad)",
0),
('GIZMO', "Gizmos",
"Control transform gizmos",
1),
),
description=(
"Action when 'Tilde' is pressed"
),
default='VIEW',
update=update_fn,
)
v3d_mmb_action: EnumProperty(
name="MMB Action",
items=(
('ORBIT', "Orbit",
"",
0),
('PAN', "Pan",
"",
1),
),
description=(
"The action when Middle-Mouse dragging in the viewport. "
"Shift-Middle-Mouse is used for the other action. "
"This applies to trackpad as well"
),
update=update_fn,
)
v3d_alt_mmb_drag_action: EnumProperty(
name="Alt-MMB Drag Action",
items=(
('RELATIVE', "Relative",
"Set the view axis where each mouse direction maps to an axis relative to the current orientation",
0),
('ABSOLUTE', "Absolute",
"Set the view axis where each mouse direction always maps to the same axis",
1),
),
description=(
"Action when Alt-MMB dragging in the 3D viewport"
),
update=update_fn,
)
# Developer note, this is an experimental option.
use_pie_click_drag: BoolProperty(
name="Pie Menu on Drag",
description=(
"Activate some pie menus on drag,\n"
"allowing the tapping the same key to have a secondary action.\n"
"\n"
"\u2022 Tapping Tab in the 3D view toggles edit-mode, drag for mode menu.\n"
"\u2022 Tapping Z in the 3D view toggles wireframe, drag for draw modes.\n"
"\u2022 Tapping Tilde in the 3D view for first person navigation, drag for view axes"
),
default=False,
update=update_fn,
)
use_file_single_click: BoolProperty(
name="Open Folders on Single Click",
description=(
"Navigate into folders by clicking on them once instead of twice"
),
default=False,
update=update_fn,
)
use_alt_navigation: BoolProperty(
name="Transform Navigation with Alt",
description=(
"During transformations, use Alt to navigate in the 3D View. "
"Note that if disabled, hotkeys for Proportional Editing, "
"Automatic Constraints, and Auto IK Chain Length will require holding Alt"
),
default=True,
update=update_fn,
)
def draw(self, layout):
from bpy import context
layout.use_property_split = True
layout.use_property_decorate = False
prefs = context.preferences
show_developer_ui = prefs.view.show_developer_ui
is_select_left = (self.select_mouse == 'LEFT')
use_mouse_emulate_3_button = (
prefs.inputs.use_mouse_emulate_3_button and
prefs.inputs.mouse_emulate_3_button_modifier == 'ALT'
)
# General settings.
col = layout.column()
col.row().prop(self, "select_mouse", text="Select with Mouse Button", expand=True)
col.row().prop(self, "spacebar_action", text="Spacebar Action", expand=True)
if is_select_left:
col.row().prop(self, "gizmo_action", text="Activate Gizmo Event", expand=True)
else:
col.row().prop(self, "rmb_action", text="Right Mouse Select Action", expand=True)
col.row().prop(self, "tool_key_mode", expand=True)
# Check-box sub-layout.
col = layout.column()
sub = col.column(align=True)
row = sub.row()
row.prop(self, "use_alt_click_leader")
rowsub = row.row()
if is_select_left:
rowsub.prop(self, "use_alt_tool")
else:
rowsub.prop(self, "use_alt_cursor")
rowsub.active = not use_mouse_emulate_3_button
row = sub.row()
row.prop(self, "use_select_all_toggle")
if show_developer_ui:
row = sub.row()
row.prop(self, "use_region_toggle_pie")
# 3DView settings.
col = layout.column()
col.label(text="3D View")
col.row().prop(self, "v3d_tilde_action", text="Grave Accent / Tilde Action", expand=True)
col.row().prop(self, "v3d_mmb_action", text="Middle Mouse Action", expand=True)
col.row().prop(self, "v3d_alt_mmb_drag_action", text="Alt Middle Mouse Drag Action", expand=True)
# Check-boxes sub-layout.
col = layout.column()
sub = col.column(align=True)
sub.prop(self, "use_v3d_tab_menu")
sub.prop(self, "use_pie_click_drag")
sub.prop(self, "use_v3d_shade_ex_pie")
sub.prop(self, "use_alt_navigation")
# File Browser settings.
col = layout.column()
col.label(text="File Browser")
col.row().prop(self, "use_file_single_click")
blender_default = bpy.utils.execfile(os.path.join(DIRNAME, "keymap_data", "blender_default.py"))
def load():
from sys import platform
from bpy import context
from bl_keymap_utils.io import keyconfig_init_from_data
prefs = context.preferences
kc = context.window_manager.keyconfigs.new(IDNAME)
kc_prefs = kc.preferences
show_developer_ui = prefs.view.show_developer_ui
is_select_left = (kc_prefs.select_mouse == 'LEFT')
use_mouse_emulate_3_button = (
prefs.inputs.use_mouse_emulate_3_button and
prefs.inputs.mouse_emulate_3_button_modifier == 'ALT'
)
keyconfig_data = blender_default.generate_keymaps(
blender_default.Params(
select_mouse=kc_prefs.select_mouse,
use_mouse_emulate_3_button=use_mouse_emulate_3_button,
spacebar_action=kc_prefs.spacebar_action,
use_key_activate_tools=(kc_prefs.tool_key_mode == 'TOOL'),
use_region_toggle_pie=(show_developer_ui and kc_prefs.use_region_toggle_pie),
v3d_tilde_action=kc_prefs.v3d_tilde_action,
use_v3d_mmb_pan=(kc_prefs.v3d_mmb_action == 'PAN'),
v3d_alt_mmb_drag_action=kc_prefs.v3d_alt_mmb_drag_action,
use_select_all_toggle=kc_prefs.use_select_all_toggle,
use_v3d_tab_menu=kc_prefs.use_v3d_tab_menu,
use_v3d_shade_ex_pie=kc_prefs.use_v3d_shade_ex_pie,
use_gizmo_drag=(is_select_left and kc_prefs.gizmo_action == 'DRAG'),
use_fallback_tool=True,
use_fallback_tool_select_handled=(
# LMB doesn't need additional selection fallback key-map items.
False if is_select_left else
# RMB is select and RMB must trigger the fallback tool.
# Otherwise LMB activates the fallback tool and RMB always tweak-selects.
(kc_prefs.rmb_action != 'FALLBACK_TOOL')
),
use_alt_tool_or_cursor=(
(not use_mouse_emulate_3_button) and
(kc_prefs.use_alt_tool if is_select_left else kc_prefs.use_alt_cursor)
),
use_alt_click_leader=kc_prefs.use_alt_click_leader,
use_pie_click_drag=kc_prefs.use_pie_click_drag,
use_file_single_click=kc_prefs.use_file_single_click,
use_alt_navigation=kc_prefs.use_alt_navigation,
),
)
if platform == "darwin":
from bl_keymap_utils.platform_helpers import keyconfig_data_oskey_from_ctrl_for_macos
keyconfig_data = keyconfig_data_oskey_from_ctrl_for_macos(keyconfig_data)
keyconfig_init_from_data(kc, keyconfig_data)
if __name__ == "__main__":
bpy.utils.register_class(Prefs)
load()
@@ -1,106 +0,0 @@
# SPDX-FileCopyrightText: 2018-2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
# Notes on this key-map:
#
# This uses Blender's key-map, running with `legacy=True`.
#
# The intention of this key-map is to match Blender 2.7x which had many more key-map items available.
#
# There are some differences with the original Blender 2.7x key-map.
# There is no intention to change these are they are not considered significant
# enough to make a 1:1 match with the previous Blender version.
#
# These include:
#
# 3D View
# =======
#
# - Border Render (`Shift-B` -> `Ctrl-B`)
# Both `Shift-B` and `Ctrl-B` were used.
#
# Time Line/Animation Views
# =========================
#
# - Start Frame/End Frame (`S/E` -> `Ctrl-Home/Ctrl-End`)
#
import os
import bpy
from bpy.props import (
EnumProperty,
)
DIRNAME, FILENAME = os.path.split(__file__)
IDNAME = os.path.splitext(FILENAME)[0]
def update_fn(_self, _context):
load()
class Prefs(bpy.types.KeyConfigPreferences):
bl_idname = IDNAME
select_mouse: EnumProperty(
name="Select Mouse",
items=(
('LEFT', "Left",
"Use left mouse button for selection. "
"The standard behavior that works well for mouse, trackpad and tablet devices"),
('RIGHT', "Right",
"Use right mouse button for selection, and left mouse button for actions. "
"This works well primarily for keyboard and mouse devices"),
),
description=(
"Mouse button used for selection"
),
default='RIGHT',
update=update_fn,
)
def draw(self, layout):
layout.use_property_split = True
layout.use_property_decorate = False
col = layout.column()
col.row().prop(self, "select_mouse", text="Select with Mouse Button", expand=True)
blender_default = bpy.utils.execfile(os.path.join(DIRNAME, "keymap_data", "blender_default.py"))
def load():
from sys import platform
from bpy import context
from bl_keymap_utils.io import keyconfig_init_from_data
prefs = context.preferences
kc = context.window_manager.keyconfigs.new(IDNAME)
kc_prefs = kc.preferences
keyconfig_data = blender_default.generate_keymaps(
blender_default.Params(
select_mouse=kc_prefs.select_mouse,
use_mouse_emulate_3_button=(
prefs.inputs.use_mouse_emulate_3_button and
prefs.inputs.mouse_emulate_3_button_modifier == 'ALT'
),
spacebar_action='SEARCH',
use_select_all_toggle=True,
use_gizmo_drag=False,
legacy=True,
),
)
if platform == "darwin":
from bl_keymap_utils.platform_helpers import keyconfig_data_oskey_from_ctrl_for_macos
keyconfig_data = keyconfig_data_oskey_from_ctrl_for_macos(keyconfig_data)
keyconfig_init_from_data(kc, keyconfig_data)
if __name__ == "__main__":
bpy.utils.register_class(Prefs)
load()
@@ -1,41 +0,0 @@
# SPDX-FileCopyrightText: 2019-2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
import os
import bpy
# ------------------------------------------------------------------------------
# Keymap
DIRNAME, FILENAME = os.path.split(__file__)
IDNAME = os.path.splitext(FILENAME)[0]
def update_fn(_self, _context):
load()
industry_compatible = bpy.utils.execfile(os.path.join(DIRNAME, "keymap_data", "industry_compatible_data.py"))
def load():
from sys import platform
from bl_keymap_utils.io import keyconfig_init_from_data
prefs = bpy.context.preferences
kc = bpy.context.window_manager.keyconfigs.new(IDNAME)
params = industry_compatible.Params(use_mouse_emulate_3_button=prefs.inputs.use_mouse_emulate_3_button)
keyconfig_data = industry_compatible.generate_keymaps(params)
if platform == "darwin":
from bl_keymap_utils.platform_helpers import keyconfig_data_oskey_from_ctrl_for_macos
keyconfig_data = keyconfig_data_oskey_from_ctrl_for_macos(keyconfig_data)
keyconfig_init_from_data(kc, keyconfig_data)
if __name__ == "__main__":
load()
@@ -1,4 +1,4 @@
keyconfig_version = (5, 0, 120)
keyconfig_version = (5, 0, 121)
keyconfig_data = \
[("3D View",
{"space_type": 'VIEW_3D', "region_type": 'WINDOW'},
@@ -107,15 +107,7 @@ keyconfig_data = \
],
},
),
("view3d.view_center_camera", {"type": 'HOME', "value": 'PRESS', "repeat": True}, None),
("view3d.view_center_lock", {"type": 'HOME', "value": 'PRESS', "repeat": True}, None),
("view3d.view_all",
{"type": 'HOME', "value": 'PRESS', "repeat": True},
{"properties":
[("center", False),
],
},
),
("mcp.view_frame_home", {"type": 'HOME', "value": 'PRESS', "repeat": True}, None),
("view3d.view_all",
{"type": 'HOME', "value": 'PRESS', "ctrl": True, "repeat": True},
{"properties":
@@ -1534,6 +1526,211 @@ keyconfig_data = \
],
},
),
("NLA Generic",
{"space_type": 'NLA_EDITOR', "region_type": 'WINDOW'},
{"items":
[("wm.context_toggle",
{"type": 'N', "value": 'PRESS'},
{"properties": [('data_path', 'space_data.show_region_ui')]}),
("nla.tweakmode_enter",
{"type": 'TAB', "value": 'PRESS'},
{"properties": [('use_upper_stack_evaluation', True)]}),
("nla.tweakmode_exit",
{"type": 'TAB', "value": 'PRESS'},
None),
("nla.tweakmode_enter",
{"type": 'TAB', "value": 'PRESS', "shift": True},
{"properties": [('isolate_action', True)]}),
("nla.tweakmode_exit",
{"type": 'TAB', "value": 'PRESS', "shift": True},
{"properties": [('isolate_action', True)]}),
("anim.channels_select_filter",
{"type": 'F', "value": 'PRESS', "ctrl": True},
None),
],
},
),
("NLA Tracks",
{"space_type": 'NLA_EDITOR', "region_type": 'WINDOW'},
{"items":
[("nla.channels_click",
{"type": 'LEFTMOUSE', "value": 'PRESS'},
None),
("nla.channels_click",
{"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True},
{"properties": [('extend', True)]}),
("nla.tracks_add",
{"type": 'A', "value": 'PRESS', "shift": True},
{"properties": [('above_selected', False)]}),
("nla.tracks_add",
{"type": 'A', "value": 'PRESS', "shift": True, "ctrl": True},
{"properties": [('above_selected', True)]}),
("nla.tracks_delete",
{"type": 'X', "value": 'PRESS'},
None),
("nla.tracks_delete",
{"type": 'DEL', "value": 'PRESS'},
None),
("wm.call_menu",
{"type": 'RIGHTMOUSE', "value": 'PRESS'},
{"properties": [('name', 'NLA_MT_channel_context_menu')]}),
("wm.call_menu",
{"type": 'APP', "value": 'PRESS'},
{"properties": [('name', 'NLA_MT_channel_context_menu')]}),
],
},
),
("NLA Editor",
{"space_type": 'NLA_EDITOR', "region_type": 'WINDOW'},
{"items":
[("nla.click_select",
{"type": 'LEFTMOUSE', "value": 'PRESS'},
{"properties": [('deselect_all', True)]}),
("nla.click_select",
{"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True},
{"properties": [('extend', True)]}),
("nla.select_leftright",
{"type": 'LEFTMOUSE', "value": 'CLICK', "ctrl": True},
{"properties": [('mode', 'CHECK')]}),
("nla.select_leftright",
{"type": 'LEFTMOUSE', "value": 'CLICK', "ctrl": True, "shift": True},
{"properties": [('mode', 'CHECK'), ('extend', True)]}),
("nla.select_leftright",
{"type": 'LEFT_BRACKET', "value": 'PRESS'},
{"properties": [('mode', 'LEFT')]}),
("nla.select_leftright",
{"type": 'RIGHT_BRACKET', "value": 'PRESS'},
{"properties": [('mode', 'RIGHT')]}),
("nla.select_all",
{"type": 'A', "value": 'PRESS'},
{"properties": [('action', 'SELECT')]}),
("nla.select_all",
{"type": 'A', "value": 'PRESS', "alt": True},
{"properties": [('action', 'DESELECT')]}),
("nla.select_all",
{"type": 'I', "value": 'PRESS', "ctrl": True},
{"properties": [('action', 'INVERT')]}),
("nla.select_all",
{"type": 'A', "value": 'DOUBLE_CLICK'},
{"properties": [('action', 'DESELECT')]}),
("nla.select_box",
{"type": 'B', "value": 'PRESS'},
{"properties": [('axis_range', False)]}),
("nla.select_box",
{"type": 'B', "value": 'PRESS', "alt": True},
{"properties": [('axis_range', True)]}),
("nla.select_box",
{"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'},
{"properties": [('tweak', True), ('mode', 'SET')]}),
("nla.select_box",
{"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True},
{"properties": [('tweak', True), ('mode', 'ADD')]}),
("nla.select_box",
{"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True},
{"properties": [('tweak', True), ('mode', 'SUB')]}),
("nla.previewrange_set",
{"type": 'P', "value": 'PRESS', "ctrl": True, "alt": True},
None),
("nla.view_all",
{"type": 'HOME', "value": 'PRESS'},
None),
("nla.view_all",
{"type": 'NDOF_BUTTON_FIT', "value": 'PRESS'},
None),
("nla.view_selected",
{"type": 'NUMPAD_PERIOD', "value": 'PRESS'},
None),
("nla.view_frame",
{"type": 'NUMPAD_0', "value": 'PRESS'},
None),
("wm.call_menu_pie",
{"type": 'ACCENT_GRAVE', "value": 'PRESS'},
{"properties": [('name', 'NLA_MT_view_pie')]}),
("nla.actionclip_add",
{"type": 'A', "value": 'PRESS', "shift": True},
None),
("nla.transition_add",
{"type": 'T', "value": 'PRESS', "shift": True},
None),
("nla.soundclip_add",
{"type": 'K', "value": 'PRESS', "shift": True},
None),
("nla.meta_add",
{"type": 'G', "value": 'PRESS', "ctrl": True},
None),
("nla.meta_remove",
{"type": 'G', "value": 'PRESS', "ctrl": True, "alt": True},
None),
("nla.duplicate_linked_move",
{"type": 'D', "value": 'PRESS', "shift": True},
None),
("nla.duplicate_move",
{"type": 'D', "value": 'PRESS', "alt": True},
None),
("nla.make_single_user",
{"type": 'U', "value": 'PRESS'},
None),
("nla.delete",
{"type": 'X', "value": 'PRESS'},
None),
("nla.delete",
{"type": 'DEL', "value": 'PRESS'},
None),
("nla.split",
{"type": 'Y', "value": 'PRESS'},
None),
("nla.mute_toggle",
{"type": 'H', "value": 'PRESS'},
None),
("nla.swap",
{"type": 'F', "value": 'PRESS', "alt": True},
None),
("nla.move_up",
{"type": 'PAGE_UP', "value": 'PRESS', "repeat": True},
None),
("nla.move_down",
{"type": 'PAGE_DOWN', "value": 'PRESS', "repeat": True},
None),
("nla.apply_scale",
{"type": 'A', "value": 'PRESS', "ctrl": True},
None),
("nla.clear_scale",
{"type": 'S', "value": 'PRESS', "alt": True},
None),
("wm.call_menu_pie",
{"type": 'S', "value": 'PRESS', "shift": True},
{"properties": [('name', 'NLA_MT_snap_pie')]}),
("nla.fmodifier_add",
{"type": 'M', "value": 'PRESS', "shift": True, "ctrl": True},
None),
("transform.transform",
{"type": 'G', "value": 'PRESS'},
{"properties": [('mode', 'TRANSLATION')]}),
("transform.transform",
{"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'},
{"properties": [('mode', 'TRANSLATION')]}),
("transform.transform",
{"type": 'E', "value": 'PRESS'},
{"properties": [('mode', 'TIME_EXTEND')]}),
("transform.transform",
{"type": 'S', "value": 'PRESS'},
{"properties": [('mode', 'TIME_SCALE')]}),
("marker.add",
{"type": 'M', "value": 'PRESS'},
None),
("wm.call_menu",
{"type": 'RIGHTMOUSE', "value": 'PRESS'},
{"properties": [('name', 'NLA_MT_context_menu')]}),
("wm.call_menu",
{"type": 'APP', "value": 'PRESS'},
{"properties": [('name', 'NLA_MT_context_menu')]}),
("anim.change_frame",
{"type": 'RIGHTMOUSE', "value": 'PRESS', "shift": True},
{"properties": [('seq_solo_preview', True)]}),
],
},
),
("Frames",
{"space_type": 'EMPTY', "region_type": 'WINDOW'},
{"items":
@@ -4409,6 +4606,7 @@ keyconfig_data = \
("render.view_cancel", {"type": 'ESC', "value": 'PRESS', "repeat": True}, None),
("render.view_show", {"type": 'F11', "value": 'PRESS', "repeat": True}, None),
("render.play_rendered_anim", {"type": 'F11', "value": 'PRESS', "ctrl": True, "repeat": True}, None),
("screen.userpref_show", {"type": 'COMMA', "value": 'PRESS', "ctrl": True, "repeat": True}, None),
],
},
),
@@ -49,13 +49,20 @@ def blender_bundled_keyconfig_dir() -> str | None:
def is_keyconfig_deployed() -> bool:
kc = user_keyconfig_dir()
if not os.path.isdir(os.path.join(kc, "keymap_data")):
return False
for name in ("Blender.py", "Industry_Compatible.py", "fa_hotkeys.py"):
if not os.path.isfile(os.path.join(kc, name)):
return False
return True
"""True once MCP has copied fa_hotkeys.py into the user keyconfig folder."""
return os.path.isfile(os.path.join(user_keyconfig_dir(), "fa_hotkeys.py"))
def official_keyconfig_filepath(name: str) -> str | None:
"""
Path to a stock keyconfig module shipped with the running Blender (Blender.py, etc.).
Not used for fa_hotkeys that file lives in the user keyconfig after deploy.
"""
d = blender_bundled_keyconfig_dir()
if not d:
return None
path = os.path.join(d, name)
return path if os.path.isfile(path) else None
def is_startup_bundle_available() -> bool:
@@ -64,30 +71,22 @@ def is_startup_bundle_available() -> bool:
def deploy_keymap_presets() -> tuple[bool, str]:
"""
Copy Blender's default keyconfig tree from LOCAL, then overlay FA hotkeys from this add-on.
Install only the MCP fa_hotkeys keyconfig into the user keyconfig folder.
Stock Blender/Industry keymaps are activated from the running install, not copied here.
"""
src_install = blender_bundled_keyconfig_dir()
if not src_install:
return False, "Could not find Blender's bundled keyconfig folder (full installation required)."
dst = user_keyconfig_dir()
os.makedirs(dst, exist_ok=True)
try:
shutil.copytree(src_install, dst, dirs_exist_ok=True)
except OSError as e:
return False, f"Could not copy keyconfig presets: {e}"
portable_fa = os.path.join(bundled_keyconfig_dir(), "fa_hotkeys.py")
if os.path.isfile(portable_fa):
try:
shutil.copy2(portable_fa, os.path.join(dst, "fa_hotkeys.py"))
except OSError as e:
return False, f"Could not install fa_hotkeys.py: {e}"
else:
if not os.path.isfile(portable_fa):
return False, "Add-on is missing portable/scripts/presets/keyconfig/fa_hotkeys.py."
return True, f"Keymap presets deployed to:\n{dst}"
try:
shutil.copy2(portable_fa, os.path.join(dst, "fa_hotkeys.py"))
except OSError as e:
return False, f"Could not install fa_hotkeys.py: {e}"
return True, f"fa_hotkeys keymap installed to:\n{dst}"
def deploy_startup_blend() -> tuple[bool, str]:
+208 -10
View File
@@ -1,4 +1,4 @@
keyconfig_version = (5, 0, 120)
keyconfig_version = (5, 0, 121)
keyconfig_data = \
[("3D View",
{"space_type": 'VIEW_3D', "region_type": 'WINDOW'},
@@ -107,15 +107,7 @@ keyconfig_data = \
],
},
),
("view3d.view_center_camera", {"type": 'HOME', "value": 'PRESS', "repeat": True}, None),
("view3d.view_center_lock", {"type": 'HOME', "value": 'PRESS', "repeat": True}, None),
("view3d.view_all",
{"type": 'HOME', "value": 'PRESS', "repeat": True},
{"properties":
[("center", False),
],
},
),
("mcp.view_frame_home", {"type": 'HOME', "value": 'PRESS', "repeat": True}, None),
("view3d.view_all",
{"type": 'HOME', "value": 'PRESS', "ctrl": True, "repeat": True},
{"properties":
@@ -1534,6 +1526,211 @@ keyconfig_data = \
],
},
),
("NLA Generic",
{"space_type": 'NLA_EDITOR', "region_type": 'WINDOW'},
{"items":
[("wm.context_toggle",
{"type": 'N', "value": 'PRESS'},
{"properties": [('data_path', 'space_data.show_region_ui')]}),
("nla.tweakmode_enter",
{"type": 'TAB', "value": 'PRESS'},
{"properties": [('use_upper_stack_evaluation', True)]}),
("nla.tweakmode_exit",
{"type": 'TAB', "value": 'PRESS'},
None),
("nla.tweakmode_enter",
{"type": 'TAB', "value": 'PRESS', "shift": True},
{"properties": [('isolate_action', True)]}),
("nla.tweakmode_exit",
{"type": 'TAB', "value": 'PRESS', "shift": True},
{"properties": [('isolate_action', True)]}),
("anim.channels_select_filter",
{"type": 'F', "value": 'PRESS', "ctrl": True},
None),
],
},
),
("NLA Tracks",
{"space_type": 'NLA_EDITOR', "region_type": 'WINDOW'},
{"items":
[("nla.channels_click",
{"type": 'LEFTMOUSE', "value": 'PRESS'},
None),
("nla.channels_click",
{"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True},
{"properties": [('extend', True)]}),
("nla.tracks_add",
{"type": 'A', "value": 'PRESS', "shift": True},
{"properties": [('above_selected', False)]}),
("nla.tracks_add",
{"type": 'A', "value": 'PRESS', "shift": True, "ctrl": True},
{"properties": [('above_selected', True)]}),
("nla.tracks_delete",
{"type": 'X', "value": 'PRESS'},
None),
("nla.tracks_delete",
{"type": 'DEL', "value": 'PRESS'},
None),
("wm.call_menu",
{"type": 'RIGHTMOUSE', "value": 'PRESS'},
{"properties": [('name', 'NLA_MT_channel_context_menu')]}),
("wm.call_menu",
{"type": 'APP', "value": 'PRESS'},
{"properties": [('name', 'NLA_MT_channel_context_menu')]}),
],
},
),
("NLA Editor",
{"space_type": 'NLA_EDITOR', "region_type": 'WINDOW'},
{"items":
[("nla.click_select",
{"type": 'LEFTMOUSE', "value": 'PRESS'},
{"properties": [('deselect_all', True)]}),
("nla.click_select",
{"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True},
{"properties": [('extend', True)]}),
("nla.select_leftright",
{"type": 'LEFTMOUSE', "value": 'CLICK', "ctrl": True},
{"properties": [('mode', 'CHECK')]}),
("nla.select_leftright",
{"type": 'LEFTMOUSE', "value": 'CLICK', "ctrl": True, "shift": True},
{"properties": [('mode', 'CHECK'), ('extend', True)]}),
("nla.select_leftright",
{"type": 'LEFT_BRACKET', "value": 'PRESS'},
{"properties": [('mode', 'LEFT')]}),
("nla.select_leftright",
{"type": 'RIGHT_BRACKET', "value": 'PRESS'},
{"properties": [('mode', 'RIGHT')]}),
("nla.select_all",
{"type": 'A', "value": 'PRESS'},
{"properties": [('action', 'SELECT')]}),
("nla.select_all",
{"type": 'A', "value": 'PRESS', "alt": True},
{"properties": [('action', 'DESELECT')]}),
("nla.select_all",
{"type": 'I', "value": 'PRESS', "ctrl": True},
{"properties": [('action', 'INVERT')]}),
("nla.select_all",
{"type": 'A', "value": 'DOUBLE_CLICK'},
{"properties": [('action', 'DESELECT')]}),
("nla.select_box",
{"type": 'B', "value": 'PRESS'},
{"properties": [('axis_range', False)]}),
("nla.select_box",
{"type": 'B', "value": 'PRESS', "alt": True},
{"properties": [('axis_range', True)]}),
("nla.select_box",
{"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'},
{"properties": [('tweak', True), ('mode', 'SET')]}),
("nla.select_box",
{"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True},
{"properties": [('tweak', True), ('mode', 'ADD')]}),
("nla.select_box",
{"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True},
{"properties": [('tweak', True), ('mode', 'SUB')]}),
("nla.previewrange_set",
{"type": 'P', "value": 'PRESS', "ctrl": True, "alt": True},
None),
("nla.view_all",
{"type": 'HOME', "value": 'PRESS'},
None),
("nla.view_all",
{"type": 'NDOF_BUTTON_FIT', "value": 'PRESS'},
None),
("nla.view_selected",
{"type": 'NUMPAD_PERIOD', "value": 'PRESS'},
None),
("nla.view_frame",
{"type": 'NUMPAD_0', "value": 'PRESS'},
None),
("wm.call_menu_pie",
{"type": 'ACCENT_GRAVE', "value": 'PRESS'},
{"properties": [('name', 'NLA_MT_view_pie')]}),
("nla.actionclip_add",
{"type": 'A', "value": 'PRESS', "shift": True},
None),
("nla.transition_add",
{"type": 'T', "value": 'PRESS', "shift": True},
None),
("nla.soundclip_add",
{"type": 'K', "value": 'PRESS', "shift": True},
None),
("nla.meta_add",
{"type": 'G', "value": 'PRESS', "ctrl": True},
None),
("nla.meta_remove",
{"type": 'G', "value": 'PRESS', "ctrl": True, "alt": True},
None),
("nla.duplicate_linked_move",
{"type": 'D', "value": 'PRESS', "shift": True},
None),
("nla.duplicate_move",
{"type": 'D', "value": 'PRESS', "alt": True},
None),
("nla.make_single_user",
{"type": 'U', "value": 'PRESS'},
None),
("nla.delete",
{"type": 'X', "value": 'PRESS'},
None),
("nla.delete",
{"type": 'DEL', "value": 'PRESS'},
None),
("nla.split",
{"type": 'Y', "value": 'PRESS'},
None),
("nla.mute_toggle",
{"type": 'H', "value": 'PRESS'},
None),
("nla.swap",
{"type": 'F', "value": 'PRESS', "alt": True},
None),
("nla.move_up",
{"type": 'PAGE_UP', "value": 'PRESS', "repeat": True},
None),
("nla.move_down",
{"type": 'PAGE_DOWN', "value": 'PRESS', "repeat": True},
None),
("nla.apply_scale",
{"type": 'A', "value": 'PRESS', "ctrl": True},
None),
("nla.clear_scale",
{"type": 'S', "value": 'PRESS', "alt": True},
None),
("wm.call_menu_pie",
{"type": 'S', "value": 'PRESS', "shift": True},
{"properties": [('name', 'NLA_MT_snap_pie')]}),
("nla.fmodifier_add",
{"type": 'M', "value": 'PRESS', "shift": True, "ctrl": True},
None),
("transform.transform",
{"type": 'G', "value": 'PRESS'},
{"properties": [('mode', 'TRANSLATION')]}),
("transform.transform",
{"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'},
{"properties": [('mode', 'TRANSLATION')]}),
("transform.transform",
{"type": 'E', "value": 'PRESS'},
{"properties": [('mode', 'TIME_EXTEND')]}),
("transform.transform",
{"type": 'S', "value": 'PRESS'},
{"properties": [('mode', 'TIME_SCALE')]}),
("marker.add",
{"type": 'M', "value": 'PRESS'},
None),
("wm.call_menu",
{"type": 'RIGHTMOUSE', "value": 'PRESS'},
{"properties": [('name', 'NLA_MT_context_menu')]}),
("wm.call_menu",
{"type": 'APP', "value": 'PRESS'},
{"properties": [('name', 'NLA_MT_context_menu')]}),
("anim.change_frame",
{"type": 'RIGHTMOUSE', "value": 'PRESS', "shift": True},
{"properties": [('seq_solo_preview', True)]}),
],
},
),
("Frames",
{"space_type": 'EMPTY', "region_type": 'WINDOW'},
{"items":
@@ -4409,6 +4606,7 @@ keyconfig_data = \
("render.view_cancel", {"type": 'ESC', "value": 'PRESS', "repeat": True}, None),
("render.view_show", {"type": 'F11', "value": 'PRESS', "repeat": True}, None),
("render.play_rendered_anim", {"type": 'F11', "value": 'PRESS', "ctrl": True, "repeat": True}, None),
("screen.userpref_show", {"type": 'COMMA', "value": 'PRESS', "ctrl": True, "repeat": True}, None),
],
},
),
File diff suppressed because it is too large Load Diff
@@ -261,11 +261,9 @@ def km_screen(params):
def km_screen_editing(params):
items = []
keymap = (
"Screen Editing",
{"space_type": 'EMPTY', "region_type": 'WINDOW'},
{"items": items},
)
keymap = ("Screen Editing",
{"space_type": 'EMPTY', "region_type": 'WINDOW'},
{"items": items})
items.extend([
# Action zones
@@ -281,13 +279,10 @@ def km_screen_editing(params):
("screen.area_dupli", {"type": 'ACTIONZONE_AREA', "value": 'ANY', "shift": True}, None),
("screen.area_swap", {"type": 'ACTIONZONE_AREA', "value": 'ANY', "ctrl": True}, None),
("screen.region_scale", {"type": 'ACTIONZONE_REGION', "value": 'ANY'}, None),
("screen.quadview_size", {"type": 'ACTIONZONE_REGION_QUAD', "value": 'ANY'}, None),
("screen.screen_full_area", {"type": 'ACTIONZONE_FULLSCREEN', "value": 'ANY'},
{"properties": [("use_hide_panels", True)]}),
# Area move after action zones
("screen.area_move", {"type": 'LEFTMOUSE', "value": 'PRESS'}, None),
("screen.area_move", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True},
{"properties": [("snap", True)]}),
("screen.area_options", {"type": 'RIGHTMOUSE', "value": 'PRESS'}, None),
# Render
("render.render", {"type": 'RET', "value": 'PRESS', "ctrl": True},
@@ -1050,6 +1045,18 @@ def km_image(params):
("image.view_zoom", {"type": 'TRACKPADZOOM', "value": 'ANY'}, None),
("image.view_zoom", {"type": 'TRACKPADPAN', "value": 'ANY', "ctrl": True}, None),
("image.view_zoom_border", {"type": 'Z', "value": 'PRESS'}, None),
("image.view_zoom_ratio", {"type": 'F4', "value": 'PRESS', "ctrl": True},
{"properties": [("ratio", 8.0)]}),
("image.view_zoom_ratio", {"type": 'F3', "value": 'PRESS', "ctrl": True},
{"properties": [("ratio", 4.0)]}),
("image.view_zoom_ratio", {"type": 'F2', "value": 'PRESS', "ctrl": True},
{"properties": [("ratio", 2.0)]}),
("image.view_zoom_ratio", {"type": 'F4', "value": 'PRESS', "shift": True},
{"properties": [("ratio", 8.0)]}),
("image.view_zoom_ratio", {"type": 'F3', "value": 'PRESS', "shift": True},
{"properties": [("ratio", 4.0)]}),
("image.view_zoom_ratio", {"type": 'F2', "value": 'PRESS', "shift": True},
{"properties": [("ratio", 2.0)]}),
("image.view_zoom_ratio", {"type": 'F1', "value": 'PRESS'},
{"properties": [("ratio", 1.0)]}),
("image.view_zoom_ratio", {"type": 'F2', "value": 'PRESS'},
@@ -1058,22 +1065,6 @@ def km_image(params):
{"properties": [("ratio", 0.25)]}),
("image.view_zoom_ratio", {"type": 'F4', "value": 'PRESS'},
{"properties": [("ratio", 0.125)]}),
("image.view_zoom_ratio", {"type": 'F4', "value": 'PRESS', "ctrl": True},
{"properties": [("ratio", 8.0)]}),
("image.view_zoom_ratio", {"type": 'F3', "value": 'PRESS', "ctrl": True},
{"properties": [("ratio", 4.0)]}),
("image.view_zoom_ratio", {"type": 'F2', "value": 'PRESS', "ctrl": True},
{"properties": [("ratio", 2.0)]}),
("image.view_zoom_ratio", {"type": 'F1', "value": 'PRESS', "ctrl": True},
{"properties": [("ratio", 1.0)]}),
("image.view_zoom_ratio", {"type": 'F4', "value": 'PRESS', "shift": True},
{"properties": [("ratio", 8.0)]}),
("image.view_zoom_ratio", {"type": 'F3', "value": 'PRESS', "shift": True},
{"properties": [("ratio", 4.0)]}),
("image.view_zoom_ratio", {"type": 'F2', "value": 'PRESS', "shift": True},
{"properties": [("ratio", 2.0)]}),
("image.view_zoom_ratio", {"type": 'F1', "value": 'PRESS', "shift": True},
{"properties": [("ratio", 1.0)]}),
("image.change_frame", {"type": 'LEFTMOUSE', "value": 'PRESS'}, None),
("image.sample", {"type": 'LEFTMOUSE', "value": 'PRESS'}, None),
("image.curves_point_set", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True},
@@ -1187,7 +1178,8 @@ def km_node_editor(params):
{"properties": [("exit", True)]}),
("node.clipboard_copy", {"type": 'C', "value": 'PRESS', "ctrl": True}, None),
("node.clipboard_paste", {"type": 'V', "value": 'PRESS', "ctrl": True}, None),
("node.delete_copy_reconnect", {"type": 'X', "value": 'PRESS', "ctrl": True}, None),
("node.viewer_border", {"type": 'Z', "value": 'PRESS'}, None),
("node.clear_viewer_border", {"type": 'Z', "value": 'PRESS', "alt": True}, None),
("node.translate_attach", {"type": 'W', "value": 'PRESS'}, None),
("node.translate_attach", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, None),
("node.translate_attach", {"type": 'MIDDLEMOUSE', "value": 'CLICK_DRAG'}, None),
@@ -1799,31 +1791,6 @@ def km_sequencer(params):
items.extend([
("wm.search_menu", {"type": 'TAB', "value": 'PRESS'}, None),
*_template_items_animation(),
("sequencer.select", {"type": 'LEFTMOUSE', "value": 'CLICK'}, None),
("sequencer.select", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True},
{"properties": [("extend", True)]}),
("sequencer.select", {"type": 'LEFTMOUSE', "value": 'PRESS', "alt": True},
{"properties": [("linked_handle", True)]}),
("sequencer.select", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True, "alt": True},
{"properties": [("extend", True), ("linked_handle", True)]}),
("sequencer.select", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True},
{"properties": [("side_of_frame", True), ("linked_time", True)]}),
("sequencer.select", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True, "ctrl": True},
{"properties": [("extend", True), ("side_of_frame", True), ("linked_time", True)]}),
("sequencer.select_more", {"type": 'UP_ARROW', "value": 'PRESS', "repeat": True}, None),
("sequencer.select_less", {"type": 'DOWN_ARROW', "value": 'PRESS', "repeat": True}, None),
("sequencer.select_linked_pick", {"type": 'L', "value": 'PRESS', "ctrl": True},
{"properties": [("extend", False)]}),
("sequencer.select_linked_pick", {"type": 'RIGHT_BRACKET', "value": 'PRESS', "shift": True},
{"properties": [("extend", True)]}),
("sequencer.select_linked", {"type": 'L', "value": 'PRESS', "ctrl": True}, None),
("sequencer.select_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'},
{"properties": [("tweak", True), ("mode", 'SET')]}),
("sequencer.select_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True},
{"properties": [("tweak", True), ("mode", 'ADD')]}),
("sequencer.select_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True},
{"properties": [("tweak", True), ("mode", 'SUB')]}),
("sequencer.select_grouped", {"type": 'G', "value": 'PRESS', "shift": True}, None),
("sequencer.select_all", {"type": 'A', "value": 'PRESS', "ctrl": True}, {"properties": [("action", 'SELECT')]}),
("sequencer.select_all", {"type": 'A', "value": 'PRESS', "ctrl": True,
"shift": True}, {"properties": [("action", 'DESELECT')]}),
@@ -1840,15 +1807,12 @@ def km_sequencer(params):
{"properties": [("unselected", True)]}),
("sequencer.lock", {"type": 'L', "value": 'PRESS', "shift": True}, None),
("sequencer.unlock", {"type": 'L', "value": 'PRESS', "shift": True, "alt": True}, None),
("sequencer.connect", {"type": 'L', "value": 'PRESS', "ctrl": True, "alt": True},
{"properties": [("toggle", True)]}),
("sequencer.reassign_inputs", {"type": 'R', "value": 'PRESS'}, None),
("sequencer.reload", {"type": 'R', "value": 'PRESS', "ctrl": True}, None),
("sequencer.reload", {"type": 'R', "value": 'PRESS', "shift": True, "alt": True},
{"properties": [("adjust_length", True)]}),
("sequencer.offset_clear", {"type": 'O', "value": 'PRESS', "alt": True}, None),
("sequencer.duplicate_move", {"type": 'D', "value": 'PRESS', "ctrl": True}, None),
("sequencer.duplicate_move_linked", {"type": 'D', "value": 'PRESS', "ctrl": True, "alt": True}, None),
("sequencer.retiming_key_delete", {"type": 'BACK_SPACE', "value": 'PRESS'}, None),
("sequencer.retiming_key_delete", {"type": 'DEL', "value": 'PRESS'}, None),
("sequencer.delete", {"type": 'BACK_SPACE', "value": 'PRESS'}, None),
@@ -1880,7 +1844,7 @@ def km_sequencer(params):
("sequencer.gap_remove", {"type": 'BACK_SPACE', "value": 'PRESS', "shift": True},
{"properties": [("all", True)]}),
("sequencer.gap_insert", {"type": 'EQUAL', "value": 'PRESS', "shift": True}, None),
("sequencer.snap", {"type": 'X', "value": 'PRESS'}, {"properties": [("keep_offset", True)]}),
("sequencer.snap", {"type": 'X', "value": 'PRESS'}, None),
("sequencer.swap_inputs", {"type": 'S', "value": 'PRESS', "alt": True}, None),
*(
(("sequencer.split_multicam",
@@ -1889,22 +1853,41 @@ def km_sequencer(params):
for i in range(10)
)
),
("sequencer.select", {"type": 'LEFTMOUSE', "value": 'PRESS'}, None),
("sequencer.select", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True},
{"properties": [("extend", True)]}),
("sequencer.select", {"type": 'LEFTMOUSE', "value": 'PRESS', "alt": True},
{"properties": [("linked_handle", True)]}),
("sequencer.select", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True, "alt": True},
{"properties": [("extend", True), ("linked_handle", True)]}),
("sequencer.select", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True},
{"properties": [("side_of_frame", True), ("linked_time", True)]}),
("sequencer.select", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True, "ctrl": True},
{"properties": [("extend", True), ("side_of_frame", True), ("linked_time", True)]}),
("sequencer.select_more", {"type": 'UP_ARROW', "value": 'PRESS', "repeat": True}, None),
("sequencer.select_less", {"type": 'DOWN_ARROW', "value": 'PRESS', "repeat": True}, None),
("sequencer.select_linked_pick", {"type": 'L', "value": 'PRESS', "ctrl": True},
{"properties": [("extend", False)]}),
("sequencer.select_linked_pick", {"type": 'RIGHT_BRACKET', "value": 'PRESS', "shift": True},
{"properties": [("extend", True)]}),
("sequencer.select_linked", {"type": 'L', "value": 'PRESS', "ctrl": True}, None),
("sequencer.select_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'},
{"properties": [("tweak", True), ("mode", 'SET')]}),
("sequencer.select_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True},
{"properties": [("tweak", True), ("mode", 'ADD')]}),
("sequencer.select_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True},
{"properties": [("tweak", True), ("mode", 'SUB')]}),
("sequencer.select_grouped", {"type": 'G', "value": 'PRESS', "shift": True}, None),
("sequencer.slip", {"type": 'R', "value": 'PRESS'}, None),
("wm.context_set_int", {"type": 'O', "value": 'PRESS'},
{"properties": [("data_path", "scene.sequence_editor.overlay_frame"), ("value", 0)]}),
("transform.seq_slide", {"type": 'W', "value": 'PRESS'},
{"properties": [("view2d_edge_pan", True)]}),
("transform.seq_slide", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'},
{"properties": [("view2d_edge_pan", True), ("use_restore_handle_selection", True)]}),
("transform.seq_slide", {"type": 'MIDDLEMOUSE', "value": 'CLICK_DRAG'},
{"properties": [("view2d_edge_pan", True), ("use_restore_handle_selection", True)]}),
("transform.seq_slide", {"type": 'W', "value": 'PRESS'}, None),
("transform.seq_slide", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, None),
("transform.seq_slide", {"type": 'MIDDLEMOUSE', "value": 'CLICK_DRAG'}, None),
("transform.transform", {"type": 'E', "value": 'PRESS'},
{"properties": [("mode", 'TIME_EXTEND')]}),
("marker.add", {"type": 'M', "value": 'PRESS'}, None),
*_template_items_context_menu("SEQUENCER_MT_context_menu", {"type": 'RIGHTMOUSE', "value": 'PRESS'}),
op_menu("SEQUENCER_MT_retiming", {"type": 'I', "value": 'PRESS'}),
("sequencer.retiming_segment_speed_set", {"type": 'R', "value": 'PRESS', "shift": True}, None),
("sequencer.retiming_show", {"type": 'R', "value": 'PRESS', "ctrl": True, "shift": True}, None),
("marker.add", {"type": 'M', "value": 'PRESS'}, None),
# Tools
op_tool_cycle("builtin.select_box", {"type": 'Q', "value": 'PRESS'}),
op_tool_cycle("builtin.blade", {"type": 'B', "value": 'PRESS'}),
@@ -2720,16 +2703,15 @@ def _template_paint_radial_control(
items.extend([
("wm.radial_control", {"type": 'F', "value": 'PRESS', "ctrl": True, "alt": True},
radial_control_properties(
paint, "mask_texture_slot.angle", None, secondary_rotation=secondary_rotation, color=color,
)),
paint, "mask_texture_slot.angle", None, secondary_rotation=secondary_rotation, color=color)),
])
if weight:
items.extend([
("wm.radial_control", {"type": 'F', "value": 'PRESS', "ctrl": True, "alt": True},
radial_control_properties(
paint, "mask_texture_slot.angle", None, secondary_rotation=secondary_rotation, color=color,
)),
paint, "mask_texture_slot.angle", None, secondary_rotation=secondary_rotation, color=color)),
("wm.radial_control", {"type": 'F', "value": 'PRESS', "ctrl": True},
radial_control_properties(
paint, "weight", "use_unified_weight"))
@@ -2784,10 +2766,8 @@ def km_image_paint(params):
("wm.context_toggle", {"type": 'L', "value": 'PRESS'},
{"properties": [("data_path", "tool_settings.image_paint.brush.use_smooth_stroke")]}),
# Context menu.
*_template_items_context_panel(
"VIEW3D_PT_paint_texture_context_menu",
{"type": 'RIGHTMOUSE', "value": 'PRESS'},
),
*_template_items_context_panel("VIEW3D_PT_paint_texture_context_menu",
{"type": 'RIGHTMOUSE', "value": 'PRESS'}),
# Tools
op_tool_cycle("builtin.select_box", {"type": 'Q', "value": 'PRESS'}),
op_tool_cycle("builtin.annotate", {"type": 'D', "value": 'PRESS'}),
@@ -2813,10 +2793,9 @@ def km_vertex_paint(params):
("paint.vertex_paint", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True},
{"properties": [("mode", 'INVERT')]}),
("paint.vertex_paint", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True},
{"properties": [("brush_toggle", 'SMOOTH')]}),
{"properties": [("mode", 'SMOOTH')]}),
# Colors
("paint.sample_color", {"type": 'I', "value": 'PRESS'}, {"properties": [("merged", False)]}),
("paint.sample_color", {"type": 'I', "value": 'PRESS', "shift": True}, {"properties": [("merged", True)]}),
("paint.brush_colors_flip", {"type": 'X', "value": 'PRESS'}, None),
("paint.vertex_color_set", {"type": 'BACK_SPACE', "value": 'PRESS'}, None),
# Brush properties
@@ -2872,7 +2851,7 @@ def km_weight_paint(params):
("paint.weight_paint", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True},
{"properties": [("mode", 'INVERT')]}),
("paint.weight_paint", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True},
{"properties": [("brush_toggle", 'SMOOTH')]}),
{"properties": [("mode", 'SMOOTH')]}),
# Weight
("paint.weight_sample", {"type": 'I', "value": 'PRESS'}, None),
("paint.weight_sample_group", {"type": 'I', "value": 'PRESS', "alt": True}, None),
@@ -2929,11 +2908,7 @@ def km_sculpt(params):
("sculpt.brush_stroke", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True},
{"properties": [("mode", 'INVERT')]}),
("sculpt.brush_stroke", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True},
{"properties": [("brush_toggle", 'SMOOTH')]}),
("sculpt.brush_stroke", {"type": 'LEFTMOUSE', "value": 'PRESS', "alt": True},
{"properties": [("brush_toggle", 'MASK')]}),
("sculpt.brush_stroke", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True, "alt": True},
{"properties": [("mode", 'INVERT'), ("brush_toggle", 'MASK')]}),
{"properties": [("mode", 'SMOOTH')]}),
# Expand
("sculpt.expand", {"type": 'A', "value": 'PRESS', "shift": True},
{"properties": [
@@ -2998,8 +2973,7 @@ def km_sculpt(params):
("object.voxel_remesh", {"type": 'D', "value": 'PRESS', "ctrl": True}, None),
("object.voxel_size_edit", {"type": 'D', "value": 'PRESS', "shift": True, "ctrl": True}, None),
# Color
("paint.sample_color", {"type": 'I', "value": 'PRESS'}, {"properties": [("merged", False)]}),
("paint.sample_color", {"type": 'I', "value": 'PRESS', "shift": True}, {"properties": [("merged", True)]}),
("sculpt.sample_color", {"type": 'I', "value": 'PRESS'}, None),
("paint.brush_colors_flip", {"type": 'X', "value": 'PRESS'}, None),
# Brush properties
("brush.scale_size", {"type": 'LEFT_BRACKET', "value": 'PRESS', "repeat": True},
@@ -3054,18 +3028,18 @@ def km_mesh(params):
items.extend([
# Selection
("mesh.loop_select", {"type": 'LEFTMOUSE', "value": 'DOUBLE_CLICK'},
{"properties": [("extend", False), ("deselect", False), ("toggle", False)]}),
{"properties": [("extend", False), ("deselect", False), ("toggle", False), ("ring", False)]}),
("mesh.loop_select", {"type": 'LEFTMOUSE', "value": 'DOUBLE_CLICK', "shift": True},
{"properties": [("extend", True), ("deselect", False), ("toggle", False)]}),
{"properties": [("extend", True), ("deselect", False), ("toggle", False), ("ring", False)]}),
("mesh.loop_select", {"type": 'LEFTMOUSE', "value": 'DOUBLE_CLICK', "ctrl": True},
{"properties": [("extend", False), ("deselect", True), ("toggle", False)]}),
{"properties": [("extend", False), ("deselect", True), ("toggle", False), ("ring", False)]}),
("mesh.edgering_select", {"type": 'LEFTMOUSE', "value": 'DOUBLE_CLICK', "alt": True},
{"properties": [("extend", False), ("deselect", False), ("toggle", False)]}),
("mesh.edgering_select", {"type": 'LEFTMOUSE', "value": 'DOUBLE_CLICK', "alt": True, "shift": True},
{"properties": [("extend", True), ("deselect", False), ("toggle", False)]}),
("mesh.edgering_select", {"type": 'LEFTMOUSE', "value": 'DOUBLE_CLICK', "alt": True, "ctrl": True},
{"properties": [("extend", False), ("deselect", True), ("toggle", False)]}),
("mesh.loop_select", {"type": 'LEFTMOUSE', "value": 'DOUBLE_CLICK', "alt": True},
{"properties": [("extend", False), ("deselect", False), ("toggle", False), ("ring", True)]}),
("mesh.loop_select", {"type": 'LEFTMOUSE', "value": 'DOUBLE_CLICK', "alt": True, "shift": True},
{"properties": [("extend", True), ("deselect", False), ("toggle", False), ("ring", True)]}),
("mesh.loop_select", {"type": 'LEFTMOUSE', "value": 'DOUBLE_CLICK', "alt": True, "ctrl": True},
{"properties": [("extend", False), ("deselect", True), ("toggle", False), ("ring", True)]}),
("mesh.shortest_path_pick", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True, "ctrl": True},
{"properties": [("use_fill", False)]}),
@@ -3427,7 +3401,7 @@ def km_sculpt_curves(params):
("sculpt_curves.brush_stroke", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True},
{"properties": [("mode", 'INVERT')]}),
("sculpt_curves.brush_stroke", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True},
{"properties": [("brush_toggle", 'SMOOTH')]}),
{"properties": [("mode", 'SMOOTH')]}),
# Selection modes
("curves.set_selection_domain", {"type": 'ONE', "value": 'PRESS'}, {"properties": [("domain", 'POINT')]}),
("curves.set_selection_domain", {"type": 'TWO', "value": 'PRESS'}, {"properties": [("domain", 'CURVE')]}),
@@ -3846,10 +3820,10 @@ def generate_keymaps_impl(params=None):
# Tool System.
km_3d_view_tool_select(params),
km_3d_view_tool_interactive_add(params),
km_image_editor_tool_uv_select(params),
km_sequencer_editor_tool_select_preview(params),
km_sequencer_editor_tool_select_timeline(params),
km_3d_view_tool_interactive_add(params),
]