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\ D:\Amazon\00_external-files\
N:\1. CHARACTERS\remapping\ N:\1. CHARACTERS\remapping\
[Recent] [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\ C:\Users\Nathan\Downloads\
D:\2026-04-14 BCONNA2026_MARI\ 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": { "flamenco_version": {
"version": "3.8.2", "version": "3.8.5",
"shortversion": "3.8.2", "shortversion": "3.8.5",
"name": "Flamenco", "name": "Flamenco",
"git": "51a41a19" "git": "690bf6ce"
}, },
"shared_storage": { "shared_storage": {
"location": "R:\\mnt\\user\\Cerberus Max\\Flamenco", "location": "F:\\jobs",
"audience": "users", "audience": "users",
"platform": "windows", "platform": "windows",
"shaman_enabled": false "shaman_enabled": true
}, },
"job_types": { "job_types": {
"job_types": [ "job_types": [
@@ -283,197 +283,6 @@
"etag": "fe38e5ae9b3e640119d0d55fc7cbf4f8f15f2982", "etag": "fe38e5ae9b3e640119d0d55fc7cbf4f8f15f2982",
"description": "Create a viewport preview/playblast using Cycles OPTIX GPU rendering and generate a video file" "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", "name": "TalkingHeads Custom Render",
"label": "TalkingHeads Custom Render", "label": "TalkingHeads Custom Render",
@@ -517,18 +326,6 @@
"subtype": "dir_path", "subtype": "dir_path",
"visible": "submission" "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", "key": "use_submodule",
"type": "bool", "type": "bool",
@@ -555,7 +352,7 @@
"type": "string", "type": "string",
"description": "Final file path of where render output will be saved", "description": "Final file path of where render output will be saved",
"editable": false, "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" "subtype": "file_path"
}, },
{ {
@@ -596,7 +393,7 @@
"visible": "web" "visible": "web"
} }
], ],
"etag": "7773b03fd0d493d6d823d7d45bb3ef7f987384f3", "etag": "5f716e7195e55712daee20c237d95f748e2c3d1e",
"description": "Render a sequence of frames, and create a preview video file" "description": "Render a sequence of frames, and create a preview video file"
}, },
{ {
@@ -634,18 +431,6 @@
"subtype": "dir_path", "subtype": "dir_path",
"visible": "submission" "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", "key": "use_submodule",
"type": "bool", "type": "bool",
@@ -672,7 +457,7 @@
"type": "string", "type": "string",
"description": "Final file path of where render output will be saved", "description": "Final file path of where render output will be saved",
"editable": false, "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" "subtype": "file_path"
}, },
{ {
@@ -741,7 +526,7 @@
"visible": "hidden" "visible": "hidden"
} }
], ],
"etag": "5e49db0874b8ee1211f6dbc0a59d66137b6996b2", "etag": "758c56b672b0970896f30b85ee5165a5c096aec4",
"description": "OPTIX GPU rendering + extra checkboxes for some experimental features + extra CLI args for Blender" "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 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 Bunny2.blend
D:\2026-04-14 BCONNA2026_MARI\Sample Stanford Bunny1.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 D:\2025-12-28 ytshorts gigaproj\Blends\animations\FreddyEpstein\FreddyEpstein.blend
P:\250827_FestivalTurf\Blends\animations\02 Preparing the Base\06_Watering.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 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\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 2D_SPA.blend
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 2B.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_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_24d.blend
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\stills\iRSR_fluid_unload_24e.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_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_24a.blend
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\stills\iRSR_fluid_unload_4b.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 5B_insert.blend
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 5.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_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\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\SPA_noncon_OOG_short_animation 9a.blend
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_short_animation 9a_v3.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\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\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\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_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_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_2a.blend
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\stills\iP&S_2026_1b.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-19_P&S2025_anim_2a.blend
C:\Users\Nathan\AppData\Local\Temp\2026-03-27_15-21_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_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 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\06 Infill And Powerbrooming\Visual 6.blend
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\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_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 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", "id": "atomic_data_manager",
"name": "Atomic Data Manager", "name": "Atomic Data Manager",
"tagline": "Smart cleanup and inspection of Blender data-blocks", "tagline": "Smart cleanup and inspection of Blender data-blocks",
"version": "2.6.2", "version": "2.6.3",
"type": "add-on", "type": "add-on",
"maintainer": "RaincloudTheDragon", "maintainer": "RaincloudTheDragon",
"license": [ "license": [
@@ -70,9 +70,9 @@
"management", "management",
"cleanup" "cleanup"
], ],
"archive_url": "https://github.com/RaincloudTheDragon/atomic-data-manager/releases/download/v2.6.2/Atomic_Data_Manager.v2.6.2.zip", "archive_url": "https://github.com/RaincloudTheDragon/atomic-data-manager/releases/download/v2.6.3/Atomic_Data_Manager.v2.6.3.zip",
"archive_size": 121191, "archive_size": 122700,
"archive_hash": "sha256:1f4af882cdf73d3bb0b8cf1badc094b179bf9e982486ee516c45a6a2d478c05d" "archive_hash": "sha256:b444463a0443864077abcfe97332406e606a697d3567ac32f85843760da11372"
}, },
{ {
"schema_version": "1.0.0", "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 ## [v2.6.2] - 2026-04-06
### Fixes ### Fixes
@@ -2,7 +2,7 @@ schema_version = "1.0.0"
id = "atomic_data_manager" id = "atomic_data_manager"
name = "Atomic Data Manager" name = "Atomic Data Manager"
version = "2.6.2" version = "2.6.3"
type = "add-on" type = "add-on"
author = "RaincloudTheDragon" author = "RaincloudTheDragon"
maintainer = "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 import os
from .. import config from .. import config
from ..utils import compat from ..utils import compat
from . import ghost_users
# Data-block types we care about for dependency analysis # Data-block types we care about for dependency analysis
@@ -1197,6 +1198,8 @@ def analyze_unused_from_graph(graph, category, include_fake_users=None):
if category == 'materials': if category == 'materials':
try: try:
if datablock.users > 0 and not datablock.use_fake_user: if datablock.users > 0 and not datablock.use_fake_user:
if not ghost_users.material_blender_users_fully_cc3_ghosts(
datablock):
continue continue
except (AttributeError, RuntimeError, ReferenceError): except (AttributeError, RuntimeError, ReferenceError):
pass pass
@@ -28,6 +28,7 @@ from .. import config
from ..utils import compat from ..utils import compat
from ..utils import version from ..utils import version
from . import users from . import users
from . import ghost_users
def shallow(data): def shallow(data):
@@ -223,9 +224,9 @@ def materials_deep():
# check if material has a fake user or if ignore fake users # check if material has a fake user or if ignore fake users
# is enabled # is enabled
if not material.use_fake_user or config.include_fake_users: 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: if material.users > 0 and not material.use_fake_user:
# 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 continue
unused.append(material.name) unused.append(material.name)
else: else:
@@ -1,6 +1,7 @@
import bpy import bpy
from ..stats import unused from ..stats import unused
from ..stats import users from ..stats import users
from . import ghost_users
from .. import config from .. import config
from ..utils import compat from ..utils import compat
@@ -128,6 +129,7 @@ def _has_any_unused_materials():
if not users.material_all(material.name): if not users.material_all(material.name):
if not material.use_fake_user or config.include_fake_users: if not material.use_fake_user or config.include_fake_users:
if material.users > 0 and not material.use_fake_user: if material.users > 0 and not material.use_fake_user:
if not ghost_users.material_blender_users_fully_cc3_ghosts(material):
continue continue
return True return True
else: else:
+6 -5
View File
@@ -20,7 +20,7 @@
bl_info = { bl_info = {
"name": "Animation Layers", "name": "Animation Layers",
"author": "Tal Hershkovich", "author": "Tal Hershkovich",
"version" : (2, 3, 8), "version" : (2, 4, 0),
"blender" : (3, 2, 0), "blender" : (3, 2, 0),
"location": "View3D - Properties - Animation Panel", "location": "View3D - Properties - Animation Panel",
"description": "Simplifying the NLA editor into an animation layers UI and workflow", "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) bpy.utils.unregister_class(panel)
for panel in panels: for panel in panels:
#print (panel.bl_category)
panel.bl_category = context.preferences.addons[__name__].preferences.category panel.bl_category = context.preferences.addons[__name__].preferences.category
bpy.utils.register_class(panel) bpy.utils.register_class(panel)
@@ -176,7 +175,8 @@ class AnimLayersAddonPreferences(bpy.types.AddonPreferences):
items = [('ANIMLAYERS', 'Anim Layers Settings', 'Use Anim Layers properties to adjust custom frame range'), 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')]) ('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 #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'}, proceed: bpy.props.EnumProperty(name="Choose how to proceed", description="Select an option how to proceed with Anim Layers", override = {'LIBRARY_OVERRIDABLE'},
@@ -260,8 +260,9 @@ class AnimLayersAddonPreferences(bpy.types.AddonPreferences):
row.label(text = "Custom Frame Range Settings") row.label(text = "Custom Frame Range Settings")
row.prop(self, "frame_range_settings", text = '') 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) classes = (AnimLayersSettings, AnimLayersSceneSettings, AnimLayersItems, AnimLayersObjects)
+27 -22
View File
@@ -399,7 +399,7 @@ def get_fcu_layer_keyframes(obj, context, track):
keyframes = [] keyframes = []
# fcurves = get_fcurves(track.strips[0].action) # fcurves = get_fcurves(track.strips[0].action)
# fcurves = track.strips[0].action.fcurves # 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 #store all the keyframe locations from the fcurves of the layer
for fcu in fcurves: for fcu in fcurves:
if fcu.group is not None: if fcu.group is not None:
@@ -1546,6 +1546,8 @@ def strip_action_recalc(self, strip):
###################################################### HELPER FUNCTIONS ################################################ ###################################################### HELPER FUNCTIONS ################################################
def redraw_areas(areas): def redraw_areas(areas):
if not len(bpy.context.window_manager.windows):
return
for area in bpy.context.window_manager.windows[0].screen.areas: for area in bpy.context.window_manager.windows[0].screen.areas:
if area.type in areas: if area.type in areas:
area.tag_redraw() area.tag_redraw()
@@ -1621,7 +1623,7 @@ def select_layer_bones(self, context):
###################################################### CLASSES ########################################################### ###################################################### CLASSES ###########################################################
class SelectBonesInLayer(bpy.types.Operator): 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_idname = "anim.bones_in_layer"
bl_label = "Select layer bones" bl_label = "Select layer bones"
bl_icon = "BONE_DATA" bl_icon = "BONE_DATA"
@@ -1864,18 +1866,9 @@ class AutoCustomFrameRange(bpy.types.Operator):
# return {'CANCELLED'} # return {'CANCELLED'}
def restore(self, context): def restore(self, context):
if hasattr(subscriptions, 'frame_range'): print('restore')
frame_start, frame_end = subscriptions.frame_range subscriptions.frameend_update_callback()
else:
frame_start, frame_end = subscriptions.get_frame_range(context.scene)
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) subscriptions.subscriptions_add(context.scene)
def update_action_list(scene): def update_action_list(scene):
@@ -2596,9 +2589,7 @@ class RemoveFcurves(bpy.types.Operator):
if mod.type == 'CYCLES': if mod.type == 'CYCLES':
fcu.modifiers.remove(mod) fcu.modifiers.remove(mod)
fcu.update() fcu.update()
for area in context.window_manager.windows[0].screen.areas: redraw_areas(['GRAPH_EDITOR', 'VIEW_3D'])
if area.type == 'GRAPH_EDITOR' or area.type == 'VIEW_3D':
area.tag_redraw()
break break
return {'FINISHED'} return {'FINISHED'}
@@ -3301,25 +3292,36 @@ def copy_action(action):
return new_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''' '''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'): if not hasattr(action, 'slots'):
return None 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: for slot in action.slots:
if slot.target_id_type != data_type: if slot.target_id_type != data_type:
continue 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(): if data_type == 'KEY' and obj.data.shape_keys in slot.users():
return slot return slot
elif obj in slot.users(): elif obj in slot.users():
return slot 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'): if hasattr(action, 'layers'):
slot = get_obj_slot(obj, action, data_type) 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 action.fcurves
return [] 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 '''Getting the container of the fcurves, either the action or channelbag
Using this when adding a new group to the action''' 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'): if hasattr(action, 'layers'):
slot = get_obj_slot(obj, action, data_type) slot = get_obj_slot(obj, action, data_type)
channelbag = None channelbag = None
@@ -1,16 +1,9 @@
{ {
"last_check": "2026-04-21 12:45:18.211769", "last_check": "2026-04-21 12:47:39.126086",
"backup_date": "March-27-2026", "backup_date": "April-21-2026",
"update_ready": true, "update_ready": false,
"ignore": false, "ignore": false,
"just_restored": false, "just_restored": false,
"just_updated": false, "just_updated": false,
"version_text": { "version_text": {}
"link": "https://gitlab.com/api/v4/projects/22294607/repository/archive.zip?sha=dddd6932039b8a3e5fae3ce2de957f21a5942c84",
"version": [
2,
4,
0
]
}
} }
@@ -20,7 +20,7 @@
bl_info = { bl_info = {
"name": "Animation Layers", "name": "Animation Layers",
"author": "Tal Hershkovich", "author": "Tal Hershkovich",
"version" : (2, 3, 7), "version" : (2, 3, 8),
"blender" : (3, 2, 0), "blender" : (3, 2, 0),
"location": "View3D - Properties - Animation Panel", "location": "View3D - Properties - Animation Panel",
"description": "Simplifying the NLA editor into an animation layers UI and workflow", "description": "Simplifying the NLA editor into an animation layers UI and workflow",
@@ -271,7 +271,6 @@ def register_layers(obj, nla_tracks):
continue continue
strip = track.strips[0] strip = track.strips[0]
use_animated_influence(strip) use_animated_influence(strip)
strip.influence = 1
#updating the ui list with the nla track names #updating the ui list with the nla track names
def visible_layers(obj, nla_tracks): def visible_layers(obj, nla_tracks):
@@ -1298,7 +1297,6 @@ def load_action(self, context):
subscriptions.frameend_update_callback() subscriptions.frameend_update_callback()
strip.use_sync_length = False strip.use_sync_length = False
use_animated_influence(strip) use_animated_influence(strip)
strip.influence = 1
return return
subscriptions.subscriptions_remove() subscriptions.subscriptions_remove()
strip = track.strips[0] 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.blend_type = blend_type
new_strip.use_sync_length = False new_strip.use_sync_length = False
use_animated_influence(new_strip) use_animated_influence(new_strip)
new_strip.influence = 1
return new_track return new_track
@@ -1,16 +1,16 @@
{ {
"last_check": "2026-03-27 11:33:07.238724", "last_check": "2026-04-21 12:47:39.126086",
"backup_date": "March-20-2026", "backup_date": "March-27-2026",
"update_ready": true, "update_ready": true,
"ignore": false, "ignore": false,
"just_restored": false, "just_restored": false,
"just_updated": false, "just_updated": false,
"version_text": { "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": [ "version": [
2, 2,
3, 4,
8 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 baked_action = track.strips[0].action
clean_no_user_slots(baked_action) clean_no_user_slots(baked_action)
#create the baked fcurve #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_channelbag = anim_layers.get_channelbag(obj, baked_action)
baked_fcurves = baked_channelbag.fcurves baked_fcurves = baked_channelbag.fcurves
@@ -73,7 +73,6 @@ def animlayers_frame(scene, context):
scene['framerange_preview'] = scene.use_preview_range scene['framerange_preview'] = scene.use_preview_range
frameend_update_callback() frameend_update_callback()
return return
frame_start, frame_end = bake_ops.frame_start_end(scene) frame_start, frame_end = bake_ops.frame_start_end(scene)
# frame_start, frame_end = get_frame_range(scene) # frame_start, frame_end = get_frame_range(scene)
reset_subscription = False reset_subscription = False
@@ -106,7 +105,6 @@ def animlayers_frame(scene, context):
for i, track in enumerate(nla_tracks): for i, track in enumerate(nla_tracks):
if len(track.strips) != 1: if len(track.strips) != 1:
continue continue
#checks if the layer has a custom frame range #checks if the layer has a custom frame range
layer = obj.Anim_Layers[i] layer = obj.Anim_Layers[i]
if layer.custom_frame_range: if layer.custom_frame_range:
@@ -127,6 +125,7 @@ def animlayers_frame(scene, context):
if strip.frame_start < 0: if strip.frame_start < 0:
strip.frame_start = 0 strip.frame_start = 0
anim_layers.update_action_frame_range(0, frame_end, layer, strip) 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) anim_layers.update_action_frame_range(strip.frame_start, current + 10.0, layer, strip)
strip.frame_end = current + 10.0 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: if obj.als.layer_index > len(obj.Anim_Layers)-1:
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 #update new layer with strip settings
frame_start, frame_end = get_frame_range(bpy.context.scene) frame_start, frame_end = get_frame_range(bpy.context.scene)
@@ -243,7 +245,6 @@ def track_layer_synchronization(obj, nla_tracks):
continue continue
if (strip.frame_start, strip.frame_end) != (frame_start, frame_end): if (strip.frame_start, strip.frame_end) != (frame_start, frame_end):
subscriptions_remove() 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') bpy.ops.anim.custom_frame_range_warning('INVOKE_DEFAULT')
return return
@@ -311,11 +312,16 @@ def sync_frame_range(scene, track, layer):
return return
#Turn on custom frame range if the current strip is not following the scene frame range #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)): if (round(strip.frame_start, 2), round(strip.frame_end, 2)) != (round(frame_start, 2), round(frame_end, 2)):
if bpy.context.preferences.addons[__package__].preferences.auto_custom_range:
subscriptions_remove() subscriptions_remove()
# print('315 custom frame range') # print('321 custom frame range')
bpy.ops.anim.custom_frame_range_warning('INVOKE_DEFAULT') bpy.ops.anim.custom_frame_range_warning('INVOKE_DEFAULT')
return return
else:
frameend_update_callback()
return
def sync_strip_range(scene): def sync_strip_range(scene):
'''Checking all the strips if a value was changed in the nla (not including UI changes) '''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)): if (strip_frame_start, round(strip_frame_end, 2)) != (frame_start, float(frame_end)):
subscriptions_remove() subscriptions_remove()
# print('357 custom_frame_range_warning ') # 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') bpy.ops.anim.custom_frame_range_warning('INVOKE_DEFAULT')
return 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 # Subscribing to preview frame end since it's not registering in the depsgraph
subscribe_preview_end = scene.path_resolve("frame_preview_end", False) subscribe_preview_end = scene.path_resolve("frame_preview_end", False)
subscribe_use_preview = scene.path_resolve("use_preview_range", 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]: for subscribe in [subscribe_preview_end, subscribe_use_preview]:
bpy.msgbus.subscribe_rna( bpy.msgbus.subscribe_rna(
+1 -1
View File
@@ -5,7 +5,7 @@
bl_info = { bl_info = {
"name": "Flamenco", "name": "Flamenco",
"author": "Sybren A. Stüvel", "author": "Sybren A. Stüvel",
"version": (3, 8, 2), "version": (3, 8, 5),
"blender": (3, 1, 0), "blender": (3, 1, 0),
"description": "Flamenco client for Blender.", "description": "Flamenco client for Blender.",
"location": "Output Properties > Flamenco", "location": "Output Properties > Flamenco",
+1 -1
View File
@@ -10,7 +10,7 @@
""" """
__version__ = "3.8.2" __version__ = "3.8.5"
# import ApiClient # import ApiClient
from flamenco.manager.api_client import ApiClient from flamenco.manager.api_client import ApiClient
@@ -76,7 +76,7 @@ class ApiClient(object):
self.default_headers[header_name] = header_value self.default_headers[header_name] = header_value
self.cookie = cookie self.cookie = cookie
# Set default User-Agent. # 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): def __enter__(self):
return self return self
@@ -404,7 +404,7 @@ conf = flamenco.manager.Configuration(
"OS: {env}\n"\ "OS: {env}\n"\
"Python Version: {pyversion}\n"\ "Python Version: {pyversion}\n"\
"Version of the API: 1.0.0\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) format(env=sys.platform, pyversion=sys.version)
def get_host_settings(self): 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: The `flamenco.manager` package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.0.0 - API version: 1.0.0
- Package version: 3.8.2 - Package version: 3.8.5
- Build package: org.openapitools.codegen.languages.PythonClientCodegen - Build package: org.openapitools.codegen.languages.PythonClientCodegen
For more information, please visit [https://flamenco.blender.org/](https://flamenco.blender.org/) For more information, please visit [https://flamenco.blender.org/](https://flamenco.blender.org/)
+302 -7
View File
@@ -3,6 +3,7 @@
import datetime import datetime
import logging import logging
import os
import time import time
from pathlib import Path, PurePosixPath from pathlib import Path, PurePosixPath
from typing import Optional, TYPE_CHECKING from typing import Optional, TYPE_CHECKING
@@ -363,11 +364,132 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
return filepath 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: def _submit_files(self, context: bpy.types.Context, blendfile: Path) -> bool:
"""Ensure that the files are somewhere in the shared storage. """Ensure that the files are somewhere in the shared storage.
Returns True if a packing thread has been started, and False otherwise. 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 from .bat import interface as bat_interface
@@ -405,6 +527,179 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
return True 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( def _bat_pack_filesystem(
self, context: bpy.types.Context, blendfile: Path self, context: bpy.types.Context, blendfile: Path
) -> PurePosixPath: ) -> PurePosixPath:
@@ -414,9 +709,7 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
""" """
from .bat import interface as bat_interface from .bat import interface as bat_interface
# Get project path from addon preferences. project_path = self._bat_project_path(context, blendfile)
prefs = preferences.get(context)
project_path: Path = prefs.project_root()
project_path = bpathlib.make_absolute(Path(bpy.path.abspath(str(project_path)))) project_path = bpathlib.make_absolute(Path(bpy.path.abspath(str(project_path))))
if not project_path.exists(): if not project_path.exists():
@@ -442,7 +735,9 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
project=project_path, project=project_path,
target=str(pack_target_dir), target=str(pack_target_dir),
exclusion_filter="", # TODO: get from GUI. 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()) 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 assert self.job is not None
self.log.info("Sending BAT pack to Shaman") self.log.info("Sending BAT pack to Shaman")
prefs = preferences.get(context) project_path = self._bat_project_path(context, blendfile)
project_path: Path = prefs.project_root()
self.packthread = bat_interface.copy( self.packthread = bat_interface.copy(
base_blendfile=blendfile, base_blendfile=blendfile,
project=project_path, project=project_path,
target="/", # Target directory irrelevant for Shaman transfers. target="/", # Target directory irrelevant for Shaman transfers.
exclusion_filter="", # TODO: get from GUI. 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_class=bat_shaman.Packer,
packer_kwargs=dict( packer_kwargs=dict(
api_client=self.get_api_client(context), api_client=self.get_api_client(context),
+14
View File
@@ -65,6 +65,16 @@ class FlamencoPreferences(bpy.types.AddonPreferences):
items=_project_finder_enum_items, 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 # Property that gets its value from the above _job_storage, and cannot be
# set. This makes it read-only in the GUI. # set. This makes it read-only in the GUI.
job_storage_for_gui: bpy.props.StringProperty( # type: ignore job_storage_for_gui: bpy.props.StringProperty( # type: ignore
@@ -118,6 +128,10 @@ class FlamencoPreferences(bpy.types.AddonPreferences):
else: else:
text_row(col, str(project_root)) 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: def project_root(self) -> Path:
"""Use the configured project finder to find the project root directory.""" """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. 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 **Original Author:** Jesse Doyle | Form Affinity
**License:** GPL-3.0-or-later **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 ## 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 ### Version 1.6 / Extension 1.7.0
- **Extension Format:** Complete port to modern Blender Extension platform - **Extension Format:** Complete port to modern Blender Extension platform
- **Multi-Version Support:** Compatible with Blender 4.2, 4.5 LTS, and 5.0+ - **Multi-Version Support:** Compatible with Blender 4.2, 4.5 LTS, and 5.0+
@@ -2,7 +2,7 @@
Maya Config Pro - Blender Extension Maya Config Pro - Blender Extension
A Maya-like configuration, shortcuts, and UI elements for Blender. A Maya-like configuration, shortcuts, and UI elements for Blender.
Version: 1.7.0 Version: 1.8.0
Author: Form Affinity (Jesse Doyle) Author: Form Affinity (Jesse Doyle)
""" """
@@ -12,7 +12,7 @@ Author: Form Affinity (Jesse Doyle)
bl_info = { bl_info = {
"name": "Maya Config Pro", "name": "Maya Config Pro",
"author": "Form Affinity (Jesse Doyle)", "author": "Form Affinity (Jesse Doyle)",
"version": (1, 7, 0), "version": (1, 8, 0),
"blender": (4, 2, 0), "blender": (4, 2, 0),
"location": "View3D > Sidebar > Maya Config", "location": "View3D > Sidebar > Maya Config",
"description": "Maya-like configuration, shortcuts, UI panels, and keymap presets", "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. # not only blender_manifest.toml id — otherwise AddonPreferences.draw never appears.
_ADDON_MODULE = __package__ or "form_affinity_maya_config_pro" _ADDON_MODULE = __package__ or "form_affinity_maya_config_pro"
# Import all submodules
from .panels import ( from .panels import (
panel_pro, panel_pro,
panel_sidebar, panel_sidebar,
@@ -46,12 +45,34 @@ from .ops import (
object_select, object_select,
mesh_select_mode, mesh_select_mode,
delete_ops, delete_ops,
view_frame,
) )
from .keyconfigs import fa_hotkeys from .keyconfigs import fa_hotkeys
from .utils import deploy as deploy_util from .utils import deploy as deploy_util
from .ops import deploy_ops 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 # Addon preferences
class MCP_AddonPreferences(bpy.types.AddonPreferences): class MCP_AddonPreferences(bpy.types.AddonPreferences):
"""Maya Config Pro preferences""" """Maya Config Pro preferences"""
@@ -59,8 +80,24 @@ class MCP_AddonPreferences(bpy.types.AddonPreferences):
auto_load_keymap: bpy.props.BoolProperty( auto_load_keymap: bpy.props.BoolProperty(
name="Auto-load FA Keymap", name="Auto-load FA Keymap",
description="Automatically load the Maya-like keymap when Blender starts", description=(
default=True "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): def draw(self, context):
@@ -100,13 +137,14 @@ class MCP_AddonPreferences(bpy.types.AddonPreferences):
col.separator() col.separator()
col.label( col.label(
text="Copies Blender's keymaps (and MCP fa_hotkeys) into your Scripts folder, " text="Installs MCP fa_hotkeys into your keyconfig folder; default/Industry use "
"and optional startup.blend into your config. Needed for Pro panel keymap buttons.", "this Blender's built-in keymaps. Optional startup.blend deploys to your config.",
icon="INFO", icon="INFO",
) )
layout.separator() layout.separator()
layout.prop(self, "auto_load_keymap") layout.prop(self, "auto_load_keymap")
layout.prop(self, "viewport_anim_shelf")
# List of all classes in this module # List of all classes in this module
@@ -130,40 +168,32 @@ submodules = [
object_select, object_select,
mesh_select_mode, mesh_select_mode,
delete_ops, delete_ops,
view_frame,
fa_hotkeys, fa_hotkeys,
] ]
def register(): def register():
"""Register all classes and submodules""" """Register all classes and submodules"""
# Register main classes fa_hotkeys.set_addon_id(_ADDON_MODULE)
for cls in classes: for cls in classes:
compat.safe_register_class(cls) compat.safe_register_class(cls)
# Register all submodules
for module in submodules: for module in submodules:
try: try:
module.register() module.register()
except Exception as e: except Exception as e:
print(f"Warning: Failed to register {module.__name__}: {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: try:
fa_hotkeys.register_keymaps() fa_hotkeys.sync_addon_keymaps()
except Exception as e: except Exception as e:
print(f"Warning: Failed to register keymaps: {e}") print(f"Warning: Failed to sync keymaps: {e}")
def unregister(): def unregister():
"""Unregister all classes and submodules""" """Unregister all classes and submodules"""
# Unregister keymaps first # Unregister all submodules (in reverse order; fa_hotkeys clears addon keymaps + listeners)
try:
fa_hotkeys.unregister_keymaps()
except Exception as e:
print(f"Warning: Failed to unregister keymaps: {e}")
# Unregister all submodules (in reverse order)
for module in reversed(submodules): for module in reversed(submodules):
try: try:
module.unregister() module.unregister()
@@ -3,7 +3,7 @@ schema_version = "1.0.0"
id = "form_affinity_maya_config_pro" id = "form_affinity_maya_config_pro"
name = "Maya Config Pro" name = "Maya Config Pro"
tagline = "Maya-like configuration, shortcuts, and UI elements for Blender" tagline = "Maya-like configuration, shortcuts, and UI elements for Blender"
version = "1.7.0" version = "1.8.0"
type = "add-on" type = "add-on"
maintainer = "Form Affinity" maintainer = "Form Affinity"
@@ -2,7 +2,10 @@
Maya Config Pro - Keymap Configuration Maya Config Pro - Keymap Configuration
Maya-like keymap preset for Blender. 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 import bpy
@@ -11,27 +14,59 @@ from ..utils import compat
# Import the keyconfig data # Import the keyconfig data
from . import fa_hotkeys_data from . import fa_hotkeys_data
# Addon keymaps # Addon keymaps: (keymap, keymap_item) and created keymaps to remove for clean sync
addon_keymaps = [] 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(): 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 wm = bpy.context.window_manager
kc = wm.keyconfigs.addon kc = wm.keyconfigs.addon
if not kc: if not kc:
return return
# Try to load the keyconfig data
try: 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: 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) km = kc.keymaps.new(name=km_name, **km_args)
addon_keymap_objects.append(km)
# Add keymap items
if "items" in km_content: if "items" in km_content:
for item in km_content["items"]: for item in km_content["items"]:
if len(item) >= 2: if len(item) >= 2:
@@ -39,28 +74,25 @@ def register_keymaps():
keymap_item = item[1] keymap_item = item[1]
props = item[2] if len(item) > 2 else None props = item[2] if len(item) > 2 else None
# Create keymap item
kmi = km.keymap_items.new( kmi = km.keymap_items.new(
idname, idname,
type=keymap_item.get("type", 'A'), type=keymap_item.get("type", "A"),
value=keymap_item.get("value", 'PRESS'), value=keymap_item.get("value", "PRESS"),
any=keymap_item.get("any", False), any=keymap_item.get("any", False),
shift=keymap_item.get("shift", 0), shift=keymap_item.get("shift", 0),
ctrl=keymap_item.get("ctrl", 0), ctrl=keymap_item.get("ctrl", 0),
alt=keymap_item.get("alt", 0), alt=keymap_item.get("alt", 0),
oskey=keymap_item.get("oskey", False), 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), 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: if props and "properties" in props:
for prop_name, prop_value in props["properties"]: for prop_name, prop_value in props["properties"]:
if hasattr(kmi.properties, prop_name): if hasattr(kmi.properties, prop_name):
setattr(kmi.properties, prop_name, prop_value) setattr(kmi.properties, prop_name, prop_value)
# Handle active state
if props and "active" in props: if props and "active" in props:
kmi.active = props["active"] kmi.active = props["active"]
@@ -70,15 +102,15 @@ def register_keymaps():
def unregister_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 wm = bpy.context.window_manager
kc = wm.keyconfigs.addon kc = wm.keyconfigs.addon
if not kc: if not kc:
return return
# Remove all registered keymap items for km, kmi in list(addon_keymaps):
for km, kmi in addon_keymaps:
try: try:
km.keymap_items.remove(kmi) km.keymap_items.remove(kmi)
except Exception: except Exception:
@@ -86,23 +118,85 @@ def unregister_keymaps():
addon_keymaps.clear() 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 to register (none needed for keyconfig)
_classes = [] _classes = []
def register(): def register():
"""Register keymap module"""
for cls in _classes: for cls in _classes:
compat.safe_register_class(cls) compat.safe_register_class(cls)
register_keymap_listeners()
# Don't auto-register keymaps here - let preferences control this
# register_keymaps()
def unregister(): def unregister():
"""Unregister keymap module""" unregister_keymap_listeners()
unregister_keymaps() unregister_keymaps()
for cls in reversed(_classes): for cls in reversed(_classes):
compat.safe_unregister_class(cls) compat.safe_unregister_class(cls)
@@ -1,4 +1,4 @@
keyconfig_version = (5, 0, 120) keyconfig_version = (5, 0, 121)
keyconfig_data = \ keyconfig_data = \
[("3D View", [("3D View",
{"space_type": 'VIEW_3D', "region_type": 'WINDOW'}, {"space_type": 'VIEW_3D', "region_type": 'WINDOW'},
@@ -107,15 +107,7 @@ keyconfig_data = \
], ],
}, },
), ),
("view3d.view_center_camera", {"type": 'HOME', "value": 'PRESS', "repeat": True}, None), ("mcp.view_frame_home", {"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),
],
},
),
("view3d.view_all", ("view3d.view_all",
{"type": 'HOME', "value": 'PRESS', "ctrl": True, "repeat": True}, {"type": 'HOME', "value": 'PRESS', "ctrl": True, "repeat": True},
{"properties": {"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", ("Frames",
{"space_type": 'EMPTY', "region_type": 'WINDOW'}, {"space_type": 'EMPTY', "region_type": 'WINDOW'},
{"items": {"items":
@@ -4409,6 +4606,7 @@ keyconfig_data = \
("render.view_cancel", {"type": 'ESC', "value": 'PRESS', "repeat": True}, None), ("render.view_cancel", {"type": 'ESC', "value": 'PRESS', "repeat": True}, None),
("render.view_show", {"type": 'F11', "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), ("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 import bpy
from ..keyconfigs import fa_hotkeys
from ..utils import compat from ..utils import compat
from ..utils import deploy as deploy_util 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_idname = "mcp.deploy_keymap_presets"
bl_label = "Deploy Keymap Presets" bl_label = "Deploy Keymap Presets"
bl_description = ( bl_description = (
"Copy Blender default keymaps (and MCP fa_hotkeys) into your user scripts folder " "Install the MCP fa_hotkeys keymap into your user keyconfig folder so FA Hotkeys works"
"so keymap switching buttons work"
) )
bl_options = {"REGISTER", "INTERNAL"} bl_options = {"REGISTER", "INTERNAL"}
@@ -23,6 +23,10 @@ class MCP_OT_DeployKeymapPresets(bpy.types.Operator):
ok, msg = deploy_util.deploy_keymap_presets() ok, msg = deploy_util.deploy_keymap_presets()
if ok: if ok:
self.report({"INFO"}, msg.replace("\n", " - ")) self.report({"INFO"}, msg.replace("\n", " - "))
try:
fa_hotkeys.sync_addon_keymaps()
except Exception:
pass
else: else:
self.report({"ERROR"}, msg) self.report({"ERROR"}, msg)
return {"FINISHED"} if ok else {"CANCELLED"} return {"FINISHED"} if ok else {"CANCELLED"}
@@ -62,25 +66,28 @@ class MCP_OT_ActivateKeymapPreset(bpy.types.Operator):
) )
def execute(self, context): 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 = { names = {
"BLENDER": "Blender.py", "BLENDER": "Blender.py",
"FA_HOTKEYS": "fa_hotkeys.py", "FA_HOTKEYS": "fa_hotkeys.py",
"INDUSTRY": "Industry_Compatible.py", "INDUSTRY": "Industry_Compatible.py",
} }
filename = names[self.preset] filename = names[self.preset]
filepath = os.path.join(deploy_util.user_keyconfig_dir(), filename)
if not os.path.isfile(filepath): if self.preset == "FA_HOTKEYS":
if not deploy_util.is_keyconfig_deployed():
self.report( self.report(
{"ERROR"}, {"ERROR"},
f"Missing {filename}. Open Preferences and run Deploy keymap presets again.", "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"} return {"CANCELLED"}
@@ -97,6 +104,11 @@ class MCP_OT_ActivateKeymapPreset(bpy.types.Operator):
elif self.preset in {"BLENDER", "INDUSTRY"}: elif self.preset in {"BLENDER", "INDUSTRY"}:
inputs.view_zoom_axis = "VERTICAL" inputs.view_zoom_axis = "VERTICAL"
try:
fa_hotkeys.sync_addon_keymaps()
except Exception:
pass
return {"FINISHED"} 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."""
Maya Config Pro - Panels Package
UI panels for the extension.
"""
import sys from . import panel_pro
import importlib.util from . import panel_sidebar
import os from . import panel_shelf
from . import panel_animation
# Module cache __all__ = (
_module_cache = {} "panel_pro",
"panel_sidebar",
"panel_shelf",
def _load_module(name): "panel_animation",
"""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
def register(): def register():
"""Register all panels""" """Register panels (if this package is ``register()``'d standalone). Viewport
# Register shelf header first (replaces PROPERTIES panels) 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: try:
shelf_header = _load_module("shelf_header") mod.register()
shelf_header.register()
except Exception as e: except Exception as e:
print(f"MCP: Failed to register shelf header: {e}") print(f"MCP: Failed to register {mod.__name__}: {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}")
def unregister(): def unregister():
"""Unregister all panels""" for mod in (panel_animation, panel_shelf, panel_sidebar, panel_pro):
# Unregister in reverse order
for name in ["viewport_shelf", "panel_animation", "panel_shelf", "panel_sidebar", "panel_pro", "shelf_header"]:
try: try:
if name in _module_cache: mod.unregister()
_module_cache[name].unregister()
except Exception as e: 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 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 # Animation Shelf Panel
# ------------------------------------------------------------------------ # ------------------------------------------------------------------------
@@ -22,24 +83,52 @@ class MCP_PT_AnimationShelf(bpy.types.Panel):
draw_type = "animation" draw_type = "animation"
def draw(self, context): def draw(self, context):
layout = self.layout draw_animation_shelf_row(self.layout, scale_x=2.0, scale_y=1.0)
obj = context.object
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") # 3D View — Tool header (native Layout, not POST_PIXEL)
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") class MCP_PT_AnimToolHeader(bpy.types.Panel):
row.operator('mcp.dope_sheet_ani', text="", icon="ACTION") """Larger icon row in the 3D View tool header (below the main top bar)."""
row.operator('mcp.drivers_ani', text="", icon="DRIVER") bl_idname = "MCP_PT_AnimToolHeader"
row.operator('anim.keyframe_insert_menu', text="", icon="DECORATE_ANIMATE") 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 = [ _classes = [
# Panel # Panel
MCP_PT_AnimationShelf, MCP_PT_AnimationShelf,
MCP_PT_AnimToolHeader,
MCP_PT_AnimNPanel,
# Animation Operators # Animation Operators
MCP_OT_PropertiesEditorAni, MCP_OT_PropertiesEditorAni,
MCP_OT_LinkRigAni, 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(): def register():
for cls in _classes: for cls in _classes:
compat.safe_register_class(cls) 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(): def unregister():
global _th_once
_th_once = False
for cls in reversed(_classes): for cls in reversed(_classes):
compat.safe_unregister_class(cls) 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 = \ keyconfig_data = \
[("3D View", [("3D View",
{"space_type": 'VIEW_3D', "region_type": 'WINDOW'}, {"space_type": 'VIEW_3D', "region_type": 'WINDOW'},
@@ -107,15 +107,7 @@ keyconfig_data = \
], ],
}, },
), ),
("view3d.view_center_camera", {"type": 'HOME', "value": 'PRESS', "repeat": True}, None), ("mcp.view_frame_home", {"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),
],
},
),
("view3d.view_all", ("view3d.view_all",
{"type": 'HOME', "value": 'PRESS', "ctrl": True, "repeat": True}, {"type": 'HOME', "value": 'PRESS', "ctrl": True, "repeat": True},
{"properties": {"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", ("Frames",
{"space_type": 'EMPTY', "region_type": 'WINDOW'}, {"space_type": 'EMPTY', "region_type": 'WINDOW'},
{"items": {"items":
@@ -4409,6 +4606,7 @@ keyconfig_data = \
("render.view_cancel", {"type": 'ESC', "value": 'PRESS', "repeat": True}, None), ("render.view_cancel", {"type": 'ESC', "value": 'PRESS', "repeat": True}, None),
("render.view_show", {"type": 'F11', "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), ("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: def is_keyconfig_deployed() -> bool:
kc = user_keyconfig_dir() """True once MCP has copied fa_hotkeys.py into the user keyconfig folder."""
if not os.path.isdir(os.path.join(kc, "keymap_data")): return os.path.isfile(os.path.join(user_keyconfig_dir(), "fa_hotkeys.py"))
return False
for name in ("Blender.py", "Industry_Compatible.py", "fa_hotkeys.py"):
if not os.path.isfile(os.path.join(kc, name)): def official_keyconfig_filepath(name: str) -> str | None:
return False """
return True 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: def is_startup_bundle_available() -> bool:
@@ -64,30 +71,22 @@ def is_startup_bundle_available() -> bool:
def deploy_keymap_presets() -> tuple[bool, str]: 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() dst = user_keyconfig_dir()
os.makedirs(dst, exist_ok=True) 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") portable_fa = os.path.join(bundled_keyconfig_dir(), "fa_hotkeys.py")
if os.path.isfile(portable_fa): if not os.path.isfile(portable_fa):
return False, "Add-on is missing portable/scripts/presets/keyconfig/fa_hotkeys.py."
try: try:
shutil.copy2(portable_fa, os.path.join(dst, "fa_hotkeys.py")) shutil.copy2(portable_fa, os.path.join(dst, "fa_hotkeys.py"))
except OSError as e: except OSError as e:
return False, f"Could not install fa_hotkeys.py: {e}" return False, f"Could not install fa_hotkeys.py: {e}"
else:
return False, "Add-on is missing portable/scripts/presets/keyconfig/fa_hotkeys.py."
return True, f"Keymap presets deployed to:\n{dst}" return True, f"fa_hotkeys keymap installed to:\n{dst}"
def deploy_startup_blend() -> tuple[bool, str]: 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 = \ keyconfig_data = \
[("3D View", [("3D View",
{"space_type": 'VIEW_3D', "region_type": 'WINDOW'}, {"space_type": 'VIEW_3D', "region_type": 'WINDOW'},
@@ -107,15 +107,7 @@ keyconfig_data = \
], ],
}, },
), ),
("view3d.view_center_camera", {"type": 'HOME', "value": 'PRESS', "repeat": True}, None), ("mcp.view_frame_home", {"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),
],
},
),
("view3d.view_all", ("view3d.view_all",
{"type": 'HOME', "value": 'PRESS', "ctrl": True, "repeat": True}, {"type": 'HOME', "value": 'PRESS', "ctrl": True, "repeat": True},
{"properties": {"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", ("Frames",
{"space_type": 'EMPTY', "region_type": 'WINDOW'}, {"space_type": 'EMPTY', "region_type": 'WINDOW'},
{"items": {"items":
@@ -4409,6 +4606,7 @@ keyconfig_data = \
("render.view_cancel", {"type": 'ESC', "value": 'PRESS', "repeat": True}, None), ("render.view_cancel", {"type": 'ESC', "value": 'PRESS', "repeat": True}, None),
("render.view_show", {"type": 'F11', "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), ("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): def km_screen_editing(params):
items = [] items = []
keymap = ( keymap = ("Screen Editing",
"Screen Editing",
{"space_type": 'EMPTY', "region_type": 'WINDOW'}, {"space_type": 'EMPTY', "region_type": 'WINDOW'},
{"items": items}, {"items": items})
)
items.extend([ items.extend([
# Action zones # Action zones
@@ -281,13 +279,10 @@ def km_screen_editing(params):
("screen.area_dupli", {"type": 'ACTIONZONE_AREA', "value": 'ANY', "shift": True}, None), ("screen.area_dupli", {"type": 'ACTIONZONE_AREA', "value": 'ANY', "shift": True}, None),
("screen.area_swap", {"type": 'ACTIONZONE_AREA', "value": 'ANY', "ctrl": True}, None), ("screen.area_swap", {"type": 'ACTIONZONE_AREA', "value": 'ANY', "ctrl": True}, None),
("screen.region_scale", {"type": 'ACTIONZONE_REGION', "value": 'ANY'}, 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'}, ("screen.screen_full_area", {"type": 'ACTIONZONE_FULLSCREEN', "value": 'ANY'},
{"properties": [("use_hide_panels", True)]}), {"properties": [("use_hide_panels", True)]}),
# Area move after action zones # Area move after action zones
("screen.area_move", {"type": 'LEFTMOUSE', "value": 'PRESS'}, None), ("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), ("screen.area_options", {"type": 'RIGHTMOUSE', "value": 'PRESS'}, None),
# Render # Render
("render.render", {"type": 'RET', "value": 'PRESS', "ctrl": True}, ("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": 'TRACKPADZOOM', "value": 'ANY'}, None),
("image.view_zoom", {"type": 'TRACKPADPAN', "value": 'ANY', "ctrl": True}, None), ("image.view_zoom", {"type": 'TRACKPADPAN', "value": 'ANY', "ctrl": True}, None),
("image.view_zoom_border", {"type": 'Z', "value": 'PRESS'}, 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'}, ("image.view_zoom_ratio", {"type": 'F1', "value": 'PRESS'},
{"properties": [("ratio", 1.0)]}), {"properties": [("ratio", 1.0)]}),
("image.view_zoom_ratio", {"type": 'F2', "value": 'PRESS'}, ("image.view_zoom_ratio", {"type": 'F2', "value": 'PRESS'},
@@ -1058,22 +1065,6 @@ def km_image(params):
{"properties": [("ratio", 0.25)]}), {"properties": [("ratio", 0.25)]}),
("image.view_zoom_ratio", {"type": 'F4', "value": 'PRESS'}, ("image.view_zoom_ratio", {"type": 'F4', "value": 'PRESS'},
{"properties": [("ratio", 0.125)]}), {"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.change_frame", {"type": 'LEFTMOUSE', "value": 'PRESS'}, None),
("image.sample", {"type": 'LEFTMOUSE', "value": 'PRESS'}, None), ("image.sample", {"type": 'LEFTMOUSE', "value": 'PRESS'}, None),
("image.curves_point_set", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True}, ("image.curves_point_set", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True},
@@ -1187,7 +1178,8 @@ def km_node_editor(params):
{"properties": [("exit", True)]}), {"properties": [("exit", True)]}),
("node.clipboard_copy", {"type": 'C', "value": 'PRESS', "ctrl": True}, None), ("node.clipboard_copy", {"type": 'C', "value": 'PRESS', "ctrl": True}, None),
("node.clipboard_paste", {"type": 'V', "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": 'W', "value": 'PRESS'}, None),
("node.translate_attach", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, None), ("node.translate_attach", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, None),
("node.translate_attach", {"type": 'MIDDLEMOUSE', "value": 'CLICK_DRAG'}, None), ("node.translate_attach", {"type": 'MIDDLEMOUSE', "value": 'CLICK_DRAG'}, None),
@@ -1799,31 +1791,6 @@ def km_sequencer(params):
items.extend([ items.extend([
("wm.search_menu", {"type": 'TAB', "value": 'PRESS'}, None), ("wm.search_menu", {"type": 'TAB', "value": 'PRESS'}, None),
*_template_items_animation(), *_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}, {"properties": [("action", 'SELECT')]}),
("sequencer.select_all", {"type": 'A', "value": 'PRESS', "ctrl": True, ("sequencer.select_all", {"type": 'A', "value": 'PRESS', "ctrl": True,
"shift": True}, {"properties": [("action", 'DESELECT')]}), "shift": True}, {"properties": [("action", 'DESELECT')]}),
@@ -1840,15 +1807,12 @@ def km_sequencer(params):
{"properties": [("unselected", True)]}), {"properties": [("unselected", True)]}),
("sequencer.lock", {"type": 'L', "value": 'PRESS', "shift": True}, None), ("sequencer.lock", {"type": 'L', "value": 'PRESS', "shift": True}, None),
("sequencer.unlock", {"type": 'L', "value": 'PRESS', "shift": True, "alt": 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.reassign_inputs", {"type": 'R', "value": 'PRESS'}, None),
("sequencer.reload", {"type": 'R', "value": 'PRESS', "ctrl": True}, None), ("sequencer.reload", {"type": 'R', "value": 'PRESS', "ctrl": True}, None),
("sequencer.reload", {"type": 'R', "value": 'PRESS', "shift": True, "alt": True}, ("sequencer.reload", {"type": 'R', "value": 'PRESS', "shift": True, "alt": True},
{"properties": [("adjust_length", True)]}), {"properties": [("adjust_length", True)]}),
("sequencer.offset_clear", {"type": 'O', "value": 'PRESS', "alt": True}, None), ("sequencer.offset_clear", {"type": 'O', "value": 'PRESS', "alt": True}, None),
("sequencer.duplicate_move", {"type": 'D', "value": 'PRESS', "ctrl": 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": 'BACK_SPACE', "value": 'PRESS'}, None),
("sequencer.retiming_key_delete", {"type": 'DEL', "value": 'PRESS'}, None), ("sequencer.retiming_key_delete", {"type": 'DEL', "value": 'PRESS'}, None),
("sequencer.delete", {"type": 'BACK_SPACE', "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}, ("sequencer.gap_remove", {"type": 'BACK_SPACE', "value": 'PRESS', "shift": True},
{"properties": [("all", True)]}), {"properties": [("all", True)]}),
("sequencer.gap_insert", {"type": 'EQUAL', "value": 'PRESS', "shift": True}, None), ("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.swap_inputs", {"type": 'S', "value": 'PRESS', "alt": True}, None),
*( *(
(("sequencer.split_multicam", (("sequencer.split_multicam",
@@ -1889,22 +1853,41 @@ def km_sequencer(params):
for i in range(10) 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), ("sequencer.slip", {"type": 'R', "value": 'PRESS'}, None),
("wm.context_set_int", {"type": 'O', "value": 'PRESS'}, ("wm.context_set_int", {"type": 'O', "value": 'PRESS'},
{"properties": [("data_path", "scene.sequence_editor.overlay_frame"), ("value", 0)]}), {"properties": [("data_path", "scene.sequence_editor.overlay_frame"), ("value", 0)]}),
("transform.seq_slide", {"type": 'W', "value": 'PRESS'}, ("transform.seq_slide", {"type": 'W', "value": 'PRESS'}, None),
{"properties": [("view2d_edge_pan", True)]}), ("transform.seq_slide", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, None),
("transform.seq_slide", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, ("transform.seq_slide", {"type": 'MIDDLEMOUSE', "value": 'CLICK_DRAG'}, None),
{"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.transform", {"type": 'E', "value": 'PRESS'}, ("transform.transform", {"type": 'E', "value": 'PRESS'},
{"properties": [("mode", 'TIME_EXTEND')]}), {"properties": [("mode", 'TIME_EXTEND')]}),
("marker.add", {"type": 'M', "value": 'PRESS'}, None),
*_template_items_context_menu("SEQUENCER_MT_context_menu", {"type": 'RIGHTMOUSE', "value": 'PRESS'}), *_template_items_context_menu("SEQUENCER_MT_context_menu", {"type": 'RIGHTMOUSE', "value": 'PRESS'}),
op_menu("SEQUENCER_MT_retiming", {"type": 'I', "value": 'PRESS'}), ("marker.add", {"type": 'M', "value": 'PRESS'}, None),
("sequencer.retiming_segment_speed_set", {"type": 'R', "value": 'PRESS', "shift": True}, None),
("sequencer.retiming_show", {"type": 'R', "value": 'PRESS', "ctrl": True, "shift": True}, None),
# Tools # Tools
op_tool_cycle("builtin.select_box", {"type": 'Q', "value": 'PRESS'}), op_tool_cycle("builtin.select_box", {"type": 'Q', "value": 'PRESS'}),
op_tool_cycle("builtin.blade", {"type": 'B', "value": 'PRESS'}), op_tool_cycle("builtin.blade", {"type": 'B', "value": 'PRESS'}),
@@ -2720,16 +2703,15 @@ def _template_paint_radial_control(
items.extend([ items.extend([
("wm.radial_control", {"type": 'F', "value": 'PRESS', "ctrl": True, "alt": True}, ("wm.radial_control", {"type": 'F', "value": 'PRESS', "ctrl": True, "alt": True},
radial_control_properties( 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: if weight:
items.extend([ items.extend([
("wm.radial_control", {"type": 'F', "value": 'PRESS', "ctrl": True, "alt": True}, ("wm.radial_control", {"type": 'F', "value": 'PRESS', "ctrl": True, "alt": True},
radial_control_properties( 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}, ("wm.radial_control", {"type": 'F', "value": 'PRESS', "ctrl": True},
radial_control_properties( radial_control_properties(
paint, "weight", "use_unified_weight")) paint, "weight", "use_unified_weight"))
@@ -2784,10 +2766,8 @@ def km_image_paint(params):
("wm.context_toggle", {"type": 'L', "value": 'PRESS'}, ("wm.context_toggle", {"type": 'L', "value": 'PRESS'},
{"properties": [("data_path", "tool_settings.image_paint.brush.use_smooth_stroke")]}), {"properties": [("data_path", "tool_settings.image_paint.brush.use_smooth_stroke")]}),
# Context menu. # Context menu.
*_template_items_context_panel( *_template_items_context_panel("VIEW3D_PT_paint_texture_context_menu",
"VIEW3D_PT_paint_texture_context_menu", {"type": 'RIGHTMOUSE', "value": 'PRESS'}),
{"type": 'RIGHTMOUSE', "value": 'PRESS'},
),
# Tools # Tools
op_tool_cycle("builtin.select_box", {"type": 'Q', "value": 'PRESS'}), op_tool_cycle("builtin.select_box", {"type": 'Q', "value": 'PRESS'}),
op_tool_cycle("builtin.annotate", {"type": 'D', "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}, ("paint.vertex_paint", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True},
{"properties": [("mode", 'INVERT')]}), {"properties": [("mode", 'INVERT')]}),
("paint.vertex_paint", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True}, ("paint.vertex_paint", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True},
{"properties": [("brush_toggle", 'SMOOTH')]}), {"properties": [("mode", 'SMOOTH')]}),
# Colors # Colors
("paint.sample_color", {"type": 'I', "value": 'PRESS'}, {"properties": [("merged", False)]}), ("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.brush_colors_flip", {"type": 'X', "value": 'PRESS'}, None),
("paint.vertex_color_set", {"type": 'BACK_SPACE', "value": 'PRESS'}, None), ("paint.vertex_color_set", {"type": 'BACK_SPACE', "value": 'PRESS'}, None),
# Brush properties # Brush properties
@@ -2872,7 +2851,7 @@ def km_weight_paint(params):
("paint.weight_paint", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True}, ("paint.weight_paint", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True},
{"properties": [("mode", 'INVERT')]}), {"properties": [("mode", 'INVERT')]}),
("paint.weight_paint", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True}, ("paint.weight_paint", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True},
{"properties": [("brush_toggle", 'SMOOTH')]}), {"properties": [("mode", 'SMOOTH')]}),
# Weight # Weight
("paint.weight_sample", {"type": 'I', "value": 'PRESS'}, None), ("paint.weight_sample", {"type": 'I', "value": 'PRESS'}, None),
("paint.weight_sample_group", {"type": 'I', "value": 'PRESS', "alt": True}, 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}, ("sculpt.brush_stroke", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True},
{"properties": [("mode", 'INVERT')]}), {"properties": [("mode", 'INVERT')]}),
("sculpt.brush_stroke", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True}, ("sculpt.brush_stroke", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True},
{"properties": [("brush_toggle", 'SMOOTH')]}), {"properties": [("mode", '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')]}),
# Expand # Expand
("sculpt.expand", {"type": 'A', "value": 'PRESS', "shift": True}, ("sculpt.expand", {"type": 'A', "value": 'PRESS', "shift": True},
{"properties": [ {"properties": [
@@ -2998,8 +2973,7 @@ def km_sculpt(params):
("object.voxel_remesh", {"type": 'D', "value": 'PRESS', "ctrl": True}, None), ("object.voxel_remesh", {"type": 'D', "value": 'PRESS', "ctrl": True}, None),
("object.voxel_size_edit", {"type": 'D', "value": 'PRESS', "shift": True, "ctrl": True}, None), ("object.voxel_size_edit", {"type": 'D', "value": 'PRESS', "shift": True, "ctrl": True}, None),
# Color # Color
("paint.sample_color", {"type": 'I', "value": 'PRESS'}, {"properties": [("merged", False)]}), ("sculpt.sample_color", {"type": 'I', "value": 'PRESS'}, None),
("paint.sample_color", {"type": 'I', "value": 'PRESS', "shift": True}, {"properties": [("merged", True)]}),
("paint.brush_colors_flip", {"type": 'X', "value": 'PRESS'}, None), ("paint.brush_colors_flip", {"type": 'X', "value": 'PRESS'}, None),
# Brush properties # Brush properties
("brush.scale_size", {"type": 'LEFT_BRACKET', "value": 'PRESS', "repeat": True}, ("brush.scale_size", {"type": 'LEFT_BRACKET', "value": 'PRESS', "repeat": True},
@@ -3054,18 +3028,18 @@ def km_mesh(params):
items.extend([ items.extend([
# Selection # Selection
("mesh.loop_select", {"type": 'LEFTMOUSE', "value": 'DOUBLE_CLICK'}, ("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}, ("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}, ("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}, ("mesh.loop_select", {"type": 'LEFTMOUSE', "value": 'DOUBLE_CLICK', "alt": True},
{"properties": [("extend", False), ("deselect", False), ("toggle", False)]}), {"properties": [("extend", False), ("deselect", False), ("toggle", False), ("ring", True)]}),
("mesh.edgering_select", {"type": 'LEFTMOUSE', "value": 'DOUBLE_CLICK', "alt": True, "shift": True}, ("mesh.loop_select", {"type": 'LEFTMOUSE', "value": 'DOUBLE_CLICK', "alt": True, "shift": True},
{"properties": [("extend", True), ("deselect", False), ("toggle", False)]}), {"properties": [("extend", True), ("deselect", False), ("toggle", False), ("ring", True)]}),
("mesh.edgering_select", {"type": 'LEFTMOUSE', "value": 'DOUBLE_CLICK', "alt": True, "ctrl": True}, ("mesh.loop_select", {"type": 'LEFTMOUSE', "value": 'DOUBLE_CLICK', "alt": True, "ctrl": True},
{"properties": [("extend", False), ("deselect", True), ("toggle", False)]}), {"properties": [("extend", False), ("deselect", True), ("toggle", False), ("ring", True)]}),
("mesh.shortest_path_pick", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True, "ctrl": True}, ("mesh.shortest_path_pick", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True, "ctrl": True},
{"properties": [("use_fill", False)]}), {"properties": [("use_fill", False)]}),
@@ -3427,7 +3401,7 @@ def km_sculpt_curves(params):
("sculpt_curves.brush_stroke", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True}, ("sculpt_curves.brush_stroke", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True},
{"properties": [("mode", 'INVERT')]}), {"properties": [("mode", 'INVERT')]}),
("sculpt_curves.brush_stroke", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True}, ("sculpt_curves.brush_stroke", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True},
{"properties": [("brush_toggle", 'SMOOTH')]}), {"properties": [("mode", 'SMOOTH')]}),
# Selection modes # Selection modes
("curves.set_selection_domain", {"type": 'ONE', "value": 'PRESS'}, {"properties": [("domain", 'POINT')]}), ("curves.set_selection_domain", {"type": 'ONE', "value": 'PRESS'}, {"properties": [("domain", 'POINT')]}),
("curves.set_selection_domain", {"type": 'TWO', "value": 'PRESS'}, {"properties": [("domain", 'CURVE')]}), ("curves.set_selection_domain", {"type": 'TWO', "value": 'PRESS'}, {"properties": [("domain", 'CURVE')]}),
@@ -3846,10 +3820,10 @@ def generate_keymaps_impl(params=None):
# Tool System. # Tool System.
km_3d_view_tool_select(params), km_3d_view_tool_select(params),
km_3d_view_tool_interactive_add(params),
km_image_editor_tool_uv_select(params), km_image_editor_tool_uv_select(params),
km_sequencer_editor_tool_select_preview(params), km_sequencer_editor_tool_select_preview(params),
km_sequencer_editor_tool_select_timeline(params), km_sequencer_editor_tool_select_timeline(params),
km_3d_view_tool_interactive_add(params),
] ]