work
save startup blend for animation tab & whatnot
This commit is contained in:
@@ -10,13 +10,13 @@ D:\Work\9 iClone\Amazon\
|
||||
D:\Amazon\00_external-files\
|
||||
N:\1. CHARACTERS\remapping\
|
||||
[Recent]
|
||||
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\
|
||||
P:\250827_FestivalTurf\Blends\animations\02 Preparing the Base\Watering Ground\
|
||||
P:\250827_FestivalTurf\Assets\Blends\
|
||||
P:\250827_FestivalTurf\Blends\animations\01 Develop Your Turf Plan\SPA\
|
||||
P:\250827_FestivalTurf\Assets\Blends\Char\
|
||||
P:\260217_Jarvis-Defense\Blends\animations\comp_RR\
|
||||
C:\Users\Nathan\AppData\Local\Temp\
|
||||
T:\260217_Jarvis-Defense\Renders\Shot_4a\
|
||||
F:\renders\Shot_4a_holdout_test\2026-03-25_101242\
|
||||
F:\renders\
|
||||
P:\250827_FestivalTurf\Blends\animations\01 Develop Your Turf Plan\
|
||||
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\
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\
|
||||
P:\250827_FestivalTurf\Assets\
|
||||
P:\250827_FestivalTurf\Blends\animations\02 Preparing the Base\bakes\07_compacting\
|
||||
P:\250827_FestivalTurf\Blends\animations\02 Preparing the Base\
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"flamenco_version": {
|
||||
"version": "3.8.2",
|
||||
"shortversion": "3.8.2",
|
||||
"version": "3.8.5",
|
||||
"shortversion": "3.8.5",
|
||||
"name": "Flamenco",
|
||||
"git": "51a41a19"
|
||||
"git": "690bf6ce"
|
||||
},
|
||||
"shared_storage": {
|
||||
"location": "F:\\jobs",
|
||||
|
||||
+171
-171
@@ -1,3 +1,174 @@
|
||||
P:\250827_FestivalTurf\Blends\animations\02 Preparing the Base\06_Watering.blend
|
||||
T:\250827_FestivalTurf\Blends\animations\02 Preparing the Base\06_Watering.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 2B_SPA.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\FT-lipsync_SPA.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 2D_SPA.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 2B.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 2C.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 2D.blend
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\stills\2025_old\iP&S2025_23.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\01 Develop Your Turf Plan\SPA\FT-lipsync.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\01 Develop Your Turf Plan\SPA\01_Blueprint_SPA.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\04 Securing Your Seam\SPA\06_FT_Hammering_insert_SPA.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\07 Final Touches And Maintenance\SPA\Visual_0_opening.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 3B.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\SPA\Visual 3B_SPA.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\SPA\04_stretching pattern.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\SPA\Visual 6_INSERT.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\SPA\Visual 8 insert.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\SPA\03_FT Shuffle.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\SPA\Visual 8 insert2.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\SPA\01_intro_SPA.blend
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\animations\BD2\P&S_BD2_animation 12b.blend
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\animations\BD2\P&S_BD2_animation 12a.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\02 Preparing the Base\07_compacting.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\02 Preparing the Base\03_dirt to gravel.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\02 Preparing the Base\09_level ground.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\02 Preparing the Base\05_raking.blend
|
||||
P:\250827_FestivalTurf\Assets\Blends\Props\Tools.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\01 Develop Your Turf Plan\01_Blueprint.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_animation 6defgh.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_animation 6bc.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_animation 6a.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\02 Preparing the Base\04_Rock Yard.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_animation 6I.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\02 Preparing the Base\01_intro.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\07 Final Touches And Maintenance\Visual_new_final.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\07 Final Touches And Maintenance\Visual_8.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\07 Final Touches And Maintenance\Visual_7.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\07 Final Touches And Maintenance\Visual_7_phone_insert.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\07 Final Touches And Maintenance\Visual_7_reframe.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\07 Final Touches And Maintenance\Visual_7_phone_insert_alt.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\07 Final Touches And Maintenance\Visual_5_heat_damage_insert_1.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\07 Final Touches And Maintenance\Visual_5.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\07 Final Touches And Maintenance\Visual_4_leaf_blower_insert.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\07 Final Touches And Maintenance\Visual_4_leaf_blower.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\07 Final Touches And Maintenance\Visual_3_PE_spread.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\07 Final Touches And Maintenance\Visual_2_push_broom.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\07 Final Touches And Maintenance\Visual_2_power_broom.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_SHORT_3b.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_SHORT_5b.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_SHORT_5aA.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_SHORT_4bC.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_SHORT_4bB.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_SHORT_4bA.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_SHORT_4aB.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_SHORT_4aA_part2.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_SHORT_4aA.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_SHORT_3c.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_SHORT_3a_insert2.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_SHORT_3a_insert1.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_SHORT_3a.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_SHORT_2c.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_SHORT_2b.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_SHORT_2a.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_animation 4.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_animation 3A.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_animation 5a.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_animation 5d.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_animation 5bc.blend
|
||||
F:\jobs\RSR_fluid_unload_animation 5a\RSR_fluid_unload_animation 5a.flamenco.blend
|
||||
A:\@GMT-2026.04.02-18.00.00\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_animation 5a.blend
|
||||
A:\@GMT-2026.04.02-17.00.00\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_animation 5a.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_animation 4b.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_animation 3C.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_animation 2.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_animation 1D.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_animation 1c.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_animation 1b_c.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_animation 1a.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_animation 1b_b.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_animation 1b_a.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\animations\RSR_fluid_unload_animation 1b.blend
|
||||
A:\1 Amazon_Active_Projects\260225_Problem-Solve_2026\Blends\animations\PS_2026_animation short 1b.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\stills\iRSR_fluid_unload_7d.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\stills\iRSR_fluid_unload_7c.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\stills\iRSR_fluid_unload_24c.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\stills\iRSR_fluid_unload_25b.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_24e.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_animation 1_CYCLES.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\stills\iRSR_fluid_unload_16e.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\stills\iRSR_fluid_unload_24a.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\stills\iRSR_fluid_unload_4b.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\stills\iRSR_fluid_unload_24b.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\stills\iRSR_fluid_unload_16o.blend
|
||||
A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Props\Pallete_Broken_Planks.blend
|
||||
A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Props\Pallete_Broken.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\stills\iRSR_fluid_unload_24b_.blend
|
||||
C:\Users\Nathan\AppData\Local\Temp\2026-04-02_13-44_iRSR_fluid_unload_24b.blend
|
||||
T:\1 BlenderAssets\Amazon\Props\Pallete_Broken.blend
|
||||
A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\amazon-3Dworld-assets_v4.0.blend
|
||||
C:\Users\Nathan\AppData\Local\Temp\2026-04-02_12-09_RSR_fluid_unload_SHORT_3a.blend
|
||||
C:\Users\Nathan\AppData\Local\Temp\2026-04-02_12-11_RSR_fluid_unload_SHORT_3a.blend
|
||||
C:\Users\Nathan\AppData\Local\Temp\2026-04-02_12-13_RSR_fluid_unload_SHORT_3a.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\stills\iRSR_fluid_unload_19c.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\stills\iRSR_fluid_unload_23b.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\stills\iRSR_fluid_unload_23a.blend
|
||||
A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Props\Long-Pallete.blend
|
||||
A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Props\Shuttle_v2.0.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\stills\iRSR_fluid_unload_19d.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\stills\iRSR_fluid_unload_7.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\stills\i_noncon_OOG_2c.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Blends\stills\iRSR_fluid_unload_19e.blend
|
||||
A:\1 Amazon_Active_Projects\260206_Dock_Unified\Blends\animations\Dock\DOCK_shrinkwrap_s3.blend
|
||||
A:\1 Amazon_Active_Projects\260206_Dock_Unified\Blends\animations\Dock\DOCK_shrinkwrap_s4.blend
|
||||
A:\1 Amazon_Active_Projects\260206_Dock_Unified\Blends\animations\Dock\DOCK_shrinkwrap_s2.blend
|
||||
A:\1 Amazon_Active_Projects\260206_Dock_Unified\Blends\animations\Dock\DOCK_shrinkwrap_s1.blend
|
||||
A:\1 Amazon_Active_Projects\260326_RSR-Fluid-Unload\Assets\Blends\PAM.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\07 Final Touches And Maintenance\Visual_2_broom.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\07 Final Touches And Maintenance\Visual_1_respray_insert.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\07 Final Touches And Maintenance\Visual_1_respray.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\07 Final Touches And Maintenance\Visual_0_5_sheering_insert.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\07 Final Touches And Maintenance\Visual_0_5_talking.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
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\animations\2025_old\P&S_safety_refresher_1d_rev2.blend
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\animations\2025_old\P&S_safety_refresher_1c_rev2.blend
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\animations\2025_old\P&S_safety_refresher_1c.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\SPA_noncon_OOG_short_animation 9a.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_short_animation 9a_v3.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_short_animation 9a.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_short_animation 3a.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\260127_Pick-and-Stage_2026_edits\Blends\animations\2026_new\P&S_2026_animation short 1c.blend
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\animations\2025_old\P&S2025_anim_2a.blend
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\animations\2025_old\P&S2025_anim_2b.blend
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\animations\2025_old\P&S2025_anim_2c.blend
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\stills\iP&S_2026_2b.blend
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\stills\iP&S_2026_2a.blend
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\stills\iP&S_2026_1b.blend
|
||||
T:\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\2026_new\iP&S_2026_2b.blend
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\stills\2026_new\iP&S_2026_2a.blend
|
||||
C:\Users\Nathan\AppData\Local\Temp\2026-03-27_15-23_2026-03-27_15-13_P&S2025_anim_2a.blend
|
||||
C:\Users\Nathan\AppData\Local\Temp\2026-03-27_15-13_P&S2025_anim_2a.blend
|
||||
C:\Users\Nathan\AppData\Local\Temp\2026-03-27_15-19_P&S2025_anim_2a.blend
|
||||
C:\Users\Nathan\AppData\Local\Temp\2026-03-27_15-21_P&S2025_anim_2a.blend
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\animations\2025_old\P&S2025_anim_3a.blend
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\animations\2025_old\P&S2025_anim_3b.blend
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\animations\2025_old\P&S2025_anim_1a.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 6.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\Visual 6.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 5B_alt_vo.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 5B.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 4B.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 4A.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 4A_2.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 3A.blend
|
||||
E:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 3A.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 3A_.blend
|
||||
T:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 3A.blend
|
||||
T:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 3B.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\01_opening.blend
|
||||
T:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 2D.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 2A.blend
|
||||
T:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 2C.blend
|
||||
T:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 2B.blend
|
||||
P:\250827_FestivalTurf\Assets\Blends\Char\FT-rig_moustache.blend
|
||||
P:\250827_FestivalTurf\Assets\Blends\Char\FT-rig_moustache_fixed.blend
|
||||
T:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\Visual 11.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\Visual 11.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\Visual 10.blend
|
||||
@@ -8,7 +179,6 @@ P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\Visual 8 insert.ble
|
||||
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\Visual 6.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
|
||||
@@ -16,8 +186,6 @@ 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
|
||||
P:\250827_FestivalTurf\Blends\animations\01 Develop Your Turf Plan\SPA\01_Blueprint_SPA.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\01 Develop Your Turf Plan\01_Blueprint.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
|
||||
@@ -30,171 +198,3 @@ P:\260217_Jarvis-Defense\Assets\Blends\3075 Decade Dr.blend
|
||||
P:\260217_Jarvis-Defense\Blends\animations\comp_RR\Shot_4_holdout.blend
|
||||
P:\260217_Jarvis-Defense\Blends\animations\Shot_4_redo_fall.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\01 Develop Your Turf Plan\06_Closing.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\01 Develop Your Turf Plan\05_Call for Help.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\01 Develop Your Turf Plan\04_Kid.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\01 Develop Your Turf Plan\01_Blueprint_insert.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\01 Develop Your Turf Plan\03_Window.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\01 Develop Your Turf Plan\02_Roll Sizes.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\01 Develop Your Turf Plan\04_Dog.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\01 Develop Your Turf Plan\04_FT.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\04 Securing Your Seam\06_FT_Hammering_insert.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\04 Securing Your Seam\SPA\06_FT_Hammering_insert_SPA.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\SPA\Visual 7.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\07 Final Touches And Maintenance\Visual_7.blend
|
||||
A:\1 Amazon_Active_Projects\260225_Problem-Solve_2026\Blends\animations\PS_2026_animation short 1a.blend
|
||||
C:\Users\Nathan\Downloads\Shot_5c.blend
|
||||
C:\Users\Nathan\Downloads\Shot_5b.blend
|
||||
C:\Users\Nathan\Downloads\Shot_4d_f.blend
|
||||
P:\260217_Jarvis-Defense\Blends\animations\Shot_2.blend
|
||||
F:\jobs\Shot_4d_f-4pth\Shot_4_redo_fall.flamenco.blend
|
||||
F:\jobs\Shot_5b\Shot_Q.flamenco.blend
|
||||
C:\Users\Nathan\Downloads\Visual_new_final\Visual_new_final.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\07 Final Touches And Maintenance\Visual_new_final.blend
|
||||
C:\Users\Nathan\Downloads\Shot_7.blend
|
||||
C:\Users\Nathan\Downloads\Shot_4.blend
|
||||
F:\jobs\Shot_7-p1na\Shot_4.flamenco.blend
|
||||
P:\260217_Jarvis-Defense\Blends\animations\Shot_3_retimed.blend
|
||||
F:\jobs\Shot_3a-fpm9\Shot_3_retimed.flamenco.blend
|
||||
C:\Users\Nathan\Downloads\Shot_2a.blend
|
||||
T:\260217_Jarvis-Defense\Blends\animations\Shot_4_redo_fall.blend
|
||||
C:\Users\Nathan\Downloads\Shot_1c.blend
|
||||
C:\Users\Nathan\Downloads\Shot_1b.blend
|
||||
C:\Users\Nathan\Downloads\Shot_1.flamenco.blend
|
||||
P:\260217_Jarvis-Defense\Assets\Blends\Char\Priest_v3.0.blend
|
||||
F:\jobs\shot_2a-ja00\Shot_2.flamenco.blend
|
||||
C:\Users\Nathan\Downloads\Shot_3_retimed.flamenco.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 1c.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\stills\i_noncon_OOG_9o.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\stills\i_noncon_OOG_9m.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\stills\i_noncon_OOG_9k.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\stills\i_noncon_OOG_9h.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_short_animation 13bJ.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\stills\i_noncon_OOG_9e.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\stills\i_noncon_OOG_9b.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\stills\i_noncon_OOG_9d.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_short_animation 10bF.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\stills\i_noncon_OOG_9a.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_short_animation 13bC.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_short_animation 13bD.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_short_animation 9bC.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_short_animation 9bG.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_short_animation 9bB.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_short_animation 9bD_2.blend
|
||||
T:\1 BlenderAssets\Amazon\Char\Cartoon3\Steve_v1.2.blend
|
||||
T:\1 BlenderAssets\Amazon\Char\Cartoon2\AS\Sarah_v3.3.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\stills\i_noncon_OOG_4b.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\stills\i_noncon_OOG_4a.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\stills\i_noncon_OOG_2j.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\stills\i_noncon_OOG_2i.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\stills\i_noncon_OOG_1b.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\stills\i_noncon_OOG_2a.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\stills\i_noncon_OOG_1a.blend
|
||||
A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Scenes\AMZ-warehouse_v6.1_small.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_short_animation 10bD.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\stills\i_noncon_OOG_10a.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\stills\i_noncon_OOG_9l.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\stills\i_noncon_OOG_9c.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\stills\i_noncon_OOG_8f.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\stills\i_noncon_OOG_8d.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\stills\i_noncon_OOG_8c.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\stills\i_noncon_OOG_8g.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\stills\i_noncon_OOG_5b.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_v2.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\stills\i_noncon_OOG_7b.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\stills\i_noncon_OOG_7c.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\stills\i_noncon_OOG_7a.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Assets\Blends\AMZ-warehouse_v6.2_small.blend
|
||||
T:\260317_NONCON_2026\Blends\animations\noncon_OOG_short_animation 9bB.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\stills\i_noncon_OOG_2f.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\stills\i_noncon_OOG_2c.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\stills\i_noncon_OOG_9i.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_short_animation 12C.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_short_animation 12A.blend
|
||||
A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Char\Cartoon3\Liam_v1.2.blend
|
||||
A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Char\Cartoon3\Tony_v1.2.blend
|
||||
A:\1 Amazon_Active_Projects\260206_Dock_Unified\Blends\stills\Path 3\iDock_Path4_4d_red.blend
|
||||
A:\1 Amazon_Active_Projects\260206_Dock_Unified\Blends\stills\Path 3\iDock_Path4_4d_yellow.blend
|
||||
A:\1 Amazon_Active_Projects\260206_Dock_Unified\Blends\stills\Path 3\iDock_Path3_6_yellow.blend
|
||||
A:\1 Amazon_Active_Projects\260206_Dock_Unified\Blends\stills\Path 3\iDock_Path3_6_red.blend
|
||||
A:\1 Amazon_Active_Projects\260206_Dock_Unified\Blends\stills\Waterspider A\WS_A_go-cart loading_f.blend
|
||||
T:\260206_Dock_Unified\Blends\stills\Waterspider A\WS_A_go-cart loading_f.blend
|
||||
T:\260206_Dock_Unified\Blends\stills\Waterspider A\WS_A_go-cart loading_d.blend
|
||||
T:\260206_Dock_Unified\Blends\stills\Waterspider A\WS_A_go-cart loading_b.blend
|
||||
T:\260206_Dock_Unified\Blends\stills\Waterspider A\iWS_A_6.blend
|
||||
T:\260206_Dock_Unified\Blends\stills\Waterspider B\MISC_WS-B_swapping.blend
|
||||
T:\260206_Dock_Unified\Blends\animations\go_cart_loading\WS_A_go-cart loading_a.blend
|
||||
G:\Amazon\2024\240322_Amazon_Dock-Safety\blends\ANIMATIONS\Path 3\path 3 images\iDock_Path3_6.blend
|
||||
T:\260206_Dock_Unified\Blends\stills\Waterspider B\WS_A_go-cart loading_e.blend
|
||||
T:\260206_Dock_Unified\Blends\stills\Waterspider B\WS_A_go-cart loading_f.blend
|
||||
A:\1 Amazon_Active_Projects\260206_Dock_Unified\Blends\stills\Waterspider A\WS_A_go-cart loading_f_red.blend
|
||||
A:\1 Amazon_Active_Projects\260206_Dock_Unified\Blends\stills\Waterspider A\WS_A_go-cart loading_e.blend
|
||||
A:\1 Amazon_Active_Projects\260206_Dock_Unified\Blends\stills\Waterspider A\WS_A_go-cart loading_a.blend
|
||||
A:\1 Amazon_Active_Projects\260206_Dock_Unified\Blends\stills\Path 3\iDock_path3_4c.blend
|
||||
A:\1 Amazon_Active_Projects\260206_Dock_Unified\Blends\animations\Path 3\Path 3_Animation 3a.blend
|
||||
A:\1 Amazon_Active_Projects\260206_Dock_Unified\Blends\animations\Path 3\Path 3_Animation 3_Aa.blend
|
||||
A:\1 Amazon_Active_Projects\260206_Dock_Unified\Blends\stills\Path 3\iDock_Path3_6.blend
|
||||
T:\250827_FestivalTurf\Blends\animations\07 Final Touches And Maintenance\Visual_new_final.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_short_animation 2e.blend
|
||||
T:\260206_Dock_Unified\Blends\animations\Path 3\Path 3_Animation 3_Aa.blend
|
||||
A:\1 Amazon_Active_Projects\260206_Dock_Unified\Blends\animations\Path 3\Path 3_Animation 3e.blend
|
||||
A:\1 Amazon_Active_Projects\260206_Dock_Unified\Blends\animations\Path 3\Path 3_Animation 3d.blend
|
||||
A:\1 Amazon_Active_Projects\260206_Dock_Unified\Blends\animations\Path 3\Path 3_Animation 3c.blend
|
||||
A:\1 Amazon_Active_Projects\260206_Dock_Unified\Blends\animations\Path 3\Path 3_Animation 3b.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_short_animation 1a_part2.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_short_animation 1a_part1.blend
|
||||
A:\1 Amazon_Active_Projects\260206_Dock_Unified\Blends\animations\Waterspider A\WS_A_move go-cart from trailer A.blend
|
||||
A:\1 Amazon_Active_Projects\260206_Dock_Unified\Blends\stills\Waterspider A\WS_A_go-cart loading_e_red.blend
|
||||
C:\Users\Nathan\Desktop\Untitled.blend
|
||||
A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Char\Cartoon1\Regina_v4.3.blend
|
||||
T:\1 BlenderAssets\Amazon\Char\Cartoon2\AS\Paul_v3.4.blend
|
||||
T:\1 BlenderAssets\Amazon\Char\Cartoon2\AS\Kennedy_v3.3.blend
|
||||
T:\1 BlenderAssets\Amazon\Char\Cartoon1\Regina_v4.3.blend
|
||||
T:\1 BlenderAssets\Amazon\Char\Cartoon1\Hailey_v4.3.blend
|
||||
A:\1 Amazon_Active_Projects\260311_PnS-Beyond-Day-2\Blends\animations\P&S_BD2_animation 10a.blend
|
||||
A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Char\Cartoon1\Chan_v4.3.blend
|
||||
A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Char\Cartoon1\Manny_v4.3.blend
|
||||
A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Char\Cartoon1\Dennis_v4.3.blend
|
||||
A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Char\Cartoon1\Joe_v4.3.blend
|
||||
A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Char\Cartoon1\Kirk_v4.4.blend
|
||||
A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Mat\MAT_Char.blend
|
||||
A:\1 Amazon_Active_Projects\260311_PnS-Beyond-Day-2\Blends\animations\P&S_BD2_animation 8c_part2.blend
|
||||
A:\1 Amazon_Active_Projects\260206_PAE_2026\Blends\animations\PAE_Animation 4B.blend
|
||||
A:\1 Amazon_Active_Projects\260206_PAE_2026\Blends\animations\PAE_Animation 3C.blend
|
||||
A:\1 Amazon_Active_Projects\260206_PAE_2026\Blends\animations\PAE_Animation 3B.blend
|
||||
A:\1 Amazon_Active_Projects\260225_Problem-Solve_2026\Blends\animations\PS_2026_animation 5G.blend
|
||||
A:\1 Amazon_Active_Projects\260225_Problem-Solve_2026\Blends\animations\PS_2026_animation 5E.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_short_animation 2f.blend
|
||||
A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Mat\MATERIALS_BSDF_pallette_v1.0.blend
|
||||
A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Scenes\AMZ-warehouse_v5.1.blend
|
||||
A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Scenes\AMZ-warehouse_BSDF_v4.0.blend
|
||||
C:\Users\Nathan\Downloads\noncon_OOG_short_animation 2d.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_short_animation 2d.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_short_animation 9a.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_short_animation 2a.blend
|
||||
T:\260317_NONCON_2026\Blends\animations\noncon_OOG_short_animation 9a.blend
|
||||
A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Props\Dog-Food-Bag.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_short_animation 3d.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_short_animation 3c.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_short_animation 13a.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_short_animation 3b.blend
|
||||
A:\1 Amazon_Active_Projects\260317_NONCON_2026\Blends\animations\noncon_OOG_short_animation 3a.blend
|
||||
C:\Users\Nathan\AppData\Local\Temp\2026-03-17_11-18_noncon_OOG_short_animation 3b.blend
|
||||
A:\1 Amazon_Active_Projects\260206_Dock_Unified\Blends\animations\Path 1\Path 1_Animation 1_Scene 3_1.blend
|
||||
A:\1 Amazon_Active_Projects\260206_Dock_Unified\Blends\animations\New\Dock_safety_new animation 1.blend
|
||||
A:\1 Amazon_Active_Projects\260311_PnS-Beyond-Day-2\Blends\stills\iP&S_BD2_18h.blend
|
||||
A:\1 Amazon_Active_Projects\260311_PnS-Beyond-Day-2\Blends\stills\iP&S_BD2_18g.blend
|
||||
A:\1 Amazon_Active_Projects\260311_PnS-Beyond-Day-2\Blends\stills\iP&S_BD2_3d.blend
|
||||
A:\1 Amazon_Active_Projects\260311_PnS-Beyond-Day-2\Blends\stills\iP&S_BD2_18f.blend
|
||||
A:\1 Amazon_Active_Projects\260311_PnS-Beyond-Day-2\Blends\stills\iP&S_BD2_18e.blend
|
||||
A:\1 Amazon_Active_Projects\260311_PnS-Beyond-Day-2\Blends\stills\iP&S_BD2_18c.blend
|
||||
A:\1 Amazon_Active_Projects\260311_PnS-Beyond-Day-2\Blends\stills\iP&S_BD2_18d.blend
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\stills\iP&S_2026_10.blend
|
||||
A:\1 Amazon_Active_Projects\260311_PnS-Beyond-Day-2\Blends\stills\iP&S_BD2_18a.blend
|
||||
A:\1 Amazon_Active_Projects\260311_PnS-Beyond-Day-2\Blends\stills\iP&S_BD2_18b.blend
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\stills\iP&S_2026_3a-b.blend
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\stills\iP&S_2026_1a.blend
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\stills\iP&S_2026_1c.blend
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\stills\iP&S_2026_1d.blend
|
||||
C:\Users\Nathan\Downloads\P&S_BD2_animation 6d.blend
|
||||
|
||||
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
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
@@ -31,6 +31,7 @@ if "bpy" in locals():
|
||||
importlib.reload(add_mesh_menger_sponge)
|
||||
importlib.reload(add_mesh_vertex)
|
||||
importlib.reload(add_empty_as_parent)
|
||||
importlib.reload(add_mesh_equilateral_grid)
|
||||
importlib.reload(add_mesh_beam_builder)
|
||||
importlib.reload(Blocks)
|
||||
importlib.reload(Wallfactory)
|
||||
@@ -59,6 +60,7 @@ else:
|
||||
from . import Wallfactory
|
||||
from . import add_mesh_triangles
|
||||
from . import preferences
|
||||
from . import add_mesh_equilateral_grid
|
||||
|
||||
from .add_mesh_rocks import __init__
|
||||
from .add_mesh_rocks import rockgen
|
||||
@@ -154,6 +156,7 @@ class VIEW3D_MT_mesh_extras_add(Menu):
|
||||
oper.change = False
|
||||
oper = layout.operator("mesh.primitive_teapot_add", text="Teapot+")
|
||||
oper = layout.operator("mesh.menger_sponge_add", text="Menger Sponge")
|
||||
oper = layout.operator("mesh.add_equilateral_grid", text="Equilateral Grid")
|
||||
|
||||
|
||||
class VIEW3D_MT_mesh_torus_add(Menu):
|
||||
@@ -209,7 +212,7 @@ def menu_func(self, context):
|
||||
|
||||
if prefs.show_single_vert:
|
||||
layout.menu("VIEW3D_MT_mesh_vert_add", text="Single Vert", icon='DECORATE')
|
||||
|
||||
|
||||
if prefs.show_torus_objects:
|
||||
layout.menu("VIEW3D_MT_mesh_torus_add", text="Torus Objects", icon='MESH_TORUS')
|
||||
|
||||
@@ -218,7 +221,7 @@ def menu_func(self, context):
|
||||
|
||||
if prefs.show_gears:
|
||||
layout.menu("VIEW3D_MT_mesh_gears_add", text="Gears", icon='PREFERENCES')
|
||||
|
||||
|
||||
if prefs.show_pipe_joints:
|
||||
layout.menu("VIEW3D_MT_mesh_pipe_joints_add", text="Pipe Joints", icon='IPO_CONSTANT')
|
||||
|
||||
@@ -409,6 +412,7 @@ classes = [
|
||||
add_mesh_menger_sponge.AddMengerSponge,
|
||||
add_mesh_vertex.AddVert,
|
||||
add_mesh_vertex.AddEmptyVert,
|
||||
add_mesh_equilateral_grid.MESH_OT_add_equilateral_grid,
|
||||
add_mesh_vertex.AddSymmetricalEmpty,
|
||||
add_mesh_vertex.AddSymmetricalVert,
|
||||
add_empty_as_parent.P2E,
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
# SPDX-FileCopyrightText: 2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
# Author: Luis-Lerga
|
||||
|
||||
|
||||
import bpy
|
||||
import bmesh
|
||||
from math import sqrt
|
||||
from bpy_extras import object_utils
|
||||
|
||||
|
||||
class MESH_OT_add_equilateral_grid(bpy.types.Operator):
|
||||
"""Add an equilateral triangular grid with hexagonal pattern"""
|
||||
bl_idname = "mesh.add_equilateral_grid"
|
||||
bl_label = "Add Equilateral Grid"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
# Dimensions
|
||||
width: bpy.props.FloatProperty(
|
||||
name="Width",
|
||||
description="Horizontal width of the rectangle",
|
||||
default=10.0,
|
||||
min=0.1,
|
||||
max=100.0,
|
||||
step=100,
|
||||
precision=2,
|
||||
unit='LENGTH'
|
||||
)
|
||||
|
||||
height: bpy.props.FloatProperty(
|
||||
name="Height",
|
||||
description="Vertical height of the rectangle",
|
||||
default=10.0,
|
||||
min=0.1,
|
||||
max=100.0,
|
||||
step=100,
|
||||
precision=2,
|
||||
unit='LENGTH'
|
||||
)
|
||||
|
||||
density: bpy.props.IntProperty(
|
||||
name="Density",
|
||||
description="Number of horizontal segments",
|
||||
default=20,
|
||||
min=4,
|
||||
max=200,
|
||||
step=1
|
||||
)
|
||||
|
||||
# UVs
|
||||
use_uv: bpy.props.BoolProperty(
|
||||
name="Generate UVs",
|
||||
description="Generate UV coordinates for the mesh",
|
||||
default=True
|
||||
)
|
||||
|
||||
# Alignment
|
||||
align: bpy.props.EnumProperty(
|
||||
name="Align",
|
||||
description="Mesh alignment orientation",
|
||||
items=[
|
||||
('WORLD', "World", "Align to world origin"),
|
||||
('VIEW', "View", "Align to current view"),
|
||||
('CURSOR', "3D Cursor", "Align to 3D cursor position"),
|
||||
],
|
||||
default='WORLD'
|
||||
)
|
||||
|
||||
# Transformations
|
||||
location: bpy.props.FloatVectorProperty(
|
||||
name="Location",
|
||||
description="Object location",
|
||||
subtype='TRANSLATION',
|
||||
default=(0.0, 0.0, 0.0),
|
||||
unit='LENGTH'
|
||||
)
|
||||
|
||||
rotation: bpy.props.FloatVectorProperty(
|
||||
name="Rotation",
|
||||
description="Object rotation (Euler XYZ)",
|
||||
subtype='EULER',
|
||||
default=(0.0, 0.0, 0.0),
|
||||
unit='ROTATION'
|
||||
)
|
||||
|
||||
|
||||
def execute(self, context):
|
||||
# Create geometry & mesh.
|
||||
verts, faces = self.create_geometry(context)
|
||||
mesh = bpy.data.meshes.new("Equilateral Grid")
|
||||
mesh.from_pydata(verts, [], faces)
|
||||
mesh.update()
|
||||
|
||||
# Clean-up.
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
|
||||
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
|
||||
# Generate UVs.
|
||||
if self.use_uv:
|
||||
self.generate_uvs(mesh)
|
||||
|
||||
# Create an object.
|
||||
obj = object_utils.object_data_add(context, mesh, operator=self)
|
||||
|
||||
if context.preferences.edit.use_enter_edit_mode:
|
||||
bpy.ops.object.mode_set(mode = 'EDIT')
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def create_geometry(self, context):
|
||||
"""Generate geometry with equilateral triangles and perfect rectangular borders"""
|
||||
|
||||
L = self.width / self.density
|
||||
tri_height = L * sqrt(3) / 2
|
||||
|
||||
# Calculate EVEN number of triangle rows for straight top/bottom borders
|
||||
target_rows = self.height / tri_height
|
||||
rows = int(round(target_rows / 2.0)) * 2 # Always even
|
||||
rows = max(2, rows)
|
||||
|
||||
# Actual adjusted dimensions
|
||||
actual_width = self.density * L
|
||||
actual_height = rows * tri_height
|
||||
|
||||
# === Generate vertices WITHOUT protruding vertices ===
|
||||
verts = []
|
||||
row_offsets = [] # Track starting index of each row
|
||||
|
||||
for row in range(rows + 1): # rows+1 vertex rows
|
||||
y = -actual_height / 2 + row * tri_height
|
||||
row_offsets.append(len(verts))
|
||||
|
||||
if row % 2 == 0:
|
||||
# Even rows: density+1 vertices spanning full width
|
||||
num_verts = self.density + 1
|
||||
offset = 0.0
|
||||
else:
|
||||
# Odd rows: density vertices (NO protruding vertex)
|
||||
num_verts = self.density
|
||||
offset = L * 0.5
|
||||
|
||||
for col in range(num_verts):
|
||||
x = -actual_width / 2 + col * L + offset
|
||||
verts.append((x, y, 0.0))
|
||||
|
||||
# === Generate interior faces (equilateral triangles) ===
|
||||
faces = []
|
||||
for row in range(rows):
|
||||
if row % 2 == 0:
|
||||
# Even row → next row is odd (fewer vertices)
|
||||
for col in range(self.density):
|
||||
i00 = row_offsets[row] + col
|
||||
i01 = i00 + 1
|
||||
i10 = row_offsets[row + 1] + col
|
||||
i11 = i10 + 1 if col < self.density - 1 else None
|
||||
|
||||
faces.append((i00, i10, i01))
|
||||
if i11 is not None:
|
||||
faces.append((i01, i10, i11))
|
||||
else:
|
||||
# Odd row → next row is even (more vertices)
|
||||
for col in range(self.density):
|
||||
i00 = row_offsets[row] + col
|
||||
i01 = i00 + 1 if col < self.density - 1 else None
|
||||
i10 = row_offsets[row + 1] + col
|
||||
i11 = i10 + 1
|
||||
|
||||
if i01 is not None:
|
||||
faces.append((i00, i11, i01))
|
||||
faces.append((i00, i10, i11))
|
||||
|
||||
# === Fill LEFT/RIGHT borders for odd rows ===
|
||||
for row in range(1, rows, 2): # Only odd rows (1, 3, 5...)
|
||||
y = -actual_height / 2 + row * tri_height
|
||||
|
||||
# Add LEFT border vertex at exact x = -width/2
|
||||
left_idx = len(verts)
|
||||
verts.append((-actual_width / 2, y, 0.0))
|
||||
|
||||
# Add RIGHT border vertex at exact x = +width/2
|
||||
right_idx = len(verts)
|
||||
verts.append((actual_width / 2, y, 0.0))
|
||||
|
||||
# Connect LEFT border vertex
|
||||
base_idx = row_offsets[row]
|
||||
above_idx = row_offsets[row - 1] # Even row above
|
||||
below_idx = row_offsets[row + 1] # Even row below
|
||||
|
||||
# Upper triangle: left border → first interior → vertex above first interior
|
||||
faces.append((left_idx, base_idx, above_idx))
|
||||
# Lower triangle: left border → vertex below first interior → first interior
|
||||
faces.append((left_idx, below_idx, base_idx))
|
||||
|
||||
# Connect RIGHT border vertex
|
||||
last_interior = base_idx + self.density - 1
|
||||
above_last = above_idx + self.density # Last vertex of even row above
|
||||
below_last = below_idx + self.density # Last vertex of even row below
|
||||
|
||||
# Upper triangle: right border → last interior → vertex above last interior
|
||||
faces.append((right_idx, last_interior, above_last))
|
||||
# Lower triangle: right border → vertex below last interior → last interior
|
||||
faces.append((right_idx, below_last, last_interior))
|
||||
|
||||
return verts, faces
|
||||
|
||||
|
||||
def generate_uvs(self, mesh):
|
||||
"""Generate simple 0-1 UV coordinates based on X,Y coordinates"""
|
||||
if not mesh.uv_layers:
|
||||
mesh.uv_layers.new(name="UVMap")
|
||||
|
||||
uv_layer = mesh.uv_layers.active.data
|
||||
|
||||
# Calculate bounds for normalization
|
||||
xs = [v.co.x for v in mesh.vertices]
|
||||
ys = [v.co.y for v in mesh.vertices]
|
||||
min_x, max_x = min(xs), max(xs)
|
||||
min_y, max_y = min(ys), max(ys)
|
||||
width = max_x - min_x or 1.0
|
||||
height = max_y - min_y or 1.0
|
||||
|
||||
# Assign UVs
|
||||
for poly in mesh.polygons:
|
||||
for loop_idx in poly.loop_indices:
|
||||
vert_idx = mesh.loops[loop_idx].vertex_index
|
||||
v = mesh.vertices[vert_idx].co
|
||||
uv = (
|
||||
(v.x - min_x) / width,
|
||||
(v.y - min_y) / height
|
||||
)
|
||||
uv_layer[loop_idx].uv = uv
|
||||
|
||||
|
||||
def draw(self, context):
|
||||
"""Draw the options panel in the dialog"""
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
|
||||
# Dimensions
|
||||
layout.label(text="Dimensions:")
|
||||
col = layout.column(align=True)
|
||||
col.prop(self, "width")
|
||||
col.prop(self, "height")
|
||||
col.prop(self, "density")
|
||||
|
||||
# Additional options
|
||||
layout.separator()
|
||||
layout.prop(self, "use_uv")
|
||||
layout.prop(self, "align")
|
||||
|
||||
# Transformations
|
||||
layout.separator()
|
||||
|
||||
col = layout.column(align=True)
|
||||
col.prop(self, "location", text="Location X", index=0)
|
||||
col.prop(self, "location", text="Y", index=1)
|
||||
col.prop(self, "location", text="Z", index=2)
|
||||
|
||||
col = layout.column(align=True)
|
||||
col.prop(self, "rotation", text="Rotation X", index=0)
|
||||
col.prop(self, "rotation", text="Y", index=1)
|
||||
col.prop(self, "rotation", text="Z", index=2)
|
||||
@@ -1,7 +1,7 @@
|
||||
schema_version = "1.0.0"
|
||||
id = "extra_mesh_objects"
|
||||
name = "Extra Mesh Objects"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
tagline = "Add extra mesh object types"
|
||||
maintainer = "Community"
|
||||
type = "add-on"
|
||||
|
||||
@@ -4,7 +4,7 @@ Documentation: https://weisl.github.io/renaming_overview/
|
||||
|
||||
<h1>Introduction</h1>
|
||||
|
||||
<p><b> Simple Renaming Panel </b> is a small, but powerful tool to rename more objects at once. The tool includes basic functionalities of adding suffixes, prefixes, search and replace, add suffixes depending on the object type and much more. Over the time more advanced features like a variable system were added. The tool gives a lot of power to you!
|
||||
<p><b> Simple Renaming</b> is a small, but powerful tool to rename more objects at once. The tool includes basic functionalities of adding suffixes, prefixes, search and replace, add suffixes depending on the object type and much more. Over the time more advanced features like a variable system were added. The tool gives a lot of power to you!
|
||||
You decide which kind of objects will be affected by the renaming task. Rename all or just selected objects, specify the affected object types like image textures, materials, objects, object data, bones, or collections. This tool can be a real everyday helper. Renaming multiple objects is often needed and keeping the naming conventions can be tedious. The tool provides you with a clear feedback of what has been renamed. This tool is kept simple to be user-friendly but offers everything you need to stay organized. </p>
|
||||
|
||||
Join the discussion at [Blender Artists](https://blenderartists.org/t/simple-renaming-panel/676639 "Blender Artists").
|
||||
|
||||
@@ -92,7 +92,8 @@ enumObjectTypesExt = [('EMPTY', "", "Rename empty objects", 'OUTLINER_OB_EMPTY',
|
||||
('GPENCIL', "", "Rename greace pencil objects", 'OUTLINER_OB_GREASEPENCIL', 512),
|
||||
('METABALL', "", "Rename metaball objects", 'OUTLINER_OB_META', 2048),
|
||||
('COLLECTION', "", "Rename collections", 'GROUP', 4096),
|
||||
('BONE', "", "", 'BONE_DATA', 8192), ]
|
||||
('BONE', "", "", 'BONE_DATA', 8192),
|
||||
('POINTCLOUD', "", "Rename point cloud objects", 'OUTLINER_OB_POINTCLOUD', 16384), ]
|
||||
|
||||
|
||||
def register():
|
||||
@@ -113,7 +114,8 @@ def register():
|
||||
'MESH',
|
||||
'ARMATURE', 'LIGHT', 'CAMERA', 'EMPTY',
|
||||
'GPENCIL',
|
||||
'TEXT', 'BONE', 'COLLECTION'}
|
||||
'TEXT', 'BONE', 'COLLECTION',
|
||||
'POINTCLOUD'}
|
||||
)
|
||||
|
||||
id_store.renaming_suffix_prefix_material = StringProperty(name='Material', default='')
|
||||
@@ -135,6 +137,7 @@ def register():
|
||||
id_store.renaming_suffix_prefix_bone = StringProperty(name="Bones", default='')
|
||||
id_store.renaming_suffix_prefix_speakers = StringProperty(name="Speakers", default='')
|
||||
id_store.renaming_suffix_prefix_lightprops = StringProperty(name="LightProps", default='')
|
||||
id_store.renaming_suffix_prefix_pointcloud = StringProperty(name="Point Cloud", default='')
|
||||
|
||||
id_store.renaming_inputContext = StringProperty(name="LightProps", default='')
|
||||
|
||||
|
||||
+13
-3
@@ -40,9 +40,6 @@ class VIEW3D_OT_add_type_suf_pre(bpy.types.Operator):
|
||||
|
||||
option: StringProperty()
|
||||
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
|
||||
def get_selection_all(self):
|
||||
|
||||
context = self.context
|
||||
@@ -266,6 +263,18 @@ class VIEW3D_OT_add_type_suf_pre(bpy.types.Operator):
|
||||
icon='OUTLINER_OB_META')
|
||||
return
|
||||
|
||||
def pointcloud(self):
|
||||
context = self.context
|
||||
wm = context.scene
|
||||
obj_list = []
|
||||
|
||||
for obj in self.get_selection_all():
|
||||
if obj.type == 'POINTCLOUD':
|
||||
obj_list.append(obj)
|
||||
self.rename_suffix_prefix(obj_list, pre_suffix=wm.renaming_suffix_prefix_pointcloud, object_type='POINTCLOUD',
|
||||
icon='OUTLINER_OB_POINTCLOUD')
|
||||
return
|
||||
|
||||
def collection(self):
|
||||
context = self.context
|
||||
wm = context.scene
|
||||
@@ -303,6 +312,7 @@ class VIEW3D_OT_add_type_suf_pre(bpy.types.Operator):
|
||||
self.metaball()
|
||||
self.collection()
|
||||
self.bone()
|
||||
self.pointcloud()
|
||||
self.material()
|
||||
self.data()
|
||||
|
||||
|
||||
@@ -26,4 +26,5 @@ paths_exclude_pattern = [
|
||||
"__pycache__/",
|
||||
"/.git/",
|
||||
"/*.zip",
|
||||
"/tests/",
|
||||
]
|
||||
@@ -9,6 +9,9 @@ from bpy.props import (
|
||||
)
|
||||
|
||||
from . import add_pre_suffix
|
||||
from . import case_transform
|
||||
from . import reload_addon
|
||||
from .version_check import start_version_check
|
||||
from . import name_from_data
|
||||
from . import name_replace
|
||||
from . import numerate
|
||||
@@ -30,7 +33,8 @@ enumObjectTypes = [('EMPTY', "", "Rename empty objects", 'OUTLINER_OB_EMPTY', 1)
|
||||
('META', "", "Rename metaball objects", 'OUTLINER_OB_META', 1024),
|
||||
('SPEAKER', "", "Rename empty speakers", 'OUTLINER_OB_SPEAKER', 2048),
|
||||
('LIGHT_PROBE', "", "Rename mesh lightpropes", 'OUTLINER_OB_LIGHTPROBE', 4096),
|
||||
('VOLUME', "", "Rename mesh volumes", 'OUTLINER_OB_VOLUME', 8192)]
|
||||
('VOLUME', "", "Rename mesh volumes", 'OUTLINER_OB_VOLUME', 8192),
|
||||
('POINTCLOUD', "", "Rename point cloud objects", 'OUTLINER_OB_POINTCLOUD', 16384)]
|
||||
|
||||
enumObjectTypesAdd = [('SPEAKER', "", "Rename empty speakers", 'OUTLINER_OB_SPEAKER', 1),
|
||||
('LIGHT_PROBE', "", "Rename mesh lightpropes", 'OUTLINER_OB_LIGHTPROBE', 2)]
|
||||
@@ -60,6 +64,8 @@ renamingEntitiesItems = [('OBJECT', "Object", "Scene Objects"),
|
||||
None,
|
||||
('PARTICLESYSTEM', "Particle Systems", "Rename particle systems"),
|
||||
('PARTICLESETTINGS', "Particle Settings", "Rename particle settings"),
|
||||
None,
|
||||
('NODE_GROUPS', "Node Groups", "Rename node groups"),
|
||||
]
|
||||
|
||||
classes = (
|
||||
@@ -71,6 +77,13 @@ classes = (
|
||||
add_pre_suffix.VIEW3D_OT_add_prefix,
|
||||
numerate.VIEW3D_OT_renaming_numerate,
|
||||
name_from_data.VIEW3D_OT_use_objectname_for_data,
|
||||
case_transform.VIEW3D_OT_case_upper,
|
||||
case_transform.VIEW3D_OT_case_lower,
|
||||
case_transform.VIEW3D_OT_case_pascal,
|
||||
case_transform.VIEW3D_OT_case_camel,
|
||||
case_transform.VIEW3D_OT_case_snake,
|
||||
case_transform.VIEW3D_OT_case_kebab,
|
||||
reload_addon.VIEW3D_OT_reload_addon,
|
||||
)
|
||||
|
||||
enum_sort_items = [('X', "X Axis", "Sort the object based on the X axis."),
|
||||
@@ -120,7 +133,8 @@ def register():
|
||||
options={'ENUM_FLAG'},
|
||||
default={'CURVE', 'LATTICE', 'SURFACE', 'MESH',
|
||||
'ARMATURE', 'LIGHT', 'CAMERA', 'EMPTY', 'GPENCIL',
|
||||
'FONT', 'SPEAKER', 'LIGHT_PROBE', 'VOLUME'}
|
||||
'FONT', 'SPEAKER', 'LIGHT_PROBE', 'VOLUME',
|
||||
'POINTCLOUD'}
|
||||
)
|
||||
|
||||
id_store.renaming_sort_enum = EnumProperty(
|
||||
@@ -166,12 +180,31 @@ def register():
|
||||
id_store.renaming_digits_numerate = IntProperty(name="Number Length", default=3)
|
||||
id_store.renaming_trim_indices = IntVectorProperty(name="Trim Size", default=(0, 0), min=0, soft_min=0, size=2)
|
||||
|
||||
id_store.renaming_active_only = BoolProperty(
|
||||
name="Active Only",
|
||||
description="Only rename the active layer on each object",
|
||||
default=False,
|
||||
)
|
||||
id_store.renaming_filter_by_index = BoolProperty(
|
||||
name="By Index",
|
||||
description="Only rename the layer at the specified index on each object",
|
||||
default=False,
|
||||
)
|
||||
id_store.renaming_index_target = IntProperty(name="Index", default=0, min=0)
|
||||
|
||||
id_store.renaming_also_rename_data = BoolProperty(
|
||||
name="Also Rename Data",
|
||||
description="Also rename the linked data block (mesh, curve, etc.) to match the object name",
|
||||
default=False,
|
||||
)
|
||||
|
||||
from bpy.utils import register_class
|
||||
|
||||
for cls in classes:
|
||||
register_class(cls)
|
||||
|
||||
bpy.app.handlers.depsgraph_update_post.append(PostChange)
|
||||
start_version_check()
|
||||
|
||||
|
||||
def unregister():
|
||||
@@ -192,5 +225,9 @@ def unregister():
|
||||
del IDStore.renaming_base_numerate
|
||||
del IDStore.renaming_digits_numerate
|
||||
del IDStore.renaming_trim_indices
|
||||
del IDStore.renaming_also_rename_data
|
||||
del IDStore.renaming_active_only
|
||||
del IDStore.renaming_filter_by_index
|
||||
del IDStore.renaming_index_target
|
||||
|
||||
bpy.app.handlers.depsgraph_update_post.remove(PostChange)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import time
|
||||
|
||||
import bpy
|
||||
|
||||
from .renaming_operators import switch_to_edit_mode
|
||||
from ..operators.renaming_utilities import get_renaming_list, call_renaming_popup, call_error_popup
|
||||
from ..operators.renaming_utilities import get_renaming_list, call_renaming_popup, call_error_popup, rename_data_if_enabled, update_bone_drivers, log_timing
|
||||
from ..variable_replacer.variable_replacer import VariableReplacer
|
||||
|
||||
|
||||
@@ -23,9 +25,11 @@ class VIEW3D_OT_add_suffix(bpy.types.Operator):
|
||||
call_error_popup(context)
|
||||
return {'CANCELLED'}
|
||||
|
||||
t_start = time.perf_counter()
|
||||
msg = wm.renaming_messages
|
||||
|
||||
VariableReplacer.reset()
|
||||
VariableReplacer.prepare(context)
|
||||
if len(renaming_list) > 0:
|
||||
for entity in renaming_list:
|
||||
if entity is not None:
|
||||
@@ -34,11 +38,15 @@ class VIEW3D_OT_add_suffix(bpy.types.Operator):
|
||||
oldName = entity.name
|
||||
new_name = entity.name + suffix
|
||||
entity.name = new_name
|
||||
rename_data_if_enabled(wm, entity)
|
||||
if wm.renaming_object_types == 'BONE':
|
||||
update_bone_drivers(oldName, entity.name)
|
||||
msg.add_message(oldName, entity.name)
|
||||
else:
|
||||
msg.add_message(None, None, "Insert Valid String")
|
||||
if switch_edit_mode:
|
||||
switch_to_edit_mode(context)
|
||||
log_timing(context, "add_suffix", t_start, len(renaming_list))
|
||||
call_renaming_popup(context)
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -62,7 +70,9 @@ class VIEW3D_OT_add_prefix(bpy.types.Operator):
|
||||
call_error_popup(context)
|
||||
return {'CANCELLED'}
|
||||
|
||||
t_start = time.perf_counter()
|
||||
VariableReplacer.reset()
|
||||
VariableReplacer.prepare(context)
|
||||
|
||||
if len(renaming_list) > 0:
|
||||
for entity in renaming_list:
|
||||
@@ -72,8 +82,12 @@ class VIEW3D_OT_add_prefix(bpy.types.Operator):
|
||||
oldName = entity.name
|
||||
new_name = pre + entity.name
|
||||
entity.name = new_name
|
||||
rename_data_if_enabled(wm, entity)
|
||||
if wm.renaming_object_types == 'BONE':
|
||||
update_bone_drivers(oldName, entity.name)
|
||||
msg.add_message(oldName, entity.name)
|
||||
|
||||
log_timing(context, "add_prefix", t_start, len(renaming_list))
|
||||
call_renaming_popup(context)
|
||||
if switch_edit_mode:
|
||||
switch_to_edit_mode(context)
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import re
|
||||
|
||||
import bpy
|
||||
|
||||
from .renaming_operators import switch_to_edit_mode
|
||||
from ..operators.renaming_utilities import get_renaming_list, call_renaming_popup, call_error_popup, rename_data_if_enabled, update_bone_drivers
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Word splitting — handles snake_case, kebab-case, PascalCase, camelCase
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def split_words(name):
|
||||
"""Split a name into words regardless of input convention."""
|
||||
# camelCase / PascalCase boundaries: lowercase→Uppercase
|
||||
name = re.sub(r'([a-z0-9])([A-Z])', r'\1 \2', name)
|
||||
# Runs of capitals before a capitalised word: XMLParser → XML Parser
|
||||
name = re.sub(r'([A-Z]+)([A-Z][a-z])', r'\1 \2', name)
|
||||
# Replace separators with spaces
|
||||
name = re.sub(r'[-_\s]+', ' ', name)
|
||||
return [w for w in name.split(' ') if w]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Conversion helpers (also imported by search_replace for \u \l \U \L)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def to_upper(text):
|
||||
return text.upper()
|
||||
|
||||
|
||||
def to_lower(text):
|
||||
return text.lower()
|
||||
|
||||
|
||||
def upper_first(text):
|
||||
"""Uppercase only the first character, leave the rest unchanged."""
|
||||
return text[:1].upper() + text[1:] if text else text
|
||||
|
||||
|
||||
def lower_first(text):
|
||||
"""Lowercase only the first character, leave the rest unchanged."""
|
||||
return text[:1].lower() + text[1:] if text else text
|
||||
|
||||
|
||||
def to_pascal_case(name):
|
||||
"""hello_world → HelloWorld"""
|
||||
return ''.join(w.capitalize() for w in split_words(name))
|
||||
|
||||
|
||||
def to_camel_case(name):
|
||||
"""hello_world → helloWorld"""
|
||||
words = split_words(name)
|
||||
if not words:
|
||||
return name
|
||||
return words[0].lower() + ''.join(w.capitalize() for w in words[1:])
|
||||
|
||||
|
||||
def to_snake_case(name):
|
||||
"""HelloWorld → hello_world"""
|
||||
return '_'.join(w.lower() for w in split_words(name))
|
||||
|
||||
|
||||
def to_kebab_case(name):
|
||||
"""HelloWorld → hello-world"""
|
||||
return '-'.join(w.lower() for w in split_words(name))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Operator base
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class _CaseOperatorBase(bpy.types.Operator):
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def _transform(self, name):
|
||||
raise NotImplementedError
|
||||
|
||||
def execute(self, context):
|
||||
scene = context.scene
|
||||
renaming_list, switch_edit_mode, errMsg = get_renaming_list(context)
|
||||
|
||||
if errMsg is not None:
|
||||
scene.renaming_error_messages.add_message(errMsg)
|
||||
call_error_popup(context)
|
||||
return {'CANCELLED'}
|
||||
|
||||
msg = scene.renaming_messages
|
||||
for entity in renaming_list:
|
||||
if entity is not None:
|
||||
old_name = entity.name
|
||||
entity.name = self._transform(entity.name)
|
||||
rename_data_if_enabled(scene, entity)
|
||||
if scene.renaming_object_types == 'BONE':
|
||||
update_bone_drivers(old_name, entity.name)
|
||||
msg.add_message(old_name, entity.name)
|
||||
|
||||
call_renaming_popup(context)
|
||||
if switch_edit_mode:
|
||||
switch_to_edit_mode(context)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Operators
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class VIEW3D_OT_case_upper(_CaseOperatorBase):
|
||||
bl_idname = "renaming.case_upper"
|
||||
bl_label = "UPPERCASE"
|
||||
bl_description = "Convert name to UPPERCASE (hello_world → HELLO_WORLD)"
|
||||
|
||||
def _transform(self, name):
|
||||
return to_upper(name)
|
||||
|
||||
|
||||
class VIEW3D_OT_case_lower(_CaseOperatorBase):
|
||||
bl_idname = "renaming.case_lower"
|
||||
bl_label = "lowercase"
|
||||
bl_description = "Convert name to lowercase (Hello_World → hello_world)"
|
||||
|
||||
def _transform(self, name):
|
||||
return to_lower(name)
|
||||
|
||||
|
||||
class VIEW3D_OT_case_pascal(_CaseOperatorBase):
|
||||
bl_idname = "renaming.case_pascal"
|
||||
bl_label = "PascalCase"
|
||||
bl_description = "Convert name to PascalCase (hello_world → HelloWorld)"
|
||||
|
||||
def _transform(self, name):
|
||||
return to_pascal_case(name)
|
||||
|
||||
|
||||
class VIEW3D_OT_case_camel(_CaseOperatorBase):
|
||||
bl_idname = "renaming.case_camel"
|
||||
bl_label = "camelCase"
|
||||
bl_description = "Convert name to camelCase (hello_world → helloWorld)"
|
||||
|
||||
def _transform(self, name):
|
||||
return to_camel_case(name)
|
||||
|
||||
|
||||
class VIEW3D_OT_case_snake(_CaseOperatorBase):
|
||||
bl_idname = "renaming.case_snake"
|
||||
bl_label = "snake_case"
|
||||
bl_description = "Convert name to snake_case (HelloWorld → hello_world)"
|
||||
|
||||
def _transform(self, name):
|
||||
return to_snake_case(name)
|
||||
|
||||
|
||||
class VIEW3D_OT_case_kebab(_CaseOperatorBase):
|
||||
bl_idname = "renaming.case_kebab"
|
||||
bl_label = "kebab-case"
|
||||
bl_description = "Convert name to kebab-case (HelloWorld → hello-world)"
|
||||
|
||||
def _transform(self, name):
|
||||
return to_kebab_case(name)
|
||||
@@ -1,9 +1,11 @@
|
||||
import time
|
||||
|
||||
import bpy
|
||||
|
||||
from .renaming_operators import getAllVertexGroups, getAllAttributes, getAllBones, getAllModifiers, getAllUvMaps, \
|
||||
getAllColorAttributes, getAllParticleNames, getAllParticleSettingsNames, getAllDataNames, getAllShapeKeys
|
||||
from .renaming_operators import getAllModifiers, \
|
||||
getAllParticleNames, getAllParticleSettingsNames, getAllDataNames
|
||||
from .renaming_operators import switch_to_edit_mode, numerate_entity_name
|
||||
from ..operators.renaming_utilities import get_renaming_list, call_renaming_popup, call_error_popup
|
||||
from ..operators.renaming_utilities import get_renaming_list, call_renaming_popup, call_error_popup, rename_data_if_enabled, update_bone_drivers, log_timing
|
||||
from ..variable_replacer.variable_replacer import VariableReplacer
|
||||
|
||||
|
||||
@@ -27,57 +29,67 @@ class VIEW3D_OT_replace_name(bpy.types.Operator):
|
||||
return {'CANCELLED'}
|
||||
|
||||
old_mode = context.mode
|
||||
t_start = time.perf_counter()
|
||||
|
||||
# settings for numerating the new name
|
||||
msg = scene.renaming_messages
|
||||
|
||||
vertexGroupNameList = []
|
||||
particleSettingsList = []
|
||||
particleList = []
|
||||
uvmapsList = []
|
||||
dataList = []
|
||||
attributeList = []
|
||||
colorAttributeList = []
|
||||
shapeKeyNamesList = []
|
||||
modifierNamesList = []
|
||||
boneList = []
|
||||
per_object_types = {'SHAPEKEYS', 'VERTEXGROUPS', 'UVMAPS', 'COLORATTRIBUTES', 'ATTRIBUTES', 'BONE'}
|
||||
per_obj_owner_items = {
|
||||
'SHAPEKEYS': lambda o: o.key_blocks,
|
||||
'VERTEXGROUPS': lambda o: o.vertex_groups,
|
||||
'UVMAPS': lambda o: o.uv_layers,
|
||||
'COLORATTRIBUTES': lambda o: o.color_attributes,
|
||||
'ATTRIBUTES': lambda o: o.attributes,
|
||||
'BONE': lambda o: o.edit_bones if old_mode == 'EDIT_ARMATURE' else o.bones,
|
||||
}
|
||||
|
||||
particleSettingsList = set()
|
||||
particleList = set()
|
||||
dataList = set()
|
||||
modifierNamesList = set()
|
||||
|
||||
if context.scene.renaming_object_types == 'VERTEXGROUPS':
|
||||
vertexGroupNameList = getAllVertexGroups()
|
||||
if scene.renaming_object_types == 'PARTICLESYSTEM':
|
||||
particleList = getAllParticleNames()
|
||||
particleList = set(getAllParticleNames())
|
||||
if scene.renaming_object_types == 'PARTICLESETTINGS':
|
||||
particleSettingsList = getAllParticleSettingsNames()
|
||||
if context.scene.renaming_object_types == 'UVMAPS':
|
||||
uvmapsList = getAllUvMaps()
|
||||
if context.scene.renaming_object_types == 'COLORATTRIBUTES':
|
||||
colorAttributeList = getAllColorAttributes()
|
||||
if context.scene.renaming_object_types == 'ATTRIBUTES':
|
||||
attributeList = getAllAttributes()
|
||||
if scene.renaming_object_types == 'SHAPEKEYS':
|
||||
shapeKeyNamesList = getAllShapeKeys()
|
||||
particleSettingsList = set(getAllParticleSettingsNames())
|
||||
if scene.renaming_object_types == 'MODIFIERS':
|
||||
modifierNamesList = getAllModifiers()
|
||||
if scene.renaming_object_types == 'BONE':
|
||||
boneList = getAllBones(old_mode)
|
||||
modifierNamesList = set(getAllModifiers())
|
||||
if scene.renaming_object_types == 'DATA':
|
||||
dataList = getAllDataNames()
|
||||
dataList = set(getAllDataNames())
|
||||
|
||||
current_owner = None
|
||||
per_obj_name_list = set()
|
||||
|
||||
VariableReplacer.reset()
|
||||
VariableReplacer.prepare(context)
|
||||
|
||||
if len(str(replaceName)) > 0: # New name != empty
|
||||
if len(renaming_list) > 0: # List of objects to rename != empty
|
||||
for entity in renaming_list:
|
||||
if entity is not None:
|
||||
|
||||
if scene.renaming_object_types in per_object_types:
|
||||
owner = entity.id_data
|
||||
if owner != current_owner:
|
||||
current_owner = owner
|
||||
VariableReplacer.reset()
|
||||
per_obj_name_list = {item.name for item in per_obj_owner_items[scene.renaming_object_types](owner)}
|
||||
|
||||
replaceName = VariableReplacer.replaceInputString(context, scene.renaming_new_name, entity)
|
||||
|
||||
oldName = entity.name
|
||||
new_name = ''
|
||||
|
||||
if not scene.renaming_use_enumerate:
|
||||
entity.name = replaceName
|
||||
msg.add_message(oldName, entity.name)
|
||||
try:
|
||||
entity.name = replaceName
|
||||
rename_data_if_enabled(scene, entity)
|
||||
if scene.renaming_object_types == 'BONE':
|
||||
update_bone_drivers(oldName, entity.name)
|
||||
msg.add_message(oldName, entity.name)
|
||||
except AttributeError:
|
||||
print("Attribute {} is read only".format(replaceName))
|
||||
|
||||
else: # if scene.renaming_use_enumerate == True
|
||||
|
||||
@@ -94,57 +106,37 @@ class VIEW3D_OT_replace_name(bpy.types.Operator):
|
||||
new_name, dataList = numerate_entity_name(context, replaceName, dataList, entity.name,
|
||||
return_type_list=True)
|
||||
|
||||
elif scene.renaming_object_types == 'BONE':
|
||||
new_name, boneList = numerate_entity_name(context, replaceName, boneList, entity.name,
|
||||
return_type_list=True)
|
||||
|
||||
elif scene.renaming_object_types == 'COLLECTION':
|
||||
new_name = numerate_entity_name(context, replaceName, bpy.data.collections, entity.name)
|
||||
|
||||
elif scene.renaming_object_types == 'ACTIONS':
|
||||
new_name = numerate_entity_name(context, replaceName, bpy.data.actions, entity.name)
|
||||
|
||||
elif scene.renaming_object_types == 'SHAPEKEYS':
|
||||
new_name, shapeKeyNamesList = numerate_entity_name(context, replaceName,
|
||||
shapeKeyNamesList, entity.name,
|
||||
elif scene.renaming_object_types in per_object_types:
|
||||
new_name, per_obj_name_list = numerate_entity_name(context, replaceName,
|
||||
per_obj_name_list, entity.name,
|
||||
return_type_list=True)
|
||||
|
||||
elif scene.renaming_object_types == 'MODIFIERS':
|
||||
new_name, modifierNamesList = numerate_entity_name(context, replaceName,
|
||||
modifierNamesList, entity.name,
|
||||
return_type_list=True)
|
||||
elif context.scene.renaming_object_types == 'VERTEXGROUPS':
|
||||
new_name, vertexGroupNameList = numerate_entity_name(context, replaceName,
|
||||
vertexGroupNameList, entity.name,
|
||||
return_type_list=True)
|
||||
|
||||
elif context.scene.renaming_object_types == 'PARTICLESYSTEM':
|
||||
elif scene.renaming_object_types == 'PARTICLESYSTEM':
|
||||
new_name, particleList = numerate_entity_name(context, replaceName,
|
||||
particleList, entity.name,
|
||||
return_type_list=True)
|
||||
|
||||
elif context.scene.renaming_object_types == 'PARTICLESETTINGS':
|
||||
elif scene.renaming_object_types == 'PARTICLESETTINGS':
|
||||
new_name, particleSettingsList = numerate_entity_name(context, replaceName,
|
||||
particleSettingsList, entity.name,
|
||||
return_type_list=True)
|
||||
|
||||
|
||||
|
||||
elif context.scene.renaming_object_types == 'UVMAPS':
|
||||
new_name, uvmapsList = numerate_entity_name(context, replaceName,
|
||||
uvmapsList, entity.name,
|
||||
return_type_list=True)
|
||||
|
||||
elif context.scene.renaming_object_types == 'ATTRIBUTES':
|
||||
new_name, attributeList = numerate_entity_name(context, replaceName,
|
||||
attributeList, entity.name,
|
||||
return_type_list=True)
|
||||
elif context.scene.renaming_object_types == 'COLORATTRIBUTES':
|
||||
new_name, colorAttributeList = numerate_entity_name(context, replaceName,
|
||||
colorAttributeList, entity.name,
|
||||
return_type_list=True)
|
||||
|
||||
try:
|
||||
entity.name = new_name
|
||||
rename_data_if_enabled(scene, entity)
|
||||
if scene.renaming_object_types == 'BONE':
|
||||
update_bone_drivers(oldName, entity.name)
|
||||
msg.add_message(oldName, entity.name)
|
||||
except AttributeError:
|
||||
print("Attribute {} is read only".format(new_name))
|
||||
@@ -153,6 +145,7 @@ class VIEW3D_OT_replace_name(bpy.types.Operator):
|
||||
else: # len(str(replaceName)) <= 0
|
||||
msg.add_message(None, None, "Insert a valid string to replace names")
|
||||
|
||||
log_timing(context, "name_replace", t_start, len(renaming_list))
|
||||
call_renaming_popup(context)
|
||||
if switch_edit_mode:
|
||||
switch_to_edit_mode(context)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import time
|
||||
|
||||
import bpy
|
||||
|
||||
from .renaming_operators import switch_to_edit_mode
|
||||
from .. import __package__ as base_package
|
||||
from ..operators.renaming_utilities import get_renaming_list, call_renaming_popup, call_error_popup
|
||||
from ..operators.renaming_utilities import get_renaming_list, call_renaming_popup, call_error_popup, rename_data_if_enabled, update_bone_drivers, log_timing
|
||||
|
||||
|
||||
class VIEW3D_OT_renaming_numerate(bpy.types.Operator):
|
||||
@@ -32,17 +34,31 @@ class VIEW3D_OT_renaming_numerate(bpy.types.Operator):
|
||||
call_error_popup(context)
|
||||
return {'CANCELLED'}
|
||||
|
||||
per_object_types = {'SHAPEKEYS', 'VERTEXGROUPS', 'UVMAPS', 'COLORATTRIBUTES', 'ATTRIBUTES', 'BONE'}
|
||||
obj_type = wm.renaming_object_types
|
||||
|
||||
t_start = time.perf_counter()
|
||||
if len(renaming_list) > 0:
|
||||
i = 0
|
||||
current_owner = None
|
||||
for entity in renaming_list:
|
||||
if entity is not None:
|
||||
if obj_type in per_object_types:
|
||||
owner = entity.id_data
|
||||
if owner != current_owner:
|
||||
current_owner = owner
|
||||
i = 0
|
||||
oldName = entity.name
|
||||
new_name = entity.name + separator + (
|
||||
'{num:{fill}{width}}'.format(num=(i * step) + start_number, fill='0', width=digits))
|
||||
entity.name = new_name
|
||||
rename_data_if_enabled(wm, entity)
|
||||
if obj_type == 'BONE':
|
||||
update_bone_drivers(oldName, entity.name)
|
||||
msg.add_message(oldName, entity.name)
|
||||
i = i + 1
|
||||
|
||||
log_timing(context, "numerate", t_start, len(renaming_list))
|
||||
call_renaming_popup(context)
|
||||
if switch_edit_mode:
|
||||
switch_to_edit_mode(context)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
|
||||
|
||||
class VIEW3D_OT_reload_addon(Operator):
|
||||
"""Reload all Simple Renaming scripts."""
|
||||
bl_idname = "renaming.reload_addon"
|
||||
bl_label = "Reload Addon"
|
||||
bl_description = "Reload all Simple Renaming scripts"
|
||||
|
||||
def execute(self, context):
|
||||
import importlib
|
||||
import sys
|
||||
|
||||
# Derive the addon root package name by stripping the sub-package suffix.
|
||||
# Works for both legacy addons ("simple_renaming.operators")
|
||||
# and extensions ("bl_ext.user_default.simple_renaming.operators").
|
||||
root_pkg = __package__.rsplit(".", 1)[0]
|
||||
|
||||
# Snapshot the module names now, before any reload happens.
|
||||
# Sort key: deeper modules first (so core.* sub-modules reload before
|
||||
# core.__init__), and alphabetically within the same depth so that
|
||||
# "core.*" always reloads before "operators.*" before "ui.*".
|
||||
mod_names = sorted(
|
||||
[name for name in sys.modules
|
||||
if name == root_pkg or name.startswith(root_pkg + ".")],
|
||||
key=lambda n: (-n.count("."), n),
|
||||
)
|
||||
|
||||
# Defer the actual reload to the next event-loop iteration so that this
|
||||
# operator's own execute() has finished (and its class has been removed
|
||||
# from the call stack) before we unregister and reload everything.
|
||||
def _do_reload():
|
||||
root_mod = sys.modules.get(root_pkg)
|
||||
if root_mod and hasattr(root_mod, "unregister"):
|
||||
try:
|
||||
root_mod.unregister()
|
||||
except Exception as exc:
|
||||
print(f"[RENAMING] unregister error: {exc}")
|
||||
|
||||
for name in mod_names:
|
||||
mod = sys.modules.get(name)
|
||||
if mod is not None:
|
||||
try:
|
||||
importlib.reload(mod)
|
||||
except Exception as exc:
|
||||
print(f"[RENAMING] reload error for '{name}': {exc}")
|
||||
|
||||
# Re-fetch root after in-place reload to pick up any top-level changes.
|
||||
root_mod = sys.modules.get(root_pkg)
|
||||
if root_mod and hasattr(root_mod, "register"):
|
||||
try:
|
||||
root_mod.register()
|
||||
except Exception as exc:
|
||||
print(f"[RENAMING] register error: {exc}")
|
||||
|
||||
print(f"[RENAMING] Reloaded {len(mod_names)} modules from '{root_pkg}'")
|
||||
|
||||
bpy.app.timers.register(_do_reload, first_interval=0.0)
|
||||
self.report({'INFO'}, f"Queued reload of {len(mod_names)} modules…")
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,62 @@
|
||||
import time
|
||||
|
||||
import bpy
|
||||
|
||||
from ..operators.renaming_utilities import call_renaming_popup, call_error_popup, log_timing
|
||||
|
||||
INDEXED_TYPES = ('UVMAPS', 'COLORATTRIBUTES', 'ATTRIBUTES', 'VERTEXGROUPS', 'SHAPEKEYS')
|
||||
|
||||
_accessor_map = {
|
||||
'UVMAPS': lambda obj: obj.data.uv_layers,
|
||||
'COLORATTRIBUTES': lambda obj: obj.data.color_attributes,
|
||||
'ATTRIBUTES': lambda obj: obj.data.attributes,
|
||||
'VERTEXGROUPS': lambda obj: obj.vertex_groups,
|
||||
'SHAPEKEYS': lambda obj: obj.data.shape_keys.key_blocks if obj.data and obj.data.shape_keys else [],
|
||||
}
|
||||
|
||||
|
||||
class VIEW3D_OT_rename_by_index(bpy.types.Operator):
|
||||
bl_idname = "renaming.rename_by_index"
|
||||
bl_label = "Rename Slot"
|
||||
bl_description = "Rename the item at the specified index on each selected object"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
scene = context.scene
|
||||
target_index = scene.renaming_index_target
|
||||
new_name = scene.renaming_index_new_name
|
||||
entity_type = scene.renaming_object_types
|
||||
msg = scene.renaming_messages
|
||||
|
||||
if not new_name:
|
||||
error_msg = scene.renaming_error_messages
|
||||
error_msg.add_message("Name field is empty")
|
||||
call_error_popup(context)
|
||||
return {'CANCELLED'}
|
||||
|
||||
get_collection = _accessor_map.get(entity_type)
|
||||
if get_collection is None:
|
||||
return {'CANCELLED'}
|
||||
|
||||
obj_list = context.selected_objects.copy() if scene.renaming_only_selection else list(bpy.data.objects)
|
||||
|
||||
t_start = time.perf_counter()
|
||||
renamed = 0
|
||||
for obj in obj_list:
|
||||
if obj.type != 'MESH':
|
||||
continue
|
||||
try:
|
||||
items = list(get_collection(obj))
|
||||
if target_index < len(items):
|
||||
item = items[target_index]
|
||||
old_name = item.name
|
||||
item.name = new_name
|
||||
msg.add_message(old_name, item.name)
|
||||
renamed += 1
|
||||
except Exception as e:
|
||||
self.report({'WARNING'}, f"Skipped {obj.name}: {e}")
|
||||
continue
|
||||
|
||||
log_timing(context, "rename_by_index", t_start, renamed)
|
||||
call_renaming_popup(context)
|
||||
return {'FINISHED'}
|
||||
@@ -29,120 +29,64 @@ def numerate_entity_name(context, basename, type_list, active_entity_name, retur
|
||||
'{num:{fill}{width}}'.format(num=(i * step) + start_number, fill='0', width=digits))
|
||||
i += 1
|
||||
|
||||
if return_type_list: # Manually add new name to custom generated list like all bones and all shape keys
|
||||
type_list.append(new_name)
|
||||
if return_type_list: # Manually add new name to custom generated set like all bones and all shape keys
|
||||
type_list.add(new_name)
|
||||
return new_name, type_list
|
||||
|
||||
return new_name
|
||||
|
||||
|
||||
def getAllBones(mode):
|
||||
"""Get list of all bones depending on Edit or Pose Mode"""
|
||||
boneList = []
|
||||
|
||||
for arm in bpy.data.armatures:
|
||||
if mode == 'POSE':
|
||||
for bone in arm.bones:
|
||||
boneList.append(bone.name)
|
||||
else: # mode == 'EDIT':
|
||||
for bone in arm.edit_bones:
|
||||
boneList.append(bone.name)
|
||||
|
||||
return boneList
|
||||
"""Get list of all bone names depending on Edit or Pose Mode"""
|
||||
if mode == 'POSE':
|
||||
return [bone.name for arm in bpy.data.armatures for bone in arm.bones]
|
||||
else: # mode == 'EDIT'
|
||||
return [bone.name for arm in bpy.data.armatures for bone in arm.edit_bones]
|
||||
|
||||
|
||||
def getAllModifiers():
|
||||
"""get list of all modifiers"""
|
||||
modifierList = []
|
||||
|
||||
for obj in bpy.data.objects:
|
||||
for mod in obj.modifiers:
|
||||
modifierList.append(mod.name)
|
||||
|
||||
return modifierList
|
||||
"""get list of all modifier names"""
|
||||
return [mod.name for obj in bpy.data.objects for mod in obj.modifiers]
|
||||
|
||||
|
||||
def getAllShapeKeys():
|
||||
"""get list of all shape keys"""
|
||||
shapeKeyNamesList = []
|
||||
|
||||
for key_grp in bpy.data.shape_keys:
|
||||
for key in key_grp.key_blocks:
|
||||
shapeKeyNamesList.append(key.name)
|
||||
|
||||
return shapeKeyNamesList
|
||||
"""get list of all shape key names"""
|
||||
return [key.name for key_grp in bpy.data.shape_keys for key in key_grp.key_blocks]
|
||||
|
||||
|
||||
def getAllVertexGroups():
|
||||
"""get list of all vertex groups"""
|
||||
vrtx_grp_names_list = []
|
||||
|
||||
for obj in bpy.data.objects:
|
||||
for vrtGrp in obj.vertex_groups:
|
||||
vrtx_grp_names_list.append(vrtGrp.name)
|
||||
|
||||
return vrtx_grp_names_list
|
||||
"""get list of all vertex group names"""
|
||||
return [vg.name for obj in bpy.data.objects for vg in obj.vertex_groups]
|
||||
|
||||
|
||||
def getAllParticleNames():
|
||||
"""get list of all particle systems"""
|
||||
particlesNamesList = []
|
||||
|
||||
for obj in bpy.data.objects:
|
||||
for particle_system in obj.particle_systems:
|
||||
particlesNamesList.append(particle_system.name)
|
||||
return particlesNamesList
|
||||
"""get list of all particle system names"""
|
||||
return [ps.name for obj in bpy.data.objects for ps in obj.particle_systems]
|
||||
|
||||
|
||||
def getAllParticleSettingsNames():
|
||||
"""get list of all particle settings"""
|
||||
particlesNamesList = []
|
||||
for par in bpy.data.particles:
|
||||
particlesNamesList.append(par.name)
|
||||
|
||||
return particlesNamesList
|
||||
"""get list of all particle settings names"""
|
||||
return [par.name for par in bpy.data.particles]
|
||||
|
||||
|
||||
def getAllUvMaps():
|
||||
uvNamesList = []
|
||||
for obj in bpy.data.objects:
|
||||
if obj.type != 'MESH':
|
||||
continue
|
||||
for uv in obj.data.uv_layers:
|
||||
uvNamesList.append(uv)
|
||||
return uvNamesList
|
||||
"""get list of all UV map names"""
|
||||
return [uv.name for obj in bpy.data.objects if obj.type == 'MESH'
|
||||
for uv in obj.data.uv_layers]
|
||||
|
||||
|
||||
def getAllColorAttributes():
|
||||
colorAttributesList = []
|
||||
|
||||
for obj in bpy.data.objects:
|
||||
if obj.type != 'MESH':
|
||||
continue
|
||||
for color_attribute in obj.data.color_attributes:
|
||||
colorAttributesList.append(color_attribute)
|
||||
|
||||
return colorAttributesList
|
||||
"""get list of all color attribute names"""
|
||||
return [ca.name for obj in bpy.data.objects if obj.type == 'MESH'
|
||||
for ca in obj.data.color_attributes]
|
||||
|
||||
|
||||
def getAllAttributes():
|
||||
attributesList = []
|
||||
|
||||
for obj in bpy.data.objects:
|
||||
if obj.type != 'MESH':
|
||||
continue
|
||||
for color_attribute in obj.data.color_attributes:
|
||||
attributesList.append(color_attribute)
|
||||
|
||||
return attributesList
|
||||
"""get list of all attribute names"""
|
||||
return [attr.name for obj in bpy.data.objects if obj.type == 'MESH'
|
||||
for attr in obj.data.attributes]
|
||||
|
||||
|
||||
def getAllDataNames():
|
||||
"""get list of all data"""
|
||||
dataList = []
|
||||
|
||||
for obj in bpy.data.objects:
|
||||
if obj.data is not None:
|
||||
dataList.append(obj.data.name)
|
||||
|
||||
return dataList
|
||||
"""get list of all data names"""
|
||||
return [obj.data.name for obj in bpy.data.objects if obj.data is not None]
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
import time
|
||||
|
||||
import bpy
|
||||
from bpy.types import PoseBone, EditBone
|
||||
|
||||
from .. import __package__ as base_package
|
||||
|
||||
|
||||
def log_timing(context, label, t_start, entity_count):
|
||||
"""Print elapsed time to the console when debug_timing is enabled."""
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
if not prefs.debug_timing:
|
||||
return
|
||||
elapsed_ms = (time.perf_counter() - t_start) * 1000
|
||||
print(f"[RENAMING] {label}: {elapsed_ms:.1f} ms ({entity_count} entities, "
|
||||
f"{elapsed_ms / entity_count:.3f} ms/entity)" if entity_count else
|
||||
f"[RENAMING] {label}: {elapsed_ms:.1f} ms")
|
||||
|
||||
|
||||
def trim_string(string, size):
|
||||
return string[size[0]:max(0, len(string)-size[1])]
|
||||
|
||||
@@ -38,12 +51,14 @@ def get_renaming_list(context):
|
||||
|
||||
if scene.renaming_object_types == 'OBJECT':
|
||||
for obj in obj_list:
|
||||
if obj in obj_list and obj.type in scene.renaming_object_types_specified:
|
||||
if obj.type in scene.renaming_object_types_specified:
|
||||
renaming_list.append(obj)
|
||||
|
||||
elif scene.renaming_object_types == 'DATA':
|
||||
seen_data = set()
|
||||
for obj in obj_list:
|
||||
if obj.data not in renaming_list:
|
||||
if obj.data is not None and id(obj.data) not in seen_data:
|
||||
seen_data.add(id(obj.data))
|
||||
renaming_list.append(obj.data)
|
||||
|
||||
elif scene.renaming_object_types == 'MATERIAL':
|
||||
@@ -117,14 +132,25 @@ def get_renaming_list(context):
|
||||
renaming_list = list(bpy.data.collections)
|
||||
|
||||
elif scene.renaming_object_types == 'SHAPEKEYS':
|
||||
filter_index = scene.renaming_filter_by_index
|
||||
idx = scene.renaming_index_target
|
||||
if selection_only:
|
||||
for obj in context.selected_objects:
|
||||
for shape in obj.data.shape_keys.key_blocks:
|
||||
renaming_list.append(shape)
|
||||
if obj.data and obj.data.shape_keys:
|
||||
items = list(obj.data.shape_keys.key_blocks)
|
||||
if filter_index:
|
||||
if idx < len(items):
|
||||
renaming_list.append(items[idx])
|
||||
else:
|
||||
renaming_list.extend(items)
|
||||
else: # selection_only == False:
|
||||
for key_grp in bpy.data.shape_keys:
|
||||
for key in key_grp.key_blocks:
|
||||
renaming_list.append(key)
|
||||
items = list(key_grp.key_blocks)
|
||||
if filter_index:
|
||||
if idx < len(items):
|
||||
renaming_list.append(items[idx])
|
||||
else:
|
||||
renaming_list.extend(items)
|
||||
|
||||
elif scene.renaming_object_types == 'MODIFIERS':
|
||||
if selection_only:
|
||||
@@ -137,14 +163,16 @@ def get_renaming_list(context):
|
||||
renaming_list.append(mod)
|
||||
|
||||
elif context.scene.renaming_object_types == 'VERTEXGROUPS':
|
||||
if selection_only:
|
||||
for obj in context.selected_objects:
|
||||
for vtx in obj.vertex_groups:
|
||||
renaming_list.append(vtx)
|
||||
else:
|
||||
for obj in bpy.data.objects:
|
||||
for vtx in obj.vertex_groups:
|
||||
renaming_list.append(vtx)
|
||||
filter_index = scene.renaming_filter_by_index
|
||||
idx = scene.renaming_index_target
|
||||
obj_iter = context.selected_objects if selection_only else bpy.data.objects
|
||||
for obj in obj_iter:
|
||||
items = list(obj.vertex_groups)
|
||||
if filter_index:
|
||||
if idx < len(items):
|
||||
renaming_list.append(items[idx])
|
||||
else:
|
||||
renaming_list.extend(items)
|
||||
|
||||
elif context.scene.renaming_object_types == 'PARTICLESYSTEM':
|
||||
if selection_only:
|
||||
@@ -162,27 +190,64 @@ def get_renaming_list(context):
|
||||
|
||||
elif context.scene.renaming_object_types == 'UVMAPS':
|
||||
|
||||
filter_index = scene.renaming_filter_by_index
|
||||
active_only = scene.renaming_active_only
|
||||
idx = scene.renaming_index_target
|
||||
for obj in obj_list:
|
||||
if obj.type != 'MESH':
|
||||
continue
|
||||
for uv in obj.data.uv_layers:
|
||||
renaming_list.append(uv)
|
||||
if filter_index:
|
||||
items = list(obj.data.uv_layers)
|
||||
if idx < len(items):
|
||||
item = items[idx]
|
||||
if not active_only or obj.data.uv_layers.active == item:
|
||||
renaming_list.append(item)
|
||||
elif active_only:
|
||||
active = obj.data.uv_layers.active
|
||||
if active is not None:
|
||||
renaming_list.append(active)
|
||||
else:
|
||||
for uv in obj.data.uv_layers:
|
||||
renaming_list.append(uv)
|
||||
|
||||
elif context.scene.renaming_object_types == 'COLORATTRIBUTES':
|
||||
|
||||
filter_index = scene.renaming_filter_by_index
|
||||
active_only = scene.renaming_active_only
|
||||
idx = scene.renaming_index_target
|
||||
for obj in obj_list:
|
||||
if obj.type != 'MESH':
|
||||
continue
|
||||
for color_attribute in obj.data.color_attributes:
|
||||
renaming_list.append(color_attribute)
|
||||
if filter_index:
|
||||
items = list(obj.data.color_attributes)
|
||||
if idx < len(items):
|
||||
item = items[idx]
|
||||
if not active_only or obj.data.color_attributes.active_color == item:
|
||||
renaming_list.append(item)
|
||||
elif active_only:
|
||||
active = obj.data.color_attributes.active_color
|
||||
if active is not None:
|
||||
renaming_list.append(active)
|
||||
else:
|
||||
for color_attribute in obj.data.color_attributes:
|
||||
renaming_list.append(color_attribute)
|
||||
|
||||
elif context.scene.renaming_object_types == 'ATTRIBUTES':
|
||||
|
||||
filter_index = scene.renaming_filter_by_index
|
||||
idx = scene.renaming_index_target
|
||||
for obj in obj_list:
|
||||
if obj.type != 'MESH':
|
||||
continue
|
||||
for attribute in obj.data.attributes:
|
||||
renaming_list.append(attribute)
|
||||
items = list(obj.data.attributes)
|
||||
if filter_index:
|
||||
if idx < len(items):
|
||||
renaming_list.append(items[idx])
|
||||
else:
|
||||
renaming_list.extend(items)
|
||||
|
||||
elif scene.renaming_object_types == 'NODE_GROUPS':
|
||||
renaming_list = list(bpy.data.node_groups)
|
||||
|
||||
elif scene.renaming_object_types == 'ACTIONS':
|
||||
if selection_only:
|
||||
@@ -263,6 +328,46 @@ def get_sorted_objects_z(objects):
|
||||
return sorted_objects
|
||||
|
||||
|
||||
def rename_data_if_enabled(scene, entity):
|
||||
if scene.renaming_also_rename_data and \
|
||||
scene.renaming_object_types in ('OBJECT', 'ADDOBJECTS'):
|
||||
if hasattr(entity, 'data') and entity.data is not None:
|
||||
entity.data.name = entity.name
|
||||
|
||||
|
||||
def update_bone_drivers(old_name, new_name):
|
||||
"""Update all driver paths that reference a renamed bone."""
|
||||
if old_name == new_name:
|
||||
return
|
||||
|
||||
# Blender may use either double or single quotes in data_path strings.
|
||||
old_tokens = (f'pose.bones["{old_name}"]', f"pose.bones['{old_name}']")
|
||||
new_token = f'pose.bones["{new_name}"]'
|
||||
|
||||
for datablock in list(bpy.data.objects) + list(bpy.data.scenes):
|
||||
anim_data = getattr(datablock, 'animation_data', None)
|
||||
if anim_data is None:
|
||||
continue
|
||||
for fcurve in anim_data.drivers:
|
||||
# Location 1: FCurve data_path
|
||||
for old_token in old_tokens:
|
||||
if old_token in fcurve.data_path:
|
||||
fcurve.data_path = fcurve.data_path.replace(old_token, new_token)
|
||||
|
||||
driver = fcurve.driver
|
||||
if driver is None:
|
||||
continue
|
||||
for var in driver.variables:
|
||||
for target in var.targets:
|
||||
# Location 2: bone_target field
|
||||
if target.bone_target == old_name:
|
||||
target.bone_target = new_name
|
||||
# Location 3: data_path inside variable target
|
||||
for old_token in old_tokens:
|
||||
if old_token in target.data_path:
|
||||
target.data_path = target.data_path.replace(old_token, new_token)
|
||||
|
||||
|
||||
def clear_order_flag(obj):
|
||||
try:
|
||||
del obj["selection_order"]
|
||||
@@ -271,7 +376,7 @@ def clear_order_flag(obj):
|
||||
|
||||
|
||||
def update_selection_order():
|
||||
if not bpy.context.selected_objects:
|
||||
if not (getattr(bpy.context, 'selected_objects', None) or list(bpy.context.view_layer.objects.selected)):
|
||||
for o in bpy.data.objects:
|
||||
clear_order_flag(o)
|
||||
return
|
||||
|
||||
@@ -1,10 +1,102 @@
|
||||
import re
|
||||
import time
|
||||
|
||||
import bpy
|
||||
|
||||
from .renaming_operators import switch_to_edit_mode
|
||||
from ..operators.renaming_utilities import get_renaming_list, call_renaming_popup, call_error_popup
|
||||
from ..operators.renaming_utilities import get_renaming_list, call_renaming_popup, call_error_popup, rename_data_if_enabled, update_bone_drivers, log_timing
|
||||
from ..variable_replacer.variable_replacer import VariableReplacer
|
||||
from .case_transform import to_upper, to_lower, upper_first, lower_first
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Regex replace with \u \l \U \L case modifier support
|
||||
# Modifiers apply to the immediately following group reference ($N or \N).
|
||||
# \u$1 — uppercase first char of group 1
|
||||
# \l$1 — lowercase first char of group 1
|
||||
# \U$1 — uppercase all of group 1
|
||||
# \L$1 — lowercase all of group 1
|
||||
# Both $1 and \1 are accepted as group references.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _read_group_ref(repl, i, match):
|
||||
"""Read a $N or \\N group reference at position i.
|
||||
Returns (group_value, chars_consumed)."""
|
||||
if i >= len(repl):
|
||||
return '', 0
|
||||
c = repl[i]
|
||||
if c in ('$', '\\') and i + 1 < len(repl) and repl[i + 1].isdigit():
|
||||
group_num = int(repl[i + 1])
|
||||
try:
|
||||
return match.group(group_num) or '', 2
|
||||
except IndexError:
|
||||
return '', 0
|
||||
return '', 0
|
||||
|
||||
|
||||
def _expand_replacement(repl, match):
|
||||
"""Expand a replacement string, handling case modifiers and group refs."""
|
||||
result = []
|
||||
i = 0
|
||||
n = len(repl)
|
||||
|
||||
while i < n:
|
||||
c = repl[i]
|
||||
|
||||
if c == '\\' and i + 1 < n:
|
||||
next_c = repl[i + 1]
|
||||
|
||||
if next_c in ('u', 'l', 'U', 'L'):
|
||||
modifier = next_c
|
||||
i += 2
|
||||
group_val, advance = _read_group_ref(repl, i, match)
|
||||
i += advance
|
||||
if modifier == 'u':
|
||||
group_val = upper_first(group_val)
|
||||
elif modifier == 'l':
|
||||
group_val = lower_first(group_val)
|
||||
elif modifier == 'U':
|
||||
group_val = to_upper(group_val)
|
||||
elif modifier == 'L':
|
||||
group_val = to_lower(group_val)
|
||||
result.append(group_val)
|
||||
|
||||
elif next_c.isdigit():
|
||||
group_num = int(next_c)
|
||||
try:
|
||||
result.append(match.group(group_num) or '')
|
||||
except IndexError:
|
||||
result.append('\\' + next_c)
|
||||
i += 2
|
||||
|
||||
else:
|
||||
result.append(c)
|
||||
i += 1
|
||||
|
||||
elif c == '$' and i + 1 < n and repl[i + 1].isdigit():
|
||||
group_num = int(repl[i + 1])
|
||||
try:
|
||||
result.append(match.group(group_num) or '')
|
||||
except IndexError:
|
||||
result.append(c)
|
||||
i += 2
|
||||
|
||||
else:
|
||||
result.append(c)
|
||||
i += 1
|
||||
|
||||
return ''.join(result)
|
||||
|
||||
|
||||
def regex_case_sub(pattern, repl, string):
|
||||
"""re.sub that additionally supports \\u \\l \\U \\L case modifiers."""
|
||||
if not re.search(r'\\[uUlL]', repl):
|
||||
return re.sub(pattern, repl, string)
|
||||
|
||||
def replacer(match):
|
||||
return _expand_replacement(repl, match)
|
||||
|
||||
return re.sub(pattern, replacer, string)
|
||||
|
||||
|
||||
class VIEW3D_OT_search_and_replace(bpy.types.Operator):
|
||||
@@ -25,11 +117,20 @@ class VIEW3D_OT_search_and_replace(bpy.types.Operator):
|
||||
call_error_popup(context)
|
||||
return {'CANCELLED'}
|
||||
|
||||
t_start = time.perf_counter()
|
||||
searchName = wm.renaming_search
|
||||
|
||||
msg = wm.renaming_messages # variable to save messages
|
||||
|
||||
VariableReplacer.reset()
|
||||
VariableReplacer.prepare(context)
|
||||
|
||||
# When the search string contains no @ variables it is the same for
|
||||
# every entity, so the case-insensitive pattern can be compiled once.
|
||||
search_has_variables = '@' in searchName
|
||||
static_pattern = None
|
||||
if not wm.renaming_useRegex and not wm.renaming_matchcase and not search_has_variables and searchName != '':
|
||||
static_pattern = re.compile(re.escape(searchName), re.IGNORECASE)
|
||||
|
||||
if len(renaming_list) > 0:
|
||||
for entity in renaming_list: # iterate over all objects that are to be renamed
|
||||
@@ -41,19 +142,18 @@ class VIEW3D_OT_search_and_replace(bpy.types.Operator):
|
||||
if not wm.renaming_useRegex:
|
||||
if wm.renaming_matchcase:
|
||||
new_name = str(entity.name).replace(searchReplaced, replaceReplaced)
|
||||
entity.name = new_name
|
||||
msg.add_message(oldName, entity.name)
|
||||
else:
|
||||
replaceSearch = re.compile(re.escape(searchReplaced), re.IGNORECASE)
|
||||
new_name = replaceSearch.sub(replaceReplaced, entity.name)
|
||||
entity.name = new_name
|
||||
msg.add_message(oldName, entity.name)
|
||||
pattern = static_pattern or re.compile(re.escape(searchReplaced), re.IGNORECASE)
|
||||
new_name = pattern.sub(replaceReplaced, entity.name)
|
||||
else: # Use regex
|
||||
# pattern = re.compile(re.escape(searchName))
|
||||
new_name = re.sub(searchReplaced, replaceReplaced, str(entity.name))
|
||||
entity.name = new_name
|
||||
msg.add_message(oldName, entity.name)
|
||||
new_name = regex_case_sub(searchReplaced, replaceReplaced, str(entity.name))
|
||||
entity.name = new_name
|
||||
rename_data_if_enabled(wm, entity)
|
||||
if wm.renaming_object_types == 'BONE':
|
||||
update_bone_drivers(oldName, entity.name)
|
||||
msg.add_message(oldName, entity.name)
|
||||
|
||||
log_timing(context, "search_replace", t_start, len(renaming_list))
|
||||
call_renaming_popup(context)
|
||||
if switch_edit_mode:
|
||||
switch_to_edit_mode(context)
|
||||
|
||||
@@ -29,6 +29,7 @@ class VIEW3D_OT_search_and_select(VIEW3D_OT_naming):
|
||||
|
||||
def execute(self, context):
|
||||
super().execute(context)
|
||||
VariableReplacer.prepare(context)
|
||||
wm = context.scene
|
||||
|
||||
# get list of objects to be selected
|
||||
@@ -56,8 +57,15 @@ class VIEW3D_OT_search_and_select(VIEW3D_OT_naming):
|
||||
selectionList.append(entity)
|
||||
msg.add_message("selected", entityName)
|
||||
else:
|
||||
if re.search(searchReplaced, entityName, re.IGNORECASE):
|
||||
selectionList.append(entity)
|
||||
try:
|
||||
if re.search(searchReplaced, entityName, re.IGNORECASE):
|
||||
selectionList.append(entity)
|
||||
except re.error as err:
|
||||
# invalid regex, add message but continue so other names can still be processed
|
||||
error_msg = f"Invalid regular expression in search: {err}"
|
||||
wm.renaming_error_messages.add_message(error_msg)
|
||||
call_error_popup(context)
|
||||
return {'CANCELLED'}
|
||||
|
||||
if str(wm.renaming_object_types) == 'OBJECT':
|
||||
# set to object mode
|
||||
@@ -74,7 +82,7 @@ class VIEW3D_OT_search_and_select(VIEW3D_OT_naming):
|
||||
if bpy.context.mode == 'POSE':
|
||||
bpy.ops.pose.select_all(action='DESELECT')
|
||||
for bone in selectionList:
|
||||
bone.select = True
|
||||
bone.bone.select = True
|
||||
|
||||
elif bpy.context.mode == 'EDIT_ARMATURE':
|
||||
bpy.ops.armature.select_all(action='DESELECT')
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import time
|
||||
|
||||
import bpy
|
||||
|
||||
from .renaming_operators import switch_to_edit_mode
|
||||
from ..operators.renaming_utilities import get_renaming_list, trim_string, call_renaming_popup, call_error_popup
|
||||
|
||||
from ..operators.renaming_utilities import get_renaming_list, trim_string, call_renaming_popup, call_error_popup, rename_data_if_enabled, update_bone_drivers, log_timing
|
||||
|
||||
|
||||
class VIEW3D_OT_trim_string(bpy.types.Operator):
|
||||
bl_idname = "renaming.trim_string"
|
||||
bl_label = "Trim String"
|
||||
@@ -19,6 +22,7 @@ class VIEW3D_OT_trim_string(bpy.types.Operator):
|
||||
call_error_popup(context)
|
||||
return {'CANCELLED'}
|
||||
|
||||
t_start = time.perf_counter()
|
||||
msg = wm.renaming_messages
|
||||
|
||||
if len(renaming_list) > 0:
|
||||
@@ -27,11 +31,15 @@ class VIEW3D_OT_trim_string(bpy.types.Operator):
|
||||
old_name = entity.name
|
||||
new_name = trim_string(entity.name, wm.renaming_trim_indices)
|
||||
entity.name = new_name
|
||||
rename_data_if_enabled(wm, entity)
|
||||
if wm.renaming_object_types == 'BONE':
|
||||
update_bone_drivers(old_name, entity.name)
|
||||
msg.add_message(old_name, entity.name)
|
||||
|
||||
|
||||
log_timing(context, "trim_string", t_start, len(renaming_list))
|
||||
call_renaming_popup(context)
|
||||
|
||||
if switch_edit_mode:
|
||||
switch_to_edit_mode(context)
|
||||
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import threading
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import json
|
||||
|
||||
# Module-level state — read by the panel draw function
|
||||
update_available = False
|
||||
latest_version_str = ""
|
||||
|
||||
_RELEASES_URL = "https://api.github.com/repos/Weisl/simple_renaming/releases/latest"
|
||||
|
||||
|
||||
def _parse_version(version_str):
|
||||
"""Convert '2.1.4' or 'v2.1.4' to (2, 1, 4)."""
|
||||
return tuple(int(x) for x in version_str.lstrip("v").split("."))
|
||||
|
||||
|
||||
def _fetch():
|
||||
global update_available, latest_version_str
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
_RELEASES_URL,
|
||||
headers={"User-Agent": "simple-renaming-addon"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=5) as response:
|
||||
data = json.loads(response.read().decode())
|
||||
|
||||
tag = data.get("tag_name", "")
|
||||
if not tag:
|
||||
return
|
||||
|
||||
latest = _parse_version(tag)
|
||||
|
||||
# Read current version from blender_manifest.toml at the addon root
|
||||
import os
|
||||
manifest_path = os.path.join(os.path.dirname(__file__), "..", "blender_manifest.toml")
|
||||
current_str = ""
|
||||
with open(manifest_path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
if line.startswith("version"):
|
||||
current_str = line.split("=")[1].strip().strip('"')
|
||||
break
|
||||
|
||||
if not current_str:
|
||||
return
|
||||
|
||||
current = _parse_version(current_str)
|
||||
|
||||
if latest > current:
|
||||
update_available = True
|
||||
latest_version_str = tag.lstrip("v")
|
||||
else:
|
||||
print(f"[RENAMING] Addon is up to date (v{current_str})")
|
||||
|
||||
except Exception as exc:
|
||||
print(f"[RENAMING] version check failed: {exc}")
|
||||
|
||||
|
||||
def start_version_check():
|
||||
"""Fire a background thread to check for a newer release on GitHub."""
|
||||
t = threading.Thread(target=_fetch, daemon=True)
|
||||
t.start()
|
||||
@@ -26,10 +26,6 @@ class BUTTON_OT_change_key(bpy.types.Operator):
|
||||
|
||||
property_prefix: bpy.props.StringProperty()
|
||||
|
||||
def __init__(self):
|
||||
self.prefs = None
|
||||
self.my_event = ''
|
||||
|
||||
def invoke(self, context, event):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
self.prefs = prefs
|
||||
@@ -57,7 +53,7 @@ class BUTTON_OT_change_key(bpy.types.Operator):
|
||||
|
||||
|
||||
def add_keymap():
|
||||
km = bpy.context.window_manager.keyconfigs.addon.keymaps.new(name="Window")
|
||||
km = bpy.context.window_manager.keyconfigs.active.keymaps.new(name="Window")
|
||||
prefs = bpy.context.preferences.addons[base_package].preferences
|
||||
|
||||
kmi = km.keymap_items.new(idname='wm.call_panel', type=prefs.renaming_panel_type, value='PRESS',
|
||||
@@ -80,7 +76,7 @@ def add_key_to_keymap(idname, kmi, km, active=True):
|
||||
def remove_key(context, idname, properties_name):
|
||||
"""Removes addon hotkeys from the keymap"""
|
||||
wm = bpy.context.window_manager
|
||||
km = wm.keyconfigs.addon.keymaps['Window']
|
||||
km = wm.keyconfigs.active.keymaps['Window']
|
||||
|
||||
for kmi in km.keymap_items:
|
||||
if kmi.idname == idname and kmi.properties.name == properties_name:
|
||||
@@ -91,7 +87,7 @@ def remove_keymap():
|
||||
"""Removes keys from the keymap. Currently, this is only called when unregistering the addon. """
|
||||
# only works for menus and pie menus
|
||||
wm = bpy.context.window_manager
|
||||
km = wm.keyconfigs.addon.keymaps['Window']
|
||||
km = wm.keyconfigs.active.keymaps['Window']
|
||||
|
||||
for kmi in km.keymap_items:
|
||||
if hasattr(kmi.properties, 'name') and kmi.properties.name in ['VIEW3D_PT_tools_renaming_panel',
|
||||
|
||||
@@ -7,7 +7,6 @@ from bpy.props import (
|
||||
|
||||
from .renaming_keymap import remove_key
|
||||
from .. import __package__ as base_package
|
||||
from ..ui.renaming_panels import VIEW3D_PT_tools_renaming_panel, VIEW3D_PT_tools_type_suffix
|
||||
|
||||
|
||||
def label_multiline(context, text, parent):
|
||||
@@ -29,7 +28,7 @@ def add_key(km, idname, properties_name, button_assignment_type, button_assignme
|
||||
def update_key(context, operation, operator_name, property_prefix):
|
||||
# This functions gets called when the hotkey assignment is updated in the preferences
|
||||
wm = context.window_manager
|
||||
km = wm.keyconfigs.addon.keymaps["Window"]
|
||||
km = wm.keyconfigs.active.keymaps["Window"]
|
||||
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
|
||||
@@ -51,6 +50,7 @@ def update_suf_pre_key(self, context):
|
||||
|
||||
def update_panel_category(self, context):
|
||||
"""Update panel tab for collider tools"""
|
||||
from ..ui.renaming_panels import VIEW3D_PT_tools_renaming_panel, VIEW3D_PT_tools_type_suffix
|
||||
|
||||
panels = [
|
||||
VIEW3D_PT_tools_renaming_panel,
|
||||
@@ -70,6 +70,7 @@ def update_panel_category(self, context):
|
||||
|
||||
|
||||
def toggle_suffix_prefix_panel(self, context):
|
||||
from ..ui.renaming_panels import VIEW3D_PT_tools_type_suffix
|
||||
if self.renaming_show_suffix_prefix_panel:
|
||||
bpy.utils.register_class(VIEW3D_PT_tools_type_suffix)
|
||||
else:
|
||||
@@ -107,6 +108,12 @@ class VIEW3D_OT_renaming_preferences(bpy.types.AddonPreferences):
|
||||
default=True,
|
||||
)
|
||||
|
||||
debug_timing: bpy.props.BoolProperty(
|
||||
name="Debug Timing",
|
||||
description="Print operator execution time to the console after each rename operation",
|
||||
default=False,
|
||||
)
|
||||
|
||||
renamingPanel_useObjectOrder: bpy.props.BoolProperty(
|
||||
name="Use Selection Order",
|
||||
description="Use the order of selection when renaming objects",
|
||||
@@ -162,6 +169,25 @@ class VIEW3D_OT_renaming_preferences(bpy.types.AddonPreferences):
|
||||
default='',
|
||||
)
|
||||
|
||||
date_format: StringProperty(
|
||||
name="Date Format",
|
||||
description=(
|
||||
"strftime format string for the @d variable. "
|
||||
"Codes: %d=day(03), %m=month(04), %y=year(26), %Y=year(2026), %b=month abbr(Apr). "
|
||||
"Examples: %d%m%Y → 03042026 (DDMMYYYY), %m%d%y → 040326 (MMDDYY), %d%b%Y → 03Apr2026"
|
||||
),
|
||||
default="%y%m%d",
|
||||
)
|
||||
time_format: StringProperty(
|
||||
name="Time Format",
|
||||
description=(
|
||||
"strftime format string for the @i variable. "
|
||||
"Codes: %H=hour 24h(14), %M=minute(30), %S=second(05), %I=hour 12h(02), %p=AM/PM. "
|
||||
"Example: %H%M → 1430. Avoid colons — invalid in filenames on Windows"
|
||||
),
|
||||
default="%H%M",
|
||||
)
|
||||
|
||||
renaming_show_suffix_prefix_panel: bpy.props.BoolProperty(
|
||||
name="Prefix/Suffix by Type Panel",
|
||||
description="Enable or disable the Prefix/Suffix by Type Panel",
|
||||
@@ -185,7 +211,7 @@ class VIEW3D_OT_renaming_preferences(bpy.types.AddonPreferences):
|
||||
"renamingPanel_showPopup",
|
||||
"renaming_show_suffix_prefix_panel",
|
||||
"renamingPanel_useObjectOrder",
|
||||
|
||||
"debug_timing",
|
||||
]
|
||||
props_naming = [
|
||||
"renaming_separator",
|
||||
@@ -205,6 +231,11 @@ class VIEW3D_OT_renaming_preferences(bpy.types.AddonPreferences):
|
||||
"renaming_user3"
|
||||
]
|
||||
|
||||
props_date_time = [
|
||||
"date_format",
|
||||
"time_format",
|
||||
]
|
||||
|
||||
renaming_panel_type: bpy.props.StringProperty(
|
||||
name="Renaming Popup",
|
||||
default="F2",
|
||||
@@ -325,6 +356,13 @@ class VIEW3D_OT_renaming_preferences(bpy.types.AddonPreferences):
|
||||
row = box.row()
|
||||
row.prop(self, propName)
|
||||
|
||||
box = layout.box()
|
||||
row = box.row()
|
||||
row.label(text='Date & Time Variables')
|
||||
for propName in self.props_date_time:
|
||||
row = box.row()
|
||||
row.prop(self, propName)
|
||||
|
||||
box = layout.box()
|
||||
row = box.row()
|
||||
row.label(text='User Variables')
|
||||
|
||||
@@ -2,7 +2,7 @@ import bpy
|
||||
|
||||
from .info_messages import RENAMING_MESSAGES, WarningError_MESSAGES, INFO_MESSAGES
|
||||
from .renaming_panels import VIEW3D_PT_tools_renaming_panel, VIEW3D_PT_tools_type_suffix, VIEW3D_OT_SetVariable, \
|
||||
VIEW3D_OT_RenamingPopupOperator, OBJECT_MT_suffix_prefix_presets, AddPresetRenamingPresets
|
||||
VIEW3D_OT_RenamingPopupOperator, OBJECT_MT_suffix_prefix_presets, AddPresetRenamingPresets, RENAMING_MT_caseMenu
|
||||
from .renaming_panels import panel_func
|
||||
from .renaming_popup import VIEW3D_PT_renaming_popup, VIEW3D_PT_info_popup, VIEW3D_PT_error_popup
|
||||
from .renaming_variables import RENAMING_MT_variableMenu, VIEW3D_OT_inputVariables
|
||||
@@ -10,6 +10,7 @@ from .ui_helpers import PREFERENCES_OT_open_addon
|
||||
|
||||
classes = (
|
||||
RENAMING_MT_variableMenu,
|
||||
RENAMING_MT_caseMenu,
|
||||
VIEW3D_OT_inputVariables,
|
||||
VIEW3D_PT_error_popup,
|
||||
VIEW3D_PT_info_popup,
|
||||
@@ -42,6 +43,8 @@ def register():
|
||||
def unregister():
|
||||
from bpy.utils import unregister_class
|
||||
|
||||
VIEW3D_PT_tools_type_suffix.remove(panel_func)
|
||||
|
||||
for cls in reversed(classes):
|
||||
unregister_class(cls)
|
||||
|
||||
|
||||
@@ -15,6 +15,13 @@ types_of_selected = (
|
||||
|
||||
|
||||
def draw_renaming_panel(layout, context):
|
||||
from ..operators.version_check import update_available, latest_version_str
|
||||
|
||||
if update_available:
|
||||
row = layout.row(align=True)
|
||||
row.alert = True
|
||||
row.label(text=f"Update available: v{latest_version_str}", icon='ERROR')
|
||||
|
||||
scene = context.scene
|
||||
|
||||
row = layout.row(align=True)
|
||||
@@ -25,8 +32,18 @@ def draw_renaming_panel(layout, context):
|
||||
# SELECTED
|
||||
if str(scene.renaming_object_types) == 'OBJECT':
|
||||
layout.prop(scene, "renaming_object_types_specified", expand=True)
|
||||
layout.prop(scene, "renaming_also_rename_data")
|
||||
if str(scene.renaming_object_types) in types_of_selected:
|
||||
layout.prop(scene, "renaming_only_selection", text="Only Of Selected Objects")
|
||||
if str(scene.renaming_object_types) in ('UVMAPS', 'COLORATTRIBUTES', 'ATTRIBUTES', 'VERTEXGROUPS', 'SHAPEKEYS'):
|
||||
col = layout.column(align=True)
|
||||
if str(scene.renaming_object_types) in ('UVMAPS', 'COLORATTRIBUTES'):
|
||||
col.prop(scene, "renaming_active_only")
|
||||
row = col.row(align=True)
|
||||
row.prop(scene, "renaming_filter_by_index")
|
||||
sub = row.row(align=True)
|
||||
sub.enabled = scene.renaming_filter_by_index
|
||||
sub.prop(scene, "renaming_index_target", text="")
|
||||
elif str(scene.renaming_object_types) in types_selected:
|
||||
layout.prop(scene, "renaming_only_selection", text="Only Selected")
|
||||
elif str(scene.renaming_object_types) == 'COLLECTION':
|
||||
@@ -44,7 +61,7 @@ def draw_renaming_panel(layout, context):
|
||||
|
||||
box = layout
|
||||
# Sorting
|
||||
if str(scene.renaming_object_types) not in ['COLLECTION', 'IMAGE']:
|
||||
if str(scene.renaming_object_types) not in ['COLLECTION', 'IMAGE', 'NODE_GROUPS']:
|
||||
col = box.column(align=True)
|
||||
col.prop(scene, "renaming_sorting")
|
||||
if scene.renaming_sorting:
|
||||
@@ -140,7 +157,9 @@ def draw_renaming_panel(layout, context):
|
||||
layout.label(text="Other")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("renaming.numerate", icon="LINENUMBERS_ON")
|
||||
row.operator("renaming.numerate", icon="LINENUMBERS_ON")
|
||||
row = layout.row(align=True)
|
||||
row.menu("RENAMING_MT_case_menu", text="Case Transform")
|
||||
|
||||
if str(scene.renaming_object_types) in ('DATA', 'OBJECT', 'ADDOBJECTS'):
|
||||
layout.separator()
|
||||
@@ -178,6 +197,7 @@ class VIEW3D_PT_tools_renaming_panel(bpy.types.Panel):
|
||||
op = row.operator("preferences.rename_addon_search", text="", icon='PREFERENCES')
|
||||
op.addon_name = addon_name
|
||||
op.prefs_tabs = 'UI'
|
||||
row.operator("renaming.reload_addon", text="", icon='FILE_REFRESH')
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
@@ -282,10 +302,30 @@ class VIEW3D_PT_tools_type_suffix(bpy.types.Panel):
|
||||
row.prop(scene, "renaming_suffix_prefix_lightprops", text="")
|
||||
row.operator('renaming.add_suffix_prefix_by_type', text="Light Probes").option = 'lightprops'
|
||||
|
||||
row = col.row()
|
||||
row.prop(scene, "renaming_suffix_prefix_pointcloud", text="")
|
||||
row.operator('renaming.add_suffix_prefix_by_type', text="Point Clouds").option = 'pointcloud'
|
||||
|
||||
row = col.row()
|
||||
row.operator('renaming.add_suffix_prefix_by_type', text="Rename All").option = 'all'
|
||||
|
||||
|
||||
class RENAMING_MT_caseMenu(bpy.types.Menu):
|
||||
bl_label = "Case"
|
||||
bl_idname = "RENAMING_MT_case_menu"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator("renaming.case_upper", text="UPPERCASE")
|
||||
layout.operator("renaming.case_lower", text="lowercase")
|
||||
layout.separator()
|
||||
layout.operator("renaming.case_pascal", text="PascalCase")
|
||||
layout.operator("renaming.case_camel", text="camelCase")
|
||||
layout.separator()
|
||||
layout.operator("renaming.case_snake", text="snake_case")
|
||||
layout.operator("renaming.case_kebab", text="kebab-case")
|
||||
|
||||
|
||||
class VIEW3D_OT_SetVariable(bpy.types.Operator):
|
||||
"""Tooltip"""
|
||||
bl_idname = "object.renaming_set_variable"
|
||||
@@ -353,6 +393,7 @@ class AddPresetRenamingPresets(AddPresetBase, Operator):
|
||||
"scene.renaming_suffix_prefix_bone",
|
||||
"scene.renaming_suffix_prefix_speakers",
|
||||
"scene.renaming_suffix_prefix_lightprops",
|
||||
"scene.renaming_suffix_prefix_pointcloud",
|
||||
]
|
||||
|
||||
# where to store the preset
|
||||
|
||||
@@ -31,10 +31,26 @@ class RENAMING_MT_variableMenu(bpy.types.Menu):
|
||||
layout.operator("object.renaming_multivariables", text="PARENT").renaming_variables = "PARENT"
|
||||
layout.operator("object.renaming_multivariables", text="DATA").renaming_variables = "DATA"
|
||||
layout.operator("object.renaming_multivariables", text="ACTIVE").renaming_variables = "ACTIVE"
|
||||
layout.operator("object.renaming_multivariables", text='FILE').renaming_variables = 'OBJECT'
|
||||
layout.operator("object.renaming_multivariables", text='OBJECT').renaming_variables = 'OBJECT'
|
||||
layout.operator("object.renaming_multivariables", text="TYPE").renaming_variables = "TYPE"
|
||||
layout.operator("object.renaming_multivariables", text="COLLECTION").renaming_variables = "COLLECTION"
|
||||
|
||||
if wm.renaming_object_types == 'NODE_GROUPS':
|
||||
layout.separator()
|
||||
layout.operator("object.renaming_multivariables", text="TYPE").renaming_variables = "TYPE"
|
||||
|
||||
if wm.renaming_object_types in (
|
||||
'UVMAPS', 'MATERIAL', 'BONE', 'MODIFIERS', 'SHAPEKEYS',
|
||||
'VERTEXGROUPS', 'PARTICLESYSTEM', 'COLORATTRIBUTES', 'ATTRIBUTES',
|
||||
):
|
||||
layout.separator()
|
||||
layout.operator("object.renaming_multivariables", text="OBJECT").renaming_variables = "OBJECT"
|
||||
layout.operator("object.renaming_multivariables", text="TYPE").renaming_variables = "TYPE"
|
||||
layout.operator("object.renaming_multivariables", text="PARENT").renaming_variables = "PARENT"
|
||||
layout.operator("object.renaming_multivariables", text="DATA").renaming_variables = "DATA"
|
||||
layout.operator("object.renaming_multivariables", text="ACTIVE").renaming_variables = "ACTIVE"
|
||||
layout.operator("object.renaming_multivariables", text="COLLECTION").renaming_variables = "COLLECTION"
|
||||
|
||||
|
||||
class VIEW3D_OT_inputVariables(bpy.types.Operator):
|
||||
"""Tooltip"""
|
||||
|
||||
@@ -22,7 +22,7 @@ class PREFERENCES_OT_open_addon(bpy.types.Operator):
|
||||
prefs.prefs_tabs = self.prefs_tabs
|
||||
|
||||
import addon_utils
|
||||
mod = addon_utils.addons_fake_modules.get('collider_tools')
|
||||
mod = addon_utils.addons_fake_modules.get('simple_renaming')
|
||||
|
||||
# mod is None the first time the operation is called :/
|
||||
if mod:
|
||||
|
||||
+172
-50
@@ -7,6 +7,11 @@ import bpy
|
||||
|
||||
from .. import __package__ as base_package
|
||||
|
||||
# Single compiled pattern covering all supported variables.
|
||||
# Multi-char tokens (@u1/@u2/@u3) are listed before the single-char fallback
|
||||
# so the alternation matches them first.
|
||||
_VARIABLE_RE = re.compile(r'@(?:u[123]|[fdirhlobantpmc])')
|
||||
|
||||
|
||||
def generate_random_string(string_length=10):
|
||||
"""Generate a random string of fixed length """
|
||||
@@ -23,6 +28,12 @@ class VariableReplacer:
|
||||
step = 1
|
||||
start_number = 0
|
||||
|
||||
# Per-operation lookup caches built by prepare()
|
||||
_collection_cache = {} # obj_name -> concatenated collection names
|
||||
_material_to_obj = {} # material_name -> first owner object name
|
||||
_shape_key_to_obj = {} # id(Key datablock) -> owner object name
|
||||
_mesh_arm_to_obj = {} # id(obj.data) -> owner object name
|
||||
|
||||
@classmethod
|
||||
def reset(cls):
|
||||
"""reset all values to initial state"""
|
||||
@@ -38,44 +49,126 @@ class VariableReplacer:
|
||||
cls.number = 0
|
||||
|
||||
@classmethod
|
||||
def replaceInputString(cls, context, inputText, entity):
|
||||
def prepare(cls, context):
|
||||
"""Build per-operation lookup caches before the rename loop.
|
||||
|
||||
Call this once per operator execution after reset(). The caches turn
|
||||
O(collections × objects) and O(objects) per-entity lookups into O(1).
|
||||
"""
|
||||
# Collection reverse-lookup: obj_name -> concatenated collection names
|
||||
collection_cache = {}
|
||||
for col in bpy.data.collections:
|
||||
for obj in col.objects:
|
||||
if obj.name in collection_cache:
|
||||
collection_cache[obj.name] += col.name
|
||||
else:
|
||||
collection_cache[obj.name] = col.name
|
||||
cls._collection_cache = collection_cache
|
||||
|
||||
# Material -> first owner object name
|
||||
material_to_obj = {}
|
||||
for obj in bpy.data.objects:
|
||||
for slot in obj.material_slots:
|
||||
if slot.material and slot.material.name not in material_to_obj:
|
||||
material_to_obj[slot.material.name] = obj.name
|
||||
cls._material_to_obj = material_to_obj
|
||||
|
||||
# Shape key (Key datablock) -> owner object name
|
||||
shape_key_to_obj = {}
|
||||
mesh_arm_to_obj = {}
|
||||
for obj in bpy.data.objects:
|
||||
if obj.data is None:
|
||||
continue
|
||||
data_id = id(obj.data)
|
||||
if data_id not in mesh_arm_to_obj:
|
||||
mesh_arm_to_obj[data_id] = obj.name
|
||||
if hasattr(obj.data, 'shape_keys') and obj.data.shape_keys is not None:
|
||||
sk_id = id(obj.data.shape_keys)
|
||||
if sk_id not in shape_key_to_obj:
|
||||
shape_key_to_obj[sk_id] = obj.name
|
||||
cls._shape_key_to_obj = shape_key_to_obj
|
||||
cls._mesh_arm_to_obj = mesh_arm_to_obj
|
||||
|
||||
@classmethod
|
||||
def replaceInputString(cls, context, inputText, entity):
|
||||
"""Replace custom variables with the according string"""
|
||||
wm = context.scene
|
||||
cls.addon_prefs = context.preferences.addons[base_package].preferences
|
||||
|
||||
# System and Global Values #
|
||||
inputText = re.sub(r'@f', cls.getfileName(context), inputText) # file name
|
||||
inputText = re.sub(r'@d', cls.getDateName(), inputText) # date
|
||||
inputText = re.sub(r'@i', cls.getTimeName(), inputText) # time
|
||||
inputText = re.sub(r'@r', cls.getRandomString(), inputText)
|
||||
if '@' not in inputText:
|
||||
return inputText
|
||||
|
||||
# UserStrings #
|
||||
inputText = re.sub(r'@h', cls.get_high_variable(), inputText) # high
|
||||
inputText = re.sub(r'@l', cls.get_low_variable(), inputText) # low
|
||||
inputText = re.sub(r'@b', cls.get_cage_variable(), inputText) # cage
|
||||
inputText = re.sub(r'@u1', cls.getuser1(), inputText)
|
||||
inputText = re.sub(r'@u2', cls.getuser2(), inputText)
|
||||
inputText = re.sub(r'@u3', cls.getuser3(), inputText)
|
||||
# Find only the variables present in this template so we skip calling
|
||||
# getters that are not needed (lazy evaluation).
|
||||
vars_present = set(_VARIABLE_RE.findall(inputText))
|
||||
|
||||
# GetScene #
|
||||
inputText = re.sub(r'@a', cls.getActive(context), inputText) # active object
|
||||
inputText = re.sub(r'@n', cls.getNumber(), inputText)
|
||||
replacements = {}
|
||||
|
||||
if '@n' in vars_present:
|
||||
replacements['@n'] = cls.getNumber()
|
||||
if '@f' in vars_present:
|
||||
replacements['@f'] = cls.getfileName(context)
|
||||
if '@d' in vars_present:
|
||||
replacements['@d'] = cls.getDateName()
|
||||
if '@i' in vars_present:
|
||||
replacements['@i'] = cls.getTimeName()
|
||||
if '@r' in vars_present:
|
||||
replacements['@r'] = cls.getRandomString()
|
||||
if '@h' in vars_present:
|
||||
replacements['@h'] = cls.get_high_variable()
|
||||
if '@l' in vars_present:
|
||||
replacements['@l'] = cls.get_low_variable()
|
||||
if '@b' in vars_present:
|
||||
replacements['@b'] = cls.get_cage_variable()
|
||||
if '@u1' in vars_present:
|
||||
replacements['@u1'] = cls.getuser1()
|
||||
if '@u2' in vars_present:
|
||||
replacements['@u2'] = cls.getuser2()
|
||||
if '@u3' in vars_present:
|
||||
replacements['@u3'] = cls.getuser3()
|
||||
if '@a' in vars_present:
|
||||
replacements['@a'] = cls.getActive(context)
|
||||
|
||||
if wm.renaming_object_types == 'OBJECT':
|
||||
# Objects
|
||||
inputText = re.sub(r'@o', cls.getObject(entity), inputText) # object
|
||||
inputText = re.sub(r'@t', cls.getType(entity), inputText) # type
|
||||
inputText = re.sub(r'@p', cls.getParent(entity), inputText) # parent
|
||||
inputText = re.sub(r'@m', cls.getData(entity), inputText) # data
|
||||
inputText = re.sub(r'@c', cls.getCollection(entity), inputText) # collection
|
||||
if '@o' in vars_present:
|
||||
replacements['@o'] = cls.getObject(entity)
|
||||
if '@t' in vars_present:
|
||||
replacements['@t'] = cls.getType(entity)
|
||||
if '@p' in vars_present:
|
||||
replacements['@p'] = cls.getParent(entity)
|
||||
if '@m' in vars_present:
|
||||
replacements['@m'] = cls.getData(entity)
|
||||
if '@c' in vars_present:
|
||||
replacements['@c'] = cls.getCollection(entity)
|
||||
|
||||
if wm.renaming_object_types in (
|
||||
'UVMAPS', 'MATERIAL', 'BONE', 'MODIFIERS', 'SHAPEKEYS',
|
||||
'VERTEXGROUPS', 'PARTICLESYSTEM', 'COLORATTRIBUTES', 'ATTRIBUTES',
|
||||
):
|
||||
owner_obj = bpy.data.objects.get(cls.getOwnerObjectName(entity))
|
||||
if owner_obj is not None:
|
||||
if '@o' in vars_present:
|
||||
replacements['@o'] = owner_obj.name
|
||||
if '@t' in vars_present:
|
||||
replacements['@t'] = cls.getType(owner_obj)
|
||||
if '@p' in vars_present:
|
||||
replacements['@p'] = cls.getParent(owner_obj)
|
||||
if '@m' in vars_present:
|
||||
replacements['@m'] = cls.getData(owner_obj)
|
||||
if '@c' in vars_present:
|
||||
replacements['@c'] = cls.getCollection(owner_obj)
|
||||
|
||||
if wm.renaming_object_types == 'NODE_GROUPS':
|
||||
if '@t' in vars_present:
|
||||
replacements['@t'] = cls.getType(entity)
|
||||
|
||||
# IMAGES #
|
||||
if wm.renaming_object_types == 'IMAGE':
|
||||
inputText = re.sub(r'@r', 'RESOLUTION', inputText)
|
||||
inputText = re.sub(r'@i', 'FILETYPE', inputText)
|
||||
if '@r' in vars_present:
|
||||
replacements['@r'] = 'RESOLUTION'
|
||||
if '@i' in vars_present:
|
||||
replacements['@i'] = 'FILETYPE'
|
||||
|
||||
return inputText
|
||||
return _VARIABLE_RE.sub(lambda m: replacements.get(m.group(), m.group()), inputText)
|
||||
|
||||
@staticmethod
|
||||
def getRandomString():
|
||||
@@ -125,26 +218,24 @@ class VariableReplacer:
|
||||
|
||||
@classmethod
|
||||
def getfileName(cls, context):
|
||||
scn = context.scene
|
||||
|
||||
if bpy.data.is_saved:
|
||||
filename = bpy.path.display_name(context.blend_data.filepath)
|
||||
else:
|
||||
filename = "UNSAVED"
|
||||
# scn.renaming_messages.add_message(oldName, entity.name)
|
||||
context.scene.renaming_error_messages.add_message(
|
||||
"@f variable: file is unsaved, replaced with 'UNSAVED'", isError=False
|
||||
)
|
||||
return filename
|
||||
|
||||
@classmethod
|
||||
def getDateName(cls):
|
||||
t = time.localtime()
|
||||
t = time.mktime(t)
|
||||
return time.strftime("%d%b%Y", time.gmtime(t))
|
||||
date_format = cls.addon_prefs.date_format if cls.addon_prefs else "%d%b%Y"
|
||||
return time.strftime(date_format, time.localtime())
|
||||
|
||||
@classmethod
|
||||
def getTimeName(cls):
|
||||
t = time.localtime()
|
||||
t = time.mktime(t)
|
||||
return time.strftime("%H:%M", time.gmtime(t))
|
||||
time_format = cls.addon_prefs.time_format if cls.addon_prefs else "%H%M"
|
||||
return time.strftime(time_format, time.localtime())
|
||||
|
||||
@classmethod
|
||||
def getActive(cls, context):
|
||||
@@ -161,29 +252,60 @@ class VariableReplacer:
|
||||
|
||||
@classmethod
|
||||
def getType(cls, entity):
|
||||
return str(entity.type)
|
||||
if entity is None:
|
||||
return "NO_TYPE"
|
||||
try:
|
||||
return str(entity.type)
|
||||
except AttributeError:
|
||||
return "NO_TYPE"
|
||||
|
||||
@classmethod
|
||||
def getParent(cls, entity):
|
||||
if entity.parent is not None:
|
||||
return str(entity.parent.name)
|
||||
else:
|
||||
return entity.name
|
||||
if entity is None:
|
||||
return "NO_PARENT"
|
||||
try:
|
||||
if entity.parent is not None:
|
||||
return str(entity.parent.name)
|
||||
else:
|
||||
return entity.name
|
||||
except AttributeError:
|
||||
return "NO_PARENT"
|
||||
|
||||
@classmethod
|
||||
def getData(cls, entity):
|
||||
if entity.data is not None:
|
||||
return str(entity.data.name)
|
||||
else:
|
||||
return entity.name
|
||||
if entity is None:
|
||||
return "NO_DATA"
|
||||
try:
|
||||
if entity.data is not None:
|
||||
return str(entity.data.name)
|
||||
else:
|
||||
return entity.name
|
||||
except AttributeError:
|
||||
return "NO_DATA"
|
||||
|
||||
@classmethod
|
||||
def getCollection(cls, entity):
|
||||
"""O(1) lookup using cache built by prepare()."""
|
||||
return cls._collection_cache.get(entity.name, "")
|
||||
|
||||
collectionew_names = ""
|
||||
for collection in bpy.data.collections:
|
||||
collection_objects = collection.objects
|
||||
if entity.name in collection.objects and entity in collection_objects[:]:
|
||||
collectionew_names += collection.name
|
||||
@classmethod
|
||||
def getOwnerObjectName(cls, entity):
|
||||
"""Find the owner object name using caches built by prepare()."""
|
||||
id_data = getattr(entity, 'id_data', None)
|
||||
if id_data is None:
|
||||
return ""
|
||||
|
||||
return collectionew_names
|
||||
# Modifier, vertex group, particle system, pose bone — id_data is the Object directly
|
||||
if id_data.bl_rna.identifier == 'Object':
|
||||
return id_data.name
|
||||
|
||||
# Shape key — id_data is a Key datablock
|
||||
if id_data.bl_rna.identifier == 'Key':
|
||||
return cls._shape_key_to_obj.get(id(id_data), "")
|
||||
|
||||
# Material — search by material name
|
||||
if id_data.bl_rna.identifier == 'Material':
|
||||
return cls._material_to_obj.get(id_data.name, "")
|
||||
|
||||
# UV layer, bone — id_data is a Mesh or Armature datablock
|
||||
return cls._mesh_arm_to_obj.get(id(id_data), "")
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
"id": "atomic_data_manager",
|
||||
"name": "Atomic Data Manager",
|
||||
"tagline": "Smart cleanup and inspection of Blender data-blocks",
|
||||
"version": "2.5.0",
|
||||
"version": "2.6.2",
|
||||
"type": "add-on",
|
||||
"maintainer": "RaincloudTheDragon",
|
||||
"license": [
|
||||
@@ -70,9 +70,9 @@
|
||||
"management",
|
||||
"cleanup"
|
||||
],
|
||||
"archive_url": "https://github.com/RaincloudTheDragon/atomic-data-manager/releases/download/v2.5.0/Atomic_Data_Manager.v2.5.0.zip",
|
||||
"archive_size": 114674,
|
||||
"archive_hash": "sha256:4b4834ed3910a428d4cb01f1891247ad80089b6c5324fc27c6862b09e81ff1c1"
|
||||
"archive_url": "https://github.com/RaincloudTheDragon/atomic-data-manager/releases/download/v2.6.2/Atomic_Data_Manager.v2.6.2.zip",
|
||||
"archive_size": 121191,
|
||||
"archive_hash": "sha256:1f4af882cdf73d3bb0b8cf1badc094b179bf9e982486ee516c45a6a2d478c05d"
|
||||
},
|
||||
{
|
||||
"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.0",
|
||||
"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.0/Atomic_Data_Manager.v2.6.0.zip",
|
||||
"archive_size": 120303,
|
||||
"archive_hash": "sha256:cf3acc15028c1eb75b99529d10275cafa5454af970d78a2406de48c496df36ac"
|
||||
},
|
||||
{
|
||||
"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.1",
|
||||
"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.1/Atomic_Data_Manager.v2.6.1.zip",
|
||||
"archive_size": 120812,
|
||||
"archive_hash": "sha256:fe4cb2f142e40d5c89b0163b165007daa9ad1d5cbf954224886a4bb312fc7017"
|
||||
},
|
||||
{
|
||||
"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.0",
|
||||
"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.0/Atomic_Data_Manager.v2.6.0.zip",
|
||||
"archive_size": 120303,
|
||||
"archive_hash": "sha256:cf3acc15028c1eb75b99529d10275cafa5454af970d78a2406de48c496df36ac"
|
||||
},
|
||||
{
|
||||
"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,18 +1,44 @@
|
||||
## [v2.6.2] - 2026-04-06
|
||||
|
||||
### Fixes
|
||||
|
||||
- **Clean / unused materials**: if **`Material.users` > 0** (and not only a fake user) but **`material_all()`** finds no references, the material is **no longer** treated as unused—avoids false positives when a **local** material shares a name with a **linked** one or Blender holds other unmodeled refs. The same rule applies to **RNA / Smart Select** scans (the unified scanner uses `analyze_unused_from_graph`, not only `materials_deep`).
|
||||
|
||||
## [v2.6.1] - 2026-03-27
|
||||
|
||||
### Fixes
|
||||
|
||||
- **Stats → Storage**: action byte estimates include **layered/slotted actions** (Blender 4.4+): keyframes in **channel bags** on strips are counted, so values are no longer stuck at the **256 B** minimum when `Action.fcurves` is empty.
|
||||
|
||||
## [v2.6.0] - 2026-03-27
|
||||
|
||||
### Features
|
||||
|
||||
- **Stats for Nerds — Storage**: local blend storage estimates (bytes per datablock type, largest IDs with type + library-override icons and names), including **collections**; packed totals on Overview. Implementation lives in `utils/compat` for reliable `bl_ext` loading.
|
||||
- **Smart Select / Clean (issue #15)**: materials and images used in Geometry Nodes are detected on **library override** objects (not only pure locals); pure linked objects are still skipped via `is_object_linked_without_override`.
|
||||
- **Clean**: before removing rig objects, **unparent** scene objects that would be orphaned (keep transform) and **remove Armature modifiers** targeting deleted objects, with info reports.
|
||||
|
||||
## [v2.5.0] - 2026-01-28
|
||||
|
||||
### Features
|
||||
- Missing file tools: add “Relink All” and improve replacement workflow.
|
||||
|
||||
- Missing file tools:
|
||||
- Finalized Relinker and Linked Library Replacer features.
|
||||
- Add “Relink All” button to Relinker
|
||||
|
||||
### Fixes
|
||||
|
||||
- Missing file UI: fix text field paste + layout/truncation issues; center the detect-missing popup; refine replacement path handling (better dir vs file behavior).
|
||||
- RNA analysis: expand datablock coverage and refine dependency tracking to reduce false “unused” results.
|
||||
|
||||
### Internal
|
||||
|
||||
- Maintenance: remove deprecated recovery option; improve ignore rules for hidden dot-directories.
|
||||
|
||||
## [v2.4.1] - 2026-01-14
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fixed RNA analysis crashes when opening new blend files by rebuilding data-block type references dynamically
|
||||
- Fixed indentation errors that prevented RNA dump from processing most data-blocks
|
||||
- Fixed compositing nodetree detection by adding scenes as root nodes in dependency graph
|
||||
@@ -22,6 +48,7 @@
|
||||
## [v2.4.0] - 2026-01-13
|
||||
|
||||
### Features
|
||||
|
||||
- **Major Architecture Change: RNA-Based Analysis System**
|
||||
- Replaced multi-process worker system with faster, more robust RNA-based dependency analysis
|
||||
- All data types now use unified RNA introspection for dependency tracking
|
||||
@@ -32,6 +59,7 @@
|
||||
- Items now display in 4-column grid layout to reduce vertical scrolling
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fixed node groups used by objects via Geometry Nodes modifiers not being detected as used
|
||||
- Fixed RigidBodyWorld and other scene-linked data-blocks incorrectly flagged as cleanable
|
||||
- Fixed area lights and other object data-blocks in scene collections not being marked as used
|
||||
@@ -39,22 +67,26 @@
|
||||
- Fixed RNA extraction handling for objects' modifier node groups
|
||||
|
||||
### Performance
|
||||
|
||||
- Significantly faster scanning across all categories using RNA analysis
|
||||
- Single-pass dependency graph building shared across all category scans
|
||||
|
||||
## [v2.3.1] - 2026-01-13
|
||||
|
||||
### Fixes
|
||||
|
||||
- Integrate proper UDIM detection
|
||||
|
||||
## [v2.3.0] - 2026-01-06
|
||||
|
||||
### Features
|
||||
|
||||
- Added "Enable Debug Prints" preference to control debug console output
|
||||
- Debug messages now only print when this preference is enabled (default: off)
|
||||
- All debug print statements use centralized `config.debug_print()` helper
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fixed preferences not displaying in Blender 5.0 extensions
|
||||
- Preferences now correctly match the full module path (`bl_ext.vscode_development.atomic_data_manager`)
|
||||
- Added safe property setter to handle read-only context errors during file loading
|
||||
@@ -69,6 +101,7 @@
|
||||
## [v2.2.0] - 2026-01-05
|
||||
|
||||
### Features
|
||||
|
||||
- Add loading bars; non-blocking timer-based UI (#10)
|
||||
- Operations no longer freeze the UI during scanning
|
||||
- Real-time progress updates with cancel support at any time
|
||||
@@ -80,6 +113,7 @@
|
||||
- Added manual cache clear operator for testing and debugging
|
||||
|
||||
### Performance
|
||||
|
||||
- Optimized deep scan functions with caching and fast-path checks
|
||||
- Image scanning now uses cached results to avoid redundant scene scans
|
||||
- Early exit for clearly unused images using Blender's built-in user count
|
||||
@@ -88,24 +122,28 @@
|
||||
- Worlds processed one at a time incrementally
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fixed images used only by unused objects being incorrectly flagged as unused (#5)
|
||||
- Fixed material detection in brushes and node groups (#6, #7)
|
||||
- Fixed Clean operator not showing dialog when invoked programmatically (#8)
|
||||
- Improved material detection in inspection tools (brushes, node groups)
|
||||
|
||||
### Internal
|
||||
|
||||
- Refactored scanning architecture for maintainability
|
||||
- Added comprehensive debug output for troubleshooting
|
||||
|
||||
## [v2.1.0] - 2025-12-18
|
||||
|
||||
### Features
|
||||
|
||||
- Added support for detecting unused objects and armatures (#1)
|
||||
- Objects not present in any scene collections are now detected as unused
|
||||
- Armatures not used by any objects in scenes (including direct use, modifiers, and constraints like "Child Of") are detected as unused
|
||||
- Smart Select and Clean operations now support objects and armatures
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fixed material detection in Geometry Nodes Set Material nodes
|
||||
- Materials used in Geometry Nodes' "Set Material" nodes are now correctly detected as used
|
||||
- Fixed legacy issue where materials in node groups (e.g., "outline-highlight" in "box-highlight" node group) were incorrectly flagged as unused
|
||||
@@ -117,38 +155,45 @@
|
||||
- Note: Further performance improvements are limited by Blender's Python API being single-threaded and requiring sequential access to `bpy.data` collections, making true parallelization impossible without risking data corruption
|
||||
|
||||
### Internal
|
||||
|
||||
- Removed incorrect "Remington Creative" copyright notices from newly created files
|
||||
- Updated repository configuration in manifest
|
||||
|
||||
## [v2.0.3] - 2025-12-17
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fixed missing import error in missing file detection
|
||||
|
||||
## [v2.0.2] - 2025-12-17
|
||||
|
||||
### Fixes
|
||||
|
||||
- Atomic now completely ignores all library-linked and override datablocks across all operations, as originally intended.
|
||||
|
||||
## [v2.0.1] - 2025-12-16
|
||||
|
||||
### Fixes
|
||||
|
||||
- Blender 5.0 compatibility: Fixed `AttributeError` when detecting missing library files (Library objects use `packed_file` singular, Image objects use `packed_files` plural in 5.0)
|
||||
- Fixed unregistration errors in Blender 4.5 by using safe unregister functions throughout the codebase
|
||||
|
||||
## [v2.0.0] - Raincloud's first re-release
|
||||
|
||||
### Feature
|
||||
|
||||
- Multi-version Blender support (4.2 LTS, 4.5 LTS, and 5.0)
|
||||
- Version detection utilities in `utils/version.py`
|
||||
- API compatibility layer in `utils/compat.py` for handling version differences
|
||||
- Version detection utilities in `utils/version.py`
|
||||
- API compatibility layer in `utils/compat.py` for handling version differences
|
||||
|
||||
### Fixes
|
||||
|
||||
- Blender 5.0 compatibility: Fixed `AttributeError` when accessing scene compositor node tree (changed from `scene.node_tree` to `scene.compositing_node_tree`)
|
||||
- Collections assigned to `rigidbody_world.collection` are now correctly detected as used
|
||||
|
||||
### Internal
|
||||
|
||||
- GitHub Actions release workflow
|
||||
- Integrated `rainys_repo_bootstrap` into `__init__.py` so the Rainy's Extensions repository is registered on add-on enable and the bootstrap guard resets on disable.
|
||||
- Removed "Support Remington Creative" popup and all related functionality
|
||||
- Removed Support popup preferences
|
||||
- Removed Support popup preferences
|
||||
|
||||
@@ -144,6 +144,13 @@ class ATOMIC_PG_main(bpy.types.PropertyGroup):
|
||||
'FILE', # icon
|
||||
0 # number / id
|
||||
),
|
||||
(
|
||||
'STORAGE',
|
||||
'Storage',
|
||||
'Local datablocks vs linked libraries',
|
||||
'DISK_DRIVE',
|
||||
11
|
||||
),
|
||||
(
|
||||
'COLLECTIONS',
|
||||
'Collections',
|
||||
@@ -239,7 +246,14 @@ class ATOMIC_PG_main(bpy.types.PropertyGroup):
|
||||
def _on_undo_pre(scene):
|
||||
"""Handler called before undo - invalidate cache."""
|
||||
from .ops import main_ops
|
||||
from .utils.compat import invalidate_cache
|
||||
main_ops._invalidate_cache()
|
||||
invalidate_cache()
|
||||
|
||||
|
||||
def _load_post_invalidate_storage(_dummy):
|
||||
from .utils.compat import invalidate_cache
|
||||
invalidate_cache()
|
||||
|
||||
|
||||
def register():
|
||||
@@ -252,6 +266,7 @@ def register():
|
||||
|
||||
# Register undo handler to invalidate cache
|
||||
bpy.app.handlers.undo_pre.append(_on_undo_pre)
|
||||
bpy.app.handlers.load_post.append(_load_post_invalidate_storage)
|
||||
|
||||
# bootstrap Rainy's Extensions repository
|
||||
rainys_repo_bootstrap.register()
|
||||
@@ -264,6 +279,8 @@ def unregister():
|
||||
# Remove undo handler
|
||||
if _on_undo_pre in bpy.app.handlers.undo_pre:
|
||||
bpy.app.handlers.undo_pre.remove(_on_undo_pre)
|
||||
if _load_post_invalidate_storage in bpy.app.handlers.load_post:
|
||||
bpy.app.handlers.load_post.remove(_load_post_invalidate_storage)
|
||||
|
||||
# atomic package unregistration
|
||||
ui.unregister()
|
||||
|
||||
@@ -2,7 +2,7 @@ schema_version = "1.0.0"
|
||||
|
||||
id = "atomic_data_manager"
|
||||
name = "Atomic Data Manager"
|
||||
version = "2.5.0"
|
||||
version = "2.6.2"
|
||||
type = "add-on"
|
||||
author = "RaincloudTheDragon"
|
||||
maintainer = "RaincloudTheDragon"
|
||||
|
||||
@@ -250,7 +250,9 @@ class ATOMIC_OT_clean_all(bpy.types.Operator):
|
||||
clean.lights()
|
||||
clean.materials()
|
||||
clean.node_groups()
|
||||
clean.objects()
|
||||
for msg in clean.detach_scene_objects_from_removal_targets(set(self.unused_objects)):
|
||||
self.report({'INFO'}, msg)
|
||||
clean.objects(cached_list=self.unused_objects)
|
||||
clean.particles()
|
||||
clean.textures()
|
||||
clean.armatures()
|
||||
|
||||
@@ -739,6 +739,14 @@ class ATOMIC_OT_clean(bpy.types.Operator):
|
||||
bpy.ops.atomic.deselect_all()
|
||||
return {'FINISHED'}
|
||||
|
||||
# Keep in-scene objects that are parented to / deformed by objects we delete
|
||||
if atom.objects and self.unused_objects:
|
||||
from .utils import clean as clean_utils
|
||||
for msg in clean_utils.detach_scene_objects_from_removal_targets(
|
||||
set(self.unused_objects)
|
||||
):
|
||||
self.report({'INFO'}, msg)
|
||||
|
||||
# Delete all items synchronously
|
||||
deleted_count = 0
|
||||
for category, unused_list in categories_to_clean:
|
||||
|
||||
@@ -24,6 +24,52 @@ This file contains functions for cleaning out specific data categories.
|
||||
|
||||
import bpy
|
||||
from ...stats import unused
|
||||
from ...utils import compat
|
||||
|
||||
|
||||
def detach_scene_objects_from_removal_targets(object_names_to_remove):
|
||||
"""
|
||||
Unparent (world transform preserved) and remove Armature modifiers pointing at
|
||||
objects that are about to be deleted, for objects that are NOT in the removal
|
||||
set. Prevents in-scene props (e.g. parented to a rig) from being lost when the
|
||||
rig object is removed.
|
||||
|
||||
Returns:
|
||||
list[str]: Messages suitable for operator.report (one string per change).
|
||||
"""
|
||||
if not object_names_to_remove:
|
||||
return []
|
||||
remove = set(object_names_to_remove)
|
||||
reports = []
|
||||
|
||||
for obj in bpy.data.objects:
|
||||
if obj.name in remove:
|
||||
continue
|
||||
if compat.is_library_or_override(obj):
|
||||
continue
|
||||
|
||||
parts = []
|
||||
|
||||
if obj.parent is not None and obj.parent.name in remove:
|
||||
mw = obj.matrix_world.copy()
|
||||
obj.parent = None
|
||||
obj.matrix_world = mw
|
||||
parts.append("unparented (kept world transform)")
|
||||
|
||||
for mod in list(obj.modifiers):
|
||||
if mod.type != 'ARMATURE':
|
||||
continue
|
||||
target = getattr(mod, "object", None)
|
||||
if target is not None and target.name in remove:
|
||||
obj.modifiers.remove(mod)
|
||||
parts.append("removed Armature modifier targeting deleted object")
|
||||
|
||||
if parts:
|
||||
reports.append(
|
||||
"Atomic: “%s”: %s" % (obj.name, "; ".join(parts))
|
||||
)
|
||||
|
||||
return reports
|
||||
|
||||
|
||||
def collections(cached_list=None):
|
||||
@@ -137,7 +183,7 @@ def objects(cached_list=None):
|
||||
object_keys = cached_list
|
||||
else:
|
||||
object_keys = unused.objects_deep()
|
||||
|
||||
|
||||
for object_key in object_keys:
|
||||
if object_key in bpy.data.objects:
|
||||
bpy.data.objects.remove(bpy.data.objects[object_key])
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
"""
|
||||
Statistics and analysis helpers for Atomic Data Manager.
|
||||
|
||||
Blend-file storage estimates live in ``utils.compat`` (merged there so
|
||||
``bl_ext`` dev deploy cannot miss a separate ``utils/blend_storage`` file).
|
||||
"""
|
||||
@@ -915,6 +915,8 @@ def analyze_unused_from_graph(graph, category, include_fake_users=None):
|
||||
Returns:
|
||||
List of unused item names for the specified category
|
||||
"""
|
||||
from . import users
|
||||
|
||||
if include_fake_users is None:
|
||||
include_fake_users = config.include_fake_users
|
||||
|
||||
@@ -1184,6 +1186,20 @@ def analyze_unused_from_graph(graph, category, include_fake_users=None):
|
||||
item_name = datablock.name
|
||||
if (category, item_name) not in used:
|
||||
if item_name not in category_do_not_flag:
|
||||
# Objects that appear in a scene collection must stay (traceable to a scene), even
|
||||
# if the RNA graph missed them (e.g. mesh parented to an out-of-scene armature).
|
||||
if category == 'objects':
|
||||
try:
|
||||
if users.object_all(item_name):
|
||||
continue
|
||||
except (AttributeError, KeyError, RuntimeError, ReferenceError):
|
||||
pass
|
||||
if category == 'materials':
|
||||
try:
|
||||
if datablock.users > 0 and not datablock.use_fake_user:
|
||||
continue
|
||||
except (AttributeError, RuntimeError, ReferenceError):
|
||||
pass
|
||||
unused.append(item_name)
|
||||
except (AttributeError, RuntimeError, ReferenceError):
|
||||
# Datablock may be invalid
|
||||
|
||||
@@ -223,6 +223,10 @@ def materials_deep():
|
||||
# check if material has a fake user or if ignore fake users
|
||||
# is enabled
|
||||
if not material.use_fake_user or config.include_fake_users:
|
||||
# If Blender still counts users but we found none, don't flag (name collisions
|
||||
# with linked IDs, drivers, or refs we don't traverse). Fake-user purge unchanged.
|
||||
if material.users > 0 and not material.use_fake_user:
|
||||
continue
|
||||
unused.append(material.name)
|
||||
else:
|
||||
# Second check: material is used, but check if it's ONLY used by unused objects
|
||||
|
||||
@@ -127,6 +127,8 @@ def _has_any_unused_materials():
|
||||
# First check: standard unused detection
|
||||
if not users.material_all(material.name):
|
||||
if not material.use_fake_user or config.include_fake_users:
|
||||
if material.users > 0 and not material.use_fake_user:
|
||||
continue
|
||||
return True
|
||||
else:
|
||||
# Second check: material is used, but check if it's ONLY used by unused objects
|
||||
|
||||
@@ -416,8 +416,8 @@ def image_geometry_nodes(image_key):
|
||||
from ..utils import compat
|
||||
|
||||
for obj in bpy.data.objects:
|
||||
# Skip library-linked and override objects
|
||||
if compat.is_library_or_override(obj):
|
||||
# Skip purely linked objects; library overrides can have local GN modifiers (#15)
|
||||
if compat.is_object_linked_without_override(obj):
|
||||
continue
|
||||
|
||||
# Check if object is in any scene collection (reuse object_all logic)
|
||||
@@ -583,8 +583,8 @@ def material_geometry_nodes(material_key):
|
||||
from ..utils import compat
|
||||
|
||||
for obj in bpy.data.objects:
|
||||
# Skip library-linked and override objects
|
||||
if compat.is_library_or_override(obj):
|
||||
# Skip purely linked objects; library overrides can have local GN modifiers (#15)
|
||||
if compat.is_object_linked_without_override(obj):
|
||||
continue
|
||||
|
||||
# Check if object is in any scene collection (reuse object_all logic)
|
||||
|
||||
@@ -31,6 +31,13 @@ from bpy.utils import register_class
|
||||
from ..utils import compat
|
||||
from ..stats import count
|
||||
from ..stats import misc
|
||||
from ..utils.compat import (
|
||||
format_bytes,
|
||||
format_embedded_total,
|
||||
get_report,
|
||||
storage_override_icon,
|
||||
storage_type_icon,
|
||||
)
|
||||
from .utils import ui_layouts
|
||||
|
||||
|
||||
@@ -46,6 +53,8 @@ class ATOMIC_PT_stats_panel(bpy.types.Panel):
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
atom = bpy.context.scene.atomic
|
||||
# Keep blend storage scan in sync whenever the stats panel is shown
|
||||
storage_report = get_report()
|
||||
|
||||
# categories selector / header
|
||||
row = layout.row()
|
||||
@@ -66,6 +75,12 @@ class ATOMIC_PT_stats_panel(bpy.types.Panel):
|
||||
row = box.row()
|
||||
row.label(text="Blend File Size: " + misc.blend_size())
|
||||
|
||||
row = box.row()
|
||||
row.label(
|
||||
text="Packed data (local IDs): "
|
||||
+ format_embedded_total(storage_report["total_embedded_packed"])
|
||||
)
|
||||
|
||||
# cateogry statistics
|
||||
split = box.split()
|
||||
|
||||
@@ -115,6 +130,52 @@ class ATOMIC_PT_stats_panel(bpy.types.Panel):
|
||||
# world count
|
||||
col.label(text=str(count.worlds()))
|
||||
|
||||
# local blend storage (excludes linked IDs)
|
||||
elif atom.stats_mode == 'STORAGE':
|
||||
|
||||
row = box.row()
|
||||
row.label(text="Local blend storage", icon='DISK_DRIVE')
|
||||
|
||||
rep = storage_report
|
||||
|
||||
col = box.column(align=True)
|
||||
col.label(
|
||||
text="IDs linked from another .blend (library) are excluded.",
|
||||
icon='INFO',
|
||||
)
|
||||
col.label(text="Packed = exact bytes stored inside this .blend.")
|
||||
col.label(text="Other sizes are estimates; disk file may compress.")
|
||||
col.label(text="Second icon = library override (when applicable).")
|
||||
|
||||
row = col.row()
|
||||
row.label(
|
||||
text="Packed embedded total: "
|
||||
+ format_embedded_total(rep["total_embedded_packed"])
|
||||
)
|
||||
|
||||
col.separator()
|
||||
col.label(text="By datablock type (estimated total)")
|
||||
|
||||
for t, nbytes in rep["by_type"][:12]:
|
||||
col.label(text=" %s: %s" % (t, format_bytes(nbytes)))
|
||||
|
||||
col.separator()
|
||||
col.label(text="Largest local datablocks (top 40)")
|
||||
|
||||
for r in rep["rows"][:40]:
|
||||
split = col.split(factor=0.68)
|
||||
left = split.row(align=True)
|
||||
left.label(icon=storage_type_icon(r["type"]), text="")
|
||||
left.label(
|
||||
icon=storage_override_icon(r.get("is_lib_override", False)),
|
||||
text="",
|
||||
)
|
||||
left.label(text=r["name"])
|
||||
split.label(text=format_bytes(r["size_bytes"]))
|
||||
|
||||
if len(rep["rows"]) > 40:
|
||||
col.label(text=" ...")
|
||||
|
||||
# collection statistics
|
||||
elif atom.stats_mode == 'COLLECTIONS':
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ between Blender 4.2 LTS, 4.5 LTS, and 5.0.
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import bpy
|
||||
from bpy.utils import register_class, unregister_class
|
||||
from . import version
|
||||
@@ -177,3 +178,624 @@ def is_library_or_override(datablock):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def is_object_linked_without_override(obj):
|
||||
"""
|
||||
True if obj comes from another .blend file but is not a library override.
|
||||
|
||||
Override objects live in the current file and may have local modifiers
|
||||
(e.g. Geometry Nodes) that reference local materials or images; those
|
||||
must be scanned. Purely linked objects have no such local stack here.
|
||||
"""
|
||||
lib = getattr(obj, "library", None)
|
||||
ovl = getattr(obj, "override_library", None)
|
||||
return lib is not None and ovl is None
|
||||
|
||||
|
||||
# --- Blend-file storage (lives here so bl_ext dev sync cannot miss a separate module) ---
|
||||
|
||||
_cache_report = None
|
||||
_cache_light = None
|
||||
_cache_vert_sum = None
|
||||
|
||||
|
||||
def invalidate_cache():
|
||||
global _cache_report, _cache_light, _cache_vert_sum
|
||||
_cache_report = None
|
||||
_cache_light = None
|
||||
_cache_vert_sum = None
|
||||
|
||||
|
||||
def _mesh_vertex_sum_sample():
|
||||
"""Cheap geometry signature so cache invalidates on edit without save."""
|
||||
s = 0
|
||||
for m in bpy.data.meshes:
|
||||
try:
|
||||
s += len(m.vertices)
|
||||
except (AttributeError, RuntimeError, ReferenceError):
|
||||
pass
|
||||
if s > 50_000_000:
|
||||
break
|
||||
return s
|
||||
|
||||
|
||||
def _light_fingerprint():
|
||||
fp = bpy.data.filepath
|
||||
try:
|
||||
mt = os.stat(fp).st_mtime if fp else 0
|
||||
except OSError:
|
||||
mt = 0
|
||||
return (
|
||||
fp,
|
||||
mt,
|
||||
len(bpy.data.meshes),
|
||||
len(bpy.data.images),
|
||||
len(bpy.data.materials),
|
||||
len(bpy.data.node_groups),
|
||||
len(bpy.data.objects),
|
||||
len(bpy.data.armatures),
|
||||
len(bpy.data.collections),
|
||||
)
|
||||
|
||||
|
||||
def _skip_linked(id_block):
|
||||
return getattr(id_block, "library", None) is not None
|
||||
|
||||
|
||||
# Set at start of build_report(); object.data IDs whose users are overridden objects
|
||||
_storage_override_data_ids = frozenset()
|
||||
|
||||
|
||||
def _object_data_override_ids():
|
||||
"""IDs used as ob.data for at least one object with a library override."""
|
||||
s = set()
|
||||
for ob in bpy.data.objects:
|
||||
if getattr(ob, "override_library", None) and ob.data is not None:
|
||||
s.add(ob.data)
|
||||
return frozenset(s)
|
||||
|
||||
|
||||
def is_library_override_storage(id_block):
|
||||
"""
|
||||
True if this ID is a library override, including object-data reached only
|
||||
via an overridden Object (obdata may lack override_library).
|
||||
"""
|
||||
if id_block is None:
|
||||
return False
|
||||
if getattr(id_block, "override_library", None):
|
||||
return True
|
||||
return id_block in _storage_override_data_ids
|
||||
|
||||
|
||||
def _override_weight_factor(id_block):
|
||||
return 0.08 if is_library_override_storage(id_block) else 1.0
|
||||
|
||||
|
||||
def _mesh_size_bytes(m):
|
||||
"""Rough serialized footprint estimate (verts/loops/faces), scaled for overrides."""
|
||||
if _skip_linked(m):
|
||||
return None
|
||||
try:
|
||||
v, l, p = len(m.vertices), len(m.loops), len(m.polygons)
|
||||
except (AttributeError, RuntimeError, ReferenceError):
|
||||
return None
|
||||
ow = _override_weight_factor(m)
|
||||
base = (v * 28 + l * 6 + p * 10 + 512) * ow
|
||||
return max(64, int(base))
|
||||
|
||||
|
||||
def _image_entry(img):
|
||||
if _skip_linked(img):
|
||||
return None
|
||||
embedded = 0
|
||||
pf = getattr(img, "packed_file", None)
|
||||
if pf:
|
||||
try:
|
||||
data = pf.data
|
||||
if data:
|
||||
embedded = len(data)
|
||||
except (AttributeError, TypeError, RuntimeError):
|
||||
pass
|
||||
if embedded == 0:
|
||||
pfs = getattr(img, "packed_files", None)
|
||||
if pfs:
|
||||
for p in pfs:
|
||||
try:
|
||||
if hasattr(p, "data") and p.data:
|
||||
embedded += len(p.data)
|
||||
except (AttributeError, RuntimeError):
|
||||
pass
|
||||
ow = _override_weight_factor(img)
|
||||
if embedded > 0:
|
||||
size_b = max(1, int(embedded * ow))
|
||||
return ("images", img.name, embedded, size_b, "packed")
|
||||
size_b = max(1, int(256 * ow))
|
||||
return ("images", img.name, 0, size_b, "external")
|
||||
|
||||
|
||||
def _armature_size_bytes(a):
|
||||
if _skip_linked(a):
|
||||
return None
|
||||
try:
|
||||
n = len(a.bones)
|
||||
except (AttributeError, RuntimeError, ReferenceError):
|
||||
n = 0
|
||||
ow = _override_weight_factor(a)
|
||||
return int((2048 + n * 320) * ow)
|
||||
|
||||
|
||||
def _curve_size_bytes(c):
|
||||
if _skip_linked(c):
|
||||
return None
|
||||
try:
|
||||
n = sum(len(s.points) for s in c.splines)
|
||||
except (AttributeError, RuntimeError, ReferenceError):
|
||||
n = 0
|
||||
ow = _override_weight_factor(c)
|
||||
return int((1024 + n * 24) * ow)
|
||||
|
||||
|
||||
def _node_tree_size_bytes(nt):
|
||||
if not nt or _skip_linked(nt):
|
||||
return None
|
||||
try:
|
||||
n = len(nt.nodes) + len(nt.links)
|
||||
except (AttributeError, RuntimeError, ReferenceError):
|
||||
n = 0
|
||||
ow = _override_weight_factor(nt)
|
||||
return int((2048 + n * 96) * ow)
|
||||
|
||||
|
||||
def _action_keyframe_counts(act):
|
||||
"""
|
||||
Keyframe point count and F-curve count for storage estimate.
|
||||
Blender 4.4+ layered actions store curves in ActionChannelBag under
|
||||
strips; act.fcurves is often empty, so we must walk layers/slots.
|
||||
"""
|
||||
kp, fc = 0, 0
|
||||
layers = getattr(act, "layers", None)
|
||||
if layers and len(layers) > 0:
|
||||
for layer in layers:
|
||||
strips = getattr(layer, "strips", None)
|
||||
if not strips:
|
||||
continue
|
||||
for strip in strips:
|
||||
if not (hasattr(strip, "channelbags") or hasattr(strip, "channelbag")):
|
||||
continue
|
||||
bags = getattr(strip, "channelbags", None)
|
||||
if bags and len(bags) > 0:
|
||||
for bag in bags:
|
||||
for fcurve in bag.fcurves:
|
||||
fc += 1
|
||||
try:
|
||||
kp += len(fcurve.keyframe_points)
|
||||
except (TypeError, AttributeError, RuntimeError, ReferenceError):
|
||||
pass
|
||||
else:
|
||||
slots = getattr(act, "slots", None)
|
||||
if not slots:
|
||||
continue
|
||||
for slot in slots:
|
||||
try:
|
||||
bag = strip.channelbag(slot, ensure=False)
|
||||
except (TypeError, AttributeError, RuntimeError, ReferenceError):
|
||||
bag = None
|
||||
if bag is None:
|
||||
continue
|
||||
for fcurve in bag.fcurves:
|
||||
fc += 1
|
||||
try:
|
||||
kp += len(fcurve.keyframe_points)
|
||||
except (TypeError, AttributeError, RuntimeError, ReferenceError):
|
||||
pass
|
||||
return kp, fc
|
||||
fcurves = getattr(act, "fcurves", None)
|
||||
if fcurves:
|
||||
for fcurve in fcurves:
|
||||
fc += 1
|
||||
try:
|
||||
kp += len(fcurve.keyframe_points)
|
||||
except (TypeError, AttributeError, RuntimeError, ReferenceError):
|
||||
pass
|
||||
return kp, fc
|
||||
|
||||
|
||||
def _action_size_bytes(act):
|
||||
if _skip_linked(act):
|
||||
return None
|
||||
kp, fc = _action_keyframe_counts(act)
|
||||
ow = _override_weight_factor(act)
|
||||
return max(64, int((256 + kp * 20 + fc * 80) * ow))
|
||||
|
||||
|
||||
def _object_size_bytes(ob):
|
||||
if _skip_linked(ob):
|
||||
return None
|
||||
ow = _override_weight_factor(ob)
|
||||
return int(192 * ow)
|
||||
|
||||
|
||||
def _texture_size_bytes(tex):
|
||||
if _skip_linked(tex):
|
||||
return None
|
||||
ow = _override_weight_factor(tex)
|
||||
return int(512 * ow)
|
||||
|
||||
|
||||
def _volume_size_bytes(vol):
|
||||
if _skip_linked(vol):
|
||||
return None
|
||||
ow = _override_weight_factor(vol)
|
||||
return int(4096 * ow)
|
||||
|
||||
|
||||
def _pointcloud_size_bytes(pc):
|
||||
if _skip_linked(pc):
|
||||
return None
|
||||
try:
|
||||
n = len(pc.points)
|
||||
except (AttributeError, RuntimeError, ReferenceError):
|
||||
n = 0
|
||||
ow = _override_weight_factor(pc)
|
||||
return int((512 + n * 16) * ow)
|
||||
|
||||
|
||||
def _sound_entry(snd):
|
||||
if _skip_linked(snd):
|
||||
return None
|
||||
embedded = 0
|
||||
pf = getattr(snd, "packed_file", None)
|
||||
if pf:
|
||||
try:
|
||||
if hasattr(pf, "data") and pf.data:
|
||||
embedded = len(pf.data)
|
||||
except (AttributeError, TypeError, RuntimeError):
|
||||
pass
|
||||
ow = _override_weight_factor(snd)
|
||||
if embedded > 0:
|
||||
return ("sounds", snd.name, embedded, max(1, int(embedded * ow)), "packed")
|
||||
return ("sounds", snd.name, 0, max(1, int(256 * ow)), "external")
|
||||
|
||||
|
||||
def _font_entry(font):
|
||||
if _skip_linked(font):
|
||||
return None
|
||||
embedded = 0
|
||||
pf = getattr(font, "packed_file", None)
|
||||
if pf and hasattr(pf, "data") and pf.data:
|
||||
try:
|
||||
embedded = len(pf.data)
|
||||
except (TypeError, RuntimeError):
|
||||
embedded = 0
|
||||
ow = _override_weight_factor(font)
|
||||
if embedded > 0:
|
||||
return ("fonts", font.name, embedded, max(1, int(embedded * ow)), "packed")
|
||||
return None
|
||||
|
||||
|
||||
def _collection_size_bytes(coll):
|
||||
if _skip_linked(coll):
|
||||
return None
|
||||
try:
|
||||
no = len(coll.objects)
|
||||
nc = len(coll.children)
|
||||
except (AttributeError, RuntimeError, ReferenceError):
|
||||
return None
|
||||
ow = _override_weight_factor(coll)
|
||||
return max(64, int((512 + no * 96 + nc * 256) * ow))
|
||||
|
||||
|
||||
def _fmt_bytes(n):
|
||||
if n >= 1048576:
|
||||
return f"{n / 1048576:.2f} MiB"
|
||||
if n >= 1024:
|
||||
return f"{n / 1024:.2f} KiB"
|
||||
return f"{int(n)} B"
|
||||
|
||||
|
||||
def format_bytes(n):
|
||||
"""Human-readable size for storage estimates."""
|
||||
return _fmt_bytes(n)
|
||||
|
||||
|
||||
_STORAGE_TYPE_ICONS = {
|
||||
"Mesh": "MESH_DATA",
|
||||
"Image": "IMAGE_DATA",
|
||||
"Armature": "ARMATURE_DATA",
|
||||
"Material": "MATERIAL",
|
||||
"Object": "OBJECT_DATA",
|
||||
"Curve": "CURVE_DATA",
|
||||
"NodeTree": "NODETREE",
|
||||
"Action": "ACTION",
|
||||
"Texture": "TEXTURE",
|
||||
"Volume": "VOLUME_DATA",
|
||||
"PointCloud": "POINTCLOUD_DATA",
|
||||
"Sound": "SOUND",
|
||||
"Font": "FONT_DATA",
|
||||
"Collection": "OUTLINER_COLLECTION",
|
||||
}
|
||||
|
||||
|
||||
def storage_type_icon(type_name):
|
||||
"""Blender UI icon for a storage row type label."""
|
||||
return _STORAGE_TYPE_ICONS.get(type_name, "BLANK1")
|
||||
|
||||
|
||||
def storage_override_icon(is_lib_override):
|
||||
"""Second column: library override emblem vs empty spacer."""
|
||||
return "LIBRARY_DATA_OVERRIDE" if is_lib_override else "BLANK1"
|
||||
|
||||
|
||||
def build_report():
|
||||
"""Build storage report dict. Call through get_report() for caching."""
|
||||
global _storage_override_data_ids
|
||||
_storage_override_data_ids = _object_data_override_ids()
|
||||
rows = []
|
||||
|
||||
def _ov(id_block):
|
||||
return is_library_override_storage(id_block)
|
||||
|
||||
for m in bpy.data.meshes:
|
||||
sz = _mesh_size_bytes(m)
|
||||
if sz is not None:
|
||||
io = _ov(m)
|
||||
rows.append(
|
||||
{
|
||||
"type": "Mesh",
|
||||
"name": m.name,
|
||||
"embedded": 0,
|
||||
"size_bytes": sz,
|
||||
"is_lib_override": io,
|
||||
"kind": "override" if io else "local",
|
||||
}
|
||||
)
|
||||
|
||||
for img in bpy.data.images:
|
||||
e = _image_entry(img)
|
||||
if e is None:
|
||||
continue
|
||||
_typ, name, emb, sz, kind = e
|
||||
io = _ov(img)
|
||||
rows.append(
|
||||
{
|
||||
"type": "Image",
|
||||
"name": name,
|
||||
"embedded": emb,
|
||||
"size_bytes": sz,
|
||||
"is_lib_override": io,
|
||||
"kind": kind,
|
||||
}
|
||||
)
|
||||
|
||||
for a in bpy.data.armatures:
|
||||
sz = _armature_size_bytes(a)
|
||||
if sz is not None:
|
||||
io = _ov(a)
|
||||
rows.append(
|
||||
{
|
||||
"type": "Armature",
|
||||
"name": a.name,
|
||||
"embedded": 0,
|
||||
"size_bytes": sz,
|
||||
"is_lib_override": io,
|
||||
"kind": "override" if io else "local",
|
||||
}
|
||||
)
|
||||
|
||||
for c in getattr(bpy.data, "curves", []):
|
||||
sz = _curve_size_bytes(c)
|
||||
if sz is not None:
|
||||
io = _ov(c)
|
||||
rows.append(
|
||||
{
|
||||
"type": "Curve",
|
||||
"name": c.name,
|
||||
"embedded": 0,
|
||||
"size_bytes": sz,
|
||||
"is_lib_override": io,
|
||||
"kind": "override" if io else "local",
|
||||
}
|
||||
)
|
||||
|
||||
for ng in bpy.data.node_groups:
|
||||
sz = _node_tree_size_bytes(ng)
|
||||
if sz is not None:
|
||||
io = _ov(ng)
|
||||
rows.append(
|
||||
{
|
||||
"type": "NodeTree",
|
||||
"name": ng.name,
|
||||
"embedded": 0,
|
||||
"size_bytes": sz,
|
||||
"is_lib_override": io,
|
||||
"kind": "override" if io else "local",
|
||||
}
|
||||
)
|
||||
|
||||
for mat in bpy.data.materials:
|
||||
if _skip_linked(mat):
|
||||
continue
|
||||
sz = _node_tree_size_bytes(mat.node_tree) if mat.use_nodes and mat.node_tree else 256
|
||||
if sz is None:
|
||||
sz = 256
|
||||
ow = _override_weight_factor(mat)
|
||||
sz = int(sz * ow)
|
||||
io = _ov(mat)
|
||||
rows.append(
|
||||
{
|
||||
"type": "Material",
|
||||
"name": mat.name,
|
||||
"embedded": 0,
|
||||
"size_bytes": sz,
|
||||
"is_lib_override": io,
|
||||
"kind": "override" if io else "local",
|
||||
}
|
||||
)
|
||||
|
||||
if hasattr(bpy.data, "actions"):
|
||||
for act in bpy.data.actions:
|
||||
sz = _action_size_bytes(act)
|
||||
if sz is not None:
|
||||
io = _ov(act)
|
||||
rows.append(
|
||||
{
|
||||
"type": "Action",
|
||||
"name": act.name,
|
||||
"embedded": 0,
|
||||
"size_bytes": sz,
|
||||
"is_lib_override": io,
|
||||
"kind": "override" if io else "local",
|
||||
}
|
||||
)
|
||||
|
||||
for tex in getattr(bpy.data, "textures", []):
|
||||
sz = _texture_size_bytes(tex)
|
||||
if sz is not None:
|
||||
io = _ov(tex)
|
||||
rows.append(
|
||||
{
|
||||
"type": "Texture",
|
||||
"name": tex.name,
|
||||
"embedded": 0,
|
||||
"size_bytes": sz,
|
||||
"is_lib_override": io,
|
||||
"kind": "override" if io else "local",
|
||||
}
|
||||
)
|
||||
|
||||
for ob in bpy.data.objects:
|
||||
sz = _object_size_bytes(ob)
|
||||
if sz is not None:
|
||||
io = _ov(ob)
|
||||
rows.append(
|
||||
{
|
||||
"type": "Object",
|
||||
"name": ob.name,
|
||||
"embedded": 0,
|
||||
"size_bytes": sz,
|
||||
"is_lib_override": io,
|
||||
"kind": "override" if io else "local",
|
||||
}
|
||||
)
|
||||
|
||||
for vol in getattr(bpy.data, "volumes", []):
|
||||
sz = _volume_size_bytes(vol)
|
||||
if sz is not None:
|
||||
io = _ov(vol)
|
||||
rows.append(
|
||||
{
|
||||
"type": "Volume",
|
||||
"name": vol.name,
|
||||
"embedded": 0,
|
||||
"size_bytes": sz,
|
||||
"is_lib_override": io,
|
||||
"kind": "override" if io else "local",
|
||||
}
|
||||
)
|
||||
|
||||
for pc in getattr(bpy.data, "pointclouds", []):
|
||||
sz = _pointcloud_size_bytes(pc)
|
||||
if sz is not None:
|
||||
io = _ov(pc)
|
||||
rows.append(
|
||||
{
|
||||
"type": "PointCloud",
|
||||
"name": pc.name,
|
||||
"embedded": 0,
|
||||
"size_bytes": sz,
|
||||
"is_lib_override": io,
|
||||
"kind": "override" if io else "local",
|
||||
}
|
||||
)
|
||||
|
||||
for snd in getattr(bpy.data, "sounds", []):
|
||||
e = _sound_entry(snd)
|
||||
if e is None:
|
||||
continue
|
||||
_typ, name, emb, sz, kind = e
|
||||
io = _ov(snd)
|
||||
rows.append(
|
||||
{
|
||||
"type": "Sound",
|
||||
"name": name,
|
||||
"embedded": emb,
|
||||
"size_bytes": sz,
|
||||
"is_lib_override": io,
|
||||
"kind": kind,
|
||||
}
|
||||
)
|
||||
|
||||
for font in getattr(bpy.data, "fonts", []):
|
||||
e = _font_entry(font)
|
||||
if e is None:
|
||||
continue
|
||||
_typ, name, emb, sz, kind = e
|
||||
io = _ov(font)
|
||||
rows.append(
|
||||
{
|
||||
"type": "Font",
|
||||
"name": name,
|
||||
"embedded": emb,
|
||||
"size_bytes": sz,
|
||||
"is_lib_override": io,
|
||||
"kind": kind,
|
||||
}
|
||||
)
|
||||
|
||||
for coll in bpy.data.collections:
|
||||
sz = _collection_size_bytes(coll)
|
||||
if sz is not None:
|
||||
io = _ov(coll)
|
||||
rows.append(
|
||||
{
|
||||
"type": "Collection",
|
||||
"name": coll.name,
|
||||
"embedded": 0,
|
||||
"size_bytes": sz,
|
||||
"is_lib_override": io,
|
||||
"kind": "override" if io else "local",
|
||||
}
|
||||
)
|
||||
|
||||
rows.sort(key=lambda r: r["size_bytes"], reverse=True)
|
||||
|
||||
by_type = {}
|
||||
total_estimated = 0
|
||||
total_emb = 0
|
||||
for r in rows:
|
||||
t = r["type"]
|
||||
by_type[t] = by_type.get(t, 0) + r["size_bytes"]
|
||||
total_estimated += r["size_bytes"]
|
||||
total_emb += r.get("embedded", 0)
|
||||
|
||||
type_order = sorted(by_type.keys(), key=lambda t: -by_type[t])
|
||||
by_type_sizes = [(t, by_type[t]) for t in type_order]
|
||||
|
||||
return {
|
||||
"rows": rows,
|
||||
"by_type": by_type_sizes,
|
||||
"total_estimated_bytes": total_estimated,
|
||||
"total_embedded_packed": total_emb,
|
||||
}
|
||||
|
||||
|
||||
def get_report():
|
||||
global _cache_report, _cache_light, _cache_vert_sum
|
||||
light = _light_fingerprint()
|
||||
vs = _mesh_vertex_sum_sample()
|
||||
if (
|
||||
_cache_report is not None
|
||||
and _cache_light == light
|
||||
and _cache_vert_sum == vs
|
||||
):
|
||||
return _cache_report
|
||||
_cache_report = build_report()
|
||||
_cache_light = light
|
||||
_cache_vert_sum = vs
|
||||
return _cache_report
|
||||
|
||||
|
||||
def format_embedded_total(n):
|
||||
"""Human-readable total packed bytes embedded in the .blend."""
|
||||
return _fmt_bytes(n)
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
|
||||
bl_info = {
|
||||
"name": "BlenderKit Online Asset Library",
|
||||
"author": "Vilem Duha, Petr Dlouhy, A. Gajdosik",
|
||||
"version": (3, 18, 1, 251219), # X.Y.Z.yymmdd
|
||||
"author": "Vilem Duha, Petr Dlouhy, A. Gajdosik, Michal Hons",
|
||||
"version": (3, 19, 1, 260402), # X.Y.Z.yymmdd
|
||||
"blender": (3, 0, 0),
|
||||
"location": "View3D > Properties > BlenderKit",
|
||||
"description": "Boost your workflow with drag&drop assets from the community driven library.",
|
||||
@@ -28,7 +28,7 @@ bl_info = {
|
||||
"tracker_url": "https://github.com/BlenderKit/blenderkit/issues",
|
||||
"category": "3D View",
|
||||
}
|
||||
VERSION = (3, 18, 1, 251219)
|
||||
VERSION = (3, 19, 1, 260402)
|
||||
|
||||
import logging
|
||||
import random
|
||||
@@ -96,6 +96,7 @@ if "bpy" in locals():
|
||||
ui = reload(ui)
|
||||
ui_bgl = reload(ui_bgl)
|
||||
ui_panels = reload(ui_panels)
|
||||
keymap_utils = reload(keymap_utils)
|
||||
upload = reload(upload)
|
||||
upload_bg = reload(upload_bg)
|
||||
utils = reload(utils)
|
||||
@@ -152,6 +153,7 @@ else:
|
||||
from . import ui
|
||||
from . import ui_bgl
|
||||
from . import ui_panels
|
||||
from . import keymap_utils
|
||||
from . import upload
|
||||
from . import upload_bg
|
||||
from . import utils
|
||||
@@ -273,7 +275,7 @@ if bpy.app.version >= (4, 5, 0):
|
||||
EXTRA_PATH_OPTIONS = {"options": {"PATH_SUPPORTS_BLEND_RELATIVE"}}
|
||||
|
||||
|
||||
def udate_down_up(self, context):
|
||||
def update_down_up(self, context):
|
||||
"""Perform a search if results are empty."""
|
||||
props = bpy.context.window_manager.blenderkitUI
|
||||
if search.get_search_results() is None and props.down_up == "SEARCH":
|
||||
@@ -308,6 +310,19 @@ def asset_type_callback(self, context):
|
||||
|
||||
if bpy.app.version >= (4, 2, 0):
|
||||
items.append(("ADDON", "Add-ons", "Find add-ons", "PLUGIN", 7))
|
||||
addon = bpy.context.preferences.addons.get(__package__)
|
||||
if addon is not None:
|
||||
preferences = addon.preferences
|
||||
if preferences.experimental_features and preferences.author_tab:
|
||||
items.append(
|
||||
(
|
||||
"AUTHOR",
|
||||
"Authors",
|
||||
"Find authors",
|
||||
pcoll["asset_type_author"].icon_id,
|
||||
8,
|
||||
),
|
||||
)
|
||||
else:
|
||||
items = [
|
||||
("MODEL", "Model", "Upload a model", "OBJECT_DATAMODE", 0),
|
||||
@@ -330,6 +345,8 @@ def asset_type_callback(self, context):
|
||||
if bpy.app.version >= (4, 2, 0):
|
||||
items.append(("ADDON", "Add-on", "Upload an addon", "PLUGIN", 7))
|
||||
|
||||
# Author is search-only, no upload entry needed
|
||||
|
||||
return items
|
||||
|
||||
|
||||
@@ -337,6 +354,10 @@ def run_drag_drop_update(self, context):
|
||||
if self.drag_init_button:
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
|
||||
if ui_props.dragging:
|
||||
self.drag_init_button = False
|
||||
return
|
||||
|
||||
bpy.ops.view3d.close_popup_button("INVOKE_DEFAULT")
|
||||
bpy.ops.view3d.asset_drag_drop(
|
||||
"INVOKE_DEFAULT",
|
||||
@@ -356,7 +377,7 @@ class BlenderKitUIProps(PropertyGroup):
|
||||
),
|
||||
description="BlenderKit",
|
||||
default="SEARCH",
|
||||
update=udate_down_up,
|
||||
update=update_down_up,
|
||||
)
|
||||
asset_type: EnumProperty(
|
||||
name=" ",
|
||||
@@ -505,11 +526,11 @@ class BlenderKitUIProps(PropertyGroup):
|
||||
|
||||
ui_scale = 1
|
||||
|
||||
thumb_size_def = 96
|
||||
thumb_size_def = 128
|
||||
margin_def = 0
|
||||
|
||||
thumb_size: IntProperty(
|
||||
name="Thumbnail Size", default=thumb_size_def, min=-1, max=256
|
||||
name="Thumbnail Size", default=thumb_size_def, min=48, max=256
|
||||
)
|
||||
|
||||
margin: IntProperty(name="Margin", default=margin_def, min=-1, max=256)
|
||||
@@ -552,7 +573,6 @@ class BlenderKitUIProps(PropertyGroup):
|
||||
description="Click or drag into scene for download.\nUse mouse wheel during drag to rotate the asset. Cancel the drag by pressing 'Esc'.",
|
||||
update=run_drag_drop_update,
|
||||
)
|
||||
drag_length: IntProperty(name="Drag length", default=0)
|
||||
draw_drag_image: BoolProperty(name="Draw Drag Image", default=False)
|
||||
draw_snapped_bounds: BoolProperty(name="Draw Snapped Bounds", default=False)
|
||||
|
||||
@@ -622,6 +642,32 @@ class BlenderKitUIProps(PropertyGroup):
|
||||
name="Upload HDR", type=bpy.types.Image, description="Pick an image to upload"
|
||||
)
|
||||
|
||||
hdr_use_custom_thumbnail_tone: BoolProperty(
|
||||
name="Use Custom Thumbnail Tone",
|
||||
description="Use custom exposure and gamma for HDR thumbnail conversion",
|
||||
default=False,
|
||||
)
|
||||
|
||||
hdr_thumbnail_exposure: FloatProperty(
|
||||
name="Thumbnail Exposure",
|
||||
description="Exposure offset used only for HDR thumbnail conversion",
|
||||
default=0.0,
|
||||
min=-5.0,
|
||||
max=5.0,
|
||||
soft_min=-2.0,
|
||||
soft_max=2.0,
|
||||
)
|
||||
|
||||
hdr_thumbnail_gamma: FloatProperty(
|
||||
name="Thumbnail Gamma",
|
||||
description="Gamma used only for HDR thumbnail conversion",
|
||||
default=1.0,
|
||||
min=0.2,
|
||||
max=3.0,
|
||||
soft_min=0.7,
|
||||
soft_max=1.6,
|
||||
)
|
||||
|
||||
nodegroup_upload: PointerProperty(
|
||||
name="Upload Tool",
|
||||
type=bpy.types.GeometryNodeTree,
|
||||
@@ -785,7 +831,7 @@ def update_free(self, context):
|
||||
message="Any material uploaded to BlenderKit is free."
|
||||
" However, it can still earn money for the author,"
|
||||
" based on our fair share system. "
|
||||
"Part of subscription is sent to artists based on usage by paying users.\n",
|
||||
"Part of subscription is sent to authors based on usage by paying users.\n",
|
||||
)
|
||||
|
||||
|
||||
@@ -868,6 +914,21 @@ class BlenderKitCommonUploadProps(object):
|
||||
description="License. Please read our help for choosing the right licenses",
|
||||
)
|
||||
|
||||
# verification mainly to retrigger processing
|
||||
verification_status: EnumProperty(
|
||||
name="Verification status",
|
||||
description="Verification status of the asset, set by moderators",
|
||||
items=(
|
||||
("UPLOADING", "Uploading", "uploading"),
|
||||
("UPLOADED", "Uploaded", "uploaded"),
|
||||
("VALIDATED", "Validated", "validated"),
|
||||
("ON_HOLD", "On Hold", "on_hold"),
|
||||
("REJECTED", "Rejected", "rejected"),
|
||||
("DELETED", "Deleted", "deleted"),
|
||||
),
|
||||
default="UPLOADING",
|
||||
)
|
||||
|
||||
is_private: EnumProperty(
|
||||
name="Thumbnail Style",
|
||||
items=(("PRIVATE", "Private", ""), ("PUBLIC", "Public", "")),
|
||||
@@ -1187,13 +1248,17 @@ class BlenderKitAddonSearchProps(PropertyGroup, BlenderKitCommonSearchProps):
|
||||
description="Show only addons that are already installed in Blender",
|
||||
default=False,
|
||||
update=lambda self, context: (
|
||||
search.refresh_search()
|
||||
search.search_update(self, context)
|
||||
if context.window_manager.blenderkitUI.asset_type == "ADDON"
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class BlenderKitAuthorSearchProps(PropertyGroup, BlenderKitCommonSearchProps):
|
||||
pass
|
||||
|
||||
|
||||
class BlenderKitHDRUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
|
||||
texture_resolution_max: IntProperty(
|
||||
name="Texture Resolution Max",
|
||||
@@ -1207,6 +1272,15 @@ class BlenderKitHDRUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
|
||||
|
||||
|
||||
class BlenderKitBrushUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
|
||||
thumbnail: StringProperty(
|
||||
name="Thumbnail",
|
||||
description="Thumbnail path - at least 1024x1024 .jpg or .png",
|
||||
subtype="FILE_PATH",
|
||||
default="",
|
||||
update=autothumb.update_upload_brush_preview,
|
||||
**EXTRA_PATH_OPTIONS,
|
||||
)
|
||||
|
||||
mode: EnumProperty(
|
||||
name="Mode",
|
||||
items=(
|
||||
@@ -1526,6 +1600,13 @@ class BlenderKitModelUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
|
||||
update=autothumb.update_upload_model_preview,
|
||||
)
|
||||
|
||||
is_generating_wire_thumbnail: BoolProperty(
|
||||
name="Generating Wire Thumbnail",
|
||||
description="True when background process is running",
|
||||
default=False,
|
||||
update=autothumb.update_wire_thumbnail_preview,
|
||||
)
|
||||
|
||||
has_autotags: BoolProperty(
|
||||
name="Has Autotagging Done",
|
||||
description="True when autotagging done",
|
||||
@@ -1546,6 +1627,26 @@ class BlenderKitModelUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
|
||||
default=False,
|
||||
)
|
||||
|
||||
wire_thumbnail: StringProperty(
|
||||
name="Wireframe Thumbnail",
|
||||
description="Wireframe thumbnail (JPG or PNG, preferred size is 1024x1024 or higher)",
|
||||
subtype="FILE_PATH",
|
||||
default="",
|
||||
update=autothumb.update_wire_thumbnail_preview,
|
||||
**EXTRA_PATH_OPTIONS,
|
||||
)
|
||||
wire_thumbnail_will_upload_on_website: BoolProperty(
|
||||
name="I will upload wireframe thumbnail on website",
|
||||
description="True if the wireframe thumbnail will upload on the website\n please read upload tutorial for more information",
|
||||
default=False,
|
||||
)
|
||||
|
||||
wire_thumbnail_generating_state: StringProperty(
|
||||
name="Wire Thumbnail Generating State",
|
||||
description="bg process reports for wireframe thumbnail generation",
|
||||
default="Please add wireframe thumbnail (jpg or png, at least 1024x1024)",
|
||||
)
|
||||
|
||||
|
||||
class BlenderKitSceneUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
|
||||
style: EnumProperty(
|
||||
@@ -1923,6 +2024,14 @@ class BlenderKitHDRSearchProps(PropertyGroup, BlenderKitCommonSearchProps):
|
||||
)
|
||||
|
||||
|
||||
def our_keymap_draw(self, context):
|
||||
try:
|
||||
keymap_utils.draw_keymap(self, context)
|
||||
except Exception:
|
||||
bk_logger.exception("Failed to draw keymap in preferences")
|
||||
return
|
||||
|
||||
|
||||
class BlenderKitSceneSearchProps(PropertyGroup, BlenderKitCommonSearchProps):
|
||||
search_style: EnumProperty(
|
||||
name="Style",
|
||||
@@ -2090,6 +2199,12 @@ class BlenderKitAddonPreferences(AddonPreferences):
|
||||
default=True,
|
||||
)
|
||||
|
||||
assetbar_follows_cursor: BoolProperty(
|
||||
name="Assetbar follows active viewport",
|
||||
description="Make the assetbar follow the cursor across the screen",
|
||||
default=False,
|
||||
)
|
||||
|
||||
global_dir: StringProperty(
|
||||
name="Global Directory",
|
||||
description="Global storage for your assets, will use subdirectories for the contents. Client will place its files in subdirectory 'client'",
|
||||
@@ -2151,6 +2266,13 @@ class BlenderKitAddonPreferences(AddonPreferences):
|
||||
update=update_unpack,
|
||||
)
|
||||
|
||||
write_asset_metadata: BoolProperty(
|
||||
name="Write Asset Metadata",
|
||||
description="Write BlenderKit metadata into downloaded files so tags, description, and preview show in other scenes",
|
||||
default=True,
|
||||
update=utils.save_prefs,
|
||||
)
|
||||
|
||||
# resolution download/import settings
|
||||
resolution: EnumProperty(
|
||||
name="Max resolution",
|
||||
@@ -2317,6 +2439,13 @@ In this case you should also set path to your system CA bundle containing proxy'
|
||||
update=utils.save_prefs,
|
||||
)
|
||||
|
||||
thumbnail_disable_subdivision: BoolProperty(
|
||||
name="Disable Subdivision for Thumbnails Rendering (For assets upload)",
|
||||
description="By default this is off. Disable this for wireframe thumbnails to render faster",
|
||||
default=False,
|
||||
update=utils.save_prefs,
|
||||
)
|
||||
|
||||
maximized_assetbar_rows: IntProperty(
|
||||
name="Maximized Assetbar Rows",
|
||||
description="Maximum rows of assetbar in the 3D view when expanded",
|
||||
@@ -2334,8 +2463,8 @@ In this case you should also set path to your system CA bundle containing proxy'
|
||||
|
||||
thumb_size: IntProperty(
|
||||
name="Assetbar Thumbnail Size",
|
||||
default=96,
|
||||
min=-1,
|
||||
default=128,
|
||||
min=48, # must newer be zero
|
||||
max=256,
|
||||
update=utils.save_prefs,
|
||||
description="Size of thumbnails of the assetbar in 3D view",
|
||||
@@ -2352,7 +2481,21 @@ In this case you should also set path to your system CA bundle containing proxy'
|
||||
|
||||
experimental_features: BoolProperty(
|
||||
name="Enable experimental features",
|
||||
description="Enable experimental features of BlenderKit. Note: There are no experimental features in this version.",
|
||||
description="Enable experimental features of BlenderKit, such as the Authors tab",
|
||||
default=False,
|
||||
update=utils.save_prefs,
|
||||
)
|
||||
|
||||
author_tab: BoolProperty(
|
||||
name="Show Authors tab",
|
||||
description="Show Authors tab in the assetbar. This tab allows you to see all assets of a specific author and is also used for showing your profile and assets",
|
||||
default=False,
|
||||
update=utils.save_prefs,
|
||||
)
|
||||
|
||||
author_asset_type_picker: BoolProperty(
|
||||
name="Author asset type picker",
|
||||
description="Enable the Authors tab in the asset type picker. When disabled, clicking an author searches in the current tab",
|
||||
default=False,
|
||||
update=utils.save_prefs,
|
||||
)
|
||||
@@ -2458,22 +2601,45 @@ In this case you should also set path to your system CA bundle containing proxy'
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
enable_wire_thumbnail_upload: BoolProperty(
|
||||
name="Enable wire thumbnail upload",
|
||||
description="If enabled, wireframe thumbnails will be uploaded.",
|
||||
default=False,
|
||||
# do not save prefs here, it's experimental
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
login_box = layout.box()
|
||||
login_box.label(text="Login Options")
|
||||
if self.api_key.strip() == "":
|
||||
ui_panels.draw_login_buttons(layout)
|
||||
layout.label(
|
||||
ui_panels.draw_login_buttons(login_box)
|
||||
login_box.label(
|
||||
text="Sign up to bookmark your favorite assets. Get 200 MiB of private storage in Free Plan."
|
||||
)
|
||||
else:
|
||||
layout.operator("wm.blenderkit_logout", text="Logout", icon="URL")
|
||||
layout.prop(self, "api_key", text="Your API Key")
|
||||
layout.prop(self, "keep_preferences")
|
||||
community_row = layout.row()
|
||||
login_box.operator("wm.blenderkit_logout", text="Logout", icon="URL")
|
||||
login_box.prop(self, "api_key", text="Your API Key")
|
||||
login_box.prop(self, "keep_preferences")
|
||||
community_row = login_box.row()
|
||||
community_row.prop(self, "experimental_features")
|
||||
community_row.operator("wm.blenderkit_join_discord", icon="URL")
|
||||
|
||||
if utils.profile_is_validator():
|
||||
layout.prop(self, "categories_fix")
|
||||
validator_box = layout.box()
|
||||
validator_box.label(text="Validator Settings")
|
||||
validator_box.prop(self, "categories_fix")
|
||||
|
||||
# REPORT PATHS
|
||||
report_settings = layout.box()
|
||||
report_settings.label(text="Report a Bug")
|
||||
report_settings.label(
|
||||
text="Create an issue report with version information to help us resolve the issue faster.",
|
||||
)
|
||||
report_settings.operator(
|
||||
"wm.blenderkit_report_bug", text="Submit Full Bug Report", icon="ERROR"
|
||||
)
|
||||
|
||||
# FILE PATHS
|
||||
locations_settings = layout.box()
|
||||
@@ -2484,6 +2650,7 @@ In this case you should also set path to your system CA bundle containing proxy'
|
||||
if self.directory_behaviour in ("BOTH", "LOCAL"):
|
||||
locations_settings.prop(self, "project_subdir")
|
||||
locations_settings.prop(self, "unpack_files")
|
||||
locations_settings.prop(self, "write_asset_metadata")
|
||||
|
||||
# GUI SETTINGS
|
||||
gui_settings = layout.box()
|
||||
@@ -2498,6 +2665,7 @@ In this case you should also set path to your system CA bundle containing proxy'
|
||||
gui_settings.prop(self, "show_VIEW3D_MT_blenderkit_model_properties")
|
||||
gui_settings.prop(self, "tips_on_start")
|
||||
gui_settings.prop(self, "announcements_on_start")
|
||||
gui_settings.prop(self, "assetbar_follows_cursor")
|
||||
gui_settings.prop(self, "use_clipboard_scan")
|
||||
|
||||
# NETWORKING SETTINGS
|
||||
@@ -2516,16 +2684,11 @@ In this case you should also set path to your system CA bundle containing proxy'
|
||||
# UPDATER SETTINGS
|
||||
addon_updater_ops.update_settings_ui(self, context)
|
||||
|
||||
# EXPERIMENTAL SETTINGS
|
||||
# only if experimental features enabled
|
||||
if self.experimental_features:
|
||||
experimental_settings = layout.box()
|
||||
experimental_settings.alignment = "EXPAND"
|
||||
experimental_settings.label(text="Experimental settings")
|
||||
experimental_settings.prop(self, "ignore_env_for_thumbnails")
|
||||
|
||||
# RUNTIME INFO
|
||||
globdir_op = layout.operator(
|
||||
directory_box = layout.box()
|
||||
directory_box.label(text="Directories and Paths")
|
||||
|
||||
globdir_op = directory_box.operator(
|
||||
"wm.blenderkit_open_global_directory",
|
||||
text=f"Global directory: {self.global_dir}",
|
||||
icon="FILE_FOLDER",
|
||||
@@ -2533,7 +2696,7 @@ In this case you should also set path to your system CA bundle containing proxy'
|
||||
globdir_op.directory = self.global_dir
|
||||
|
||||
clientlog_path = client_lib.get_client_log_path()
|
||||
clientlog_op = layout.operator(
|
||||
clientlog_op = directory_box.operator(
|
||||
"wm.blenderkit_open_client_log",
|
||||
text=f"Client log: {clientlog_path}",
|
||||
icon="FILE_FOLDER",
|
||||
@@ -2541,7 +2704,7 @@ In this case you should also set path to your system CA bundle containing proxy'
|
||||
clientlog_op.directory = clientlog_path
|
||||
|
||||
addondir = path.dirname(__file__)
|
||||
addondir_op = layout.operator(
|
||||
addondir_op = directory_box.operator(
|
||||
"wm.blenderkit_open_addon_directory",
|
||||
text=f"Installed at: {addondir}",
|
||||
icon="FILE_FOLDER",
|
||||
@@ -2549,13 +2712,27 @@ In this case you should also set path to your system CA bundle containing proxy'
|
||||
addondir_op.directory = addondir
|
||||
|
||||
tempdir = paths.get_temp_dir()
|
||||
tempdir_op = layout.operator(
|
||||
tempdir_op = directory_box.operator(
|
||||
"wm.blenderkit_open_temp_directory",
|
||||
text=f"Temp directory: {tempdir}",
|
||||
icon="FILE_FOLDER",
|
||||
)
|
||||
tempdir_op.directory = tempdir
|
||||
|
||||
# try to draw also custom keymaps
|
||||
our_keymap_draw(self, context)
|
||||
|
||||
# EXPERIMENTAL SETTINGS
|
||||
# only if experimental features enabled
|
||||
if self.experimental_features:
|
||||
experimental_settings = layout.box()
|
||||
experimental_settings.alignment = "EXPAND"
|
||||
experimental_settings.label(text="Experimental settings")
|
||||
experimental_settings.prop(self, "author_tab")
|
||||
experimental_settings.prop(self, "author_asset_type_picker")
|
||||
experimental_settings.prop(self, "ignore_env_for_thumbnails")
|
||||
# experimental_settings.prop(self, "enable_wire_thumbnail_upload")
|
||||
|
||||
|
||||
# registration
|
||||
classes = (
|
||||
@@ -2574,6 +2751,7 @@ classes = (
|
||||
BlenderKitGeoToolSearchProps,
|
||||
BlenderKitNodeGroupUploadProps,
|
||||
BlenderKitAddonSearchProps,
|
||||
BlenderKitAuthorSearchProps,
|
||||
)
|
||||
|
||||
|
||||
@@ -2582,6 +2760,9 @@ def register():
|
||||
global_vars.VERSION = VERSION
|
||||
bpy.utils.register_class(BlenderKitAddonPreferences)
|
||||
|
||||
# Drop any downloads that might have been left running if the add-on was re-enabled mid-transfer.
|
||||
download.cancel_running_downloads("addon register")
|
||||
|
||||
addon_updater_ops.register({"version": VERSION})
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
@@ -2643,9 +2824,13 @@ def register():
|
||||
bpy.types.WindowManager.blenderkit_addon = PointerProperty(
|
||||
type=BlenderKitAddonSearchProps
|
||||
)
|
||||
bpy.types.WindowManager.blenderkit_author = PointerProperty(
|
||||
type=BlenderKitAuthorSearchProps
|
||||
)
|
||||
if bpy.app.factory_startup is False:
|
||||
user_preferences = bpy.context.preferences.addons[__package__].preferences
|
||||
global_vars.PREFS = utils.get_preferences_as_dict()
|
||||
paths.ensure_asset_library_path(user_preferences.global_dir)
|
||||
client_lib.reorder_ports(user_preferences.client_port)
|
||||
timer.update_trusted_CA_certs(user_preferences.trusted_ca_certs)
|
||||
|
||||
@@ -2694,6 +2879,8 @@ def register():
|
||||
|
||||
def unregister():
|
||||
bk_logger.info("Unregistering BlenderKit add-on")
|
||||
# Stop any in-flight downloads to avoid leaving stale UI state when disabling the add-on.
|
||||
download.cancel_running_downloads("addon unregister")
|
||||
timer.unregister_timers()
|
||||
ui_panels.unregister_ui_panels()
|
||||
ui.unregister_ui()
|
||||
@@ -2726,6 +2913,8 @@ def unregister():
|
||||
del bpy.types.WindowManager.blenderkit_brush
|
||||
del bpy.types.WindowManager.blenderkit_mat
|
||||
del bpy.types.WindowManager.blenderkit_nodegroup
|
||||
del bpy.types.WindowManager.blenderkit_addon
|
||||
del bpy.types.WindowManager.blenderkit_author
|
||||
|
||||
del bpy.types.Scene.blenderkit
|
||||
del bpy.types.Object.blenderkit
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
include:
|
||||
- "**/*.py"
|
||||
|
||||
exclude_dirs:
|
||||
- "tests"
|
||||
- ".venv"
|
||||
- "__pycache__"
|
||||
|
||||
skips:
|
||||
- "B404" # https://bandit.readthedocs.io/en/1.7.10/blacklists/blacklist_imports.html#b404-import-subprocess
|
||||
- "B603" # https://bandit.readthedocs.io/en/1.7.10/plugins/b603_subprocess_without_shell_equals_true.html
|
||||
- "B608" # https://bandit.readthedocs.io/en/1.7.10/plugins/b608_hardcoded_sql_expressions.html
|
||||
@@ -29,6 +29,7 @@ import fnmatch
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import logging
|
||||
import shutil
|
||||
import ssl
|
||||
import threading
|
||||
@@ -43,6 +44,10 @@ import addon_utils
|
||||
# Blender imports, used in limited cases.
|
||||
import bpy
|
||||
|
||||
from . import tasks_queue
|
||||
|
||||
|
||||
bk_logger = logging.getLogger(__name__)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# The main class
|
||||
@@ -134,15 +139,15 @@ class SingletonUpdater:
|
||||
self._select_link = select_link_function
|
||||
|
||||
def print_trace(self):
|
||||
"""Print handled exception details when use_print_traces is set"""
|
||||
"""Logs handled exception details when use_print_traces is set"""
|
||||
if self._use_print_traces:
|
||||
traceback.print_exc()
|
||||
bk_logger.error("%s", traceback.format_exc())
|
||||
|
||||
def print_verbose(self, msg):
|
||||
"""Print out a verbose logging message if verbose is true."""
|
||||
"""Logs verbose messages if verbose is true."""
|
||||
if not self._verbose:
|
||||
return
|
||||
print("🔄 {}: ".format(self.addon) + msg)
|
||||
bk_logger.info("%s", msg)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Getters and setters
|
||||
@@ -177,7 +182,7 @@ class SingletonUpdater:
|
||||
def auto_reload_post_update(self, value):
|
||||
try:
|
||||
self._auto_reload_post_update = bool(value)
|
||||
except:
|
||||
except Exception:
|
||||
raise ValueError("auto_reload_post_update must be a boolean value")
|
||||
|
||||
@property
|
||||
@@ -227,7 +232,7 @@ class SingletonUpdater:
|
||||
elif type(tuple_values) is not tuple:
|
||||
try:
|
||||
tuple(tuple_values)
|
||||
except:
|
||||
except Exception:
|
||||
raise ValueError("current_version must be a tuple of integers")
|
||||
for i in tuple_values:
|
||||
if type(i) is not int:
|
||||
@@ -277,7 +282,7 @@ class SingletonUpdater:
|
||||
def include_branch_auto_check(self, value):
|
||||
try:
|
||||
self._include_branch_auto_check = bool(value)
|
||||
except:
|
||||
except Exception:
|
||||
raise ValueError("include_branch_autocheck must be a boolean")
|
||||
|
||||
@property
|
||||
@@ -295,7 +300,7 @@ class SingletonUpdater:
|
||||
)
|
||||
else:
|
||||
self._include_branch_list = value
|
||||
except:
|
||||
except Exception:
|
||||
raise ValueError("include_branch_list should be a list of valid branches")
|
||||
|
||||
@property
|
||||
@@ -306,7 +311,7 @@ class SingletonUpdater:
|
||||
def include_branches(self, value):
|
||||
try:
|
||||
self._include_branches = bool(value)
|
||||
except:
|
||||
except Exception:
|
||||
raise ValueError("include_branches must be a boolean value")
|
||||
|
||||
@property
|
||||
@@ -329,7 +334,7 @@ class SingletonUpdater:
|
||||
def manual_only(self, value):
|
||||
try:
|
||||
self._manual_only = bool(value)
|
||||
except:
|
||||
except Exception:
|
||||
raise ValueError("manual_only must be a boolean value")
|
||||
|
||||
@property
|
||||
@@ -377,7 +382,7 @@ class SingletonUpdater:
|
||||
def repo(self, value):
|
||||
try:
|
||||
self._repo = str(value)
|
||||
except:
|
||||
except Exception:
|
||||
raise ValueError("repo must be a string value")
|
||||
|
||||
@property
|
||||
@@ -404,8 +409,8 @@ class SingletonUpdater:
|
||||
elif value is not None and not os.path.exists(value):
|
||||
try:
|
||||
os.makedirs(value)
|
||||
except:
|
||||
self.print_verbose("Error trying to staging path")
|
||||
except Exception:
|
||||
self.print_verbose("Error trying to create staging path")
|
||||
self.print_trace()
|
||||
return
|
||||
self._updater_path = value
|
||||
@@ -453,7 +458,7 @@ class SingletonUpdater:
|
||||
def use_releases(self, value):
|
||||
try:
|
||||
self._use_releases = bool(value)
|
||||
except:
|
||||
except Exception:
|
||||
raise ValueError("use_releases must be a boolean value")
|
||||
|
||||
@property
|
||||
@@ -464,7 +469,7 @@ class SingletonUpdater:
|
||||
def user(self, value):
|
||||
try:
|
||||
self._user = str(value)
|
||||
except:
|
||||
except Exception:
|
||||
raise ValueError("User must be a string value")
|
||||
|
||||
@property
|
||||
@@ -476,7 +481,7 @@ class SingletonUpdater:
|
||||
try:
|
||||
self._verbose = bool(value)
|
||||
self.print_verbose("Verbose is enabled")
|
||||
except:
|
||||
except Exception:
|
||||
raise ValueError("Verbose must be a boolean value")
|
||||
|
||||
@property
|
||||
@@ -487,7 +492,7 @@ class SingletonUpdater:
|
||||
def use_print_traces(self, value):
|
||||
try:
|
||||
self._use_print_traces = bool(value)
|
||||
except:
|
||||
except Exception:
|
||||
raise ValueError("use_print_traces must be a boolean value")
|
||||
|
||||
@property
|
||||
@@ -687,7 +692,7 @@ class SingletonUpdater:
|
||||
request = urllib.request.Request(url)
|
||||
try:
|
||||
context = ssl._create_unverified_context()
|
||||
except:
|
||||
except Exception:
|
||||
# Some blender packaged python versions don't have this, largely
|
||||
# useful for local network setups otherwise minimal impact.
|
||||
context = None
|
||||
@@ -712,24 +717,24 @@ class SingletonUpdater:
|
||||
if str(e.code) == "403":
|
||||
self._error = "HTTP error (access denied)"
|
||||
self._error_msg = str(e.code) + " - server error response"
|
||||
print(self._error, self._error_msg)
|
||||
bk_logger.error("%s %s", self._error, self._error_msg)
|
||||
else:
|
||||
self._error = "HTTP error"
|
||||
self._error_msg = str(e.code)
|
||||
print(self._error, self._error_msg)
|
||||
self.print_trace()
|
||||
bk_logger.error("%s %s", self._error, self._error_msg)
|
||||
# self.print_trace()
|
||||
self._update_ready = None
|
||||
except urllib.error.URLError as e:
|
||||
reason = str(e.reason)
|
||||
if "TLSV1_ALERT" in reason or "SSL" in reason.upper():
|
||||
self._error = "Connection rejected, download manually"
|
||||
self._error_msg = reason
|
||||
print(self._error, self._error_msg)
|
||||
bk_logger.error("%s %s", self._error, self._error_msg)
|
||||
else:
|
||||
self._error = "URL error, check internet connection"
|
||||
self._error_msg = reason
|
||||
print(self._error, self._error_msg)
|
||||
self.print_trace()
|
||||
bk_logger.error("%s %s", self._error, self._error_msg)
|
||||
# self.print_trace()
|
||||
self._update_ready = None
|
||||
return None
|
||||
else:
|
||||
@@ -748,7 +753,7 @@ class SingletonUpdater:
|
||||
self._error = "API response has invalid JSON format"
|
||||
self._error_msg = str(e.reason)
|
||||
self._update_ready = None
|
||||
print(self._error, self._error_msg)
|
||||
bk_logger.error("%s %s", self._error, self._error_msg)
|
||||
self.print_trace()
|
||||
return None
|
||||
else:
|
||||
@@ -766,13 +771,13 @@ class SingletonUpdater:
|
||||
try:
|
||||
shutil.rmtree(local)
|
||||
os.makedirs(local)
|
||||
except:
|
||||
except Exception:
|
||||
error = "failed to remove existing staging directory"
|
||||
self.print_trace()
|
||||
else:
|
||||
try:
|
||||
os.makedirs(local)
|
||||
except:
|
||||
except Exception:
|
||||
error = "failed to create staging directory"
|
||||
self.print_trace()
|
||||
|
||||
@@ -811,8 +816,8 @@ class SingletonUpdater:
|
||||
except Exception as e:
|
||||
self._error = "Error retrieving download, bad link?"
|
||||
self._error_msg = "Error: {}".format(e)
|
||||
print("Error retrieving download, bad link?")
|
||||
print("Error: {}".format(e))
|
||||
bk_logger.error("Error retrieving download, bad link?")
|
||||
bk_logger.error("Error: %s", e)
|
||||
self.print_trace()
|
||||
return False
|
||||
|
||||
@@ -829,7 +834,7 @@ class SingletonUpdater:
|
||||
if os.path.isdir(local):
|
||||
try:
|
||||
shutil.rmtree(local)
|
||||
except:
|
||||
except Exception:
|
||||
self.print_verbose(
|
||||
"Failed to removed previous backup folder, continuing"
|
||||
)
|
||||
@@ -840,7 +845,7 @@ class SingletonUpdater:
|
||||
if os.path.isdir(tempdest):
|
||||
try:
|
||||
shutil.rmtree(tempdest)
|
||||
except:
|
||||
except Exception:
|
||||
self.print_verbose("Failed to remove existing temp folder, continuing")
|
||||
self.print_trace()
|
||||
|
||||
@@ -852,15 +857,15 @@ class SingletonUpdater:
|
||||
tempdest,
|
||||
ignore=shutil.ignore_patterns(*self._backup_ignore_patterns),
|
||||
)
|
||||
except:
|
||||
print("Failed to create backup, still attempting update.")
|
||||
except Exception:
|
||||
self.print_verbose("Failed to create backup, still attempting update.")
|
||||
self.print_trace()
|
||||
return
|
||||
else:
|
||||
try:
|
||||
shutil.copytree(self._addon_root, tempdest)
|
||||
except:
|
||||
print("Failed to create backup, still attempting update.")
|
||||
except Exception:
|
||||
self.print_verbose("Failed to create backup, still attempting update.")
|
||||
self.print_trace()
|
||||
return
|
||||
shutil.move(tempdest, local)
|
||||
@@ -906,23 +911,23 @@ class SingletonUpdater:
|
||||
try:
|
||||
shutil.rmtree(outdir)
|
||||
self.print_verbose("Source folder cleared")
|
||||
except:
|
||||
except Exception:
|
||||
self.print_verbose("Error occurred while clearing extract dir")
|
||||
self.print_trace()
|
||||
|
||||
# Create parent directories if needed, would not be relevant unless
|
||||
# installing addon into another location or via an addon manager.
|
||||
try:
|
||||
os.mkdir(outdir)
|
||||
except Exception as err:
|
||||
print("Error occurred while making extract dir:")
|
||||
print(str(err))
|
||||
except Exception:
|
||||
self.print_verbose("Error occurred while making extract dir")
|
||||
self.print_trace()
|
||||
self._error = "Install failed"
|
||||
self._error_msg = "Failed to make extract directory"
|
||||
return -1
|
||||
|
||||
if not os.path.isdir(outdir):
|
||||
print("Failed to create source directory")
|
||||
bk_logger.error("Failed to create source directory")
|
||||
self._error = "Install failed"
|
||||
self._error_msg = "Failed to create extract directory"
|
||||
return -1
|
||||
@@ -972,7 +977,7 @@ class SingletonUpdater:
|
||||
if not os.path.isdir(unpath):
|
||||
self._error = "Install failed"
|
||||
self._error_msg = "Extracted path does not exist"
|
||||
print("Extracted path does not exist: ", unpath)
|
||||
bk_logger.error("Extracted path does not exist: %s", unpath)
|
||||
return -1
|
||||
|
||||
if self._subfolder_path:
|
||||
@@ -991,9 +996,8 @@ class SingletonUpdater:
|
||||
# Smarter check for additional sub folders for a single folder
|
||||
# containing the __init__.py file.
|
||||
if not os.path.isfile(os.path.join(unpath, "__init__.py")):
|
||||
print("Not a valid addon found")
|
||||
print("Paths:")
|
||||
print(dirlist)
|
||||
bk_logger.error("Not a valid addon found")
|
||||
bk_logger.error("Paths: %s", dirlist)
|
||||
self._error = "Install failed"
|
||||
self._error_msg = "No __init__ file found in new source"
|
||||
return -1
|
||||
@@ -1052,8 +1056,10 @@ class SingletonUpdater:
|
||||
self.print_verbose(
|
||||
"Clean removing file {}".format(os.path.join(base, f))
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error removing file {os.path.join(base, f)}: {e}")
|
||||
except Exception:
|
||||
bk_logger.exception(
|
||||
"Error removing file %s:", os.path.join(base, f)
|
||||
)
|
||||
for f in folders:
|
||||
if os.path.join(base, f) is self._updater_path:
|
||||
continue
|
||||
@@ -1064,12 +1070,14 @@ class SingletonUpdater:
|
||||
os.path.join(base, f)
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error removing folder {os.path.join(base, f)}: {e}")
|
||||
except Exception:
|
||||
bk_logger.exception(
|
||||
"Error removing folder %s:", os.path.join(base, f)
|
||||
)
|
||||
|
||||
except Exception as err:
|
||||
except Exception:
|
||||
error = "failed to create clean existing addon folder"
|
||||
print(error, str(err))
|
||||
self.print_verbose(error)
|
||||
self.print_trace()
|
||||
|
||||
# Walk through the base addon folder for rules on pre-removing
|
||||
@@ -1087,7 +1095,7 @@ class SingletonUpdater:
|
||||
os.remove(fl)
|
||||
self.print_verbose("Pre-removed file " + file)
|
||||
except OSError:
|
||||
print("Failed to pre-remove " + file)
|
||||
self.print_verbose("Failed to pre-remove " + file)
|
||||
self.print_trace()
|
||||
|
||||
# Walk through the temp addon sub folder for replacements
|
||||
@@ -1134,13 +1142,13 @@ class SingletonUpdater:
|
||||
# File did not previously exist, simply move it over.
|
||||
os.rename(srcFile, dest_file)
|
||||
self.print_verbose("New file " + os.path.basename(dest_file))
|
||||
except Exception as e:
|
||||
print(f"Error replacing file {file}: {e}")
|
||||
except Exception:
|
||||
bk_logger.exception("Error replacing file %s:", file)
|
||||
|
||||
# now remove the temp staging folder and downloaded zip
|
||||
try:
|
||||
shutil.rmtree(staging_path)
|
||||
except:
|
||||
except Exception:
|
||||
error = (
|
||||
"Error: Failed to remove existing staging directory, "
|
||||
"consider manually removing "
|
||||
@@ -1152,7 +1160,7 @@ class SingletonUpdater:
|
||||
# if post_update false, skip this function
|
||||
# else, unload/reload addon & trigger popup
|
||||
if not self._auto_reload_post_update:
|
||||
print("Restart blender to reload addon and complete update")
|
||||
bk_logger.info("Restart blender to reload addon and complete update")
|
||||
return
|
||||
|
||||
self.print_verbose("Reloading addon...")
|
||||
@@ -1165,12 +1173,12 @@ class SingletonUpdater:
|
||||
bpy.ops.wm.addon_disable(module=self._addon_package)
|
||||
bpy.ops.wm.addon_refresh()
|
||||
bpy.ops.wm.addon_enable(module=self._addon_package)
|
||||
print("2.7 reload complete")
|
||||
bk_logger.info("2.7 reload complete")
|
||||
else: # 2.8
|
||||
bpy.ops.preferences.addon_disable(module=self._addon_package)
|
||||
bpy.ops.preferences.addon_refresh()
|
||||
bpy.ops.preferences.addon_enable(module=self._addon_package)
|
||||
print("2.8 reload complete")
|
||||
bk_logger.info("2.8 reload complete")
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Other non-api functions and setups
|
||||
@@ -1190,10 +1198,10 @@ class SingletonUpdater:
|
||||
while 1:
|
||||
data = url_file.read(chunk)
|
||||
if not data:
|
||||
# print("done.")
|
||||
# bk_logger.info("done.")
|
||||
break
|
||||
f.write(data)
|
||||
# print("Read %s bytes" % len(data))
|
||||
# bk_logger.info("Read %s bytes" % len(data))
|
||||
f.close()
|
||||
|
||||
def version_tuple_from_text(self, text):
|
||||
@@ -1249,7 +1257,9 @@ class SingletonUpdater:
|
||||
self.print_verbose("Skipping async check, already started")
|
||||
# already running the bg thread
|
||||
elif self._update_ready is None:
|
||||
print("{} updater: Running background check for update".format(self.addon))
|
||||
bk_logger.info(
|
||||
"%s updater: Running background check for update", self.addon
|
||||
)
|
||||
self.start_async_check_update(False, callback)
|
||||
|
||||
def check_for_update_now(self, callback=None):
|
||||
@@ -1266,7 +1276,7 @@ class SingletonUpdater:
|
||||
self.start_async_check_update(True, callback)
|
||||
|
||||
def check_for_update(self, now=False):
|
||||
"""Check for update not in a syncrhonous manner.
|
||||
"""Check for update not in a synchronous manner.
|
||||
|
||||
This function is not async, will always return in sequential fashion
|
||||
but should have a parent which calls it in another thread.
|
||||
@@ -1449,7 +1459,7 @@ class SingletonUpdater:
|
||||
|
||||
res = self.stage_repository(self._update_link)
|
||||
if not res:
|
||||
print("Error in staging repository: " + str(res))
|
||||
bk_logger.error("Error in staging repository: %s", str(res))
|
||||
if callback is not None:
|
||||
callback(self._addon_package, self._error_msg)
|
||||
return self._error_msg
|
||||
@@ -1467,7 +1477,7 @@ class SingletonUpdater:
|
||||
|
||||
res = self.stage_repository(self._update_link)
|
||||
if not res:
|
||||
print("Error in staging repository: " + str(res))
|
||||
bk_logger.error("Error in staging repository: %s", str(res))
|
||||
if callback:
|
||||
callback(self._addon_package, self._error_msg)
|
||||
return self._error_msg
|
||||
@@ -1525,9 +1535,11 @@ class SingletonUpdater:
|
||||
os.rename(old_json_path, json_path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except Exception as err:
|
||||
print("Other OS error occurred while trying to rename old JSON")
|
||||
print(err)
|
||||
except Exception:
|
||||
self.print_verbose(
|
||||
"Other OS error occurred while trying to rename old JSON"
|
||||
)
|
||||
|
||||
self.print_trace()
|
||||
return json_path
|
||||
|
||||
@@ -1571,7 +1583,7 @@ class SingletonUpdater:
|
||||
|
||||
jpath = self.get_json_path()
|
||||
if not os.path.isdir(os.path.dirname(jpath)):
|
||||
print(
|
||||
bk_logger.error(
|
||||
"State error: Directory does not exist, cannot save json: ",
|
||||
os.path.basename(jpath),
|
||||
)
|
||||
@@ -1580,8 +1592,9 @@ class SingletonUpdater:
|
||||
with open(jpath, "w") as outf:
|
||||
data_out = json.dumps(self._json, indent=4)
|
||||
outf.write(data_out)
|
||||
except:
|
||||
print("Failed to open/save data to json: ", jpath)
|
||||
self.print_verbose(f"Wrote updater JSON settings to file: {jpath}")
|
||||
except Exception:
|
||||
self.print_verbose(f"Failed to open/save data to json: {jpath}")
|
||||
self.print_trace()
|
||||
self.print_verbose("Wrote out updater JSON settings with content:")
|
||||
self.print_verbose(str(self._json))
|
||||
@@ -1630,8 +1643,7 @@ class SingletonUpdater:
|
||||
try:
|
||||
self.check_for_update(now=now)
|
||||
except Exception as exception:
|
||||
print("Checking for update error:")
|
||||
print(exception)
|
||||
self.print_verbose(f"Checking for update error: {exception}")
|
||||
self.print_trace()
|
||||
if not self._error:
|
||||
self._update_ready = False
|
||||
@@ -1644,8 +1656,11 @@ class SingletonUpdater:
|
||||
self._check_thread = None
|
||||
|
||||
if callback:
|
||||
self.print_verbose("Finished check update, doing callback")
|
||||
callback(self._update_ready)
|
||||
self.print_verbose(
|
||||
"Finished check update, queueing callback on main thread"
|
||||
)
|
||||
# Run the callback on Blender's main thread to avoid bpy access from a background thread.
|
||||
tasks_queue.add_task((callback, (self._update_ready,)), wait=0)
|
||||
self.print_verbose("BG thread: Finished check update, no callback")
|
||||
|
||||
def stop_async_check_update(self):
|
||||
|
||||
@@ -24,11 +24,14 @@ Implements draw calls, popups, and operators that use the addon_updater.
|
||||
|
||||
import os
|
||||
import traceback
|
||||
import logging
|
||||
|
||||
import bpy
|
||||
from bpy.app.handlers import persistent
|
||||
|
||||
from . import client_lib
|
||||
from . import client_lib, utils
|
||||
|
||||
bk_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Safely import the updater.
|
||||
@@ -37,9 +40,9 @@ from . import client_lib
|
||||
try:
|
||||
from .addon_updater import Updater as updater
|
||||
except Exception as e:
|
||||
print("ERROR INITIALIZING UPDATER")
|
||||
print(str(e))
|
||||
traceback.print_exc()
|
||||
bk_logger.error("ERROR INITIALIZING UPDATER")
|
||||
bk_logger.error(str(e))
|
||||
bk_logger.error("%s", traceback.format_exc())
|
||||
|
||||
class SingletonUpdaterNone(object):
|
||||
"""Fake, bare minimum fields and functions for the updater object."""
|
||||
@@ -175,6 +178,10 @@ class AddonUpdaterInstallPopup(bpy.types.Operator):
|
||||
return True
|
||||
|
||||
def invoke(self, context, event):
|
||||
# Skip opening the popup when we already know there is nothing to show.
|
||||
if not updater.invalid_updater and updater.update_ready is False:
|
||||
updater.print_verbose("No update available; skipping popup")
|
||||
return {"CANCELLED"}
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context):
|
||||
@@ -229,9 +236,9 @@ class AddonUpdaterInstallPopup(bpy.types.Operator):
|
||||
# Should return 0, if not something happened.
|
||||
if updater.verbose:
|
||||
if res == 0:
|
||||
print("Updater returned successful")
|
||||
bk_logger.info("Updater returned successful")
|
||||
else:
|
||||
print("Updater returned {}, error occurred".format(res))
|
||||
bk_logger.info("Updater returned {}, error occurred".format(res))
|
||||
elif updater.update_ready is None:
|
||||
_ = updater.check_for_update(now=True)
|
||||
|
||||
@@ -322,9 +329,9 @@ class AddonUpdaterUpdateNow(bpy.types.Operator):
|
||||
# Should return 0, if not something happened.
|
||||
if updater.verbose:
|
||||
if res == 0:
|
||||
print("Updater returned successful")
|
||||
bk_logger.info("Updater returned successful")
|
||||
else:
|
||||
print("Updater error response: {}".format(res))
|
||||
bk_logger.info("Updater error response: {}".format(res))
|
||||
except Exception as expt:
|
||||
updater._error = "Error trying to run update"
|
||||
updater._error_msg = str(expt)
|
||||
@@ -450,7 +457,7 @@ class AddonUpdaterInstallManually(bpy.types.Operator):
|
||||
layout.label(text="Updater error")
|
||||
return
|
||||
|
||||
# Display error if a prior autoamted install failed.
|
||||
# Display error if a prior automated install failed.
|
||||
if self.error != "":
|
||||
col = layout.column()
|
||||
col.scale_y = 0.7
|
||||
@@ -692,7 +699,7 @@ def updater_run_install_popup_handler(scene):
|
||||
updater_run_install_popup_handler
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
bk_logger.error("%s", e)
|
||||
pass
|
||||
|
||||
if "ignore" in updater.json and updater.json["ignore"]:
|
||||
@@ -833,8 +840,8 @@ def check_for_update_nonthreaded(self, context):
|
||||
settings = get_user_preferences(bpy.context)
|
||||
if not settings:
|
||||
if updater.verbose:
|
||||
print(
|
||||
"Could not get {} preferences, update check skipped".format(__package__)
|
||||
bk_logger.info(
|
||||
"Could not get %s preferences, update check skipped", __package__
|
||||
)
|
||||
return
|
||||
updater.set_check_interval(
|
||||
@@ -1128,6 +1135,15 @@ def update_settings_ui(self, context, element=None):
|
||||
else:
|
||||
row.label(text="Last update check: Never")
|
||||
|
||||
version_row = box.row()
|
||||
version_row.label(
|
||||
text=f"BlenderKit v{utils.get_addon_version()} · Blender {bpy.app.version_string}",
|
||||
icon="INFO",
|
||||
)
|
||||
version_row.operator(
|
||||
"wm.blenderkit_copy_environment_info", text="Copy Info", icon="COPYDOWN"
|
||||
)
|
||||
|
||||
|
||||
def update_settings_ui_condensed(self, context, element=None):
|
||||
"""Preferences - Condensed drawing within preferences.
|
||||
@@ -1359,7 +1375,7 @@ def register(bl_info):
|
||||
"""Registering the operators in this module"""
|
||||
# Safer failure in case of issue loading module.
|
||||
if updater.error:
|
||||
print("Exiting updater registration, " + updater.error)
|
||||
bk_logger.error("Exiting updater registration, %s", updater.error)
|
||||
return
|
||||
updater.clear_state() # Clear internal vars, avoids reloading oddities.
|
||||
|
||||
|
||||
@@ -299,7 +299,7 @@ def append_material(file_name, matname=None, link=False, fake_user=True):
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
bk_logger.error(f"{e} - failed to open the asset file")
|
||||
bk_logger.error("%s - failed to open the asset file", e)
|
||||
# we have to find the new material , due to possible name changes
|
||||
mat = None
|
||||
for m in bpy.data.materials:
|
||||
@@ -532,7 +532,7 @@ def append_particle_system(
|
||||
total_max_threshold = 2000000
|
||||
# emitting too many parent particles just kills blender now.
|
||||
|
||||
# this part tuned child count, we'll leave children to artists only.
|
||||
# this part tuned child count, we'll leave children to authors only.
|
||||
# if count > total_max_threshold:
|
||||
# ratio = round(count / total_max_threshold)
|
||||
#
|
||||
@@ -621,14 +621,18 @@ def append_objects(
|
||||
|
||||
return_obs = []
|
||||
to_hidden_collection = []
|
||||
hidden_objects = []
|
||||
appended_collection = None
|
||||
main_object = None
|
||||
# get first at least one parent for sure
|
||||
# first get at least one parent for sure
|
||||
for ob in bpy.context.scene.objects: # type: ignore[union-attr]
|
||||
if ob.select_get():
|
||||
if not ob.parent:
|
||||
main_object = ob
|
||||
ob.location = location
|
||||
if ob.select_get() and not ob.parent:
|
||||
main_object = ob
|
||||
ob.location = location
|
||||
if (
|
||||
ob.hide_viewport or ob.hide_render
|
||||
): # saved assets only retain hide render state
|
||||
hidden_objects.append(ob)
|
||||
# do once again to ensure hidden objects are hidden
|
||||
for ob in bpy.context.scene.objects: # type: ignore[union-attr]
|
||||
if ob.select_get():
|
||||
@@ -654,7 +658,7 @@ def append_objects(
|
||||
main_object.matrix_world.translation = location
|
||||
|
||||
# move objects that should be hidden to a sub collection
|
||||
if len(to_hidden_collection) > 0 and appended_collection is not None:
|
||||
if to_hidden_collection and appended_collection is not None:
|
||||
hidden_collections = []
|
||||
scene_collection = bpy.context.scene.collection # type: ignore[union-attr]
|
||||
for ob in to_hidden_collection:
|
||||
@@ -682,7 +686,7 @@ def append_objects(
|
||||
if hide_collection in hidden_collections:
|
||||
continue
|
||||
# All other collections are moved to be children of the model collection
|
||||
bk_logger.info(f"{hide_collection}, {appended_collection}")
|
||||
bk_logger.info("%s, %s", hide_collection, appended_collection)
|
||||
# If target collection is specified, move collections there instead
|
||||
if collection and bpy.data.collections.get(collection):
|
||||
utils.move_collection(
|
||||
@@ -707,6 +711,12 @@ def append_objects(
|
||||
if orig_active_collection:
|
||||
bpy.context.view_layer.active_layer_collection = orig_active_collection # type: ignore[union-attr]
|
||||
|
||||
if hidden_objects:
|
||||
# only unique objects
|
||||
hidden_objects = list(set(hidden_objects))
|
||||
for ob in hidden_objects:
|
||||
ob.hide_set(True)
|
||||
|
||||
utils.selection_set(sel)
|
||||
# let collection also store info that it was created by BlenderKit, for purging reasons
|
||||
|
||||
@@ -753,16 +763,18 @@ def append_objects(
|
||||
obj.select_set(True)
|
||||
# we need to unhide object so make_local op can use those too.
|
||||
if link == True:
|
||||
if obj.hide_viewport:
|
||||
if (
|
||||
obj.hide_viewport or obj.hide_render
|
||||
): # saved assets only retain hide render state
|
||||
hidden_objects.append(obj)
|
||||
obj.hide_viewport = False
|
||||
obj.hide_set(False)
|
||||
return_obs.append(obj)
|
||||
|
||||
# Only after all objects are in scene! Otherwise gets broken relationships
|
||||
if link == True:
|
||||
bpy.ops.object.make_local(type="SELECT_OBJECT")
|
||||
for ob in hidden_objects:
|
||||
ob.hide_viewport = True
|
||||
ob.hide_set(True)
|
||||
|
||||
if kwargs.get("rotation") is not None:
|
||||
main_object.rotation_euler = kwargs["rotation"] # type: ignore[union-attr]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@ import logging
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
from typing import Any, Optional, Set, Tuple, Union
|
||||
|
||||
import bpy
|
||||
import mathutils
|
||||
@@ -28,8 +29,6 @@ from bpy.props import IntProperty, StringProperty
|
||||
from bpy_extras import view3d_utils
|
||||
from mathutils import Vector
|
||||
|
||||
from typing import Any, Optional, Tuple, Set, Union
|
||||
|
||||
from . import (
|
||||
bg_blender,
|
||||
colors,
|
||||
@@ -38,11 +37,12 @@ from . import (
|
||||
image_utils,
|
||||
paths,
|
||||
reports,
|
||||
search,
|
||||
ui,
|
||||
ui_bgl,
|
||||
ui_panels,
|
||||
utils,
|
||||
search,
|
||||
viewport_utils,
|
||||
)
|
||||
from .bl_ui_widgets.bl_ui_button import BL_UI_Button
|
||||
from .bl_ui_widgets.bl_ui_drag_panel import BL_UI_Drag_Panel
|
||||
@@ -56,11 +56,8 @@ handler_2d = None
|
||||
handler_3d = None
|
||||
|
||||
|
||||
DEAD_ZONE = 5 # pixels
|
||||
"""Number of pixels mouse must move to start drag operation."""
|
||||
|
||||
DRAG_THRESHOLD = 10 # pixels
|
||||
"""Number of pixels mouse must move to consider as a drag (vs click)."""
|
||||
DEFAULT_DRAG_THRESHOLD = 30 # pixels
|
||||
"""Pointer travel in pixels needed before we start rendering full drag hints."""
|
||||
|
||||
|
||||
def is_draw_cb_available(self: bpy.types.Operator, context: bpy.types.Context) -> bool:
|
||||
@@ -106,8 +103,8 @@ def draw_callback_dragging(
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
# Only draw 2D elements in the active region where the mouse is. Guard against destroyed operator.
|
||||
|
||||
# Only draw 2D elements in the active region where the mouse is. Guard against destroyed operator.
|
||||
if not is_draw_cb_available(self, context):
|
||||
return
|
||||
|
||||
@@ -356,6 +353,10 @@ def draw_callback_3d_dragging(
|
||||
if not utils.guard_from_crash():
|
||||
return
|
||||
|
||||
# ignore unless we are dragging
|
||||
if not self.drag:
|
||||
return
|
||||
|
||||
# Only draw 3D elements in VIEW_3D areas, not in outliner
|
||||
if context.area.type != "VIEW_3D":
|
||||
return
|
||||
@@ -522,19 +523,6 @@ def draw_progress(
|
||||
ui_bgl.draw_text(text, x, y + 8, 16, color)
|
||||
|
||||
|
||||
def find_and_activate_instancers(
|
||||
obj: bpy.types.Object,
|
||||
) -> Optional[bpy.types.Object]:
|
||||
for ob in bpy.context.visible_objects:
|
||||
if (
|
||||
ob.instance_type == "COLLECTION"
|
||||
and ob.instance_collection
|
||||
and obj.name in ob.instance_collection.objects
|
||||
):
|
||||
utils.activate(ob)
|
||||
return ob
|
||||
|
||||
|
||||
def mouse_raycast(
|
||||
region: bpy.types.Region, rv3d: bpy.types.RegionView3D, mx: int, my: int
|
||||
) -> Tuple[
|
||||
@@ -741,11 +729,9 @@ def deep_ray_cast(ray_origin: Vector, vec: Vector) -> Tuple[
|
||||
def object_in_particle_collection(o: bpy.types.Object) -> bool:
|
||||
"""checks if an object is in a particle system as instance, to not snap to it and not to try to attach material."""
|
||||
for p in bpy.data.particles:
|
||||
if p.render_type == "COLLECTION":
|
||||
if p.instance_collection:
|
||||
for o1 in p.instance_collection.objects:
|
||||
if o1 == o:
|
||||
return True
|
||||
if p.render_type == "COLLECTION" and p.instance_collection:
|
||||
if o in p.instance_collection.objects:
|
||||
return True
|
||||
if p.render_type == "COLLECTION":
|
||||
if p.instance_object == o:
|
||||
return True
|
||||
@@ -774,22 +760,6 @@ def get_node_tree(context: bpy.types.Context) -> bpy.types.NodeTree:
|
||||
return context.scene.compositing_node_group
|
||||
|
||||
|
||||
def assign_node_tree(
|
||||
node_space: bpy.types.SpaceNodeEditor, node_tree: bpy.types.NodeTree
|
||||
) -> None:
|
||||
"""Blender version invariant way to assign a node tree to the current node editor."""
|
||||
if bpy.app.version < (5, 0, 0):
|
||||
node_space.node_tree = node_tree
|
||||
return
|
||||
|
||||
# blender 5.0+
|
||||
# recover the node_group from data and assign it
|
||||
if hasattr(node_space, "node_group"):
|
||||
node_space.node_group = bpy.data.node_groups[node_tree.name]
|
||||
elif hasattr(node_space, "node_tree"):
|
||||
node_space.node_tree = node_tree
|
||||
|
||||
|
||||
class AssetDragOperator(bpy.types.Operator):
|
||||
"""Drag & drop assets into scene. Operator being drawn when dragging asset."""
|
||||
|
||||
@@ -797,9 +767,9 @@ class AssetDragOperator(bpy.types.Operator):
|
||||
bl_label = "BlenderKit asset drag drop"
|
||||
|
||||
asset_search_index: IntProperty(name="Active Index", default=0) # type: ignore
|
||||
drag_length: IntProperty(name="Drag_length", default=0) # type: ignore
|
||||
|
||||
object_name = None
|
||||
active_operator_id = None
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -813,32 +783,28 @@ class AssetDragOperator(bpy.types.Operator):
|
||||
self.hovered_outliner_element: Union[bpy.types.Object, bpy.types.Collection] = (
|
||||
None
|
||||
)
|
||||
self.active_window = None
|
||||
self.active_area = None
|
||||
self.active_region = None
|
||||
|
||||
self.orig_active_object = None
|
||||
self.orig_selected_objects = None
|
||||
self.orig_active_collection = None
|
||||
|
||||
self.downloader = None
|
||||
|
||||
# Mouse tracking variables
|
||||
self.start_mouse_x = None
|
||||
self.start_mouse_y = None
|
||||
self.start_mouse_x = 0
|
||||
self.start_mouse_y = 0
|
||||
|
||||
self.mouse_x = 0
|
||||
self.mouse_y = 0
|
||||
self.mouse_screen_x = 0
|
||||
self.mouse_screen_y = 0
|
||||
self.steps = 0
|
||||
|
||||
# Store the initial active region pointer
|
||||
self.active_region_pointer = None
|
||||
|
||||
# Initialize outliner tracking variables
|
||||
self.hovered_outliner_element = None
|
||||
self.outliner_area = None
|
||||
self.outliner_region = None
|
||||
self.orig_selected_objects = None
|
||||
self.orig_active_object = None
|
||||
self.orig_active_collection = None
|
||||
self.prev_area_type = None
|
||||
|
||||
# Initialize node editor tracking
|
||||
@@ -858,6 +824,8 @@ class AssetDragOperator(bpy.types.Operator):
|
||||
|
||||
self.iname = ""
|
||||
self.drag = False
|
||||
self.steps = 0
|
||||
self.closed_assetbar = False
|
||||
|
||||
def handlers_remove(self) -> None:
|
||||
"""Remove all draw handlers."""
|
||||
@@ -873,11 +841,8 @@ class AssetDragOperator(bpy.types.Operator):
|
||||
self, nodegroup_type: str, editor_type: Optional[str] = None
|
||||
) -> bool:
|
||||
"""Check if a nodegroup of a specific type is compatible with the given editor type."""
|
||||
# Direct matches
|
||||
if nodegroup_type == editor_type:
|
||||
return True
|
||||
# Generic nodegroups can work in any editor
|
||||
elif nodegroup_type is None:
|
||||
# Direct matches, or invalid editor
|
||||
if not nodegroup_type or nodegroup_type == editor_type:
|
||||
return True
|
||||
# Otherwise, not compatible
|
||||
return False
|
||||
@@ -958,7 +923,7 @@ class AssetDragOperator(bpy.types.Operator):
|
||||
|
||||
if obj.type == "MESH":
|
||||
temp_mesh = object_eval.to_mesh()
|
||||
mapping = create_material_mapping(obj, temp_mesh)
|
||||
_mapping = create_material_mapping(obj, temp_mesh)
|
||||
target_slot = temp_mesh.polygons[self.face_index].material_index
|
||||
object_eval.to_mesh_clear()
|
||||
else:
|
||||
@@ -1073,7 +1038,7 @@ class AssetDragOperator(bpy.types.Operator):
|
||||
target_collection = ""
|
||||
|
||||
# Check what type of element we're dropping on
|
||||
element_type = type(self.hovered_outliner_element).__name__
|
||||
_element_type = type(self.hovered_outliner_element).__name__
|
||||
|
||||
# If dropping on a collection, set target_collection parameter
|
||||
if isinstance(self.hovered_outliner_element, bpy.types.Collection):
|
||||
@@ -1480,10 +1445,10 @@ class AssetDragOperator(bpy.types.Operator):
|
||||
|
||||
for window in wins:
|
||||
# first let's test if it's in this window, so we know we shall continue
|
||||
window_x = window.x * self.resolution_factor
|
||||
window_y = window.y * self.resolution_factor
|
||||
window_width = window.width * self.resolution_factor
|
||||
window_height = window.height * self.resolution_factor
|
||||
window_x = window.x
|
||||
window_y = window.y
|
||||
window_width = window.width
|
||||
window_height = window.height
|
||||
if (
|
||||
x < window_x
|
||||
or x > window_x + window_width
|
||||
@@ -1513,7 +1478,6 @@ class AssetDragOperator(bpy.types.Operator):
|
||||
return None
|
||||
|
||||
context = bpy.context
|
||||
scene = context.scene
|
||||
view_layer = context.view_layer
|
||||
selected_objects = context.selected_objects
|
||||
active_object = context.active_object
|
||||
@@ -1630,11 +1594,14 @@ class AssetDragOperator(bpy.types.Operator):
|
||||
):
|
||||
"""Get the active object under the mouse cursor during drag."""
|
||||
|
||||
region_data = None
|
||||
|
||||
for space in active_area.spaces:
|
||||
if space.type == "VIEW_3D":
|
||||
region_data = space.region_3d
|
||||
# precise placement in ortho views, and quad view
|
||||
region_data = viewport_utils.region_data_for_view(active_area, active_region)
|
||||
if region_data is None:
|
||||
for space in active_area.spaces:
|
||||
if space.type == "VIEW_3D":
|
||||
region_data = getattr(space, "region_3d", None)
|
||||
if region_data is not None:
|
||||
break
|
||||
|
||||
# Need to temporarily override context for raycasting
|
||||
if bpy.app.version < (3, 2, 0): # B3.0, B3.1 - custom context override
|
||||
@@ -1643,9 +1610,7 @@ class AssetDragOperator(bpy.types.Operator):
|
||||
"screen": active_window.screen,
|
||||
"area": active_area,
|
||||
"region": active_region,
|
||||
"region_data": active_area.spaces[
|
||||
0
|
||||
].region_3d, # Get region_data from space_data
|
||||
"region_data": region_data,
|
||||
"scene": context.scene,
|
||||
"view_layer": context.view_layer,
|
||||
}
|
||||
@@ -1757,69 +1722,70 @@ class AssetDragOperator(bpy.types.Operator):
|
||||
self.in_node_editor = False
|
||||
self.node_editor_type = None
|
||||
|
||||
def _cleanup_drag(self, ui_props, cls, *, reopen_assetbar: bool = False) -> None:
|
||||
"""Shared teardown: remove handlers, restore cursor, reset drag state."""
|
||||
self.handlers_remove()
|
||||
bpy.context.window.cursor_modal_restore()
|
||||
ui_props.dragging = False
|
||||
if self.closed_assetbar:
|
||||
bpy.ops.view3d.run_assetbar_fix_context(keep_running=True, do_search=False)
|
||||
if reopen_assetbar:
|
||||
bpy.ops.view3d.blenderkit_asset_bar_widget(
|
||||
"INVOKE_REGION_WIN", do_search=False
|
||||
)
|
||||
cls.active_operator_id = None
|
||||
|
||||
def modal(self, context: bpy.types.Context, event: bpy.types.Event) -> Set[str]:
|
||||
cls = type(self)
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
|
||||
self.resolution_factor = (
|
||||
bpy.context.preferences.system.pixel_size
|
||||
/ bpy.context.preferences.view.ui_scale
|
||||
)
|
||||
self.mouse_screen_x = int(
|
||||
context.window.x * self.resolution_factor + event.mouse_x
|
||||
)
|
||||
self.mouse_screen_y = int(
|
||||
context.window.y * self.resolution_factor + event.mouse_y
|
||||
)
|
||||
self.mouse_screen_x = int(context.window.x + event.mouse_x)
|
||||
self.mouse_screen_y = int(context.window.y + event.mouse_y)
|
||||
|
||||
# Find the active region under the mouse cursor using actual screen coordinates
|
||||
self.active_window, self.active_area, self.active_region = (
|
||||
self.find_active_region(self.mouse_screen_x, self.mouse_screen_y)
|
||||
found_window, found_area, found_region = self.find_active_region(
|
||||
self.mouse_screen_x, self.mouse_screen_y
|
||||
)
|
||||
if (
|
||||
found_region is not None
|
||||
and found_area is not None
|
||||
and found_window is not None
|
||||
):
|
||||
self.active_window, self.active_area, self.active_region = (
|
||||
found_window,
|
||||
found_area,
|
||||
found_region,
|
||||
)
|
||||
# --- CURSOR VISIBILITY FIX ---
|
||||
if self.active_region is None or self.active_area is None:
|
||||
bpy.context.window.cursor_modal_set("STOP")
|
||||
# bpy.context.window.cursor_modal_set("STOP")
|
||||
bpy.context.window.cursor_modal_restore()
|
||||
return {"PASS_THROUGH"}
|
||||
elif self.drag:
|
||||
|
||||
if self.drag:
|
||||
bpy.context.window.cursor_modal_set("NONE")
|
||||
|
||||
# Convert screen coords (bottom-left) to region-local coords
|
||||
# window.x/y and region.x/y are also in bottom-left coordinate system
|
||||
self.mouse_x = int(
|
||||
self.mouse_screen_x
|
||||
- self.active_window.x * self.resolution_factor
|
||||
- self.active_region.x
|
||||
self.mouse_screen_x - self.active_window.x - self.active_region.x
|
||||
)
|
||||
self.mouse_y = int(
|
||||
self.mouse_screen_y
|
||||
- self.active_window.y * self.resolution_factor
|
||||
- self.active_region.y
|
||||
self.mouse_screen_y - self.active_window.y - self.active_region.y
|
||||
)
|
||||
|
||||
if self.start_mouse_x is None or self.start_mouse_y is None:
|
||||
self.start_mouse_x = self.mouse_x
|
||||
self.start_mouse_y = self.mouse_y
|
||||
|
||||
# --- REDRAW ALL WINDOWS/AREAS FOR MULTI-WINDOW DRAG ---
|
||||
# redraw all windows to update cursor and other elements
|
||||
for window in bpy.context.window_manager.windows:
|
||||
for area in window.screen.areas:
|
||||
area.tag_redraw()
|
||||
|
||||
current_area_type = self.active_area.type if self.active_area else None
|
||||
current_area_type = self.active_area.type
|
||||
|
||||
# Check if we're transitioning out of the outliner
|
||||
if (
|
||||
self.prev_area_type
|
||||
and self.prev_area_type == "OUTLINER"
|
||||
and current_area_type != "OUTLINER"
|
||||
):
|
||||
# If we're leaving the outliner, restore the original selection
|
||||
if self.prev_area_type == "OUTLINER" and current_area_type != "OUTLINER":
|
||||
self.restore_original_selection()
|
||||
|
||||
# shift pressed
|
||||
if event.shift:
|
||||
self.shift_pressed = True
|
||||
else:
|
||||
self.shift_pressed = False
|
||||
self.shift_pressed = event.shift
|
||||
|
||||
# Track if we're in a node editor
|
||||
self._handle_node_editor_type(current_area_type, self.active_area)
|
||||
@@ -1832,12 +1798,8 @@ class AssetDragOperator(bpy.types.Operator):
|
||||
# Store the active region pointer for drawing 2D elements only in this region
|
||||
self.active_region_pointer = self.active_region.as_pointer()
|
||||
|
||||
# Make sure all 3D views get redrawn
|
||||
for area in context.screen.areas:
|
||||
area.tag_redraw()
|
||||
|
||||
# Handle outliner interaction
|
||||
if self.active_area.type == "OUTLINER":
|
||||
if current_area_type == "OUTLINER":
|
||||
self.hovered_outliner_element = self.find_outliner_element_under_mouse()
|
||||
self.outliner_window = self.active_window
|
||||
self.outliner_area = self.active_area
|
||||
@@ -1853,48 +1815,46 @@ class AssetDragOperator(bpy.types.Operator):
|
||||
self.active_region_pointer = context.region.as_pointer()
|
||||
|
||||
# are we dragging already?
|
||||
delta_x = abs(self.start_mouse_x - self.mouse_screen_x)
|
||||
delta_y = abs(self.start_mouse_y - self.mouse_screen_y)
|
||||
if not self.drag and (
|
||||
abs(self.start_mouse_x - self.mouse_x) > DRAG_THRESHOLD
|
||||
or abs(self.start_mouse_y - self.mouse_y) > DRAG_THRESHOLD
|
||||
delta_x > DEFAULT_DRAG_THRESHOLD or delta_y > DEFAULT_DRAG_THRESHOLD
|
||||
):
|
||||
self.drag = True
|
||||
|
||||
if self.drag and ui_props.assetbar_on:
|
||||
# turn off asset bar here, shout start again after finishing drag drop.
|
||||
if self.drag and ui_props.assetbar_on and not self.closed_assetbar:
|
||||
# turn off asset bar here; reopen after placement when we actually dragged
|
||||
ui_props.turn_off = True
|
||||
|
||||
if (
|
||||
event.type == "ESC"
|
||||
or not ui.mouse_in_region(context.region, self.mouse_x, self.mouse_y)
|
||||
) and (not self.drag or self.steps < DEAD_ZONE):
|
||||
# this case is for canceling from inside popup card when there's an escape attempt to close the window
|
||||
return {"PASS_THROUGH"}
|
||||
self.closed_assetbar = True
|
||||
|
||||
if event.type in {"RIGHTMOUSE", "ESC"}:
|
||||
# Restore original selection if we changed it
|
||||
self.restore_original_selection()
|
||||
|
||||
self.handlers_remove()
|
||||
bpy.context.window.cursor_modal_restore()
|
||||
ui_props.dragging = False
|
||||
bpy.ops.view3d.blenderkit_asset_bar_widget(
|
||||
"INVOKE_REGION_WIN", do_search=False
|
||||
)
|
||||
|
||||
self._cleanup_drag(ui_props, cls, reopen_assetbar=True)
|
||||
return {"CANCELLED"}
|
||||
|
||||
self.steps += 1
|
||||
|
||||
if (
|
||||
event.type == "ESC"
|
||||
or not ui.mouse_in_region(context.region, self.mouse_x, self.mouse_y)
|
||||
) and (not self.drag or self.steps < 5):
|
||||
# this case is for canceling from inside popup card when there's an escape attempt to close the window
|
||||
return {"PASS_THROUGH"}
|
||||
|
||||
sprops = bpy.context.window_manager.blenderkit_models
|
||||
if event.type == "WHEELUPMOUSE":
|
||||
sprops.offset_rotation_amount += sprops.offset_rotation_step
|
||||
elif event.type == "WHEELDOWNMOUSE":
|
||||
sprops.offset_rotation_amount -= sprops.offset_rotation_step
|
||||
|
||||
if (
|
||||
event.type == "MOUSEMOVE"
|
||||
or event.type == "WHEELUPMOUSE"
|
||||
or event.type == "WHEELDOWNMOUSE"
|
||||
):
|
||||
|
||||
if event.type in {
|
||||
"MOUSEMOVE",
|
||||
"INBETWEEN_MOUSEMOVE",
|
||||
"WHEELUPMOUSE",
|
||||
"WHEELDOWNMOUSE",
|
||||
}:
|
||||
# sometimes active area or region can be None, so we need to check for that
|
||||
if self.active_area is None or self.active_region is None:
|
||||
return {"RUNNING_MODAL"}
|
||||
@@ -1904,11 +1864,7 @@ class AssetDragOperator(bpy.types.Operator):
|
||||
self.has_hit = False
|
||||
|
||||
# Only perform raycasting in 3D view areas
|
||||
if (
|
||||
self.active_region
|
||||
and self.active_area
|
||||
and self.active_area.type == "VIEW_3D"
|
||||
):
|
||||
if current_area_type == "VIEW_3D":
|
||||
# prefetch the drag active object info
|
||||
self.drag_raycast_3d_view(
|
||||
context,
|
||||
@@ -1921,21 +1877,15 @@ class AssetDragOperator(bpy.types.Operator):
|
||||
if self.asset_data["assetType"] in ["model", "printable"]:
|
||||
self.snapped_bbox_min = Vector(self.asset_data["bbox_min"])
|
||||
self.snapped_bbox_max = Vector(self.asset_data["bbox_max"])
|
||||
elif self.active_area.type != "VIEW_3D":
|
||||
elif current_area_type != "VIEW_3D":
|
||||
# In outliner, don't do raycasting, but keep has_hit to avoid errors
|
||||
self.has_hit = False
|
||||
|
||||
if event.type == "LEFTMOUSE" and event.value == "RELEASE":
|
||||
self.mouse_release(context) # Pass context here
|
||||
self.handlers_remove()
|
||||
bpy.context.window.cursor_modal_restore()
|
||||
|
||||
bpy.ops.view3d.run_assetbar_fix_context(keep_running=True, do_search=False)
|
||||
ui_props.dragging = False
|
||||
self.mouse_release(context)
|
||||
self._cleanup_drag(ui_props, cls)
|
||||
return {"FINISHED"}
|
||||
|
||||
self.steps += 1
|
||||
|
||||
# pass event to assetbar so it can close itself
|
||||
if ui_props.assetbar_on and ui_props.turn_off:
|
||||
return {"PASS_THROUGH"}
|
||||
@@ -1946,9 +1896,29 @@ class AssetDragOperator(bpy.types.Operator):
|
||||
# Before registering callbacks, check for canceling situations: login and localdir popups, sculpt popup/switch
|
||||
sr = search.get_search_results()
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
if ui_props.dragging:
|
||||
return {"CANCELLED"}
|
||||
cls = type(self)
|
||||
if cls.active_operator_id is not None and cls.active_operator_id != id(self):
|
||||
return {"CANCELLED"}
|
||||
# Acquire drag lock immediately so concurrent invoke paths cannot race while this invoke initializes.
|
||||
ui_props.dragging = True
|
||||
cls.active_operator_id = id(self)
|
||||
self.closed_assetbar = False
|
||||
# Use the asset_search_index parameter passed to the operator, not the global ui_props.active_index
|
||||
# This is critical for multi-window support where active_index is shared across windows
|
||||
self.asset_data = dict(sr[self.asset_search_index])
|
||||
|
||||
# Initialize drag-start coordinates immediately in invoke. If mouse-move
|
||||
# events are sparse (or arrive late), we still compute threshold against
|
||||
# the true click/press origin instead of first modal tick.
|
||||
self.mouse_screen_x = int(context.window.x + event.mouse_x)
|
||||
self.mouse_screen_y = int(context.window.y + event.mouse_y)
|
||||
self.start_mouse_x = self.mouse_screen_x
|
||||
self.start_mouse_y = self.mouse_screen_y
|
||||
# Author assets should not be dragged, cancel immediately
|
||||
if self.asset_data.get("assetType") == "author":
|
||||
return {"CANCELLED"}
|
||||
# add-ons
|
||||
if self.asset_data.get("assetType") == "addon" and not self.asset_data.get(
|
||||
"canDownload"
|
||||
@@ -1959,6 +1929,8 @@ class AssetDragOperator(bpy.types.Operator):
|
||||
bpy.ops.wm.blenderkit_url_dialog(
|
||||
"INVOKE_REGION_WIN", url=url, message=message, link_text=link_text
|
||||
)
|
||||
ui_props.dragging = False
|
||||
cls.active_operator_id = None
|
||||
return {"CANCELLED"}
|
||||
|
||||
if not self.asset_data.get("canDownload"):
|
||||
@@ -1969,6 +1941,8 @@ class AssetDragOperator(bpy.types.Operator):
|
||||
bpy.ops.wm.blenderkit_url_dialog(
|
||||
"INVOKE_REGION_WIN", url=url, message=message, link_text=link_text
|
||||
)
|
||||
ui_props.dragging = False
|
||||
cls.active_operator_id = None
|
||||
return {"CANCELLED"}
|
||||
|
||||
prefs = bpy.context.preferences.addons[__package__].preferences
|
||||
@@ -1982,26 +1956,31 @@ class AssetDragOperator(bpy.types.Operator):
|
||||
bpy.ops.wm.blenderkit_url_dialog(
|
||||
"INVOKE_REGION_WIN", url=url, message=message, link_text=link_text
|
||||
)
|
||||
ui_props.dragging = False
|
||||
cls.active_operator_id = None
|
||||
return {"CANCELLED"}
|
||||
|
||||
if self.asset_data.get("assetType") == "brush":
|
||||
if not (context.sculpt_object or context.image_paint_object):
|
||||
# either switch to sculpt mode and layout automatically or show a popup message
|
||||
if context.active_object and context.active_object.type == "MESH":
|
||||
bpy.ops.object.mode_set(mode="SCULPT")
|
||||
self.mouse_release(context) # does the main job with assets
|
||||
if self.asset_data.get("assetType") == "brush" and not (
|
||||
context.sculpt_object or context.image_paint_object
|
||||
):
|
||||
# either switch to sculpt mode and layout automatically or show a popup message
|
||||
if context.active_object and context.active_object.type == "MESH":
|
||||
bpy.ops.object.mode_set(mode="SCULPT")
|
||||
self.mouse_release(context) # does the main job with assets
|
||||
|
||||
if bpy.data.workspaces.get("Sculpting") is not None:
|
||||
bpy.context.window.workspace = bpy.data.workspaces["Sculpting"]
|
||||
reports.add_report(
|
||||
"Automatically switched to sculpt mode to use brushes."
|
||||
)
|
||||
else:
|
||||
message = "Select a mesh and switch to sculpt or image paint modes to use the brushes."
|
||||
bpy.ops.wm.blenderkit_popup_dialog(
|
||||
"INVOKE_REGION_WIN", message=message, width=500
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
if bpy.data.workspaces.get("Sculpting") is not None:
|
||||
bpy.context.window.workspace = bpy.data.workspaces["Sculpting"]
|
||||
reports.add_report(
|
||||
"Automatically switched to sculpt mode to use brushes."
|
||||
)
|
||||
else:
|
||||
message = "Select a mesh and switch to sculpt or image paint modes to use the brushes."
|
||||
bpy.ops.wm.blenderkit_popup_dialog(
|
||||
"INVOKE_REGION_WIN", message=message, width=500
|
||||
)
|
||||
ui_props.dragging = False
|
||||
cls.active_operator_id = None
|
||||
return {"CANCELLED"}
|
||||
|
||||
# the arguments we pass the the callback
|
||||
args = (self, context)
|
||||
@@ -2044,48 +2023,25 @@ class AssetDragOperator(bpy.types.Operator):
|
||||
# but if RNA Struct fails we are not longer able to remove it, so we log an error and store None
|
||||
self._handlers_universal[space_type] = handler
|
||||
except (AttributeError, TypeError) as e:
|
||||
bk_logger.error(f"Could not register handler for {space_type}: {e}")
|
||||
bk_logger.error("Could not register handler for %s: %s", space_type, e)
|
||||
self._handlers_universal[space_type] = None
|
||||
|
||||
self.mouse_x = 0
|
||||
self.mouse_y = 0
|
||||
self.mouse_screen_x = 0
|
||||
self.mouse_screen_y = 0
|
||||
self.steps = 0
|
||||
self.mouse_screen_x = self.start_mouse_x
|
||||
self.mouse_screen_y = self.start_mouse_y
|
||||
# Store the initial active region pointer
|
||||
self.active_region_pointer = context.region.as_pointer()
|
||||
|
||||
# Initialize outliner tracking variables
|
||||
self.hovered_outliner_element = None
|
||||
self.outliner_area = None
|
||||
self.outliner_region = None
|
||||
self.orig_selected_objects = None
|
||||
self.orig_active_object = None
|
||||
self.orig_active_collection = None
|
||||
self.prev_area_type = context.area.type # Track previous area type
|
||||
|
||||
# Initialize node editor tracking
|
||||
self.in_node_editor = False
|
||||
self.node_editor_type = None
|
||||
|
||||
self.shift_pressed = False
|
||||
|
||||
# Initialize has_hit to False, and set other 3D properties
|
||||
# We'll only use these in 3D views, not in outliner
|
||||
self.has_hit = False
|
||||
self.snapped_location = (0, 0, 0)
|
||||
self.snapped_normal = (0, 0, 1)
|
||||
self.snapped_rotation = (0, 0, 0)
|
||||
self.face_index = 0
|
||||
self.matrix = None
|
||||
|
||||
self.iname = f".{self.asset_data['thumbnail_small']}"
|
||||
self.iname = (self.iname[:63]) if len(self.iname) > 63 else self.iname
|
||||
|
||||
bpy.context.window.cursor_modal_set("NONE")
|
||||
bpy.context.window.cursor_modal_restore()
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
ui_props.dragging = True
|
||||
self.drag = False
|
||||
self.steps = 0
|
||||
context.window_manager.modal_handler_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
@@ -2245,7 +2201,7 @@ class DownloadGizmoOperator(BL_UI_OT_draw_operator):
|
||||
self.button_close.set_image_size((button_size, button_size))
|
||||
self.button_close.set_image_position((0, 0))
|
||||
|
||||
directory = paths.get_temp_dir("%s_search" % self.asset_data["assetType"])
|
||||
directory = paths.get_temp_dir(f"{self.asset_data['assetType']}_search")
|
||||
thumbnail_path = os.path.join(directory, self.asset_data["thumbnail_small"])
|
||||
|
||||
self.image.set_image(thumbnail_path)
|
||||
@@ -2296,7 +2252,10 @@ class DownloadGizmoOperator(BL_UI_OT_draw_operator):
|
||||
bk_logger.debug("unregistering class %s", cls)
|
||||
instances_copy = cls.instances.copy()
|
||||
for instance in instances_copy:
|
||||
bk_logger.debug("- class instance %s", instance)
|
||||
try:
|
||||
bk_logger.debug("- class instance %s", instance)
|
||||
except ReferenceError:
|
||||
bk_logger.debug("- class instance <deleted>")
|
||||
try:
|
||||
instance.unregister_handlers(instance.context)
|
||||
except Exception as e:
|
||||
@@ -2407,37 +2366,6 @@ def create_material_mapping(obj, temp_mesh):
|
||||
return mapping
|
||||
|
||||
|
||||
def add_set_material_node(tree):
|
||||
"""Add a Set Material node at the end of the node tree"""
|
||||
# Find output node
|
||||
output_node = None
|
||||
for node in tree.nodes:
|
||||
if node.type == "GROUP_OUTPUT":
|
||||
output_node = node
|
||||
break
|
||||
|
||||
if output_node:
|
||||
# Create Set Material node
|
||||
set_mat_node = tree.nodes.new("GeometryNodeSetMaterial")
|
||||
# Position it before output
|
||||
set_mat_node.location = (output_node.location.x - 200, output_node.location.y)
|
||||
|
||||
# Connect nodes
|
||||
last_geometry_socket = None
|
||||
for source in output_node.inputs:
|
||||
if source.type == "GEOMETRY":
|
||||
if source.is_linked:
|
||||
last_geometry_socket = source.links[0].from_socket
|
||||
break
|
||||
|
||||
if last_geometry_socket:
|
||||
tree.links.new(last_geometry_socket, set_mat_node.inputs["Geometry"])
|
||||
tree.links.new(set_mat_node.outputs["Geometry"], output_node.inputs[0])
|
||||
|
||||
return set_mat_node
|
||||
return None
|
||||
|
||||
|
||||
classes = (
|
||||
AssetDragOperator,
|
||||
DownloadGizmoOperator,
|
||||
|
||||
@@ -30,6 +30,13 @@ RENDER_OBTYPES = ["MESH", "CURVE", "SURFACE", "METABALL", "TEXT"]
|
||||
_BLE_5_PLUS = bpy.app.version >= (5, 0, 0)
|
||||
|
||||
|
||||
def _image_tile_count(image) -> int:
|
||||
"""Return the number of tiles for a UDIM image, 1 for regular images."""
|
||||
if getattr(image, "source", "") == "TILED" and hasattr(image, "tiles"):
|
||||
return max(1, len(image.tiles))
|
||||
return 1
|
||||
|
||||
|
||||
def check_material(props, mat):
|
||||
e = bpy.context.scene.render.engine
|
||||
shaders = []
|
||||
@@ -62,7 +69,10 @@ def check_material(props, mat):
|
||||
if n.image not in textures:
|
||||
textures.append(n.image)
|
||||
props.texture_count += 1
|
||||
total_pixels += n.image.size[0] * n.image.size[1]
|
||||
# Multiply per-tile pixel area by tile count so that
|
||||
# UDIM images (source='TILED') report the correct total.
|
||||
n_tiles = _image_tile_count(n.image)
|
||||
total_pixels += n_tiles * n.image.size[0] * n.image.size[1]
|
||||
|
||||
maxres = max(n.image.size[0], n.image.size[1])
|
||||
props.texture_resolution_max = max(
|
||||
@@ -138,7 +148,10 @@ def check_render_engine(props, obs):
|
||||
|
||||
textures.append(n.image)
|
||||
props.texture_count += 1
|
||||
total_pixels += n.image.size[0] * n.image.size[1]
|
||||
# Multiply per-tile pixel area by tile count so that
|
||||
# UDIM images (source='TILED') report the correct total.
|
||||
n_tiles = _image_tile_count(n.image)
|
||||
total_pixels += n_tiles * n.image.size[0] * n.image.size[1]
|
||||
|
||||
maxres = max(n.image.size[0], n.image.size[1])
|
||||
props.texture_resolution_max = max(
|
||||
@@ -344,7 +357,7 @@ def check_meshprops(props, obs):
|
||||
props.manifold = manifold
|
||||
|
||||
|
||||
def countObs(props, obs):
|
||||
def count_objects(props, obs):
|
||||
ob_types = {}
|
||||
count = len(obs)
|
||||
for ob in obs:
|
||||
@@ -425,7 +438,21 @@ def get_autotags():
|
||||
check_anim(props, obs)
|
||||
check_meshprops(props, obs)
|
||||
check_modifiers(props, obs)
|
||||
countObs(props, obs)
|
||||
count_objects(props, obs)
|
||||
|
||||
elif ui.asset_type == "SCENE":
|
||||
scene = bpy.context.scene
|
||||
props = scene.blenderkit
|
||||
if props.name == "":
|
||||
props.name = scene.name
|
||||
dim, bbox_min, bbox_max = utils.get_scene_dimensions(scene)
|
||||
props.dimensions = dim
|
||||
props.bbox_min = bbox_min
|
||||
props.bbox_max = bbox_max
|
||||
scene_objects = list(scene.objects)
|
||||
check_meshprops(props, scene_objects)
|
||||
count_objects(props, scene_objects)
|
||||
|
||||
elif ui.asset_type == "MATERIAL":
|
||||
# reset some properties here, because they might not get re-filled at all when they aren't needed anymore.
|
||||
|
||||
@@ -434,6 +461,7 @@ def get_autotags():
|
||||
props.texture_resolution_max = 0
|
||||
props.texture_resolution_min = 0
|
||||
check_material(props, mat)
|
||||
|
||||
elif ui.asset_type == "HDR":
|
||||
# reset some properties here, because they might not get re-filled at all when they aren't needed anymore.
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ from bpy.props import (
|
||||
FloatProperty,
|
||||
IntProperty,
|
||||
FloatVectorProperty,
|
||||
StringProperty,
|
||||
)
|
||||
|
||||
from . import bg_blender, global_vars, paths, tasks_queue, utils, upload, search
|
||||
@@ -81,16 +82,31 @@ def get_texture_ui(tpath, iname):
|
||||
return tex
|
||||
|
||||
|
||||
def check_thumbnail(props, imgpath):
|
||||
def check_thumbnail(
|
||||
props,
|
||||
imgpath,
|
||||
*,
|
||||
texture_name="upload_preview",
|
||||
flag_attr="has_thumbnail",
|
||||
state_attr="thumbnail_generating_state",
|
||||
):
|
||||
"""Reload a thumbnail preview and update status attributes."""
|
||||
|
||||
def _set_prop(attr_name, value):
|
||||
if attr_name and hasattr(props, attr_name):
|
||||
setattr(props, attr_name, value)
|
||||
|
||||
# TODO implement check if the file exists, if size is correct etc. needs some care
|
||||
if imgpath == "":
|
||||
props.has_thumbnail = False
|
||||
_set_prop(flag_attr, False)
|
||||
return None
|
||||
img = utils.get_hidden_image(imgpath, "upload_preview", force_reload=True)
|
||||
img = utils.get_hidden_image(imgpath, texture_name, force_reload=True)
|
||||
if img is not None: # and img.size[0] == img.size[1] and img.size[0] >= 512 and (
|
||||
# img.file_format == 'JPEG' or img.file_format == 'PNG'):
|
||||
props.has_thumbnail = True
|
||||
props.thumbnail_generating_state = ""
|
||||
_set_prop(flag_attr, True)
|
||||
if hasattr(props, "THUMBNAIL_GENERATING_STATE"):
|
||||
props.THUMBNAIL_GENERATING_STATE = ""
|
||||
_set_prop(state_attr, "")
|
||||
|
||||
utils.get_hidden_texture(img.name)
|
||||
# pcoll = icons.icon_collections["previews"]
|
||||
@@ -98,7 +114,7 @@ def check_thumbnail(props, imgpath):
|
||||
|
||||
return img
|
||||
else:
|
||||
props.has_thumbnail = False
|
||||
_set_prop(flag_attr, False)
|
||||
output = ""
|
||||
if (
|
||||
img is None
|
||||
@@ -115,7 +131,7 @@ def check_thumbnail(props, imgpath):
|
||||
# output += 'image too small, should be at least 512x512\n'
|
||||
# if img.file_format != 'JPEG' or img.file_format != 'PNG':
|
||||
# output += 'image has to be a jpeg or png'
|
||||
props.thumbnail_generating_state = output
|
||||
_set_prop(state_attr, output)
|
||||
|
||||
|
||||
def update_upload_model_preview(self, context):
|
||||
@@ -126,6 +142,20 @@ def update_upload_model_preview(self, context):
|
||||
check_thumbnail(props, imgpath)
|
||||
|
||||
|
||||
def update_wire_thumbnail_preview(self, context):
|
||||
ob = utils.get_active_model()
|
||||
if ob is not None:
|
||||
props = ob.blenderkit
|
||||
imgpath = props.wire_thumbnail
|
||||
check_thumbnail(
|
||||
props,
|
||||
imgpath,
|
||||
texture_name=".upload_preview_wire",
|
||||
flag_attr=None,
|
||||
state_attr="wire_thumbnail_generating_state",
|
||||
)
|
||||
|
||||
|
||||
def update_upload_scene_preview(self, context):
|
||||
s = bpy.context.scene
|
||||
props = s.blenderkit
|
||||
@@ -149,7 +179,7 @@ def update_upload_brush_preview(self, context):
|
||||
brush = utils.get_active_brush()
|
||||
if brush is not None:
|
||||
props = brush.blenderkit
|
||||
imgpath = bpy.path.abspath(brush.icon_filepath)
|
||||
imgpath = props.thumbnail
|
||||
check_thumbnail(props, imgpath)
|
||||
|
||||
|
||||
@@ -182,9 +212,26 @@ def start_model_thumbnailer(
|
||||
):
|
||||
"""Start Blender in background and render the thumbnail."""
|
||||
SCRIPT_NAME = "autothumb_model_bg.py"
|
||||
thumbnail_upload_type = (
|
||||
json_args.get("thumbnail_upload_type") if json_args else None
|
||||
)
|
||||
is_wire_upload = thumbnail_upload_type == "wire_thumbnail"
|
||||
computing_attr = (
|
||||
"is_generating_wire_thumbnail" if is_wire_upload else "is_generating_thumbnail"
|
||||
)
|
||||
state_attr = (
|
||||
"wire_thumbnail_generating_state"
|
||||
if is_wire_upload
|
||||
else "thumbnail_generating_state"
|
||||
)
|
||||
|
||||
def _set_prop(attr_name, value):
|
||||
if props and hasattr(props, attr_name):
|
||||
setattr(props, attr_name, value)
|
||||
|
||||
if props:
|
||||
props.is_generating_thumbnail = True
|
||||
props.thumbnail_generating_state = "Saving .blend file"
|
||||
_set_prop(computing_attr, True)
|
||||
_set_prop(state_attr, "Saving .blend file")
|
||||
|
||||
datafile = os.path.join(json_args["tempdir"], BLENDERKIT_EXPORT_DATA_FILE)
|
||||
user_preferences = bpy.context.preferences.addons[__package__].preferences
|
||||
@@ -194,6 +241,10 @@ def start_model_thumbnailer(
|
||||
"cycles"
|
||||
].preferences.compute_device_type
|
||||
|
||||
json_args["thumbnail_disable_subdivision"] = (
|
||||
user_preferences.thumbnail_disable_subdivision
|
||||
)
|
||||
|
||||
try:
|
||||
with open(datafile, "w", encoding="utf-8") as s:
|
||||
json.dump(json_args, s, ensure_ascii=False, indent=4)
|
||||
@@ -228,9 +279,10 @@ def start_model_thumbnailer(
|
||||
env=env,
|
||||
)
|
||||
bk_logger.info("Started Blender executing %s on file %s", SCRIPT_NAME, datafile)
|
||||
eval_path_computing = f"bpy.data.objects['{json_args['asset_name']}'].blenderkit.is_generating_thumbnail"
|
||||
eval_path_state = f"bpy.data.objects['{json_args['asset_name']}'].blenderkit.thumbnail_generating_state"
|
||||
eval_path = f"bpy.data.objects['{json_args['asset_name']}']"
|
||||
eval_path_base = f"bpy.data.objects['{json_args['asset_name']}']"
|
||||
eval_path = eval_path_base
|
||||
eval_path_computing = f"{eval_path_base}.blenderkit.{computing_attr}"
|
||||
eval_path_state = f"{eval_path_base}.blenderkit.{state_attr}"
|
||||
name = f"{json_args['asset_name']} thumbnailer"
|
||||
bg_blender.add_bg_process(
|
||||
name=name,
|
||||
@@ -241,7 +293,7 @@ def start_model_thumbnailer(
|
||||
process=proc,
|
||||
)
|
||||
if props:
|
||||
props.thumbnail_generating_state = "Started Blender instance"
|
||||
_set_prop(state_attr, "Started Blender instance")
|
||||
|
||||
if wait:
|
||||
while proc.poll() is None:
|
||||
@@ -449,6 +501,132 @@ class GenerateThumbnailOperator(bpy.types.Operator):
|
||||
return wm.invoke_props_dialog(self, width=400)
|
||||
|
||||
|
||||
class GenerateWireframeThumbnailOperator(bpy.types.Operator):
|
||||
"""Generate Cycles wireframe thumbnail for model assets"""
|
||||
|
||||
bl_idname = "object.blenderkit_generate_wireframe_thumbnail"
|
||||
bl_label = "BlenderKit Wireframe Thumbnail Generator"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return bpy.context.view_layer.objects.active is not None
|
||||
|
||||
def draw(self, context):
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
asset_type = ui_props.asset_type
|
||||
|
||||
ob = utils.get_active_model()
|
||||
props = ob.blenderkit
|
||||
layout = self.layout
|
||||
layout.label(text="thumbnailer settings")
|
||||
layout.prop(props, "thumbnail_background_lightness")
|
||||
# for printable models
|
||||
if asset_type == "PRINTABLE":
|
||||
layout.prop(props, "thumbnail_material_color")
|
||||
layout.prop(props, "thumbnail_angle")
|
||||
layout.prop(props, "thumbnail_snap_to")
|
||||
layout.prop(props, "thumbnail_samples")
|
||||
layout.prop(props, "thumbnail_resolution")
|
||||
layout.prop(props, "thumbnail_denoising")
|
||||
preferences = bpy.context.preferences.addons[__package__].preferences
|
||||
layout.prop(preferences, "thumbnail_use_gpu")
|
||||
# TODO: wireframe
|
||||
# layout.prop(preferences, "thumbnail_disable_subdivision")
|
||||
|
||||
def execute(self, context):
|
||||
if not upload.wire_thumbnail_upload_enabled():
|
||||
self.report({"ERROR"}, "Wireframe thumbnail uploads are disabled.")
|
||||
return {"CANCELLED"}
|
||||
asset = utils.get_active_model()
|
||||
asset.blenderkit.is_generating_wire_thumbnail = True
|
||||
asset.blenderkit.wire_thumbnail_generating_state = "starting blender instance"
|
||||
tempdir = tempfile.mkdtemp()
|
||||
ext = ".blend"
|
||||
filepath = os.path.join(tempdir, "thumbnailer_wf_blenderkit" + ext)
|
||||
|
||||
path_can_be_relative = True
|
||||
thumb_dir = os.path.dirname(bpy.data.filepath)
|
||||
if thumb_dir == "":
|
||||
thumb_dir = tempdir
|
||||
path_can_be_relative = False
|
||||
|
||||
an_slug = paths.slugify(asset.name)
|
||||
|
||||
# add suffix to distinguish from regular thumbnail
|
||||
an_slug += "_wf"
|
||||
|
||||
thumb_path = os.path.join(thumb_dir, an_slug)
|
||||
|
||||
if path_can_be_relative:
|
||||
rel_thumb_path = f"//{an_slug}"
|
||||
else:
|
||||
rel_thumb_path = thumb_path
|
||||
|
||||
i = 0
|
||||
while os.path.isfile(thumb_path + ".jpg"):
|
||||
thumb_name = f"{an_slug}_{str(i).zfill(4)}"
|
||||
thumb_path = os.path.join(thumb_dir, thumb_name)
|
||||
if path_can_be_relative:
|
||||
rel_thumb_path = f"//{thumb_name}"
|
||||
|
||||
i += 1
|
||||
bkit = asset.blenderkit
|
||||
|
||||
bkit.wire_thumbnail = rel_thumb_path + ".jpg"
|
||||
bkit.wire_thumbnail_generating_state = "Saving .blend file"
|
||||
|
||||
# if this isn't here, blender crashes.
|
||||
if bpy.app.version >= (3, 0, 0):
|
||||
bpy.context.preferences.filepaths.file_preview_type = "NONE"
|
||||
# save a copy of actual scene but don't interfere with the users models
|
||||
|
||||
bpy.ops.wm.save_as_mainfile(filepath=filepath, compress=False, copy=True)
|
||||
# get all included objects
|
||||
obs = utils.get_hierarchy(asset)
|
||||
obnames = []
|
||||
for ob in obs:
|
||||
obnames.append(ob.name)
|
||||
# asset type can be model or printable
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
asset_type = ui_props.asset_type
|
||||
args_dict = {
|
||||
"type": asset_type,
|
||||
"asset_name": asset.name,
|
||||
"filepath": filepath,
|
||||
"thumbnail_path": thumb_path,
|
||||
"tempdir": tempdir,
|
||||
"thumbnail_render_type": "WIREFRAME",
|
||||
"thumbnail_upload_type": "wire_thumbnail",
|
||||
}
|
||||
thumbnail_args = {
|
||||
"type": asset_type,
|
||||
"models": str(obnames),
|
||||
"thumbnail_angle": bkit.thumbnail_angle,
|
||||
"thumbnail_snap_to": bkit.thumbnail_snap_to,
|
||||
"thumbnail_background_lightness": 0.2,
|
||||
"thumbnail_material_color": (
|
||||
bkit.thumbnail_material_color[0],
|
||||
bkit.thumbnail_material_color[1],
|
||||
bkit.thumbnail_material_color[2],
|
||||
),
|
||||
"thumbnail_resolution": bkit.thumbnail_resolution,
|
||||
"thumbnail_samples": bkit.thumbnail_samples,
|
||||
"thumbnail_denoising": bkit.thumbnail_denoising,
|
||||
}
|
||||
args_dict.update(thumbnail_args)
|
||||
|
||||
start_model_thumbnailer(
|
||||
self, json_args=args_dict, props=asset.blenderkit, wait=False
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
wm = context.window_manager
|
||||
|
||||
return wm.invoke_props_dialog(self, width=400)
|
||||
|
||||
|
||||
class ReGenerateThumbnailOperator(bpy.types.Operator):
|
||||
"""
|
||||
Generate default thumbnail with Cycles renderer and upload it.
|
||||
@@ -464,6 +642,12 @@ class ReGenerateThumbnailOperator(bpy.types.Operator):
|
||||
name="Asset Index", description="asset index in search results", default=-1
|
||||
)
|
||||
|
||||
asset_type: StringProperty( # type: ignore[valid-type]
|
||||
name="Asset Type",
|
||||
description="Asset type used for thumbnail generation",
|
||||
default="",
|
||||
)
|
||||
|
||||
render_locally: BoolProperty( # type: ignore[valid-type]
|
||||
name="Render Locally",
|
||||
description="Render thumbnail locally instead of using server-side rendering",
|
||||
@@ -529,7 +713,12 @@ class ReGenerateThumbnailOperator(bpy.types.Operator):
|
||||
layout.label(text="thumbnailer settings")
|
||||
layout.prop(props, "thumbnail_background_lightness")
|
||||
# for printable models
|
||||
if self.asset_type == "PRINTABLE":
|
||||
asset_type = (
|
||||
getattr(self, "asset_type", "")
|
||||
or getattr(self, "asset_data", {}).get("assetType", "")
|
||||
or bpy.context.window_manager.blenderkitUI.asset_type
|
||||
).upper()
|
||||
if asset_type == "PRINTABLE":
|
||||
layout.prop(props, "thumbnail_material_color")
|
||||
layout.prop(props, "thumbnail_angle")
|
||||
layout.prop(props, "thumbnail_snap_to")
|
||||
@@ -545,6 +734,11 @@ class ReGenerateThumbnailOperator(bpy.types.Operator):
|
||||
|
||||
preferences = bpy.context.preferences.addons[__package__].preferences
|
||||
|
||||
# Ensure asset_type is set when execution is triggered directly.
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
if not getattr(self, "asset_type", ""):
|
||||
self.asset_type = ui_props.asset_type
|
||||
|
||||
if not self.render_locally:
|
||||
# Use server-side thumbnail regeneration
|
||||
success = upload.mark_for_thumbnail(
|
||||
@@ -575,8 +769,7 @@ class ReGenerateThumbnailOperator(bpy.types.Operator):
|
||||
thumb_path = os.path.join(tempdir, an_slug)
|
||||
|
||||
# asset type can be model or printable
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
self.asset_type = ui_props.asset_type
|
||||
self.asset_type = self.asset_type or ui_props.asset_type
|
||||
args_dict = {
|
||||
"type": self.asset_type,
|
||||
"asset_name": self.asset_data["name"],
|
||||
@@ -607,6 +800,12 @@ class ReGenerateThumbnailOperator(bpy.types.Operator):
|
||||
history_step = search.get_active_history_step()
|
||||
sr = history_step.get("search_results", [])
|
||||
self.asset_data = sr[self.asset_index]
|
||||
# Prepopulate asset_type so draw() can safely access it.
|
||||
self.asset_type = (
|
||||
self.asset_data.get("assetType", "")
|
||||
if isinstance(self.asset_data, dict)
|
||||
else ""
|
||||
).upper() or bpy.context.window_manager.blenderkitUI.asset_type
|
||||
|
||||
return wm.invoke_props_dialog(self, width=400)
|
||||
|
||||
@@ -891,6 +1090,7 @@ class ReGenerateMaterialThumbnailOperator(bpy.types.Operator):
|
||||
def register_thumbnailer():
|
||||
bpy.utils.register_class(GenerateThumbnailOperator)
|
||||
bpy.utils.register_class(ReGenerateThumbnailOperator)
|
||||
bpy.utils.register_class(GenerateWireframeThumbnailOperator)
|
||||
bpy.utils.register_class(GenerateMaterialThumbnailOperator)
|
||||
bpy.utils.register_class(ReGenerateMaterialThumbnailOperator)
|
||||
|
||||
@@ -898,5 +1098,6 @@ def register_thumbnailer():
|
||||
def unregister_thumbnailer():
|
||||
bpy.utils.unregister_class(GenerateThumbnailOperator)
|
||||
bpy.utils.unregister_class(ReGenerateThumbnailOperator)
|
||||
bpy.utils.unregister_class(GenerateWireframeThumbnailOperator)
|
||||
bpy.utils.unregister_class(GenerateMaterialThumbnailOperator)
|
||||
bpy.utils.unregister_class(ReGenerateMaterialThumbnailOperator)
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
# type: ignore
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
import colorsys
|
||||
import sys
|
||||
from traceback import print_exc
|
||||
from typing import Any, Union
|
||||
|
||||
import bpy
|
||||
|
||||
@@ -36,47 +36,66 @@ def get_obnames(BLENDERKIT_EXPORT_DATA: str):
|
||||
return obnames
|
||||
|
||||
|
||||
def center_obs_for_thumbnail(obs):
|
||||
s = bpy.context.scene
|
||||
# obs = bpy.context.selected_objects
|
||||
def center_objs_for_thumbnail(obs: list[Any]) -> None:
|
||||
"""Center and scale objects for optimal thumbnail framing.
|
||||
|
||||
Steps:
|
||||
1. Center objects in world space (handles parent-child hierarchy)
|
||||
2. Adjust camera distance based on object bounds
|
||||
3. Scale helper objects to fit the model in frame
|
||||
|
||||
Args:
|
||||
obs: List of Blender objects to center and frame.
|
||||
"""
|
||||
scene = bpy.context.scene
|
||||
parent = obs[0]
|
||||
|
||||
# Handle instanced collections (linked objects)
|
||||
if parent.type == "EMPTY" and parent.instance_collection is not None:
|
||||
obs = parent.instance_collection.objects[:]
|
||||
|
||||
# Get top-level parent
|
||||
while parent.parent is not None:
|
||||
parent = parent.parent
|
||||
# reset parent rotation, so we see how it really snaps.
|
||||
|
||||
# Reset parent rotation for accurate snapping
|
||||
parent.rotation_euler = (0, 0, 0)
|
||||
parent.location = (0, 0, 0)
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
# Calculate bounding box in world space
|
||||
minx, miny, minz, maxx, maxy, maxz = utils.get_bounds_worldspace(obs)
|
||||
|
||||
# Center object at world origin
|
||||
cx = (maxx - minx) / 2 + minx
|
||||
cy = (maxy - miny) / 2 + miny
|
||||
for ob in s.collection.objects:
|
||||
for ob in scene.collection.objects:
|
||||
ob.select_set(False)
|
||||
|
||||
bpy.context.view_layer.objects.active = parent
|
||||
# parent.location += mathutils.Vector((-cx, -cy, -minz))
|
||||
parent.location = (-cx, -cy, 0)
|
||||
|
||||
camZ = s.camera.parent.parent
|
||||
# camZ.location.z = (maxz - minz) / 2
|
||||
camZ.location.z = (maxz) / 2
|
||||
# Adjust camera position and scale based on object size
|
||||
cam_z = scene.camera.parent.parent
|
||||
cam_z.location.z = maxz / 2
|
||||
|
||||
# Calculate diagonal size of object for scaling
|
||||
dx = maxx - minx
|
||||
dy = maxy - miny
|
||||
dz = maxz - minz
|
||||
r = math.sqrt(dx * dx + dy * dy + dz * dz)
|
||||
|
||||
# Scale scene elements to fit object
|
||||
scaler = bpy.context.view_layer.objects["scaler"]
|
||||
scaler.scale = (r, r, r)
|
||||
coef = 0.7
|
||||
coef = 0.7 # Camera distance coefficient
|
||||
r *= coef
|
||||
camZ.scale = (r, r, r)
|
||||
cam_z.scale = (r, r, r)
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
|
||||
def render_thumbnails():
|
||||
def render_thumbnails() -> None:
|
||||
"""Render the current scene to a still image (no animation)."""
|
||||
bpy.ops.render.render(write_still=True, animation=False)
|
||||
|
||||
|
||||
@@ -112,17 +131,23 @@ def patch_imports(addon_module_name: str):
|
||||
print(f"- Local repository {parts[1]} added")
|
||||
|
||||
|
||||
def replace_materials(obs, material_name):
|
||||
"""Replace all materials on objects with the specified material
|
||||
def replace_materials(
|
||||
obs: list[Any], material_name: str
|
||||
) -> Union[bpy.types.Material, None]:
|
||||
"""Replace all materials on the given objects with a wireframe material.
|
||||
|
||||
Args:
|
||||
obs: List of objects to process
|
||||
material_name: Name of the material to apply to all objects
|
||||
obs: List of Blender objects to modify.
|
||||
material_name: Name of the wireframe material to use.
|
||||
"""
|
||||
material = bpy.data.materials.get(material_name)
|
||||
if not material:
|
||||
# Create or get the wireframe material
|
||||
if material_name in bpy.data.materials:
|
||||
material = bpy.data.materials[material_name]
|
||||
else:
|
||||
bg_blender.progress(f"Material {material_name} not found")
|
||||
return
|
||||
|
||||
# Assign the wireframe material to all objects
|
||||
for ob in obs:
|
||||
if ob.type == "MESH":
|
||||
# Clear all material slots and add the specified material
|
||||
@@ -131,6 +156,57 @@ def replace_materials(obs, material_name):
|
||||
return material
|
||||
|
||||
|
||||
def disable_modifier(obs: list[Any], modifier_type: str) -> None:
|
||||
"""Disable a specific type of modifier on all given objects.
|
||||
|
||||
Args:
|
||||
obs: List of Blender objects to modify.
|
||||
modifier_type: Type of the modifier to disable (e.g., 'SUBSURF').
|
||||
"""
|
||||
for ob in obs:
|
||||
if ob.type == "MESH":
|
||||
for mod in ob.modifiers:
|
||||
if mod.type == modifier_type:
|
||||
mod.show_viewport = False
|
||||
mod.show_render = False
|
||||
# disable only first found
|
||||
break
|
||||
|
||||
|
||||
def _str_to_color(s: str) -> Union[tuple[float, float, float], None]:
|
||||
"""Convert a color string to an RGB tuple.
|
||||
|
||||
Args:
|
||||
s: Color string in the format "#RRGGBB" or "R,G,B".
|
||||
|
||||
Returns:
|
||||
A tuple of (R, G, B) values as floats in the range [0.0, 1.0], or None.
|
||||
"""
|
||||
hex_size = 7 # e.g. "#RRGGBB"
|
||||
rgb_size = 5 # e.g. "R,G,B"
|
||||
rgb_count = 3
|
||||
s = s.strip()
|
||||
if s.startswith("#") and len(s) == hex_size:
|
||||
r = int(s[1:3], 16) / 255.0
|
||||
g = int(s[3:5], 16) / 255.0
|
||||
b = int(s[5:7], 16) / 255.0
|
||||
return (r, g, b)
|
||||
if len(s) == rgb_size:
|
||||
parts = s.split(",")
|
||||
if len(parts) == rgb_count:
|
||||
try:
|
||||
r = float(parts[0].strip())
|
||||
g = float(parts[1].strip())
|
||||
b = float(parts[2].strip())
|
||||
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
return (r, g, b)
|
||||
# Default to None
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
# args order must match the order in blenderkit/autothumb.py:get_thumbnailer_args()!
|
||||
@@ -144,6 +220,7 @@ if __name__ == "__main__":
|
||||
with open(BLENDERKIT_EXPORT_DATA, "r", encoding="utf-8") as s:
|
||||
data = json.load(s)
|
||||
thumbnail_use_gpu = data.get("thumbnail_use_gpu")
|
||||
thumbnail_disable_subdivision = data.get("thumbnail_disable_subdivision", False)
|
||||
|
||||
if data.get("do_download"):
|
||||
# if this isn't here, blender crashes.
|
||||
@@ -195,7 +272,7 @@ if __name__ == "__main__":
|
||||
}
|
||||
|
||||
bpy.context.scene.camera = bpy.data.objects[camdict[data["thumbnail_snap_to"]]]
|
||||
center_obs_for_thumbnail(allobs)
|
||||
center_objs_for_thumbnail(allobs)
|
||||
bpy.context.scene.render.filepath = data["thumbnail_path"]
|
||||
if thumbnail_use_gpu is True:
|
||||
bpy.context.scene.cycles.device = "GPU"
|
||||
@@ -214,8 +291,8 @@ if __name__ == "__main__":
|
||||
"SIDE": 4,
|
||||
"TOP": 5,
|
||||
}
|
||||
s = bpy.context.scene
|
||||
s.frame_set(fdict[data["thumbnail_angle"]])
|
||||
scene = bpy.context.scene
|
||||
scene.frame_set(fdict[data["thumbnail_angle"]])
|
||||
|
||||
snapdict = {
|
||||
"GROUND": "Ground",
|
||||
@@ -262,12 +339,19 @@ if __name__ == "__main__":
|
||||
1 - random_color[2],
|
||||
1,
|
||||
)
|
||||
# disable subdivision for thumbnail rendering if needed
|
||||
if thumbnail_disable_subdivision:
|
||||
disable_modifier(allobs, "SUBSURF")
|
||||
|
||||
# replace material if we need to render wireframe thumbnail
|
||||
if data.get("thumbnail_render_type") == "WIREFRAME":
|
||||
replace_materials(allobs, "bkit wireframe")
|
||||
|
||||
bpy.data.materials["bkit background"].node_tree.nodes["Value"].outputs[
|
||||
"Value"
|
||||
].default_value = data["thumbnail_background_lightness"]
|
||||
|
||||
s.cycles.samples = data["thumbnail_samples"]
|
||||
scene.cycles.samples = data["thumbnail_samples"]
|
||||
bpy.context.view_layer.cycles.use_denoising = data["thumbnail_denoising"]
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
@@ -297,14 +381,17 @@ if __name__ == "__main__":
|
||||
)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
# get sub type if we are not generating for main beauty thumbnail
|
||||
filetype = "thumbnail"
|
||||
if data.get("thumbnail_upload_type"):
|
||||
filetype = data["thumbnail_upload_type"].lower()
|
||||
bg_blender.progress("uploading thumbnail")
|
||||
fpath = data["thumbnail_path"] + ".jpg"
|
||||
ok = client_lib.complete_upload_file_blocking(
|
||||
api_key=BLENDERKIT_EXPORT_API_KEY,
|
||||
asset_id=data["asset_data"]["id"],
|
||||
filepath=fpath,
|
||||
filetype=f"thumbnail",
|
||||
filetype=filetype,
|
||||
fileindex=0,
|
||||
)
|
||||
if not ok:
|
||||
|
||||
@@ -25,7 +25,15 @@ import threading
|
||||
import bpy
|
||||
from bpy.props import EnumProperty
|
||||
|
||||
from . import utils
|
||||
from . import utils, reports
|
||||
|
||||
|
||||
def _safe_eval_target(eval_path: str, name: str):
|
||||
"""Try resolving eval_path; fallback to bpy.data.objects by name; return None if missing."""
|
||||
try:
|
||||
return eval(eval_path)
|
||||
except Exception:
|
||||
return bpy.data.objects.get(name) if name else None
|
||||
|
||||
|
||||
bk_logger = logging.getLogger(__name__)
|
||||
@@ -62,8 +70,9 @@ class ThreadCom: # object passed to threads to read background process stdout i
|
||||
|
||||
|
||||
def threadread(tcom: ThreadCom):
|
||||
"""reads stdout of background process.
|
||||
this threads basically waits for a stdout line to come in,
|
||||
"""Reads stdout of background process.
|
||||
|
||||
This thread basically waits for a stdout line to come in,
|
||||
fills the data, dies."""
|
||||
found = False
|
||||
while not found:
|
||||
@@ -74,7 +83,12 @@ def threadread(tcom: ThreadCom):
|
||||
# ignore empty lines
|
||||
if inline.strip() == "":
|
||||
continue
|
||||
bk_logger.info(inline.strip())
|
||||
# Background Blender already formats logs; strip a leading emoji/prefix to avoid double branding.
|
||||
line = inline.strip()
|
||||
line = re.sub(
|
||||
r"^(?:[🐞ℹ️⚠️❌🔥]\s*)?blenderkit:\s*", "", line, flags=re.IGNORECASE
|
||||
)
|
||||
bk_logger.info(line)
|
||||
progress = re.findall(r"progress\{(.*?)\}", inline)
|
||||
if len(progress) > 0:
|
||||
if type(progress[0]) == int or type(progress[0]) == float:
|
||||
@@ -116,8 +130,10 @@ def progress(text, n=None):
|
||||
sys.stdout.write(output)
|
||||
sys.stdout.flush()
|
||||
except Exception as e:
|
||||
print("background progress reporting race condition")
|
||||
print(e)
|
||||
bk_logger.exception(
|
||||
"background progress reporting race condition", exc_info=False
|
||||
)
|
||||
bk_logger.error(f"Error details: {e}")
|
||||
|
||||
|
||||
# @bpy.app.handlers.persistent
|
||||
@@ -140,10 +156,15 @@ def bg_update():
|
||||
for p in remove_processes:
|
||||
bk_logger.info(str(p[1].outtext))
|
||||
estring = p[1].eval_path_computing + " = False"
|
||||
try:
|
||||
exec(estring)
|
||||
except Exception as e:
|
||||
bk_logger.error(f"Exception executing eval_path_computing: {e}")
|
||||
target = _safe_eval_target(p[1].eval_path, p[1].name)
|
||||
if target is not None:
|
||||
try:
|
||||
exec(estring)
|
||||
except Exception as e:
|
||||
bk_logger.exception(
|
||||
"Exception executing eval_path_computing.", exc_info=False
|
||||
)
|
||||
bk_logger.error(f"Error details: {e}")
|
||||
bg_processes.remove(p)
|
||||
|
||||
# Parse process output
|
||||
@@ -162,7 +183,7 @@ def bg_update():
|
||||
tcom.outtext = ""
|
||||
text = tcom.lasttext.replace("'", "") # noqa: F841 needed in exec()
|
||||
estring = tcom.eval_path_state + " = text"
|
||||
# print(tcom.lasttext)
|
||||
|
||||
if "finished successfully" in tcom.lasttext:
|
||||
bk_logger.info(str(tcom.lasttext))
|
||||
bg_processes.remove(p)
|
||||
@@ -174,10 +195,20 @@ def bg_update():
|
||||
readthread.start()
|
||||
p[0] = readthread
|
||||
if estring:
|
||||
try:
|
||||
exec(estring)
|
||||
except Exception as e:
|
||||
print(f"Exception while reading from background process: {e}")
|
||||
target = _safe_eval_target(tcom.eval_path, tcom.name)
|
||||
if target is not None:
|
||||
try:
|
||||
exec(estring)
|
||||
except Exception as e:
|
||||
bk_logger.exception(
|
||||
"Exception while reading from background process.",
|
||||
exc_info=False,
|
||||
)
|
||||
bk_logger.error(f"Error details: {e}")
|
||||
else:
|
||||
bk_logger.debug(
|
||||
"Skipping state update; target missing for %s", tcom.name
|
||||
)
|
||||
|
||||
# if len(bg_processes) == 0:
|
||||
# bpy.app.timers.unregister(bg_update)
|
||||
@@ -235,6 +266,8 @@ class KillBgProcess(bpy.types.Operator):
|
||||
props.uploading = False
|
||||
if self.process_type == "THUMBNAILER":
|
||||
props.is_generating_thumbnail = False
|
||||
if hasattr(props, "is_generating_wire_thumbnail"):
|
||||
props.is_generating_wire_thumbnail = False
|
||||
# print('killing', self.process_source, self.process_type)
|
||||
# then go kill the process. this wasn't working for unsetting props and that was the reason for changing to the method above.
|
||||
|
||||
@@ -244,36 +277,43 @@ class KillBgProcess(bpy.types.Operator):
|
||||
tcom = p[1]
|
||||
# print(tcom.process_type, self.process_type)
|
||||
if tcom.process_type == self.process_type:
|
||||
source = eval(tcom.eval_path)
|
||||
source = _safe_eval_target(tcom.eval_path, tcom.name)
|
||||
kill = False
|
||||
# TODO HDR - add killing of process
|
||||
if source.bl_rna.name == "Object" and self.process_source == "MODEL":
|
||||
if source.name == bpy.context.active_object.name:
|
||||
kill = True
|
||||
if source.bl_rna.name == "Scene" and self.process_source == "SCENE":
|
||||
if source.name == bpy.context.scene.name:
|
||||
kill = True
|
||||
if source.bl_rna.name == "Image" and self.process_source == "HDR":
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
if source.name == ui_props.hdr_upload_image.name:
|
||||
kill = False
|
||||
if source is not None:
|
||||
if (
|
||||
source.bl_rna.name == "Object"
|
||||
and self.process_source == "MODEL"
|
||||
):
|
||||
if source.name == bpy.context.active_object.name:
|
||||
kill = True
|
||||
if source.bl_rna.name == "Scene" and self.process_source == "SCENE":
|
||||
if source.name == bpy.context.scene.name:
|
||||
kill = True
|
||||
if source.bl_rna.name == "Image" and self.process_source == "HDR":
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
if source.name == ui_props.hdr_upload_image.name:
|
||||
kill = False
|
||||
|
||||
if (
|
||||
source.bl_rna.name == "Material"
|
||||
and self.process_source == "MATERIAL"
|
||||
):
|
||||
if source.name == bpy.context.active_object.active_material.name:
|
||||
kill = True
|
||||
if source.bl_rna.name == "Brush" and self.process_source == "BRUSH":
|
||||
brush = utils.get_active_brush()
|
||||
if brush is not None and source.name == brush.name:
|
||||
kill = True
|
||||
if (
|
||||
source.bl_rna.name == "Object"
|
||||
and self.process_source == "PRINTABLE"
|
||||
):
|
||||
if source.name == bpy.context.active_object.name:
|
||||
kill = True
|
||||
if (
|
||||
source.bl_rna.name == "Material"
|
||||
and self.process_source == "MATERIAL"
|
||||
):
|
||||
if (
|
||||
source.name
|
||||
== bpy.context.active_object.active_material.name
|
||||
):
|
||||
kill = True
|
||||
if source.bl_rna.name == "Brush" and self.process_source == "BRUSH":
|
||||
brush = utils.get_active_brush()
|
||||
if brush is not None and source.name == brush.name:
|
||||
kill = True
|
||||
if (
|
||||
source.bl_rna.name == "Object"
|
||||
and self.process_source == "PRINTABLE"
|
||||
):
|
||||
if source.name == bpy.context.active_object.name:
|
||||
kill = True
|
||||
if kill:
|
||||
estring = tcom.eval_path_computing + " = False"
|
||||
exec(estring)
|
||||
|
||||
@@ -60,7 +60,7 @@ def delete_unfinished_file(file_name):
|
||||
try:
|
||||
os.remove(file_name)
|
||||
except Exception as e:
|
||||
bk_logger.error(f"{e}")
|
||||
bk_logger.error("Could not delete unfinished file %s: %s", file_name, e)
|
||||
asset_dir = os.path.dirname(file_name)
|
||||
if len(os.listdir(asset_dir)) == 0:
|
||||
os.rmdir(asset_dir)
|
||||
|
||||
@@ -23,13 +23,24 @@ import random
|
||||
import secrets
|
||||
import string
|
||||
import time
|
||||
import uuid
|
||||
from urllib.parse import quote as urlquote
|
||||
from webbrowser import open_new_tab
|
||||
|
||||
import bpy
|
||||
from bpy.props import BoolProperty
|
||||
|
||||
from . import client_lib, client_tasks, datas, global_vars, reports, tasks_queue, utils
|
||||
from . import (
|
||||
client_lib,
|
||||
client_tasks,
|
||||
datas,
|
||||
global_vars,
|
||||
reports,
|
||||
search_price,
|
||||
tasks_queue,
|
||||
utils,
|
||||
)
|
||||
|
||||
|
||||
if bpy.app.version >= (4, 2, 0):
|
||||
from . import override_extension_draw
|
||||
@@ -104,6 +115,7 @@ def clean_login_data():
|
||||
preferences.api_key = ""
|
||||
preferences.api_key_timeout = 0
|
||||
global_vars.BKIT_PROFILE = datas.MineProfile()
|
||||
search_price.clear_price_cache()
|
||||
# Cleanup also the api key in the extensions repository setting and clean the cache
|
||||
if bpy.app.version >= (4, 2, 0):
|
||||
override_extension_draw.ensure_repository(api_key="")
|
||||
@@ -128,14 +140,19 @@ def login(signup: bool) -> None:
|
||||
code_verifier, code_challenge = generate_pkce_pair()
|
||||
state = secrets.token_urlsafe()
|
||||
client_lib.send_oauth_verification_data(code_verifier, state)
|
||||
authorize_url = f"/o/authorize?client_id={CLIENT_ID}&response_type=code&state={state}&redirect_uri={redirect_URI}&code_challenge={code_challenge}&code_challenge_method=S256"
|
||||
system_id = get_system_id()
|
||||
authorize_url = (
|
||||
f"/o/authorize?client_id={CLIENT_ID}&response_type=code&state={state}"
|
||||
f"&redirect_uri={redirect_URI}&code_challenge={code_challenge}"
|
||||
f"&code_challenge_method=S256&system_id={system_id}"
|
||||
)
|
||||
if signup:
|
||||
authorize_url = urlquote(authorize_url)
|
||||
authorize_url = f"{global_vars.SERVER}/accounts/register/?next={authorize_url}"
|
||||
else:
|
||||
authorize_url = f"{global_vars.SERVER}{authorize_url}"
|
||||
ok = open_new_tab(authorize_url)
|
||||
bk_logger.info(f"Login page in browser opened ({ok})")
|
||||
bk_logger.info("Login page in browser opened (%s)", ok)
|
||||
|
||||
|
||||
def generate_pkce_pair() -> tuple[str, str]:
|
||||
@@ -151,12 +168,17 @@ def generate_pkce_pair() -> tuple[str, str]:
|
||||
return code_verifier, code_challenge
|
||||
|
||||
|
||||
def get_system_id() -> str:
|
||||
return f"{uuid.getnode():015d}"
|
||||
|
||||
|
||||
def write_tokens(auth_token, refresh_token, oauth_response):
|
||||
preferences = bpy.context.preferences.addons[__package__].preferences
|
||||
preferences.api_key_timeout = int(time.time() + oauth_response["expires_in"])
|
||||
preferences.login_attempt = False
|
||||
preferences.api_key_refresh = refresh_token
|
||||
preferences.api_key = auth_token # triggers api_key update function
|
||||
search_price.clear_price_cache()
|
||||
# write token also to extensions repository setting and clear the cache
|
||||
if bpy.app.version >= (4, 2, 0):
|
||||
override_extension_draw.ensure_repository(api_key=auth_token)
|
||||
|
||||
+55
-18
@@ -20,7 +20,7 @@
|
||||
bl_info = {
|
||||
"name": "BlenderKit Online Asset Library",
|
||||
"author": "Vilem Duha, Petr Dlouhy, A. Gajdosik",
|
||||
"version": (3, 18, 0, 251121), # X.Y.Z.yymmdd
|
||||
"version": (3, 18, 1, 251219), # X.Y.Z.yymmdd
|
||||
"blender": (3, 0, 0),
|
||||
"location": "View3D > Properties > BlenderKit",
|
||||
"description": "Boost your workflow with drag&drop assets from the community driven library.",
|
||||
@@ -28,7 +28,7 @@ bl_info = {
|
||||
"tracker_url": "https://github.com/BlenderKit/blenderkit/issues",
|
||||
"category": "3D View",
|
||||
}
|
||||
VERSION = (3, 18, 0, 251121)
|
||||
VERSION = (3, 18, 1, 251219)
|
||||
|
||||
import logging
|
||||
import random
|
||||
@@ -242,7 +242,7 @@ engines = (
|
||||
("CYCLES", "Cycles", "Blender Cycles"),
|
||||
("EEVEE", "Eevee", "Blender eevee renderer"),
|
||||
("EEEVE_NEXT", "Eevee Next", "Blender eevee renderer (new)"),
|
||||
("OCTANE", "Octane", "Octane render enginge"),
|
||||
("OCTANE", "Octane", "Octane render engine"),
|
||||
("ARNOLD", "Arnold", "Arnold render engine"),
|
||||
("V-RAY", "V-Ray", "V-Ray renderer"),
|
||||
("UNREAL", "Unreal", "Unreal engine"),
|
||||
@@ -267,6 +267,12 @@ mesh_poly_types = (
|
||||
)
|
||||
|
||||
|
||||
EXTRA_PATH_OPTIONS = {}
|
||||
|
||||
if bpy.app.version >= (4, 5, 0):
|
||||
EXTRA_PATH_OPTIONS = {"options": {"PATH_SUPPORTS_BLEND_RELATIVE"}}
|
||||
|
||||
|
||||
def udate_down_up(self, context):
|
||||
"""Perform a search if results are empty."""
|
||||
props = bpy.context.window_manager.blenderkitUI
|
||||
@@ -461,12 +467,12 @@ class BlenderKitUIProps(PropertyGroup):
|
||||
search_blender_version: BoolProperty(
|
||||
name="Asset Blender Version",
|
||||
description="Limit the assets by version of Blender (minimum, maximum) in which they were created. "
|
||||
+ "Use maximum version limit to exclude incompatible assets from newer Blender versions than yours. Or set the minumum version to exclude assets created in quite old Blender versions",
|
||||
+ "Use maximum version limit to exclude incompatible assets from newer Blender versions than yours. Or set the minimum version to exclude assets created in quite old Blender versions",
|
||||
)
|
||||
search_blender_version_min: StringProperty(
|
||||
name="Minimal version (including, higher than or equal)",
|
||||
default="0.0",
|
||||
description="Limit the assets by minimum version of Blender in which they were created, including also the specified version and exluding all older versions from the search results. "
|
||||
description="Limit the assets by minimum version of Blender in which they were created, including also the specified version and excluding all older versions from the search results. "
|
||||
+ "Only assets created in HIGHER THAN OR EQUAL (>= min) minimum version will be shown. Use semantic versioning format: X.Y.Z.\n\n"
|
||||
+ "E.g.: exclude all Blender 2 assets by specifying 3, 3.0, or 3.0.0. Assets created in 3.0 or higher will be shown",
|
||||
update=search.search_update,
|
||||
@@ -474,7 +480,7 @@ class BlenderKitUIProps(PropertyGroup):
|
||||
search_blender_version_max: StringProperty(
|
||||
name="Maximum version (excluding, lower than)",
|
||||
default="5.99",
|
||||
description="Limit the assets by maximum version of Blender in which they were created, exluding the specified version and all newer versions from the search results. "
|
||||
description="Limit the assets by maximum version of Blender in which they were created, excluding the specified version and all newer versions from the search results. "
|
||||
+ "Only assets created in LOWER THAN (< max) maximum version will be shown. Use semantic versioning format: X.Y.Z.\n\n"
|
||||
+ "E.g.: exclude all Blender 4 assets by specifying 4, 4.0, or 4.0.0. Assets created in 3.6 and lower will be shown",
|
||||
update=search.search_update,
|
||||
@@ -580,7 +586,7 @@ class BlenderKitUIProps(PropertyGroup):
|
||||
|
||||
rating_ui_width: IntProperty(name="Rating UI Width", default=rating_ui_scale * 600)
|
||||
rating_ui_height: IntProperty(
|
||||
name="Rating UI Heightt", default=rating_ui_scale * 256
|
||||
name="Rating UI Height", default=rating_ui_scale * 256
|
||||
)
|
||||
|
||||
quality_stars_x: IntProperty(name="Rating UI Stars X", default=rating_ui_scale * 90)
|
||||
@@ -1130,6 +1136,7 @@ class BlenderKitMaterialUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
|
||||
subtype="FILE_PATH",
|
||||
default="",
|
||||
update=autothumb.update_upload_material_preview,
|
||||
**EXTRA_PATH_OPTIONS,
|
||||
)
|
||||
|
||||
is_generating_thumbnail: BoolProperty(
|
||||
@@ -1213,13 +1220,14 @@ class BlenderKitBrushUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
|
||||
)
|
||||
|
||||
|
||||
class BlenderKitNodeGroulUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
|
||||
class BlenderKitNodeGroupUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
|
||||
thumbnail: StringProperty(
|
||||
name="Thumbnail",
|
||||
description="Thumbnail path - minimum 1024x1024 square .jpg\n"
|
||||
"And make it beautiful!",
|
||||
subtype="FILE_PATH",
|
||||
default="",
|
||||
**EXTRA_PATH_OPTIONS,
|
||||
# update=autothumb.update_upload_model_preview,
|
||||
)
|
||||
# mode: EnumProperty(
|
||||
@@ -1326,6 +1334,7 @@ class BlenderKitModelUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
|
||||
subtype="FILE_PATH",
|
||||
default="",
|
||||
update=autothumb.update_upload_model_preview,
|
||||
**EXTRA_PATH_OPTIONS,
|
||||
)
|
||||
|
||||
thumbnail_background_lightness: FloatProperty(
|
||||
@@ -1529,6 +1538,7 @@ class BlenderKitModelUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
|
||||
description="Photo of the 3D printed object (JPG or PNG, preferred size is 1024x1024 or higher)",
|
||||
subtype="FILE_PATH",
|
||||
default="",
|
||||
**EXTRA_PATH_OPTIONS,
|
||||
)
|
||||
photo_thumbnail_will_upload_on_website: BoolProperty(
|
||||
name="I will upload photo on website",
|
||||
@@ -1603,6 +1613,7 @@ class BlenderKitSceneUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
|
||||
subtype="FILE_PATH",
|
||||
default="",
|
||||
update=autothumb.update_upload_scene_preview,
|
||||
**EXTRA_PATH_OPTIONS,
|
||||
)
|
||||
|
||||
use_design_year: BoolProperty(
|
||||
@@ -1766,7 +1777,7 @@ class BlenderKitModelSearchProps(PropertyGroup, BlenderKitCommonSearchProps):
|
||||
update=search.search_update,
|
||||
)
|
||||
search_design_year: BoolProperty(
|
||||
name="Sesigned in Year",
|
||||
name="Designed in Year",
|
||||
description="When the object was approximately designed. \n"
|
||||
"Useful for search of historical or future objects",
|
||||
default=False,
|
||||
@@ -1966,7 +1977,7 @@ def fix_subdir(self, context):
|
||||
|
||||
ui_panels.ui_message(
|
||||
title="Fixed to relative path",
|
||||
message="This path should be always realative.\n"
|
||||
message="This path should be always relative.\n"
|
||||
" It's a directory BlenderKit creates where your .blend is \n "
|
||||
"and uses it for storing assets.",
|
||||
)
|
||||
@@ -1992,7 +2003,7 @@ class BlenderKitAddonPreferences(AddonPreferences):
|
||||
|
||||
preferences_lock: BoolProperty(
|
||||
name="Preferences Locked",
|
||||
description="When this is on, preferences will not be saved. Used for programatical changes of preferences",
|
||||
description="When this is on, preferences will not be saved. Used for programmatic changes of preferences",
|
||||
default=False,
|
||||
)
|
||||
|
||||
@@ -2120,6 +2131,14 @@ class BlenderKitAddonPreferences(AddonPreferences):
|
||||
update=utils.save_prefs,
|
||||
)
|
||||
|
||||
# USE OF CLIPBOARD SCAN
|
||||
use_clipboard_scan: BoolProperty(
|
||||
name="Use Clipboard Scan",
|
||||
description="Use the info from BlenderKit website clipboard for visual search",
|
||||
default=True,
|
||||
update=utils.save_prefs,
|
||||
)
|
||||
|
||||
unpack_files: BoolProperty(
|
||||
name="Unpack Files",
|
||||
description="Unpack assets after download \n "
|
||||
@@ -2233,8 +2252,8 @@ class BlenderKitAddonPreferences(AddonPreferences):
|
||||
|
||||
proxy_address: StringProperty(
|
||||
name="Custom proxy address",
|
||||
description="""Set custom HTTP proxy for HTTPS requests of add-on. This setting preceeds any system wide proxy settings. If left empty custom proxy will not be set.
|
||||
|
||||
description="""Set custom HTTP proxy for HTTPS requests of add-on. This setting precedes any system wide proxy settings. If left empty custom proxy will not be set.
|
||||
|
||||
If you use simple HTTP proxy, set in format http://ip:port, or http://username:password@ip:port if your HTTP proxy requires authentication (make sure to escape special characters like #$%:^&*() etc. in username and password). You have to specify the address with http:// prefix.
|
||||
|
||||
HTTPS proxies are not supported! We wait for support in Python 3.11 and in aiohttp module. You can specify the HTTPS proxy with https:// prefix for hacking around and development purposes, but functionality cannot be guaranteed.
|
||||
@@ -2430,12 +2449,21 @@ In this case you should also set path to your system CA bundle containing proxy'
|
||||
default="[]",
|
||||
)
|
||||
|
||||
# EXPERIMENTAL AND DEBUG FEATURES CAN GO BELOW
|
||||
ignore_env_for_thumbnails: BoolProperty(
|
||||
name="Ignore ENVIRONMENT variables for thumbnails",
|
||||
description="If enabled, we will not modify the system environment variables for background thumbnail rendering.",
|
||||
default=False,
|
||||
# do not save prefs here, it's experimental
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
if self.api_key.strip() == "":
|
||||
ui_panels.draw_login_buttons(layout)
|
||||
layout.label(
|
||||
text="Sign up to bookmark your favourite assets. Get 200 MiB of private storage in Free Plan."
|
||||
text="Sign up to bookmark your favorite assets. Get 200 MiB of private storage in Free Plan."
|
||||
)
|
||||
else:
|
||||
layout.operator("wm.blenderkit_logout", text="Logout", icon="URL")
|
||||
@@ -2470,8 +2498,9 @@ In this case you should also set path to your system CA bundle containing proxy'
|
||||
gui_settings.prop(self, "show_VIEW3D_MT_blenderkit_model_properties")
|
||||
gui_settings.prop(self, "tips_on_start")
|
||||
gui_settings.prop(self, "announcements_on_start")
|
||||
gui_settings.prop(self, "use_clipboard_scan")
|
||||
|
||||
# NETWORKING SETINGS
|
||||
# NETWORKING SETTINGS
|
||||
network_settings = layout.box()
|
||||
network_settings.alignment = "EXPAND"
|
||||
network_settings.label(text="Networking settings")
|
||||
@@ -2487,6 +2516,14 @@ In this case you should also set path to your system CA bundle containing proxy'
|
||||
# UPDATER SETTINGS
|
||||
addon_updater_ops.update_settings_ui(self, context)
|
||||
|
||||
# EXPERIMENTAL SETTINGS
|
||||
# only if experimental features enabled
|
||||
if self.experimental_features:
|
||||
experimental_settings = layout.box()
|
||||
experimental_settings.alignment = "EXPAND"
|
||||
experimental_settings.label(text="Experimental settings")
|
||||
experimental_settings.prop(self, "ignore_env_for_thumbnails")
|
||||
|
||||
# RUNTIME INFO
|
||||
globdir_op = layout.operator(
|
||||
"wm.blenderkit_open_global_directory",
|
||||
@@ -2535,7 +2572,7 @@ classes = (
|
||||
BlenderKitBrushSearchProps,
|
||||
BlenderKitBrushUploadProps,
|
||||
BlenderKitGeoToolSearchProps,
|
||||
BlenderKitNodeGroulUploadProps,
|
||||
BlenderKitNodeGroupUploadProps,
|
||||
BlenderKitAddonSearchProps,
|
||||
)
|
||||
|
||||
@@ -2598,10 +2635,10 @@ def register():
|
||||
type=BlenderKitGeoToolSearchProps
|
||||
)
|
||||
bpy.types.NodeGroup.blenderkit = PointerProperty( # for uploads, not now...
|
||||
type=BlenderKitNodeGroulUploadProps
|
||||
type=BlenderKitNodeGroupUploadProps
|
||||
)
|
||||
bpy.types.NodeTree.blenderkit = PointerProperty( # for uploads, not now...
|
||||
type=BlenderKitNodeGroulUploadProps
|
||||
type=BlenderKitNodeGroupUploadProps
|
||||
)
|
||||
bpy.types.WindowManager.blenderkit_addon = PointerProperty(
|
||||
type=BlenderKitAddonSearchProps
|
||||
|
||||
+7
-1
@@ -97,7 +97,13 @@ def make_annotations(cls):
|
||||
if bl_props:
|
||||
if "__annotations__" not in cls.__dict__:
|
||||
setattr(cls, "__annotations__", {})
|
||||
annotations = cls.__dict__["__annotations__"]
|
||||
|
||||
try:
|
||||
annotations = cls.__dict__["__annotations__"]
|
||||
except KeyError:
|
||||
# Fedora 43 bug workaround #1823
|
||||
annotations = getattr(cls, "__annotations__")
|
||||
|
||||
for k, v in bl_props.items():
|
||||
annotations[k] = v
|
||||
delattr(cls, k)
|
||||
|
||||
+10
-7
@@ -42,15 +42,19 @@ def find_layer_collection(layer_collection, collection_name):
|
||||
|
||||
def append_brush(file_name, brushname=None, link=False, fake_user=True):
|
||||
"""append a brush"""
|
||||
brushes_before = bpy.data.brushes[:]
|
||||
with bpy.data.libraries.load(file_name, link=link, relative=True) as (
|
||||
data_from,
|
||||
data_to,
|
||||
):
|
||||
for m in data_from.brushes:
|
||||
if m == brushname or brushname is None:
|
||||
if brushname is None or m.strip() == brushname.strip():
|
||||
data_to.brushes = [m]
|
||||
brushname = m
|
||||
brush = bpy.data.brushes[brushname]
|
||||
for b in bpy.data.brushes:
|
||||
if b not in brushes_before:
|
||||
brush = b
|
||||
break
|
||||
brush.use_fake_user = fake_user
|
||||
return brush
|
||||
|
||||
@@ -93,8 +97,7 @@ def append_nodegroup(
|
||||
data_to,
|
||||
):
|
||||
for g in data_from.node_groups:
|
||||
print(g)
|
||||
if g == nodegroupname or nodegroupname is None:
|
||||
if nodegroupname is None or g.strip() == nodegroupname.strip():
|
||||
data_to.node_groups = [g]
|
||||
nodegroupname = g
|
||||
nodegroup = bpy.data.node_groups[nodegroupname]
|
||||
@@ -281,7 +284,7 @@ def append_material(file_name, matname=None, link=False, fake_user=True):
|
||||
):
|
||||
found = False
|
||||
for m in data_from.materials:
|
||||
if m == matname or matname is None:
|
||||
if matname is None or m.strip() == matname.strip():
|
||||
data_to.materials = [m]
|
||||
matname = m
|
||||
found = True
|
||||
@@ -319,7 +322,7 @@ def append_scene(file_name, scenename=None, link=False, fake_user=False):
|
||||
data_to,
|
||||
):
|
||||
for s in data_from.scenes:
|
||||
if s == scenename or scenename is None:
|
||||
if scenename is None or s.strip() == scenename.strip():
|
||||
data_to.scenes = [s]
|
||||
scenename = s
|
||||
scene = bpy.data.scenes[scenename]
|
||||
@@ -448,7 +451,7 @@ def link_collection(
|
||||
data_to,
|
||||
):
|
||||
for col in data_from.collections:
|
||||
if col == kwargs["name"]:
|
||||
if col.strip() == kwargs["name"].strip():
|
||||
data_to.collections = [col]
|
||||
|
||||
rotation = (0, 0, 0)
|
||||
|
||||
+226
-72
@@ -21,7 +21,7 @@ import math
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, Union
|
||||
|
||||
import bpy
|
||||
from bpy.props import BoolProperty, StringProperty
|
||||
@@ -286,6 +286,12 @@ def modal_inside(self, context, event):
|
||||
if self.check_ui_resized(context) or self.check_new_search_results(context):
|
||||
self.update_assetbar_sizes(context)
|
||||
self.update_assetbar_layout(context)
|
||||
# also update tooltip visibility
|
||||
# if there's less results and active button is not visible, hide tooltip
|
||||
# happened only when e.g. running new search from web browser (copying assetbaseid to clipboard)
|
||||
# fixes issue #1766
|
||||
if self.active_index >= len(search.get_search_results()):
|
||||
self.hide_tooltip()
|
||||
self.scroll_update(
|
||||
always=True
|
||||
) # one extra update for scroll for correct redraw, updates all buttons
|
||||
@@ -395,6 +401,17 @@ def get_tooltip_data(asset_data):
|
||||
# Add pricing information
|
||||
price_text = ""
|
||||
price_color = colors.WHITE
|
||||
price_background = (0, 0, 0, 0)
|
||||
|
||||
def format_price(value):
|
||||
if value is None:
|
||||
return ""
|
||||
value_str = str(value).strip()
|
||||
if not value_str:
|
||||
return ""
|
||||
if value_str.startswith("$"):
|
||||
return value_str
|
||||
return f"${value_str}"
|
||||
|
||||
# Check if asset is free or paid (works for all asset types)
|
||||
is_free = asset_data.get("isFree", True)
|
||||
@@ -403,23 +420,38 @@ def get_tooltip_data(asset_data):
|
||||
if asset_data.get("assetType") == "addon":
|
||||
# Get pricing info from extensions cache.
|
||||
# Pricing info is shown only for add-ons.
|
||||
base_price = asset_data.get("basePrice")
|
||||
base_price = format_price(asset_data.get("basePrice"))
|
||||
user_price = format_price(asset_data.get("userPrice"))
|
||||
is_for_sale = asset_data.get("isForSale")
|
||||
|
||||
if is_for_sale and not can_download and base_price:
|
||||
price_text = f"${base_price}"
|
||||
price_color = colors.PURPLE
|
||||
if utils.profile_is_validator():
|
||||
segments = []
|
||||
if user_price:
|
||||
segments.append(f"User {user_price}")
|
||||
if base_price:
|
||||
segments.append(f"Base {base_price}")
|
||||
price_text = " | ".join(segments)
|
||||
price_background = colors.PURPLE_PRICE
|
||||
|
||||
elif is_for_sale and not can_download and user_price and base_price:
|
||||
price_text = f"{user_price} (was {base_price})"
|
||||
price_background = colors.PURPLE_PRICE
|
||||
|
||||
elif is_for_sale and not can_download and base_price:
|
||||
price_text = base_price
|
||||
price_background = colors.PURPLE_PRICE
|
||||
|
||||
elif not is_free and not is_for_sale:
|
||||
price_text = "Full Plan"
|
||||
price_color = colors.PURPLE
|
||||
elif (
|
||||
is_for_sale and can_download
|
||||
): # purchased, but not yet downloaded, so we can't show price
|
||||
price_text = f"Purchased (${base_price})"
|
||||
price_color = colors.PURPLE
|
||||
price_background = colors.ORANGE_FULL
|
||||
|
||||
elif is_for_sale and can_download:
|
||||
price_text = "Purchased"
|
||||
price_background = colors.PURPLE_PRICE
|
||||
|
||||
else:
|
||||
price_text = "Free"
|
||||
price_color = colors.GREEN_FREE
|
||||
price_background = colors.GREEN_PRICE
|
||||
|
||||
tooltip_data = {
|
||||
"aname": aname,
|
||||
@@ -427,12 +459,15 @@ def get_tooltip_data(asset_data):
|
||||
"quality": quality,
|
||||
"price_text": price_text,
|
||||
"price_color": price_color,
|
||||
"price_background": price_background,
|
||||
}
|
||||
asset_data["tooltip_data"] = tooltip_data
|
||||
|
||||
|
||||
def set_thumb_check(
|
||||
element: BL_UI_Button, asset: Dict[str, Any], thumb_type: str = "thumbnail_small"
|
||||
element: Union[BL_UI_Button, BL_UI_Image],
|
||||
asset: Dict[str, Any],
|
||||
thumb_type: str = "thumbnail_small",
|
||||
) -> None:
|
||||
"""Set image in case it is loaded in search results. Checks global_vars.DATA["images available"].
|
||||
- if image download failed, it will be set to 'thumbnail_not_available.jpg'
|
||||
@@ -457,6 +492,8 @@ def set_thumb_check(
|
||||
|
||||
|
||||
class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
"""BlenderKit Asset Bar Operator."""
|
||||
|
||||
bl_idname = "view3d.blenderkit_asset_bar_widget"
|
||||
bl_label = "BlenderKit asset bar refresh"
|
||||
bl_description = "BlenderKit asset bar refresh"
|
||||
@@ -508,8 +545,23 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
"""Initialize the tooltip panel and its widgets."""
|
||||
self.tooltip_widgets = []
|
||||
self.tooltip_scale = 1.0
|
||||
self.tooltip_height = self.tooltip_size
|
||||
self.tooltip_width = self.tooltip_size
|
||||
|
||||
# Fallbacks in case update_tooltip_size was not called yet
|
||||
self.tooltip_width = getattr(self, "tooltip_width", self.tooltip_size)
|
||||
image_height = getattr(self, "tooltip_image_height", self.tooltip_size)
|
||||
info_height = getattr(
|
||||
self,
|
||||
"tooltip_info_height",
|
||||
max(
|
||||
int(image_height * self.bottom_panel_fraction),
|
||||
self.asset_name_text_size * 3,
|
||||
),
|
||||
)
|
||||
self.tooltip_image_height = image_height
|
||||
self.tooltip_info_height = info_height
|
||||
self.tooltip_height = self.tooltip_image_height + self.tooltip_info_height
|
||||
self.labels_start = self.tooltip_image_height
|
||||
|
||||
# total_size = tooltip# + 2 * self.margin
|
||||
self.tooltip_panel = BL_UI_Drag_Panel(
|
||||
0, 0, self.tooltip_width, self.tooltip_height
|
||||
@@ -520,20 +572,16 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
tooltip_image = BL_UI_Image(0, 0, 1, 1)
|
||||
img_path = paths.get_addon_thumbnail_path("thumbnail_notready.jpg")
|
||||
tooltip_image.set_image(img_path)
|
||||
tooltip_image.set_image_size((self.tooltip_width, self.tooltip_height))
|
||||
tooltip_image.set_image_size((self.tooltip_width, self.tooltip_image_height))
|
||||
tooltip_image.set_image_position((0, 0))
|
||||
tooltip_image.set_image_colorspace("")
|
||||
self.tooltip_image = tooltip_image
|
||||
self.tooltip_widgets.append(tooltip_image)
|
||||
|
||||
self.bottom_panel_fraction = 0.15
|
||||
self.labels_start = self.tooltip_height * (1 - self.bottom_panel_fraction)
|
||||
|
||||
dark_panel = BL_UI_Widget(
|
||||
0,
|
||||
self.labels_start,
|
||||
self.tooltip_width,
|
||||
self.tooltip_height * self.bottom_panel_fraction,
|
||||
self.tooltip_info_height,
|
||||
)
|
||||
dark_panel.bg_color = (0.0, 0.0, 0.0, 0.7)
|
||||
self.tooltip_dark_panel = dark_panel
|
||||
@@ -549,8 +597,9 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
self.asset_name = name_label
|
||||
self.tooltip_widgets.append(name_label)
|
||||
|
||||
self.gravatar_size = int(
|
||||
self.tooltip_height * self.bottom_panel_fraction - self.tooltip_margin
|
||||
self.gravatar_size = max(
|
||||
int(self.tooltip_info_height - 2 * self.tooltip_margin),
|
||||
self.asset_name_text_size,
|
||||
)
|
||||
|
||||
authors_name = self.new_text(
|
||||
@@ -566,8 +615,8 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
self.tooltip_widgets.append(authors_name)
|
||||
|
||||
gravatar_image = BL_UI_Image(
|
||||
self.tooltip_width - self.gravatar_size,
|
||||
self.tooltip_height - self.gravatar_size,
|
||||
self.tooltip_width - self.gravatar_size - self.tooltip_margin,
|
||||
self.tooltip_height - self.gravatar_size - self.tooltip_margin,
|
||||
1,
|
||||
1,
|
||||
)
|
||||
@@ -575,8 +624,8 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
gravatar_image.set_image(img_path)
|
||||
gravatar_image.set_image_size(
|
||||
(
|
||||
self.gravatar_size - 1 * self.tooltip_margin,
|
||||
self.gravatar_size - 1 * self.tooltip_margin,
|
||||
self.gravatar_size,
|
||||
self.gravatar_size,
|
||||
)
|
||||
)
|
||||
gravatar_image.set_image_position((0, 0))
|
||||
@@ -617,7 +666,14 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
height=self.asset_name_text_size,
|
||||
text_size=self.asset_name_text_size,
|
||||
)
|
||||
price_label.text_color = (1.0, 0.8, 0.2, 1.0) # Golden color for price
|
||||
price_label.background = True
|
||||
price_label.padding = (3, 4)
|
||||
price_label.text_color = (
|
||||
1.0,
|
||||
0.8,
|
||||
0.2,
|
||||
1.0,
|
||||
) # Golden color for price
|
||||
self.tooltip_widgets.append(price_label)
|
||||
self.price_label = price_label
|
||||
|
||||
@@ -728,14 +784,30 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
"""Calculate all important sizes for the tooltip"""
|
||||
region = context.region
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
ui_scale = bpy.context.preferences.view.ui_scale
|
||||
ui_scale = self.get_ui_scale()
|
||||
|
||||
base_panel_height = self.tooltip_base_size_pixels * (
|
||||
1 + self.bottom_panel_fraction
|
||||
)
|
||||
|
||||
if hasattr(self, "tooltip_panel"):
|
||||
tooltip_y_offset = abs(region.height - self.tooltip_panel.y_screen)
|
||||
tooltip_y_available_height = abs(
|
||||
region.height - self.tooltip_panel.y_screen
|
||||
)
|
||||
# if tooltip is above, we need to reduce it's size if its y is out of region height
|
||||
if self.tooltip_panel.y_screen <= 0:
|
||||
tooltip_y_available_height = (
|
||||
base_panel_height * ui_scale + self.tooltip_panel.y_screen
|
||||
)
|
||||
self.tooltip_panel.set_location(self.tooltip_panel.x, 0)
|
||||
|
||||
else:
|
||||
tooltip_y_offset = abs(region.height - (self.bar_height + self.bar_y))
|
||||
tooltip_y_available_height = abs(
|
||||
region.height - (self.bar_height + self.bar_y)
|
||||
)
|
||||
|
||||
self.tooltip_scale = min(
|
||||
1.0, tooltip_y_offset / (self.tooltip_base_size_pixels * ui_scale)
|
||||
1.0, tooltip_y_available_height / (base_panel_height * ui_scale)
|
||||
)
|
||||
self.asset_name_text_size = int(
|
||||
0.039 * self.tooltip_base_size_pixels * ui_scale * self.tooltip_scale
|
||||
@@ -750,14 +822,33 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
|
||||
if ui_props.asset_type == "HDR":
|
||||
self.tooltip_width = self.tooltip_size * 2
|
||||
self.tooltip_height = self.tooltip_size
|
||||
self.tooltip_image_height = self.tooltip_size
|
||||
else:
|
||||
self.tooltip_width = self.tooltip_size
|
||||
self.tooltip_height = self.tooltip_size
|
||||
self.tooltip_image_height = self.tooltip_size
|
||||
|
||||
self.gravatar_size = int(
|
||||
self.tooltip_height * self.bottom_panel_fraction - self.tooltip_margin
|
||||
self.tooltip_info_height = max(
|
||||
int(self.tooltip_image_height * self.bottom_panel_fraction),
|
||||
self.asset_name_text_size * 3,
|
||||
)
|
||||
self.labels_start = self.tooltip_image_height
|
||||
self.tooltip_height = self.tooltip_image_height + self.tooltip_info_height
|
||||
|
||||
self.gravatar_size = max(
|
||||
int(self.tooltip_info_height - 2 * self.tooltip_margin),
|
||||
self.asset_name_text_size,
|
||||
)
|
||||
|
||||
def get_ui_scale(self):
|
||||
"""Get the UI scale"""
|
||||
ui_scale = bpy.context.preferences.view.ui_scale
|
||||
pixel_size = bpy.context.preferences.system.pixel_size
|
||||
if pixel_size > 1:
|
||||
# for a reason unknown,
|
||||
# the pixel size is modified only on mac
|
||||
# where pixel size is 2.0
|
||||
ui_scale = pixel_size
|
||||
return ui_scale
|
||||
|
||||
def update_assetbar_sizes(self, context):
|
||||
"""Calculate all important sizes for the asset bar"""
|
||||
@@ -766,8 +857,7 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
user_preferences = bpy.context.preferences.addons[__package__].preferences
|
||||
ui_scale = bpy.context.preferences.view.ui_scale
|
||||
|
||||
ui_scale = self.get_ui_scale()
|
||||
# assetbar scaling
|
||||
self.button_margin = int(0 * ui_scale)
|
||||
self.assetbar_margin = int(2 * ui_scale)
|
||||
@@ -793,6 +883,10 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
self.bar_x = int(
|
||||
tools_width + self.button_margin + ui_props.bar_x_offset * ui_scale
|
||||
)
|
||||
# self.bar_y = region.height - ui_props.bar_y_offset * ui_scale
|
||||
|
||||
self.bar_y = int(ui_props.bar_y_offset * ui_scale)
|
||||
|
||||
self.bar_end = int(ui_width + 180 * ui_scale + self.other_button_size)
|
||||
self.bar_width = int(region.width - self.bar_x - self.bar_end)
|
||||
|
||||
@@ -810,6 +904,16 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
if search_results is not None and self.wcount > 0:
|
||||
if user_preferences.assetbar_expanded:
|
||||
max_rows = user_preferences.maximized_assetbar_rows
|
||||
available_height = (
|
||||
region.height
|
||||
- self.bar_y
|
||||
- 2 * self.assetbar_margin
|
||||
- self.other_button_size
|
||||
)
|
||||
max_rows_by_height = math.floor(available_height / self.button_size)
|
||||
max_rows = (
|
||||
min(max_rows, max_rows_by_height) if max_rows_by_height > 0 else 1
|
||||
)
|
||||
else:
|
||||
max_rows = 1
|
||||
self.hcount = min(
|
||||
@@ -821,8 +925,6 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
self.hcount = 1
|
||||
|
||||
self.bar_height = (self.button_size) * self.hcount + 2 * self.assetbar_margin
|
||||
# self.bar_y = region.height - ui_props.bar_y_offset * ui_scale
|
||||
self.bar_y = int(ui_props.bar_y_offset * ui_scale)
|
||||
if ui_props.down_up == "UPLOAD":
|
||||
self.reports_y = region.height - self.bar_y - 600
|
||||
ui_props.reports_y = region.height - self.bar_y - 600
|
||||
@@ -886,26 +988,28 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
self.tooltip_panel.width = self.tooltip_width
|
||||
self.tooltip_panel.height = self.tooltip_height
|
||||
self.tooltip_image.width = self.tooltip_width
|
||||
self.tooltip_image.height = self.tooltip_height
|
||||
self.tooltip_image.height = self.tooltip_image_height
|
||||
|
||||
self.labels_start = self.tooltip_height * (1 - self.bottom_panel_fraction)
|
||||
self.labels_start = self.tooltip_image_height
|
||||
|
||||
self.tooltip_image.set_image_size((self.tooltip_width, self.tooltip_height))
|
||||
self.tooltip_image.set_image_size(
|
||||
(self.tooltip_width, self.tooltip_image_height)
|
||||
)
|
||||
self.tooltip_image.set_location(0, 0)
|
||||
|
||||
self.gravatar_image.set_location(
|
||||
self.tooltip_width - self.gravatar_size,
|
||||
self.tooltip_height - self.gravatar_size,
|
||||
self.tooltip_width - self.gravatar_size - self.tooltip_margin,
|
||||
self.tooltip_height - self.gravatar_size - self.tooltip_margin,
|
||||
)
|
||||
self.gravatar_image.set_image_size(
|
||||
(
|
||||
self.gravatar_size - 1 * self.tooltip_margin,
|
||||
self.gravatar_size - 1 * self.tooltip_margin,
|
||||
self.gravatar_size,
|
||||
self.gravatar_size,
|
||||
)
|
||||
)
|
||||
|
||||
self.authors_name.set_location(
|
||||
self.tooltip_width - self.gravatar_size - self.tooltip_margin,
|
||||
self.tooltip_width - self.gravatar_size - (self.tooltip_margin * 2),
|
||||
self.tooltip_height - self.author_text_size - self.tooltip_margin,
|
||||
)
|
||||
self.authors_name.text_size = self.author_text_size
|
||||
@@ -922,9 +1026,7 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
0,
|
||||
self.labels_start,
|
||||
)
|
||||
self.tooltip_dark_panel.height = (
|
||||
self.tooltip_height * self.bottom_panel_fraction
|
||||
)
|
||||
self.tooltip_dark_panel.height = self.tooltip_info_height
|
||||
self.tooltip_dark_panel.width = self.tooltip_width
|
||||
|
||||
self.quality_label.set_location(
|
||||
@@ -942,6 +1044,15 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
(self.asset_name_text_size, self.asset_name_text_size)
|
||||
)
|
||||
|
||||
# right after the asset name
|
||||
self.price_label.set_location(
|
||||
self.tooltip_margin,
|
||||
self.labels_start + (self.tooltip_margin * 3) + self.asset_name.height,
|
||||
)
|
||||
self.price_label.width = self.tooltip_width - 2 * self.tooltip_margin
|
||||
self.price_label.height = self.asset_name_text_size
|
||||
self.price_label.text_size = self.asset_name_text_size
|
||||
|
||||
def update_layout(self, context, event):
|
||||
"""update UI sizes after their recalculation"""
|
||||
self.update_assetbar_layout(context)
|
||||
@@ -1044,6 +1155,7 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
self.button_bg_color = (0.2, 0.2, 0.2, 1.0)
|
||||
self.button_hover_color = (0.8, 0.8, 0.8, 1.0)
|
||||
self.button_selected_color = (0.5, 0.5, 0.5, 1.0)
|
||||
self.button_selected_color_dim = (0.3, 0.3, 0.3, 1.0)
|
||||
|
||||
self.buttons = []
|
||||
self.asset_buttons = []
|
||||
@@ -1072,7 +1184,7 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
self.other_button_size, # Same height as tab buttons
|
||||
)
|
||||
# dark blue
|
||||
self.tab_area_bg.bg_color = (0.2, 0.25, 0.4, 1.0)
|
||||
self.tab_area_bg.bg_color = colors.TOP_BAR_BLUE
|
||||
|
||||
# Add widgets to panel - add tab background first so it's behind everything
|
||||
self.widgets_panel.append(self.tab_area_bg)
|
||||
@@ -1162,8 +1274,11 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
# Add tab navigation elements
|
||||
button_size = self.other_button_size
|
||||
margin = int(button_size * 0.05)
|
||||
space = int(button_size * 0.4)
|
||||
tab_icon_size = int(button_size * 0.7) # Size for the asset type icon
|
||||
tab_width = button_size * 4 # Wider tabs to accommodate icon
|
||||
tab_width = (
|
||||
button_size * 4 + tab_icon_size
|
||||
) # Widen the tabs to accommodate type icon
|
||||
|
||||
# Back/Forward history buttons
|
||||
self.history_back_button = BL_UI_Button(
|
||||
@@ -1199,10 +1314,14 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
tabs = global_vars.TABS["tabs"]
|
||||
tab_x_start = margin * 4 + button_size * 3 # Starting x position of first tab
|
||||
|
||||
tabs_end_x = 0
|
||||
|
||||
for i, tab in enumerate(tabs):
|
||||
is_active = i == global_vars.TABS["active_tab"]
|
||||
|
||||
# Calculate positions
|
||||
tab_x = tab_x_start + i * (
|
||||
tab_width + button_size + margin
|
||||
tab_width + button_size + margin + space
|
||||
) # Space for tab and close button
|
||||
|
||||
# Tab button
|
||||
@@ -1212,13 +1331,15 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
tab_width, # Width of tab
|
||||
button_size,
|
||||
)
|
||||
tab_button.bg_color = self.button_bg_color
|
||||
if i == global_vars.TABS["active_tab"]:
|
||||
tab_button.bg_color = self.button_selected_color
|
||||
|
||||
tab_button.hover_bg_color = self.button_hover_color
|
||||
tab_button.text = tab["name"]
|
||||
tab_button.text_size = button_size * 0.5
|
||||
tab_button.text_color = self.text_color
|
||||
tab_button.bg_color = self.button_bg_color
|
||||
if is_active:
|
||||
tab_button.bg_color = self.button_selected_color
|
||||
|
||||
tab_button.tab_index = i # Store tab index
|
||||
tab_button.set_mouse_down(self.switch_tab) # Add click handler
|
||||
self.tab_buttons.append(tab_button)
|
||||
@@ -1226,7 +1347,7 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
# Set asset type icon as tab button image
|
||||
tab_button.set_image_size((tab_icon_size, tab_icon_size))
|
||||
tab_button.set_image_position(
|
||||
(margin, (button_size - tab_icon_size) / 2)
|
||||
(margin * 2, (button_size - tab_icon_size) / 2)
|
||||
) # Center vertically
|
||||
|
||||
# Only create close button if there's more than one tab
|
||||
@@ -1243,22 +1364,24 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
close_tab.text = "×" # Set text after creation
|
||||
close_tab.text_size = button_size * 0.8
|
||||
close_tab.text_color = self.text_color
|
||||
if is_active:
|
||||
close_tab.bg_color = self.button_selected_color_dim
|
||||
|
||||
close_tab.tab_index = i # Store tab index
|
||||
# if there's only one tab, the button closes asset bar instead of closing tab
|
||||
if len(tabs) > 1:
|
||||
close_tab.set_mouse_down(self.remove_tab) # Add click handler
|
||||
else:
|
||||
close_tab.set_mouse_down(self.cancel_press)
|
||||
|
||||
self.close_tab_buttons.append(close_tab)
|
||||
|
||||
tabs_end_x = close_x + button_size
|
||||
|
||||
# New tab button - position after all tabs and close buttons
|
||||
if len(tabs) > 0:
|
||||
last_tab_index = len(tabs) - 1
|
||||
last_tab_x = tab_x_start + last_tab_index * (
|
||||
tab_width + button_size + margin
|
||||
)
|
||||
new_tab_x = (
|
||||
last_tab_x + tab_width + button_size + margin * 2
|
||||
space + tabs_end_x + margin * 2
|
||||
) # After last tab and its close button
|
||||
else:
|
||||
new_tab_x = tab_x_start # If no tabs, start at the beginning
|
||||
@@ -1302,8 +1425,6 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
active_tab["history_index"] < len(active_tab["history"]) - 1
|
||||
)
|
||||
|
||||
# self.update_buttons()
|
||||
|
||||
def set_element_images(self):
|
||||
"""set ui elements images, has to be done after init of UI."""
|
||||
# img_fp = paths.get_addon_thumbnail_path("vs_rejected.png")
|
||||
@@ -1345,7 +1466,7 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
icon_path = paths.get_addon_thumbnail_path(
|
||||
f"asset_type_{asset_type}.png"
|
||||
)
|
||||
if not os.path.exists(icon_path):
|
||||
if not paths.icon_path_exists(icon_path):
|
||||
icon_path = paths.get_addon_thumbnail_path(
|
||||
"asset_type_model.png"
|
||||
)
|
||||
@@ -1444,7 +1565,7 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
"""Initialize the asset bar operator."""
|
||||
self.tooltip_base_size_pixels = 512
|
||||
self.tooltip_scale = 1.0
|
||||
self.bottom_panel_fraction = 0.15
|
||||
self.bottom_panel_fraction = 0.18
|
||||
self.needs_tooltip_update = False
|
||||
self.update_ui_size(bpy.context)
|
||||
|
||||
@@ -1679,9 +1800,13 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
price_color = asset_data["tooltip_data"].get(
|
||||
"price_color", (1.0, 0.8, 0.2, 1.0)
|
||||
)
|
||||
price_background = asset_data["tooltip_data"].get(
|
||||
"price_background", (0.2, 0.2, 0.2, 0.0)
|
||||
)
|
||||
self.price_label.text = price_text
|
||||
self.price_label.text_color = price_color
|
||||
self.price_label.visible = bool(price_text)
|
||||
self.price_label.bg_color = price_background
|
||||
|
||||
# preview comments for validators
|
||||
self.update_comments_for_validators(asset_data)
|
||||
@@ -1721,7 +1846,22 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
- properties_width
|
||||
),
|
||||
)
|
||||
tooltip_y = int(widget.y_screen + widget.height)
|
||||
|
||||
# Calculate space above and below the button
|
||||
ui_scale = self.get_ui_scale()
|
||||
full_tooltip_height = self.tooltip_panel.height
|
||||
space_above = widget.y_screen
|
||||
space_below = bpy.context.region.height - (widget.y_screen + widget.height)
|
||||
# If space below is insufficient (would make tooltip < 70% size), position above
|
||||
if (
|
||||
space_below < full_tooltip_height
|
||||
and space_below < full_tooltip_height * 0.7
|
||||
and space_below < space_above
|
||||
):
|
||||
tooltip_y = int(widget.y_screen - full_tooltip_height)
|
||||
else:
|
||||
tooltip_y = int(widget.y_screen + widget.height)
|
||||
|
||||
# need to set image here because of context issues.
|
||||
img_path = paths.get_addon_thumbnail_path("star_grey.png")
|
||||
self.quality_star.set_image(img_path)
|
||||
@@ -1730,7 +1870,7 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
self.tooltip_panel.set_location(tooltip_x, tooltip_y)
|
||||
self.update_tooltip_size(bpy.context)
|
||||
self.update_tooltip_layout(bpy.context)
|
||||
self.tooltip_panel.set_location(tooltip_x, tooltip_y)
|
||||
self.tooltip_panel.set_location(self.tooltip_panel.x, self.tooltip_panel.y)
|
||||
self.tooltip_panel.layout_widgets()
|
||||
# show bookmark button - always on mouse enter
|
||||
if widget.bookmark_button:
|
||||
@@ -2317,6 +2457,10 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
return # Already on this tab and history step
|
||||
# make original tab original background color
|
||||
self.tab_buttons[global_vars.TABS["active_tab"]].bg_color = self.button_bg_color
|
||||
# make also tab close button original background color
|
||||
self.close_tab_buttons[global_vars.TABS["active_tab"]].bg_color = (
|
||||
self.button_bg_color
|
||||
)
|
||||
|
||||
global_vars.TABS["active_tab"] = tab_index
|
||||
global_vars.TABS["tabs"][tab_index]["history_index"] = history_index
|
||||
@@ -2354,14 +2498,24 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
|
||||
|
||||
# Update history button visibility
|
||||
active_tab = global_vars.TABS["tabs"][tab_index]
|
||||
|
||||
self.history_back_button.visible = active_tab["history_index"] > 0
|
||||
self.history_forward_button.visible = (
|
||||
active_tab["history_index"] < len(active_tab["history"]) - 1
|
||||
)
|
||||
|
||||
# make active tab a bit darker
|
||||
if len(self.tab_buttons) > tab_index:
|
||||
self.tab_buttons[tab_index].bg_color = self.button_selected_color
|
||||
# update tab colors
|
||||
for tab_button in self.tab_buttons:
|
||||
c_tab_index = tab_button.tab_index
|
||||
if c_tab_index == tab_index:
|
||||
tab_button.bg_color = self.button_selected_color
|
||||
self.close_tab_buttons[tab_index].bg_color = (
|
||||
self.button_selected_color_dim
|
||||
)
|
||||
|
||||
else:
|
||||
tab_button.bg_color = self.button_bg_color
|
||||
self.close_tab_buttons[c_tab_index].bg_color = self.button_bg_color
|
||||
|
||||
# update filters
|
||||
search.update_filters()
|
||||
|
||||
+12
-2
@@ -1467,8 +1467,18 @@ class AssetDragOperator(bpy.types.Operator):
|
||||
Tuple[None, None, None],
|
||||
]:
|
||||
"""Find the window, region and area under the mouse cursor."""
|
||||
# Iterate windows backwards, so we go from the top-most window to the bottommost window
|
||||
for window in reversed(bpy.context.window_manager.windows):
|
||||
|
||||
wins = bpy.context.window_manager.windows[:]
|
||||
# reverse the list, seemed to work well at least on windows.
|
||||
wins.reverse()
|
||||
context_win = bpy.context.window
|
||||
|
||||
# let's prioritize the context window
|
||||
if context_win is not None:
|
||||
wins.remove(context_win)
|
||||
wins.insert(0, context_win)
|
||||
|
||||
for window in wins:
|
||||
# first let's test if it's in this window, so we know we shall continue
|
||||
window_x = window.x * self.resolution_factor
|
||||
window_y = window.y * self.resolution_factor
|
||||
|
||||
+32
-8
@@ -27,6 +27,8 @@ from . import utils
|
||||
|
||||
RENDER_OBTYPES = ["MESH", "CURVE", "SURFACE", "METABALL", "TEXT"]
|
||||
|
||||
_BLE_5_PLUS = bpy.app.version >= (5, 0, 0)
|
||||
|
||||
|
||||
def check_material(props, mat):
|
||||
e = bpy.context.scene.render.engine
|
||||
@@ -217,17 +219,39 @@ def check_rig(props, obs):
|
||||
props.rig = True
|
||||
|
||||
|
||||
def has_keyframes(obj):
|
||||
"""Checks if object has animation data with keyframes.
|
||||
|
||||
This function only checks for keyframes,
|
||||
may return false negatives for objects animated with constraints, drivers, etc.
|
||||
"""
|
||||
if obj.animation_data is None:
|
||||
return False
|
||||
|
||||
a = obj.animation_data.action
|
||||
if a is None:
|
||||
return False
|
||||
|
||||
# should work from at least Blender4.2+
|
||||
if _BLE_5_PLUS:
|
||||
# combined fcurves ranges
|
||||
# check if start and end frames are different
|
||||
if a.curve_frame_range[0] != a.curve_frame_range[1]:
|
||||
return True
|
||||
else:
|
||||
# older Blender versions
|
||||
for c in a.fcurves:
|
||||
if len(c.keyframe_points) > 1:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def check_anim(props, obs):
|
||||
animated = False
|
||||
for ob in obs:
|
||||
if ob.animation_data is not None:
|
||||
a = ob.animation_data.action
|
||||
if a is not None:
|
||||
for c in a.fcurves:
|
||||
if len(c.keyframe_points) > 1:
|
||||
animated = True
|
||||
|
||||
# c.keyframe_points.remove(c.keyframe_points[0])
|
||||
if has_keyframes(ob):
|
||||
animated = True
|
||||
break
|
||||
if animated:
|
||||
props.animated = True
|
||||
|
||||
|
||||
+19
-2
@@ -209,8 +209,17 @@ def start_model_thumbnailer(
|
||||
blender_user_scripts_dir = (
|
||||
Path(__file__).resolve().parents[2]
|
||||
) # scripts/addons/blenderkit/autothumb.py
|
||||
|
||||
env = {"BLENDER_USER_SCRIPTS": str(blender_user_scripts_dir)}
|
||||
env.update(os.environ)
|
||||
|
||||
# both must be enabled
|
||||
if (
|
||||
user_preferences.experimental_features
|
||||
and user_preferences.ignore_env_for_thumbnails
|
||||
):
|
||||
env = None
|
||||
|
||||
proc = subprocess.Popen(
|
||||
args,
|
||||
stdout=subprocess.PIPE,
|
||||
@@ -218,7 +227,7 @@ def start_model_thumbnailer(
|
||||
creationflags=utils.get_process_flags(),
|
||||
env=env,
|
||||
)
|
||||
bk_logger.info(f"Started Blender executing {SCRIPT_NAME} on file {datafile}")
|
||||
bk_logger.info("Started Blender executing %s on file %s", SCRIPT_NAME, datafile)
|
||||
eval_path_computing = f"bpy.data.objects['{json_args['asset_name']}'].blenderkit.is_generating_thumbnail"
|
||||
eval_path_state = f"bpy.data.objects['{json_args['asset_name']}'].blenderkit.thumbnail_generating_state"
|
||||
eval_path = f"bpy.data.objects['{json_args['asset_name']}']"
|
||||
@@ -284,8 +293,16 @@ def start_material_thumbnailer(
|
||||
blender_user_scripts_dir = (
|
||||
Path(__file__).resolve().parents[2]
|
||||
) # scripts/addons/blenderkit/autothumb.py
|
||||
|
||||
env = {"BLENDER_USER_SCRIPTS": str(blender_user_scripts_dir)}
|
||||
env.update(os.environ)
|
||||
|
||||
if (
|
||||
user_preferences.experimental_features
|
||||
and user_preferences.ignore_env_for_thumbnails
|
||||
):
|
||||
env = None
|
||||
|
||||
proc = subprocess.Popen(
|
||||
args,
|
||||
stdout=subprocess.PIPE,
|
||||
@@ -293,7 +310,7 @@ def start_material_thumbnailer(
|
||||
creationflags=utils.get_process_flags(),
|
||||
env=env,
|
||||
)
|
||||
bk_logger.info(f"Started Blender executing {SCRIPT_NAME} on file {datafile}")
|
||||
bk_logger.info("Started Blender executing %s on file %s", SCRIPT_NAME, datafile)
|
||||
|
||||
eval_path_computing = f"bpy.data.materials['{json_args['asset_name']}'].blenderkit.is_generating_thumbnail"
|
||||
eval_path_state = f"bpy.data.materials['{json_args['asset_name']}'].blenderkit.thumbnail_generating_state"
|
||||
|
||||
+19
-4
@@ -164,11 +164,20 @@ if __name__ == "__main__":
|
||||
ob.data.texspace_size.x = 1 # / tscale
|
||||
ob.data.texspace_size.y = 1 # / tscale
|
||||
ob.data.texspace_size.z = 1 # / tscale
|
||||
if data["adaptive_subdivision"] == True:
|
||||
ob.cycles.use_adaptive_subdivision = True
|
||||
|
||||
# this option was moved in Blender 5.0 from cycles directly to modifier
|
||||
if bpy.app.version >= (5, 0, 0):
|
||||
for mod in ob.modifiers:
|
||||
if mod.type == "SUBSURF":
|
||||
if data["adaptive_subdivision"] == True:
|
||||
mod.use_adaptive_subdivision = True
|
||||
else:
|
||||
mod.use_adaptive_subdivision = False
|
||||
else:
|
||||
ob.cycles.use_adaptive_subdivision = False
|
||||
if data["adaptive_subdivision"] == True:
|
||||
ob.cycles.use_adaptive_subdivision = True
|
||||
else:
|
||||
ob.cycles.use_adaptive_subdivision = False
|
||||
ts = data["texture_size_meters"]
|
||||
if data["thumbnail_type"] in ["BALL", "BALL_COMPLEX", "CLOTH"]:
|
||||
utils.automap(
|
||||
@@ -179,7 +188,13 @@ if __name__ == "__main__":
|
||||
)
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
s.cycles.volume_step_size = tscale * 0.1
|
||||
# this option was removed in Blender 5.0
|
||||
# but we have option to set biased volumes
|
||||
if bpy.app.version >= (5, 0, 0):
|
||||
# usually small speedup with little quality loss
|
||||
s.cycles.volume_biased = True
|
||||
else:
|
||||
s.cycles.volume_step_size = tscale * 0.1
|
||||
|
||||
if thumbnail_use_gpu is True:
|
||||
bpy.context.scene.cycles.device = "GPU"
|
||||
|
||||
+3
@@ -71,6 +71,9 @@ def threadread(tcom: ThreadCom):
|
||||
return # process terminated
|
||||
inline = tcom.proc.stdout.readline()
|
||||
inline = inline.decode("utf-8")
|
||||
# ignore empty lines
|
||||
if inline.strip() == "":
|
||||
continue
|
||||
bk_logger.info(inline.strip())
|
||||
progress = re.findall(r"progress\{(.*?)\}", inline)
|
||||
if len(progress) > 0:
|
||||
|
||||
+2
-4
@@ -139,8 +139,8 @@ def login(signup: bool) -> None:
|
||||
|
||||
|
||||
def generate_pkce_pair() -> tuple[str, str]:
|
||||
"""Generate PKCE pair - a code verifier and code challange.
|
||||
The challange should be sent first to the server, the verifier is used in next steps to verify identity (handles Client).
|
||||
"""Generate PKCE pair - a code verifier and code challenge.
|
||||
The challenge should be sent first to the server, the verifier is used in next steps to verify identity (handles Client).
|
||||
"""
|
||||
rand = random.SystemRandom()
|
||||
code_verifier = "".join(rand.choices(string.ascii_letters + string.digits, k=128))
|
||||
@@ -162,8 +162,6 @@ def write_tokens(auth_token, refresh_token, oauth_response):
|
||||
override_extension_draw.ensure_repository(api_key=auth_token)
|
||||
override_extension_draw.clear_repo_cache()
|
||||
|
||||
#
|
||||
|
||||
|
||||
def ensure_token_refresh() -> bool:
|
||||
"""Check if API token needs refresh, call refresh and return True if so.
|
||||
|
||||
+5
-5
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"last_check": "2026-01-12 10:24:10.400844",
|
||||
"backup_date": "December-1-2025",
|
||||
"last_check": "2026-04-02 12:41:20.491300",
|
||||
"backup_date": "January-12-2026",
|
||||
"update_ready": true,
|
||||
"ignore": false,
|
||||
"just_restored": false,
|
||||
"just_updated": false,
|
||||
"version_text": {
|
||||
"link": "https://github.com/BlenderKit/BlenderKit/releases/download/v3.18.1.251219/blenderkit-v3.18.1.251219.zip",
|
||||
"link": "https://github.com/BlenderKit/BlenderKit/releases/download/v3.19.1.260402/blenderkit-v3.19.1.260402.zip",
|
||||
"version": [
|
||||
3,
|
||||
18,
|
||||
19,
|
||||
1,
|
||||
251219
|
||||
260402
|
||||
]
|
||||
}
|
||||
}
|
||||
+78
-7
@@ -1,5 +1,10 @@
|
||||
import blf
|
||||
import bpy
|
||||
import gpu
|
||||
|
||||
from typing import Tuple, Union
|
||||
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
|
||||
from .bl_ui_widget import BL_UI_Widget
|
||||
|
||||
@@ -17,6 +22,9 @@ class BL_UI_Label(BL_UI_Widget):
|
||||
self.multiline = False
|
||||
self.row_height = 20
|
||||
|
||||
self.padding: Union[Tuple[float, float], float] = 0
|
||||
self.background = False
|
||||
|
||||
@property
|
||||
def text_color(self):
|
||||
return self._text_color
|
||||
@@ -61,6 +69,30 @@ class BL_UI_Label(BL_UI_Widget):
|
||||
blf.size(font_id, self._text_size, 72)
|
||||
else:
|
||||
blf.size(font_id, self._text_size)
|
||||
lines = self._text.split("\n") if self.multiline else [self._text]
|
||||
if not lines:
|
||||
return
|
||||
|
||||
default_line_height = self.row_height if self.multiline else self._text_size
|
||||
line_metrics = []
|
||||
max_line_width = 0.0
|
||||
total_height = 0.0
|
||||
|
||||
for line in lines:
|
||||
width, height = blf.dimensions(font_id, line)
|
||||
if height == 0:
|
||||
height = default_line_height
|
||||
line_height = (
|
||||
self.row_height if self.multiline else max(height, self._text_size)
|
||||
)
|
||||
if line_height == 0:
|
||||
line_height = default_line_height
|
||||
line_metrics.append((line, width, line_height))
|
||||
max_line_width = max(max_line_width, width)
|
||||
total_height += line_height
|
||||
|
||||
if not line_metrics:
|
||||
return
|
||||
|
||||
textpos_y = area_height - self.y_screen - self.height
|
||||
|
||||
@@ -76,16 +108,55 @@ class BL_UI_Label(BL_UI_Widget):
|
||||
if self._valign == "CENTER":
|
||||
y -= height // 2
|
||||
# bottom could be here but there's no reason for it
|
||||
|
||||
first_line_height = line_metrics[0][2]
|
||||
|
||||
if self.background and (max_line_width > 0 or total_height > 0):
|
||||
pad_x, pad_y = self._padding_tuple()
|
||||
text_top = y + first_line_height
|
||||
text_bottom = text_top - total_height
|
||||
left = x - pad_x
|
||||
right = x + max_line_width + pad_x
|
||||
top = text_top + pad_y
|
||||
bottom = text_bottom - pad_y
|
||||
self._draw_background_rect(left, right, bottom, top)
|
||||
|
||||
current_y = y
|
||||
if not self.multiline:
|
||||
blf.position(font_id, x, y, 0)
|
||||
|
||||
blf.position(font_id, x, current_y, 0)
|
||||
blf.color(font_id, r, g, b, a)
|
||||
|
||||
blf.draw(font_id, self._text)
|
||||
else:
|
||||
lines = self._text.split("\n")
|
||||
for line in lines:
|
||||
blf.position(font_id, x, y, 0)
|
||||
for line, _, line_height in line_metrics:
|
||||
blf.position(font_id, x, current_y, 0)
|
||||
blf.color(font_id, r, g, b, a)
|
||||
blf.draw(font_id, line)
|
||||
y -= self.row_height
|
||||
current_y -= line_height
|
||||
|
||||
def _padding_tuple(self) -> Tuple[float, float]:
|
||||
pad = self.padding
|
||||
if isinstance(pad, (list, tuple)):
|
||||
if len(pad) == 0:
|
||||
return (0.0, 0.0)
|
||||
if len(pad) == 1:
|
||||
value = float(pad[0])
|
||||
return (value, value)
|
||||
return (float(pad[0]), float(pad[1]))
|
||||
value = float(pad)
|
||||
return (value, value)
|
||||
|
||||
def _draw_background_rect(self, left, right, bottom, top):
|
||||
vertices = (
|
||||
(left, top),
|
||||
(left, bottom),
|
||||
(right, bottom),
|
||||
(right, top),
|
||||
)
|
||||
indices = ((0, 1, 2), (0, 2, 3))
|
||||
gpu.state.blend_set("ALPHA")
|
||||
self.shader.bind()
|
||||
self.shader.uniform_float("color", self._bg_color)
|
||||
batch = batch_for_shader(
|
||||
self.shader, "TRIS", {"pos": vertices}, indices=indices
|
||||
)
|
||||
batch.draw(self.shader)
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ schema_version = "1.0.0"
|
||||
|
||||
id = "blenderkit"
|
||||
type = "add-on"
|
||||
version = "3.18.0-251121" # X.Y.Z-YYYYMMDD, must have a dash instead of a dot
|
||||
version = "3.18.1-251219" # X.Y.Z-YYYYMMDD, must have a dash instead of a dot
|
||||
|
||||
name = "BlenderKit Online Asset Library"
|
||||
tagline = "Drag & drop of assets from the community driven library"
|
||||
|
||||
+1
-1
@@ -91,7 +91,7 @@ def ensure_minimal_data(data: Optional[dict] = None) -> dict:
|
||||
return data
|
||||
|
||||
|
||||
def ensure_minimal_data_class(data_class):
|
||||
def ensure_minimal_data_class(data_class: datas.SearchData) -> datas.SearchData:
|
||||
"""Ensure that the data send to the BlenderKit-Client contains:
|
||||
- app_id is the process ID of the Blender instance, so BlenderKit-client can return reports to the correct instance.
|
||||
- api_key is the authentication token for the BlenderKit server, so BlenderKit-Client can authenticate the user.
|
||||
|
||||
+26
-7
@@ -19,16 +19,35 @@
|
||||
Module colors defines color palette for BlenderKit UI.
|
||||
"""
|
||||
|
||||
# UI Colors
|
||||
|
||||
TOP_BAR_BLUE = (0.2, 0.25, 0.4, 1.0)
|
||||
"""TOP_BAR_BLUE Color for BlenderKit UI top bar."""
|
||||
|
||||
WHITE = (1, 1, 1, 0.9)
|
||||
|
||||
TEXT = (0.9, 0.9, 0.9, 0.6)
|
||||
GREEN = (0.9, 1, 0.9, 0.6)
|
||||
RED = (1, 0.5, 0.5, 0.8)
|
||||
BLUE = (0.8, 0.8, 1, 0.8)
|
||||
TEXT = (0.9, 0.9, 0.9, 0.9)
|
||||
"""TEXT Color for BlenderKit UI text."""
|
||||
|
||||
PURPLE = (0.8, 0.4, 1.0, 1.0) # Full Plan purple
|
||||
GREEN_FREE = (0.4, 0.8, 0.4, 1.0) # Green for free addons
|
||||
"""Color for validator reports."""
|
||||
TEXT_DIM = (0.8, 0.8, 0.8, 0.9)
|
||||
|
||||
GREEN = (0.9, 1, 0.9, 0.6)
|
||||
"""GREEN Color for validator reports."""
|
||||
|
||||
RED = (1, 0.5, 0.5, 0.8)
|
||||
"""RED Color for validator reports."""
|
||||
|
||||
BLUE = (0.8, 0.8, 1, 0.8)
|
||||
"""BLUE Color for validator reports."""
|
||||
|
||||
GREEN_PRICE = (0.42, 0.49, 0.19, 1.0)
|
||||
"""Emerald Green to be used on "discounted" add-ons."""
|
||||
|
||||
PURPLE_PRICE = (0.59, 0.05, 0.82, 1.0)
|
||||
"""Lavender Purple to be used on "for sale" add-ons."""
|
||||
|
||||
ORANGE_FULL = (0.702, 0.349, 0.208, 1.0)
|
||||
"""Burnt Orange associated with full plan assets and add-ons."""
|
||||
|
||||
GRAY = (0.7, 0.7, 0.7, 0.6)
|
||||
"""Default color for debug reports."""
|
||||
|
||||
+19
-8
@@ -109,8 +109,10 @@ def get_addon_installation_status(asset_data):
|
||||
if not is_enabled:
|
||||
extension_module_name = f"bl_ext.www_blenderkit_com.{extension_id}"
|
||||
is_enabled = extension_module_name in enabled_addons
|
||||
bk_logger.info(
|
||||
f"Checking extension format: {extension_module_name} -> enabled: {is_enabled}"
|
||||
bk_logger.debug(
|
||||
"Checking extension format: %s -> enabled: %s",
|
||||
extension_module_name,
|
||||
is_enabled,
|
||||
)
|
||||
|
||||
# Also try other possible repository name formats
|
||||
@@ -210,10 +212,15 @@ def get_addon_installation_status(asset_data):
|
||||
if "blenderkit" in addon.lower() or addon.endswith(extension_id)
|
||||
]
|
||||
if blenderkit_addons:
|
||||
bk_logger.info(f"Found BlenderKit-related enabled addons: {blenderkit_addons}")
|
||||
bk_logger.debug(
|
||||
"Found BlenderKit-related enabled addons: %s", blenderkit_addons
|
||||
)
|
||||
|
||||
bk_logger.info(
|
||||
f"Addon status check for '{extension_id}': installed={is_installed}, enabled={is_enabled}"
|
||||
bk_logger.debug(
|
||||
"Addon status check for '%s': installed=%s, enabled=%s",
|
||||
extension_id,
|
||||
is_installed,
|
||||
is_enabled,
|
||||
)
|
||||
|
||||
return {
|
||||
@@ -877,8 +884,13 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None,
|
||||
if asset_blender_version < (4, 3, 0) and bpy.app.version >= (4, 3, 0):
|
||||
brush.asset_clear()
|
||||
brush.asset_mark()
|
||||
brush.icon_filepath = asset_thumb_path
|
||||
|
||||
if bpy.app.version <= (4, 5, 0):
|
||||
brush.icon_filepath = asset_thumb_path
|
||||
else:
|
||||
# load asset thumbnail into brush if it's not already present
|
||||
if brush.preview is None:
|
||||
with bpy.context.temp_override(id=brush):
|
||||
bpy.ops.ed.lib_id_load_custom_preview(filepath=asset_thumb_path)
|
||||
# set the brush active
|
||||
if bpy.context.view_layer.objects.active.mode == "SCULPT":
|
||||
if bpy.app.version < (4, 3, 0):
|
||||
@@ -897,7 +909,6 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None,
|
||||
relative_asset_identifier=f"Brush{os.sep}{brush.name}"
|
||||
)
|
||||
# TODO add grease pencil brushes!
|
||||
|
||||
# bpy.context.tool_settings.image_paint.brush = brush
|
||||
asset_main = brush
|
||||
|
||||
|
||||
+6
-1
@@ -16,7 +16,7 @@
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
from logging import INFO, WARN
|
||||
from logging import DEBUG, INFO, WARN
|
||||
from os import environ
|
||||
from subprocess import Popen
|
||||
from typing import Any, Optional
|
||||
@@ -59,6 +59,11 @@ BKIT_AUTHORS: dict[int, datas.UserProfile] = {}
|
||||
"""All loaded profiles of other users. Current user is also present in stripped down version. Key is the UserProfile.id."""
|
||||
|
||||
LOGGING_LEVEL_BLENDERKIT = INFO
|
||||
|
||||
# read special DEBUG env var to set logging level to DEBUG
|
||||
if environ.get("BLENDERKIT_DEBUG", "0") == "1":
|
||||
LOGGING_LEVEL_BLENDERKIT = DEBUG
|
||||
|
||||
LOGGING_LEVEL_IMPORTED = WARN
|
||||
PREFS = {}
|
||||
|
||||
|
||||
+4
-2
@@ -27,8 +27,10 @@ import bpy
|
||||
icon_collections = {}
|
||||
|
||||
icons_read = {
|
||||
"fp.png": "free",
|
||||
"flp.png": "full",
|
||||
"free_plan.png": "free",
|
||||
"full_plan.png": "full",
|
||||
"promo_sale_symbol.png": "promo_sale_symbol",
|
||||
"sale_purple.png": "for_sale",
|
||||
"trophy.png": "trophy",
|
||||
"dumbbell.png": "dumbbell",
|
||||
"cc0.png": "cc0",
|
||||
|
||||
+13
@@ -24,6 +24,7 @@ import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from functools import lru_cache
|
||||
|
||||
import bpy
|
||||
|
||||
@@ -39,6 +40,9 @@ BLENDERKIT_REPORT_URL = f"{global_vars.SERVER}/usage_report"
|
||||
BLENDERKIT_USER_ASSETS_URL = f"{global_vars.SERVER}/my-assets"
|
||||
BLENDERKIT_MANUAL_URL = "https://youtu.be/0P8ZjfbUjeA"
|
||||
BLENDERKIT_MODEL_UPLOAD_INSTRUCTIONS_URL = f"{global_vars.SERVER}/docs/upload/"
|
||||
BLENDERKIT_PRINTABLE_UPLOAD_INSTRUCTIONS_URL = (
|
||||
f"{global_vars.SERVER}/docs/upload-printables/"
|
||||
)
|
||||
BLENDERKIT_MATERIAL_UPLOAD_INSTRUCTIONS_URL = (
|
||||
f"{global_vars.SERVER}/docs/uploading-material/"
|
||||
)
|
||||
@@ -463,6 +467,8 @@ def get_addon_file(subpath=""):
|
||||
script_path = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
|
||||
# cache this for minor performance boost
|
||||
@lru_cache(maxsize=128)
|
||||
def get_addon_thumbnail_path(name):
|
||||
global script_path
|
||||
# fpath = os.path.join(p, subpath)
|
||||
@@ -474,6 +480,13 @@ def get_addon_thumbnail_path(name):
|
||||
return os.path.join(script_path, subpath)
|
||||
|
||||
|
||||
# cache this for minor performance boost
|
||||
@lru_cache(maxsize=128)
|
||||
def icon_path_exists(path: str) -> bool:
|
||||
"""Cached version of os.path.exists"""
|
||||
return os.path.exists(path)
|
||||
|
||||
|
||||
def get_config_dir_path() -> str:
|
||||
"""Get the path to the config directory in global_dir."""
|
||||
global_dir = bpy.context.preferences.addons[__package__].preferences.global_dir # type: ignore
|
||||
|
||||
+17
-16
@@ -203,7 +203,7 @@ class SetBookmark(bpy.types.Operator):
|
||||
"""Add or remove bookmarking of the asset.\nShortcut: hover over asset in the asset bar and press 'B'."""
|
||||
|
||||
bl_idname = "wm.blenderkit_bookmark_asset"
|
||||
bl_label = "BlenderKit bookmark assest"
|
||||
bl_label = "BlenderKit bookmark assets"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
asset_id: StringProperty( # type: ignore[valid-type]
|
||||
@@ -233,28 +233,29 @@ class SetBookmark(bpy.types.Operator):
|
||||
ratings_utils.store_rating_local(
|
||||
self.asset_id, rating_type="bookmarks", value=bookmark_value
|
||||
)
|
||||
client_lib.send_rating(self.asset_id, "bookmarks", bookmark_value)
|
||||
client_lib.send_rating(self.asset_id, "bookmarks", str(bookmark_value))
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def rating_menu_draw(self, context):
|
||||
layout = self.layout
|
||||
## NOT USED ANYMORE
|
||||
# def rating_menu_draw(self, context):
|
||||
# layout = self.layout
|
||||
|
||||
ui_props = context.window_manager.blenderkitUI
|
||||
sr = search.get_search_results()
|
||||
# ui_props = context.window_manager.blenderkitUI
|
||||
# sr = search.get_search_results()
|
||||
|
||||
asset_search_index = ui_props.active_index
|
||||
if asset_search_index > -1:
|
||||
asset_data = dict(sr["results"][asset_search_index])
|
||||
# asset_search_index = ui_props.active_index
|
||||
# if asset_search_index > -1:
|
||||
# asset_data = dict(sr["results"][asset_search_index])
|
||||
|
||||
col = layout.column()
|
||||
layout.label(text="Admin rating Tools:")
|
||||
col.operator_context = "INVOKE_DEFAULT"
|
||||
# col = layout.column()
|
||||
# layout.label(text="Admin rating Tools:")
|
||||
# col.operator_context = "INVOKE_DEFAULT"
|
||||
|
||||
op = col.operator("wm.blenderkit_menu_rating_upload", text="Add Rating")
|
||||
op.asset_id = asset_data["id"]
|
||||
op.asset_name = asset_data["name"]
|
||||
op.asset_type = asset_data["assetType"]
|
||||
# op = col.operator("wm.blenderkit_menu_rating_upload", text="Add Rating")
|
||||
# op.asset_id = asset_data["id"]
|
||||
# op.asset_name = asset_data["name"]
|
||||
# op.asset_type = asset_data["assetType"]
|
||||
|
||||
|
||||
# Coordinates (each one is a triangle).
|
||||
|
||||
+86
-13
@@ -20,6 +20,7 @@ import copy
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
from functools import lru_cache
|
||||
import os
|
||||
import re
|
||||
import unicodedata
|
||||
@@ -43,6 +44,7 @@ from . import (
|
||||
image_utils,
|
||||
paths,
|
||||
reports,
|
||||
search_price,
|
||||
resolutions,
|
||||
tasks_queue,
|
||||
utils,
|
||||
@@ -53,6 +55,51 @@ bk_logger = logging.getLogger(__name__)
|
||||
search_tasks = {}
|
||||
|
||||
|
||||
def _inject_user_price_data(assets: list[dict]) -> None:
|
||||
"""Augment search results with per-user pricing info when available."""
|
||||
if not assets:
|
||||
bk_logger.debug("User price lookup skipped: empty assets list.")
|
||||
return
|
||||
|
||||
version_uuids: list[str] = [ass["id"] for ass in assets]
|
||||
if not version_uuids:
|
||||
bk_logger.debug("User price lookup skipped: empty version UUIDs list.")
|
||||
return
|
||||
|
||||
try:
|
||||
price_response = search_price.query_user_price(
|
||||
version_uuids=version_uuids,
|
||||
page_size=len(version_uuids),
|
||||
)
|
||||
except Exception as exc:
|
||||
bk_logger.warning("Failed to fetch user prices: %s", exc)
|
||||
return
|
||||
|
||||
if not price_response:
|
||||
bk_logger.debug(
|
||||
"User price lookup skipped: %s",
|
||||
price_response,
|
||||
)
|
||||
return
|
||||
|
||||
price_by_uuid: dict[str, dict] = {}
|
||||
for entry in price_response:
|
||||
version_uuid = entry.get("versionUuid") # maybe assetUuid ?
|
||||
if not version_uuid:
|
||||
continue
|
||||
price_by_uuid[version_uuid] = entry
|
||||
|
||||
if not price_by_uuid:
|
||||
return
|
||||
|
||||
for asset in assets:
|
||||
version_uuid = asset["id"]
|
||||
price_info = price_by_uuid.get(version_uuid)
|
||||
if not price_info:
|
||||
continue
|
||||
asset["userPrice"] = price_info["discountedPrice"]
|
||||
|
||||
|
||||
def update_ad(ad):
|
||||
if not ad.get("assetBaseId"):
|
||||
try:
|
||||
@@ -136,22 +183,23 @@ def check_clipboard():
|
||||
"""
|
||||
global last_clipboard
|
||||
try: # could be problematic on Linux
|
||||
current_clipboard = bpy.context.window_manager.clipboard
|
||||
current_clipboard = str(bpy.context.window_manager.clipboard)
|
||||
except Exception as e:
|
||||
bk_logger.warning(f"Failed to get clipboard: {e}")
|
||||
return
|
||||
|
||||
if current_clipboard == last_clipboard:
|
||||
return
|
||||
last_clipboard = current_clipboard
|
||||
|
||||
asset_type_index = last_clipboard.find("asset_type:")
|
||||
asset_type_index = current_clipboard.find("asset_type:")
|
||||
if asset_type_index == -1:
|
||||
return
|
||||
|
||||
if not last_clipboard.startswith("asset_base_id:"):
|
||||
if not current_clipboard.startswith("asset_base_id:"):
|
||||
return
|
||||
|
||||
last_clipboard = current_clipboard
|
||||
|
||||
asset_type_string = current_clipboard[asset_type_index:].lower()
|
||||
if asset_type_string.find("model") > -1:
|
||||
target_asset_type = "MODEL"
|
||||
@@ -169,6 +217,10 @@ def check_clipboard():
|
||||
target_asset_type = "NODEGROUP"
|
||||
elif asset_type_string.find("addon") > -1 or asset_type_string.find("add-on") > -1:
|
||||
target_asset_type = "ADDON"
|
||||
else:
|
||||
bk_logger.debug("Clipboard does not contain valid asset type.")
|
||||
return
|
||||
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
if ui_props.asset_type != target_asset_type:
|
||||
ui_props.asset_type = target_asset_type # switch asset type before placing keywords, so it does not search under wrong asset type
|
||||
@@ -341,7 +393,7 @@ def handle_search_task(task: client_tasks.Task) -> bool:
|
||||
return True
|
||||
|
||||
# don't do anything while dragging - this could switch asset during drag, and make results list length different,
|
||||
# causing a lot of throuble.
|
||||
# causing a lot of trouble.
|
||||
if bpy.context.window_manager.blenderkitUI.dragging: # type: ignore[attr-defined]
|
||||
return False
|
||||
|
||||
@@ -403,6 +455,10 @@ def handle_search_task(task: client_tasks.Task) -> bool:
|
||||
asset for asset in result_field if asset.get("downloaded", 0) > 0
|
||||
]
|
||||
|
||||
# TODO: if ever needed, implement for other future types
|
||||
if result_field:
|
||||
_inject_user_price_data(result_field)
|
||||
|
||||
# Store results in history step
|
||||
history_step["search_results"] = result_field
|
||||
history_step["search_results_orig"] = task.result
|
||||
@@ -710,7 +766,7 @@ def query_to_url(
|
||||
scene_uuid: str = "",
|
||||
page_size: int = 15,
|
||||
) -> str:
|
||||
"""Build a new search request by parsing query dictionaty into appropriate URL.
|
||||
"""Build a new search request by parsing query dictionary into appropriate URL.
|
||||
Also modifies query and adds some stuff in there which is very misleading anti-pattern.
|
||||
TODO: just convert to URL here and move the sorting and adding of params to separate function.
|
||||
https://www.blenderkit.com/api/v1/search/
|
||||
@@ -1012,6 +1068,7 @@ def filter_addon_search_results(search_results, filter_installed_only=False):
|
||||
def add_search_process(
|
||||
query, get_next: bool, page_size: int, next_url: str, history_id: str
|
||||
):
|
||||
"""Initialize search task and add it to the task queue."""
|
||||
global search_tasks
|
||||
addon_version = utils.get_addon_version()
|
||||
blender_version = utils.get_blender_version()
|
||||
@@ -1232,7 +1289,7 @@ def search(get_next=False, query=None, author_id=""):
|
||||
|
||||
|
||||
def clean_filters():
|
||||
"""Cleanup filters in case search needs to be reset, typicaly when asset id is copy pasted."""
|
||||
"""Cleanup filters in case search needs to be reset, typically when asset id is copy pasted."""
|
||||
sprops = utils.get_search_props()
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
ui_props.property_unset("own_only")
|
||||
@@ -1551,6 +1608,13 @@ class SearchOperator(Operator):
|
||||
default="Runs search and displays the asset bar at the same time"
|
||||
)
|
||||
|
||||
force_clear: BoolProperty( # type: ignore[valid-type]
|
||||
name="Force clear keywords, before programmatic search",
|
||||
description="Force clear keywords before search",
|
||||
default=True,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
return properties.tooltip
|
||||
@@ -1564,16 +1628,25 @@ class SearchOperator(Operator):
|
||||
if self.esc:
|
||||
bpy.ops.view3d.close_popup_button("INVOKE_DEFAULT")
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
|
||||
search_keywords = str(ui_props.search_keywords)
|
||||
|
||||
if self.keywords != "":
|
||||
search_keywords = self.keywords
|
||||
|
||||
# remove all search keywords if force_clear is set
|
||||
if self.force_clear:
|
||||
# self.force_clear = False # reset the force clear
|
||||
search_keywords = ""
|
||||
|
||||
if self.author_id != "":
|
||||
bk_logger.info(f"Author ID: {self.author_id}")
|
||||
# if there is already an author id in the search keywords, remove it first, the author_id can be any so
|
||||
# use regex to find it
|
||||
ui_props.search_keywords = re.sub(
|
||||
r"\+author_id:\d+", "", ui_props.search_keywords
|
||||
)
|
||||
ui_props.search_keywords += f"+author_id:{self.author_id}"
|
||||
if self.keywords != "":
|
||||
ui_props.search_keywords = self.keywords
|
||||
search_keywords = re.sub(r"\+author_id:\d+", "", search_keywords)
|
||||
search_keywords += f"+author_id:{self.author_id}"
|
||||
|
||||
ui_props.search_keywords = search_keywords
|
||||
|
||||
search(get_next=self.get_next)
|
||||
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
from typing import Iterable, List, Optional, Tuple
|
||||
|
||||
from . import client_lib, paths, utils
|
||||
|
||||
|
||||
def _normalize_version_uuid_list(values: Optional[Iterable[str]]) -> List[str]:
|
||||
if values is None:
|
||||
return []
|
||||
|
||||
normalized: List[str] = []
|
||||
for value in values:
|
||||
if not value:
|
||||
continue
|
||||
as_str = str(value)
|
||||
if as_str not in normalized:
|
||||
normalized.append(as_str)
|
||||
return normalized
|
||||
|
||||
|
||||
def query_user_price(
|
||||
version_uuids: list[str] = [],
|
||||
page_size: int = 15,
|
||||
timeout: Tuple[float, float] = (1, 30),
|
||||
) -> dict:
|
||||
"""Return results for price lookup of multiple asset versions.
|
||||
|
||||
The server endpoint now expects a POST body with `version_uuids`, so we keep
|
||||
the helper focused on returning the correct URL alongside the JSON payload
|
||||
that should be sent in the request.
|
||||
"""
|
||||
|
||||
if isinstance(version_uuids, str):
|
||||
version_uuids = [version_uuids]
|
||||
|
||||
version_uuid_list = _normalize_version_uuid_list(version_uuids)
|
||||
if page_size > 0:
|
||||
version_uuid_list = version_uuid_list[:page_size]
|
||||
|
||||
payload: dict = {"version_uuids": version_uuid_list}
|
||||
|
||||
url = f"{paths.BLENDERKIT_API}/cart/request-price-bulk/"
|
||||
|
||||
if not payload["version_uuids"]:
|
||||
raise ValueError("No version UUIDs provided for price lookup.")
|
||||
|
||||
headers = utils.get_simple_headers()
|
||||
headers.setdefault("Content-Type", "application/json")
|
||||
|
||||
response = client_lib.blocking_request(
|
||||
url,
|
||||
"POST",
|
||||
headers,
|
||||
json_data=payload,
|
||||
timeout=timeout,
|
||||
)
|
||||
search_results = response.json()
|
||||
return search_results
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+8
-6
@@ -120,12 +120,14 @@ def handle_failed_reports(exception: Exception) -> float:
|
||||
|
||||
@bpy.app.handlers.persistent
|
||||
def client_communication_timer():
|
||||
"""Recieve all responses from Client and run according followup commands.
|
||||
"""Receive all responses from Client and run according followup commands.
|
||||
This function is the only one responsible for keeping the Client up and running.
|
||||
"""
|
||||
global pending_tasks
|
||||
bk_logger.debug("Getting tasks from Client")
|
||||
search.check_clipboard()
|
||||
bk_logger.log(5, "Getting tasks from Client")
|
||||
user_preferences = bpy.context.preferences.addons[__package__].preferences
|
||||
if user_preferences.use_clipboard_scan:
|
||||
search.check_clipboard()
|
||||
results = list()
|
||||
try:
|
||||
results = client_lib.get_reports(os.getpid())
|
||||
@@ -141,7 +143,7 @@ def client_communication_timer():
|
||||
wm = bpy.context.window_manager
|
||||
wm.blenderkitUI.logo_status = "logo"
|
||||
|
||||
bk_logger.debug("Handling tasks")
|
||||
bk_logger.log(5, "Handling tasks")
|
||||
results_converted_tasks = []
|
||||
|
||||
# convert to task type
|
||||
@@ -166,8 +168,8 @@ def client_communication_timer():
|
||||
for task in results_converted_tasks:
|
||||
handle_task(task)
|
||||
|
||||
bk_logger.debug("Task handling finished")
|
||||
delay = bpy.context.preferences.addons[__package__].preferences.client_polling
|
||||
bk_logger.log(5, "Task handling finished")
|
||||
delay = user_preferences.client_polling
|
||||
if len(download.download_tasks) > 0:
|
||||
return min(0.2, delay)
|
||||
return delay
|
||||
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
.updater_install_popup
|
||||
.updater_check_now
|
||||
.updater_update_now
|
||||
.updater_update_target
|
||||
.updater_install_manually
|
||||
.updater_update_successful
|
||||
.updater_restore_backup
|
||||
.updater_ignore
|
||||
.end_background_check
|
||||
view3d.asset_drag_drop
|
||||
object.blenderkit_auto_tags
|
||||
object.blenderkit_generate_thumbnail
|
||||
object.blenderkit_regenerate_thumbnail
|
||||
object.blenderkit_generate_material_thumbnail
|
||||
object.blenderkit_regenerate_material_thumbnail
|
||||
object.kill_bg_process
|
||||
wm.blenderkit_login
|
||||
wm.blenderkit_logout
|
||||
wm.blenderkit_login_cancel
|
||||
scene.blenderkit_addon_manager
|
||||
scene.blenderkit_addon_choice
|
||||
scene.blenderkit_download_kill
|
||||
scene.blenderkit_download
|
||||
wm.blenderkit_bookmark_asset
|
||||
wm.blenderkit_mark_notification_read
|
||||
wm.blenderkit_mark_notifications_read_all
|
||||
wm.blenderkit_open_notification_target
|
||||
wm.blenderkit_upvote_comment
|
||||
wm.blenderkit_is_private_comment
|
||||
wm.blenderkit_post_comment
|
||||
wm.logo_status
|
||||
wm.show_notifications
|
||||
wm.blenderkit_join_discord
|
||||
wm.blenderkit_welcome
|
||||
wm.blenderkit_open_system_directory
|
||||
wm.blenderkit_asset_popup
|
||||
view3d.blenderkit_set_comment_reply_id
|
||||
view3d.blenderkit_set_category_origin
|
||||
view3d.blenderkit_clear_search_keywords
|
||||
view3d.close_popup_button
|
||||
wm.blenderkit_popup_dialog
|
||||
wm.blenderkit_url_dialog
|
||||
wm.blenderkit_login_dialog
|
||||
wm.blenderkit_nodegroup_drop_dialog
|
||||
object.blenderkit_particles_drop
|
||||
object.blenderkit_data_trasnfer
|
||||
wm.modal_timer_operator
|
||||
view3d.run_assetbar_start_modal
|
||||
view3d.run_assetbar_fix_context
|
||||
wm.blenderkit_fast_metadata
|
||||
+34
-7
@@ -83,7 +83,7 @@ def draw_upload_common(layout, props, asset_type, context):
|
||||
url = "" # paths.BLENDERKIT_NODEGROUP_UPLOAD_INSTRUCTIONS_URL
|
||||
if asset_type == "PRINTABLE":
|
||||
url = (
|
||||
paths.BLENDERKIT_MODEL_UPLOAD_INSTRUCTIONS_URL
|
||||
paths.BLENDERKIT_PRINTABLE_UPLOAD_INSTRUCTIONS_URL
|
||||
) # Reuse model instructions since prints are similar
|
||||
if asset_type == "ADDON":
|
||||
asset_type_text = asset_type
|
||||
@@ -1721,6 +1721,10 @@ class VIEW3D_PT_blenderkit_import_settings(Panel):
|
||||
layout.prop(preferences, "resolution")
|
||||
# layout.prop(props, 'unpack_files')
|
||||
|
||||
# general settings
|
||||
# show toggle for clipboard scan
|
||||
layout.prop(preferences, "use_clipboard_scan")
|
||||
|
||||
|
||||
def deferred_set_name(props, expected_obj_name):
|
||||
"""Deferred timer to set empty name of uploaded asset to active Object's name.
|
||||
@@ -2725,15 +2729,26 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingProperties):
|
||||
is_free = self.asset_data.get("isFree")
|
||||
|
||||
# Get pricing info from extensions cache
|
||||
user_price = self.asset_data.get("userPrice")
|
||||
base_price = self.asset_data.get("basePrice")
|
||||
is_for_sale = self.asset_data.get("isForSale")
|
||||
|
||||
if self.asset_data["isPrivate"]:
|
||||
text = "Private"
|
||||
self.draw_property(box, "Access", text, icon="LOCKED")
|
||||
elif is_for_sale and not can_download and user_price and base_price:
|
||||
text = f"${user_price} (Not purchased)"
|
||||
icon = pcoll["for_sale"]
|
||||
self.draw_property(
|
||||
box,
|
||||
"Price",
|
||||
text,
|
||||
icon_value=icon.icon_id,
|
||||
tooltip="This addon is for sale but you haven't purchased it yet.\nPrice shown is your price / base price",
|
||||
)
|
||||
elif is_for_sale and not can_download and base_price:
|
||||
text = f"${base_price} (Not purchased)"
|
||||
icon = pcoll["full"]
|
||||
icon = pcoll["for_sale"]
|
||||
self.draw_property(
|
||||
box,
|
||||
"Price",
|
||||
@@ -2742,8 +2757,8 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingProperties):
|
||||
tooltip="This addon is for sale but you haven't purchased it yet",
|
||||
)
|
||||
elif is_for_sale and can_download and base_price:
|
||||
text = f"${base_price} (Purchased)"
|
||||
icon = pcoll["full"]
|
||||
text = f"Purchased"
|
||||
icon = pcoll["for_sale"]
|
||||
self.draw_property(
|
||||
box,
|
||||
"Price",
|
||||
@@ -2752,7 +2767,7 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingProperties):
|
||||
tooltip="You have purchased this addon",
|
||||
)
|
||||
elif not is_free and not is_for_sale:
|
||||
text = "Full plan required"
|
||||
text = "Full plan"
|
||||
icon = pcoll["full"]
|
||||
self.draw_property(
|
||||
box,
|
||||
@@ -2991,7 +3006,7 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingProperties):
|
||||
)
|
||||
op.tooltip = "Search all assets by this author.\nShortcut: Hover over the asset in the asset bar and press 'A'." # type: ignore[attr-defined]
|
||||
op.esc = True # type: ignore[attr-defined]
|
||||
op.keywords = "" # type: ignore[attr-defined]
|
||||
op.keywords = "" # type: ignore[attr-defined] # must not be empty otherwise search will use previous keywords
|
||||
op.author_id = str(author_id) # type: ignore[attr-defined]
|
||||
|
||||
button_row = button_row.row(align=True)
|
||||
@@ -3222,7 +3237,7 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingProperties):
|
||||
# name_row.label(text='>')
|
||||
|
||||
name_row.label(text=aname)
|
||||
push_op_left(name_row, strength=3)
|
||||
push_op_left(name_row, strength=1)
|
||||
op = name_row.operator("view3d.close_popup_button", text="", icon="CANCEL")
|
||||
|
||||
def draw_comment_response(self, context, layout, comment_id):
|
||||
@@ -3568,6 +3583,18 @@ class SetCategoryOperatorLastInPopupCard(SetCategoryOperatorOrigin):
|
||||
bl_idname = "view3d.blenderkit_set_category_in_popup_card_last"
|
||||
|
||||
|
||||
class ToggleClipboardScan(bpy.types.Operator):
|
||||
"""Toggle whether asset links are set from clipboard when copied."""
|
||||
|
||||
bl_idname = "wm.blenderkit_toggle_clipboard_scan"
|
||||
bl_label = "Toggle Clipboard Scan"
|
||||
|
||||
def execute(self, context):
|
||||
user_preferences = bpy.context.preferences.addons[__package__].preferences
|
||||
user_preferences.use_clipboard_scan = not user_preferences.use_clipboard_scan
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ClearSearchKeywords(bpy.types.Operator):
|
||||
"""Clear search keywords"""
|
||||
|
||||
|
||||
+37
-1
@@ -339,6 +339,25 @@ def get_active_asset_by_type(asset_type="model"):
|
||||
return None
|
||||
|
||||
|
||||
def get_equivalent_datablock(asset_type, name):
|
||||
"""Get the datablock that blocks us from renaming the asset, and rename it to something a bit else."""
|
||||
if asset_type == "MATERIAL":
|
||||
return bpy.data.materials.get(name)
|
||||
elif asset_type == "OBJECT":
|
||||
return bpy.data.objects.get(name)
|
||||
elif asset_type == "SCENE":
|
||||
return bpy.data.scenes.get(name)
|
||||
elif asset_type == "HDR":
|
||||
return bpy.data.images.get(name)
|
||||
elif asset_type == "BRUSH":
|
||||
return bpy.data.brushes.get(name)
|
||||
elif asset_type == "NODEGROUP":
|
||||
return bpy.data.node_groups.get(name)
|
||||
elif asset_type == "ADDON":
|
||||
return bpy.data.addons.get(name)
|
||||
return None
|
||||
|
||||
|
||||
def get_active_asset():
|
||||
scene = bpy.context.scene
|
||||
ui_props = bpy.context.window_manager.blenderkitUI
|
||||
@@ -970,12 +989,17 @@ def get_dimensions(obs):
|
||||
return dim, bbmin, bbmax
|
||||
|
||||
|
||||
def get_headers(api_key: str = "") -> dict[str, str]:
|
||||
def get_simple_headers() -> dict[str, str]:
|
||||
headers = {
|
||||
"accept": "application/json",
|
||||
"Platform-Version": platform.platform(),
|
||||
"addon-version": f"{global_vars.VERSION[0]}.{global_vars.VERSION[1]}.{global_vars.VERSION[2]}.{global_vars.VERSION[3]}",
|
||||
}
|
||||
return headers
|
||||
|
||||
|
||||
def get_headers(api_key: str = "") -> dict[str, str]:
|
||||
headers = get_simple_headers()
|
||||
if api_key != "":
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
|
||||
@@ -1129,6 +1153,18 @@ def name_update(props, context=None):
|
||||
asset = get_active_asset()
|
||||
if asset.name != fname: # Here we actually rename assets datablocks
|
||||
asset.name = fname # change name of active object to upload Name
|
||||
# we need to set the name back for proper appending later
|
||||
if asset.name != fname and re.search(r"\.\d+$", asset.name) is not None:
|
||||
# - because assets end up with .001, .002, etc. names sometimes.
|
||||
# first, let's get the datablock that blocks us from renaming the asset, and rename it to something a bit else:
|
||||
# we need to ge the equivalent datablock ,
|
||||
# then we can swap those names around.
|
||||
datablock = get_equivalent_datablock(ui_props.asset_type, fname)
|
||||
if datablock is not None:
|
||||
datablock.name = fname + "_temprename"
|
||||
replace_name = asset.name
|
||||
asset.name = fname
|
||||
datablock.name = replace_name
|
||||
|
||||
|
||||
def fmt_dimensions(p):
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"last_check": "2026-03-16 11:19:55.639825",
|
||||
"backup_date": "January-12-2026",
|
||||
"last_check": "2026-04-02 12:41:20.491300",
|
||||
"backup_date": "April-2-2026",
|
||||
"update_ready": false,
|
||||
"ignore": false,
|
||||
"just_restored": false,
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import os
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import blf
|
||||
import bpy
|
||||
import gpu
|
||||
|
||||
from .. import image_utils, ui_bgl
|
||||
from .bl_ui_widget import BL_UI_Widget
|
||||
from .bl_ui_image import BL_UI_Image
|
||||
from .bl_ui_widget import BL_UI_Widget, region_redraw
|
||||
|
||||
bk_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -18,6 +16,8 @@ class BL_UI_Button(BL_UI_Widget):
|
||||
|
||||
def __init__(self, x, y, width, height):
|
||||
super().__init__(x, y, width, height)
|
||||
self.background = True
|
||||
self.background_padding = (0, 0)
|
||||
self._text_color = (1.0, 1.0, 1.0, 1.0)
|
||||
self._hover_bg_color = (0.5, 0.5, 0.5, 1.0)
|
||||
self._select_bg_color = (0.7, 0.7, 0.7, 1.0)
|
||||
@@ -30,6 +30,8 @@ class BL_UI_Button(BL_UI_Widget):
|
||||
self.__image = None
|
||||
self.__image_size = (24, 24)
|
||||
self.__image_position = (4, 2)
|
||||
self.__image_padding = 0.0
|
||||
self.image_corner_radius = None
|
||||
|
||||
@property
|
||||
def text_color(self):
|
||||
@@ -38,7 +40,7 @@ class BL_UI_Button(BL_UI_Widget):
|
||||
@text_color.setter
|
||||
def text_color(self, value):
|
||||
if value != self._text_color:
|
||||
bpy.context.region.tag_redraw()
|
||||
region_redraw()
|
||||
self._text_color = value
|
||||
|
||||
@property
|
||||
@@ -48,7 +50,7 @@ class BL_UI_Button(BL_UI_Widget):
|
||||
@text.setter
|
||||
def text(self, value):
|
||||
if value != self._text:
|
||||
bpy.context.region.tag_redraw()
|
||||
region_redraw()
|
||||
self._text = value
|
||||
|
||||
@property
|
||||
@@ -58,7 +60,7 @@ class BL_UI_Button(BL_UI_Widget):
|
||||
@text_size.setter
|
||||
def text_size(self, value):
|
||||
if value != self._text_size:
|
||||
bpy.context.region.tag_redraw()
|
||||
region_redraw()
|
||||
self._text_size = value
|
||||
|
||||
@property
|
||||
@@ -68,7 +70,7 @@ class BL_UI_Button(BL_UI_Widget):
|
||||
@hover_bg_color.setter
|
||||
def hover_bg_color(self, value):
|
||||
if value != self._hover_bg_color:
|
||||
bpy.context.region.tag_redraw()
|
||||
region_redraw()
|
||||
self._hover_bg_color = value
|
||||
|
||||
@property
|
||||
@@ -78,7 +80,7 @@ class BL_UI_Button(BL_UI_Widget):
|
||||
@select_bg_color.setter
|
||||
def select_bg_color(self, value):
|
||||
if value != self._select_bg_color:
|
||||
bpy.context.region.tag_redraw()
|
||||
region_redraw()
|
||||
self._select_bg_color = value
|
||||
|
||||
def set_image_size(self, image_size):
|
||||
@@ -93,13 +95,16 @@ class BL_UI_Button(BL_UI_Widget):
|
||||
self.__image
|
||||
self.__image.filepath
|
||||
# self.__image.pixels
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
self.__image = None
|
||||
|
||||
def set_image_colorspace(self, colorspace: str = ""):
|
||||
image_utils.set_colorspace(self.__image, colorspace)
|
||||
|
||||
def set_image(self, rel_filepath):
|
||||
if rel_filepath is None:
|
||||
self.__image = None
|
||||
return
|
||||
# first try to access the image, for cases where it can get removed
|
||||
self.check_image_exists()
|
||||
try:
|
||||
@@ -117,6 +122,17 @@ class BL_UI_Button(BL_UI_Widget):
|
||||
return None
|
||||
return self.__image.filepath
|
||||
|
||||
@property
|
||||
def image_padding(self):
|
||||
return self.__image_padding
|
||||
|
||||
@image_padding.setter
|
||||
def image_padding(self, padding: float):
|
||||
self.__image_padding = padding
|
||||
|
||||
def get_image_padding(self):
|
||||
return self.__image_padding
|
||||
|
||||
def update(self, x, y):
|
||||
super().update(x, y)
|
||||
self._textpos = [x, y]
|
||||
@@ -127,19 +143,30 @@ class BL_UI_Button(BL_UI_Widget):
|
||||
area_height = self.get_area_height()
|
||||
|
||||
gpu.state.blend_set("ALPHA")
|
||||
fill_color = self._resolve_panel_color()
|
||||
|
||||
self.shader.bind()
|
||||
|
||||
self.set_colors()
|
||||
|
||||
self.batch_panel.draw(self.shader)
|
||||
if self.use_rounded_background:
|
||||
rect_y = area_height - self.y_screen - self.height
|
||||
self.draw_background_rect(
|
||||
self.x_screen,
|
||||
rect_y,
|
||||
self.width,
|
||||
self.height,
|
||||
fill_color,
|
||||
force=True,
|
||||
fill_color_override=fill_color,
|
||||
)
|
||||
else:
|
||||
self.shader.bind()
|
||||
self.shader.uniform_float("color", fill_color)
|
||||
self.batch_panel.draw(self.shader)
|
||||
|
||||
self.draw_image()
|
||||
|
||||
# Draw text
|
||||
self.draw_text(area_height)
|
||||
|
||||
def set_colors(self):
|
||||
def _resolve_panel_color(self):
|
||||
color = self._bg_color
|
||||
|
||||
# pressed
|
||||
@@ -150,7 +177,7 @@ class BL_UI_Button(BL_UI_Widget):
|
||||
elif self.__state == 2:
|
||||
color = self._hover_bg_color
|
||||
|
||||
self.shader.uniform_float("color", color)
|
||||
return color
|
||||
|
||||
def draw_text(self, area_height):
|
||||
font_id = 1
|
||||
@@ -165,9 +192,19 @@ class BL_UI_Button(BL_UI_Widget):
|
||||
|
||||
size = blf.dimensions(font_id, self._text)
|
||||
|
||||
# When an image is present, center text in the remaining space after the image
|
||||
image_offset = 0
|
||||
if self.__image is not None:
|
||||
image_offset = self.__image_position[0] + self.__image_size[0]
|
||||
|
||||
textpos_y = area_height - self._textpos[1] - (self.height + size[1]) / 2.0
|
||||
blf.position(
|
||||
font_id, self._textpos[0] + (self.width - size[0]) / 2.0, textpos_y + 1, 0
|
||||
font_id,
|
||||
self._textpos[0]
|
||||
+ image_offset
|
||||
+ (self.width - image_offset - size[0]) / 2.0,
|
||||
textpos_y + 1,
|
||||
0,
|
||||
)
|
||||
|
||||
r, g, b, a = self._text_color
|
||||
@@ -180,15 +217,17 @@ class BL_UI_Button(BL_UI_Widget):
|
||||
y_screen_flip = self.get_area_height() - self.y_screen
|
||||
off_x, off_y = self.__image_position
|
||||
sx, sy = self.__image_size
|
||||
pad = self.__image_padding
|
||||
ui_bgl.draw_image_runtime(
|
||||
self.x_screen + off_x,
|
||||
y_screen_flip - off_y - sy,
|
||||
sx,
|
||||
sy,
|
||||
self.x_screen + off_x + pad,
|
||||
y_screen_flip - off_y - sy + pad,
|
||||
sx - 2 * pad,
|
||||
sy - 2 * pad,
|
||||
self.__image,
|
||||
1.0,
|
||||
crop=(0, 0, 1, 1),
|
||||
batch=None,
|
||||
corner_radius=self.image_corner_radius,
|
||||
)
|
||||
return True
|
||||
return False
|
||||
@@ -203,9 +242,7 @@ class BL_UI_Button(BL_UI_Widget):
|
||||
self.mouse_down_func(self)
|
||||
except Exception:
|
||||
bk_logger.exception("BL_UI_BUTTON mouse_down() error:")
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def set_mouse_down_right(self, mouse_down_right_func):
|
||||
@@ -213,10 +250,11 @@ class BL_UI_Button(BL_UI_Widget):
|
||||
|
||||
def mouse_down_right(self, x, y):
|
||||
if self.is_in_rect(x, y):
|
||||
try:
|
||||
self.mouse_down_right_func(self)
|
||||
except Exception:
|
||||
bk_logger.exception("BL_UI_BUTTON mouse_down_right() error:")
|
||||
if hasattr(self, "mouse_down_right_func"):
|
||||
try:
|
||||
self.mouse_down_right_func(self)
|
||||
except Exception:
|
||||
bk_logger.exception("BL_UI_BUTTON mouse_down_right() error:")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user