Compare commits
6 Commits
fd22216d91
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e3310263f3 | |||
| a232ea1c9c | |||
| 6ff69f0080 | |||
| 6c3b78075b | |||
| 2f8e5f472f | |||
| b401eb4bcf |
@@ -10,13 +10,13 @@ D:\Work\9 iClone\Amazon\
|
||||
D:\Amazon\00_external-files\
|
||||
N:\1. CHARACTERS\remapping\
|
||||
[Recent]
|
||||
D:\2026-04-14 BCONNA2026_MARI\
|
||||
G:\Flamenco\renders\
|
||||
F:\renders\
|
||||
C:\Users\Nathan\Downloads\
|
||||
D:\portfolio\
|
||||
P:\250827_FestivalTurf\Blends\animations\02 Preparing the Base\Watering Ground\
|
||||
P:\250827_FestivalTurf\Assets\Blends\
|
||||
T:\250827_FestivalTurf\Blends\animations\02 Preparing the Base\
|
||||
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\stills\2025_old\img-BG\
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\stills\2025_old\
|
||||
A:\1 Amazon_Active_Projects\260706_Leader_Turbotote\Deliverable\Stills\
|
||||
A:\1 Amazon_Active_Projects\260706_Leader_Turbotote\Blends\stills\
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Deliverable\Scan Aware\
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\stills\Scan Aware\
|
||||
A:\1 Amazon_Active_Projects\1 BlenderAssets\
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\stills\BD2\
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\stills\2026_new\
|
||||
A:\1 Amazon_Active_Projects\260512_Agile_Gemba\Blends\animations\
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"flamenco_version": {
|
||||
"version": "3.8.2",
|
||||
"shortversion": "3.8.2",
|
||||
"version": "3.10-alpha0 (340bb57b)",
|
||||
"shortversion": "3.10-alpha0",
|
||||
"name": "Flamenco",
|
||||
"git": "51a41a19"
|
||||
"git": "340bb57b"
|
||||
},
|
||||
"shared_storage": {
|
||||
"location": "R:\\mnt\\user\\Cerberus Max\\Flamenco",
|
||||
"location": "F:\\jobs",
|
||||
"audience": "users",
|
||||
"platform": "windows",
|
||||
"shaman_enabled": false
|
||||
"shaman_enabled": true
|
||||
},
|
||||
"job_types": {
|
||||
"job_types": [
|
||||
@@ -283,197 +283,6 @@
|
||||
"etag": "fe38e5ae9b3e640119d0d55fc7cbf4f8f15f2982",
|
||||
"description": "Create a viewport preview/playblast using Cycles OPTIX GPU rendering and generate a video file"
|
||||
},
|
||||
{
|
||||
"name": "HoloMARI",
|
||||
"label": "HoloMARI (MARI render_one)",
|
||||
"settings": [
|
||||
{
|
||||
"key": "mari_jobs_json",
|
||||
"type": "string",
|
||||
"description": "JSON object with a \"jobs\" array: {\"jobs\":[{\"cam_name\":...,\"H\":...,\"V\":...}, ...]}. Optional \"frame\" per job for ANIM.",
|
||||
"eval": "__import__('json').dumps({'jobs':[{'cam_name':o.name,'H':int(o['H']),'V':int(o['V'])} for o in C.scene.objects if getattr(o,'type',None)=='CAMERA' and o.name.startswith('MARI_CAMERA') and 'H' in o.keys() and 'V' in o.keys()]})",
|
||||
"eval_info": {
|
||||
"show_link_button": true,
|
||||
"description": "Auto-collect MARI cameras (name MARI_CAMERA*, custom props H,V). Matches MARI Advanced / HoloMARI job JSON."
|
||||
},
|
||||
"required": true,
|
||||
"visible": "submission"
|
||||
},
|
||||
{
|
||||
"key": "mari_action",
|
||||
"type": "string",
|
||||
"choices": [
|
||||
"STILL",
|
||||
"ANIM"
|
||||
],
|
||||
"default": "STILL",
|
||||
"description": "MARI action passed to render_one / export_mari: STILL or ANIM",
|
||||
"visible": "submission"
|
||||
},
|
||||
{
|
||||
"key": "mari_mode",
|
||||
"type": "string",
|
||||
"choices": [
|
||||
"CIRCLE",
|
||||
"FRAME"
|
||||
],
|
||||
"default": "CIRCLE",
|
||||
"description": "MARI layout mode for export_mari type=FRAME|CIRCLE",
|
||||
"eval": "(lambda m: 'FRAME' if str(m) == 'FRAME' else 'CIRCLE')(str(getattr(getattr(C.scene, 'mari_props', None), 'frame', 'CIRCLE') or 'CIRCLE'))",
|
||||
"eval_info": {
|
||||
"show_link_button": true,
|
||||
"description": "FRAME vs CIRCLE from scene mari_props.frame"
|
||||
},
|
||||
"required": true,
|
||||
"visible": "submission"
|
||||
},
|
||||
{
|
||||
"key": "chunk_size",
|
||||
"type": "int32",
|
||||
"default": 1.0,
|
||||
"description": "Number of MARI render_one units (cameras/frames) per Blender task",
|
||||
"propargs": {
|
||||
"max": 4096.0,
|
||||
"min": 1.0
|
||||
},
|
||||
"visible": "submission"
|
||||
},
|
||||
{
|
||||
"key": "frames",
|
||||
"type": "string",
|
||||
"description": "Frame range for ANIM expansion. Examples: '47', '1-30'",
|
||||
"eval": "f'{C.scene.frame_start}-{C.scene.frame_end}'",
|
||||
"eval_info": {
|
||||
"show_link_button": true,
|
||||
"description": "Scene frame range (used when expanding ANIM image sequences)"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"key": "frame_start",
|
||||
"type": "int32",
|
||||
"eval": "C.scene.frame_start",
|
||||
"visible": "hidden"
|
||||
},
|
||||
{
|
||||
"key": "frame_end",
|
||||
"type": "int32",
|
||||
"eval": "C.scene.frame_end",
|
||||
"visible": "hidden"
|
||||
},
|
||||
{
|
||||
"key": "frame_step",
|
||||
"type": "int32",
|
||||
"eval": "max(1, C.scene.frame_step)",
|
||||
"visible": "hidden"
|
||||
},
|
||||
{
|
||||
"key": "render_output_root",
|
||||
"type": "string",
|
||||
"description": "Root folder for MARI output; images go under <root>/<mari_props.render_settings_name> (name comes from the scene, not this field).",
|
||||
"required": true,
|
||||
"subtype": "dir_path",
|
||||
"visible": "submission"
|
||||
},
|
||||
{
|
||||
"key": "add_path_components",
|
||||
"type": "int32",
|
||||
"default": 0.0,
|
||||
"description": "Number of path components of the current blend file to append under the render root",
|
||||
"propargs": {
|
||||
"max": 32.0,
|
||||
"min": 0.0
|
||||
},
|
||||
"required": true,
|
||||
"visible": "submission"
|
||||
},
|
||||
{
|
||||
"key": "render_output_path",
|
||||
"type": "string",
|
||||
"description": "Job artifact path (mari_job.txt). MARI JPEG/output folder = render_output_root + MARI folder name.",
|
||||
"editable": false,
|
||||
"eval": "str(Path(abspath(settings.render_output_root), last_n_dir_parts(settings.add_path_components), jobname, '{timestamp}', 'mari_job.txt'))",
|
||||
"subtype": "file_path"
|
||||
},
|
||||
{
|
||||
"key": "mari_output_dir_abs",
|
||||
"type": "string",
|
||||
"description": "Absolute MARI output folder (render root + scene folder name); used by the zip step.",
|
||||
"editable": false,
|
||||
"eval": "str(Path(abspath(settings.render_output_root)) / str(getattr(getattr(C.scene, 'mari_props', None), 'render_settings_name', '') or '').strip())",
|
||||
"visible": "hidden"
|
||||
},
|
||||
{
|
||||
"key": "mari_output_zip_abs",
|
||||
"type": "string",
|
||||
"description": "Absolute path for the final .zip next to the output folder.",
|
||||
"editable": false,
|
||||
"eval": "str(Path(abspath(settings.render_output_root)) / (str(getattr(getattr(C.scene, 'mari_props', None), 'render_settings_name', '') or '').strip() + '.zip'))",
|
||||
"visible": "hidden"
|
||||
},
|
||||
{
|
||||
"key": "zip_render_output",
|
||||
"type": "bool",
|
||||
"default": true,
|
||||
"description": "After all MARI chunk tasks finish, create a .zip of the output folder (requires a worker that runs misc tasks; uses tar).",
|
||||
"required": false,
|
||||
"visible": "submission"
|
||||
},
|
||||
{
|
||||
"key": "use_cycles_optix",
|
||||
"type": "bool",
|
||||
"default": false,
|
||||
"description": "When enabled, sets Cycles preferences to OPTIX and scene Cycles device to GPU (same idea as cycles_optix_gpu.js).",
|
||||
"label": "Cycles: OPTIX + GPU device",
|
||||
"required": false,
|
||||
"visible": "submission"
|
||||
},
|
||||
{
|
||||
"key": "experimental_gp3",
|
||||
"type": "bool",
|
||||
"description": "Experimental Flag: Grease Pencil 3",
|
||||
"label": "Experimental: GPv3",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"key": "experimental_new_anim",
|
||||
"type": "bool",
|
||||
"description": "Experimental Flag: New Animation Data-block",
|
||||
"label": "Experimental: Baklava",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"key": "blender_args_before",
|
||||
"type": "string",
|
||||
"description": "CLI arguments for Blender, placed before the .blend filename",
|
||||
"label": "Blender CLI args: Before",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"key": "blender_args_after",
|
||||
"type": "string",
|
||||
"description": "CLI arguments for Blender, placed after the .blend filename",
|
||||
"label": "After",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"key": "blendfile",
|
||||
"type": "string",
|
||||
"description": "Path of the Blend file",
|
||||
"required": true,
|
||||
"visible": "web"
|
||||
},
|
||||
{
|
||||
"key": "format",
|
||||
"type": "string",
|
||||
"eval": "C.scene.render.image_settings.file_format",
|
||||
"required": true,
|
||||
"visible": "web"
|
||||
}
|
||||
],
|
||||
"etag": "d19240e637ce81fab40f8e4218ce0249db67d95e",
|
||||
"description": "MARI Advanced render_one batches (export .mari3d prep + per-camera jobs). Optional Cycles OPTIX+GPU setup (off by default)."
|
||||
},
|
||||
{
|
||||
"name": "TalkingHeads Custom Render",
|
||||
"label": "TalkingHeads Custom Render",
|
||||
@@ -517,18 +326,6 @@
|
||||
"subtype": "dir_path",
|
||||
"visible": "submission"
|
||||
},
|
||||
{
|
||||
"key": "daily_path",
|
||||
"type": "string",
|
||||
"description": "Daily folder name under the render root, e.g. 'daily_250813'. If empty, auto-fills to today's date.",
|
||||
"eval": "__import__('datetime').datetime.now().strftime('daily_%y%m%d')",
|
||||
"eval_info": {
|
||||
"show_link_button": true,
|
||||
"description": "Auto-fill with today's daily folder name"
|
||||
},
|
||||
"required": false,
|
||||
"visible": "submission"
|
||||
},
|
||||
{
|
||||
"key": "use_submodule",
|
||||
"type": "bool",
|
||||
@@ -555,7 +352,7 @@
|
||||
"type": "string",
|
||||
"description": "Final file path of where render output will be saved",
|
||||
"editable": false,
|
||||
"eval": "str(Path(abspath(settings.render_output_root or '//'), ((str(settings.submodule or '').strip()) if (settings.use_submodule and str(settings.submodule or '').strip()) else ((__import__('os').path.basename(__import__('os').path.dirname(bpy.data.filepath))) if settings.use_submodule else '')), (settings.daily_path or __import__('datetime').datetime.now().strftime('daily_%y%m%d')), jobname, jobname + '_######'))",
|
||||
"eval": "((lambda Path, abspath, os_path, settings_obj, jobname: str(Path(abspath(settings_obj.render_output_root or '//')) / (((str(settings_obj.submodule or '').strip()) if (settings_obj.use_submodule and str(settings_obj.submodule or '').strip()) else ((os_path.basename(os_path.dirname(bpy.data.filepath))) if settings_obj.use_submodule else ''))) / jobname / (jobname + '_######')))(__import__('pathlib').Path, __import__('os').path.abspath, __import__('os').path, settings, (job.name if 'job' in dir() and job and job.name else __import__('os').path.splitext(__import__('os').path.basename(bpy.data.filepath))[0])))",
|
||||
"subtype": "file_path"
|
||||
},
|
||||
{
|
||||
@@ -596,7 +393,7 @@
|
||||
"visible": "web"
|
||||
}
|
||||
],
|
||||
"etag": "7773b03fd0d493d6d823d7d45bb3ef7f987384f3",
|
||||
"etag": "5f716e7195e55712daee20c237d95f748e2c3d1e",
|
||||
"description": "Render a sequence of frames, and create a preview video file"
|
||||
},
|
||||
{
|
||||
@@ -634,18 +431,6 @@
|
||||
"subtype": "dir_path",
|
||||
"visible": "submission"
|
||||
},
|
||||
{
|
||||
"key": "daily_path",
|
||||
"type": "string",
|
||||
"description": "Daily folder name under the render root, e.g. 'daily_250813'. If empty, auto-fills to today's date.",
|
||||
"eval": "__import__('datetime').datetime.now().strftime('daily_%y%m%d')",
|
||||
"eval_info": {
|
||||
"show_link_button": true,
|
||||
"description": "Auto-fill with today's daily folder name"
|
||||
},
|
||||
"required": false,
|
||||
"visible": "submission"
|
||||
},
|
||||
{
|
||||
"key": "use_submodule",
|
||||
"type": "bool",
|
||||
@@ -672,7 +457,7 @@
|
||||
"type": "string",
|
||||
"description": "Final file path of where render output will be saved",
|
||||
"editable": false,
|
||||
"eval": "str(Path(abspath(settings.render_output_root or '//'), ((str(settings.submodule or '').strip()) if (settings.use_submodule and str(settings.submodule or '').strip()) else ((__import__('os').path.basename(__import__('os').path.dirname(bpy.data.filepath))) if settings.use_submodule else '')), (settings.daily_path or __import__('datetime').datetime.now().strftime('daily_%y%m%d')), jobname, jobname + '_######'))",
|
||||
"eval": "((lambda Path, abspath, os_path, settings_obj, jobname: str(Path(abspath(settings_obj.render_output_root or '//')) / (((str(settings_obj.submodule or '').strip()) if (settings_obj.use_submodule and str(settings_obj.submodule or '').strip()) else ((os_path.basename(os_path.dirname(bpy.data.filepath))) if settings_obj.use_submodule else ''))) / jobname / (jobname + '_######')))(__import__('pathlib').Path, __import__('os').path.abspath, __import__('os').path, settings, (job.name if 'job' in dir() and job and job.name else __import__('os').path.splitext(__import__('os').path.basename(bpy.data.filepath))[0])))",
|
||||
"subtype": "file_path"
|
||||
},
|
||||
{
|
||||
@@ -741,7 +526,7 @@
|
||||
"visible": "hidden"
|
||||
}
|
||||
],
|
||||
"etag": "5e49db0874b8ee1211f6dbc0a59d66137b6996b2",
|
||||
"etag": "758c56b672b0970896f30b85ee5165a5c096aec4",
|
||||
"description": "OPTIX GPU rendering + extra checkboxes for some experimental features + extra CLI args for Blender"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -17,3 +17,6 @@
|
||||
{NVIDIA Corporation/NVIDIA GeForce RTX 3090/NVIDIA 595.79}=SUPPORTED
|
||||
{NVIDIA Corporation/NVIDIA GeForce GTX 1060 with Max-Q Design/NVIDIA 582.28}=SUPPORTED
|
||||
{Intel/Intel(R) HD Graphics 630/4.6.0 - Build 27.20.100.9664}=SUPPORTED
|
||||
{NVIDIA Corporation/NVIDIA GeForce RTX 4080 SUPER/NVIDIA 596.36}=SUPPORTED
|
||||
{NVIDIA Corporation/NVIDIA GeForce RTX 4080 SUPER/NVIDIA 610.47}=SUPPORTED
|
||||
{NVIDIA Corporation/NVIDIA GeForce RTX 4080 SUPER/NVIDIA 610.62}=SUPPORTED
|
||||
|
||||
+199
-199
@@ -1,200 +1,200 @@
|
||||
D:\2026-04-14 BCONNA2026_MARI\Sample Stanford Bunny2.blend
|
||||
D:\2026-04-14 BCONNA2026_MARI\Sample Stanford Bunny1.blend
|
||||
C:\Users\Nathan\Downloads\Sample Stanford Bunny1.blend
|
||||
C:\Users\Nathan\Downloads\Sample Stanford Bunny.blend
|
||||
D:\portfolio\demo.blend
|
||||
D:\2025-12-28 ytshorts gigaproj\Blends\animations\FreddyEpstein\FreddyEpstein.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\02 Preparing the Base\06_Watering.blend
|
||||
T:\250827_FestivalTurf\Blends\animations\02 Preparing the Base\06_Watering.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 2B_SPA.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\FT-lipsync_SPA.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 2D_SPA.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 2B.blend
|
||||
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
|
||||
C:\Users\Nathan\Downloads\test.blend
|
||||
A:\1 Amazon_Active_Projects\260709_Optimal_ADTA_stow\Blends\animations\OptimalStow_animation 2.blend
|
||||
F:\jobs\OptimalStow_animation 2-1bxe\_outside_project\A\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Char\Cartoon1\Hailey_v4.4.blend
|
||||
A:\1 Amazon_Active_Projects\260706_Leader_Turbotote\Blends\stills\iTurboTote_Leader_13b.blend
|
||||
A:\1 Amazon_Active_Projects\260706_Leader_Turbotote\Blends\stills\iTurboTote_Leader_13a.blend
|
||||
A:\1 Amazon_Active_Projects\260706_Leader_Turbotote\Blends\stills\iTurboTote_Leader_12b.blend
|
||||
A:\1 Amazon_Active_Projects\260706_Leader_Turbotote\Blends\stills\iTurboTote_Leader_12a.blend
|
||||
A:\1 Amazon_Active_Projects\260706_Leader_Turbotote\Blends\stills\iTurboTote_Leader_7.blend
|
||||
A:\1 Amazon_Active_Projects\260706_Leader_Turbotote\Blends\stills\iTurboTote_Reuse_8_v2.blend
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\stills\Scan Aware\iP&S_scan aware_6.blend
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\stills\Scan Aware\iP&S_scan aware_5.blend
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\stills\BD2\iP&S_BD2_24.blend
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\stills\2026_new\iP&S_2026_4n.blend
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\stills\2026_new\iP&S_2026_4p.blend
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\stills\2026_new\iP&S_2026_4a-b.blend
|
||||
A:\1 Amazon_Active_Projects\260512_Agile_Gemba\Blends\animations\gembawalk_horizontal_animation_2A_ins3.blend
|
||||
A:\1 Amazon_Active_Projects\260512_Agile_Gemba\Blends\animations\gembawalk_horizontal_animation_2A_ins0.blend
|
||||
A:\1 Amazon_Active_Projects\260512_Agile_Gemba\Blends\animations\gembawalk_horizontal_animation_2A.blend
|
||||
A:\1 Amazon_Active_Projects\260622_Stow_and_Go\Blends\animations\STAG_animation 3c.blend
|
||||
A:\1 Amazon_Active_Projects\260622_Stow_and_Go\Assets\Blends\lipsync.blend
|
||||
A:\1 Amazon_Active_Projects\260622_Stow_and_Go\Blends\animations\STAG_animation 3b.blend
|
||||
A:\1 Amazon_Active_Projects\260622_Stow_and_Go\Blends\animations\STAG_animation 2A.blend
|
||||
A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Char\Cartoon3\Koto_v1.3.blend
|
||||
A:\1 Amazon_Active_Projects\260622_Stow_and_Go\Blends\animations\STAG_animation 3a.blend
|
||||
A:\1 Amazon_Active_Projects\260622_Stow_and_Go\Blends\animations\STAG_animation 1A.blend
|
||||
A:\1 Amazon_Active_Projects\260622_Stow_and_Go\Assets\Blends\STAG_cart.blend
|
||||
A:\1 Amazon_Active_Projects\260622_Stow_and_Go\Blends\stills\iSTAG_5.blend
|
||||
A:\1 Amazon_Active_Projects\260622_Stow_and_Go\Blends\stills\iSTAG_7c.blend
|
||||
A:\1 Amazon_Active_Projects\260622_Stow_and_Go\Blends\stills\iSTAG_11b.blend
|
||||
T:\Amazon\complete\2025\251015_Safety-Walk\Blends\animations\Panorama Loops\Safety walk_PnS_Panorama_loop4.blend
|
||||
A:\1 Amazon_Active_Projects\260512_Agile_Gemba\Blends\animations\gembawalk_horizontal animation 7.blend
|
||||
A:\1 Amazon_Active_Projects\260512_Agile_Gemba\Blends\stills\iAgile_Gemba_8.blend
|
||||
T:\260423_MukBuddy\Blends\animations\Muk Buddy Animation.blend
|
||||
A:\1 Amazon_Active_Projects\260512_Agile_Gemba\Blends\animations\gembawalk_horizontal_animation_6C.blend
|
||||
C:\Users\Nathan\Downloads\gembawalk_horizontal_animation_6C\gembawalk_horizontal_animation_6C.blend
|
||||
C:\Users\Nathan\Downloads\gembawalk_horizontal_animation_6C.blend
|
||||
C:\Users\Nathan\Downloads\gembawalk_horizontal_animation_3d.blend
|
||||
A:\1 Amazon_Active_Projects\260512_Agile_Gemba\Blends\animations\gembawalk_horizontal_animation_3d.blend
|
||||
A:\1 Amazon_Active_Projects\260617_Dual_Induct\Blends\stills\iDual_Induct_3_horizontal.blend
|
||||
A:\1 Amazon_Active_Projects\260617_Dual_Induct\Blends\stills\iDual_Induct_1.blend
|
||||
A:\1 Amazon_Active_Projects\260617_Dual_Induct\Blends\stills\iDual_Induct_7.blend
|
||||
A:\1 Amazon_Active_Projects\260617_Dual_Induct\Blends\stills\iDual_Induct_6_horizontal.blend
|
||||
C:\Users\Nathan\Downloads\Dual-induct_animation 2_vertical.blend
|
||||
A:\1 Amazon_Active_Projects\260617_Dual_Induct\Blends\animations\Dual-induct_animation 2_vertical.blend
|
||||
A:\1 Amazon_Active_Projects\260617_Dual_Induct\Blends\stills\iDual_Induct_10b.blend
|
||||
A:\1 Amazon_Active_Projects\260617_Dual_Induct\Blends\stills\iDual_Induct_6.blend
|
||||
C:\Users\Nathan\AppData\Local\Temp\2026-06-19_11-38_iDual_Induct_3_horizontal.blend
|
||||
A:\1 Amazon_Active_Projects\260617_Dual_Induct\Blends\stills\iDual_Induct_11.blend
|
||||
A:\1 Amazon_Active_Projects\260617_Dual_Induct\Blends\stills\iDual_Induct_8b.blend
|
||||
A:\1 Amazon_Active_Projects\260617_Dual_Induct\Blends\stills\iDual_Induct_8a.blend
|
||||
A:\1 Amazon_Active_Projects\260617_Dual_Induct\Blends\stills\iDual_Induct_10a.blend
|
||||
A:\1 Amazon_Active_Projects\1 BlenderAssets\studio_backdrop.blend
|
||||
A:\1 Amazon_Active_Projects\260408_Hummingbird\Blends\stills\ihummingbird_3a.blend
|
||||
A:\1 Amazon_Active_Projects\260617_Dual_Induct\Blends\stills\iDual_Induct_9.blend
|
||||
A:\1 Amazon_Active_Projects\260617_Dual_Induct\Blends\animations\Dual-induct_animation 1.blend
|
||||
A:\1 Amazon_Active_Projects\260420_FLOW_2026\Blends\animations\actor\FlowCSA_actor_7.blend
|
||||
A:\1 Amazon_Active_Projects\260420_FLOW_2026\Blends\animations\actor\FlowCSA_actor_3a.blend
|
||||
A:\1 Amazon_Active_Projects\260420_FLOW_2026\Blends\animations\actor\FlowCSA_actor_3b.blend
|
||||
A:\1 Amazon_Active_Projects\260420_FLOW_2026\Blends\animations\actor\scotty.blend
|
||||
A:\1 Amazon_Active_Projects\260420_FLOW_2026\Blends\animations\actor\other_thing.blend
|
||||
A:\1 Amazon_Active_Projects\260420_FLOW_2026\Blends\animations\actor\FlowCSA_actor_5b.blend
|
||||
A:\1 Amazon_Active_Projects\260420_FLOW_2026\Blends\animations\actor\FlowCSA_actor_3b_png.blend
|
||||
A:\1 Amazon_Active_Projects\260420_FLOW_2026\Blends\animations\actor\FlowCSA_actor_3a_png.blend
|
||||
A:\1 Amazon_Active_Projects\260420_FLOW_2026\Blends\animations\actor\GRAPHS_TEST_GW.blend
|
||||
A:\1 Amazon_Active_Projects\260420_FLOW_2026\Assets\Blends\GRAPHS_TEST_GW.blend
|
||||
C:\Users\Nathan\AppData\Local\Temp\2026-06-15_17-25_FlowCSA_actor_3a.blend
|
||||
C:\Users\Nathan\AppData\Local\Temp\2026-06-15_17-27_FlowCSA_actor_3a.blend
|
||||
A:\1 Amazon_Active_Projects\260512_Agile_Gemba\Blends\animations\gembawalk_horizontal_animation_4b.blend
|
||||
F:\jobs\gembawalk_horizontal_animation_4b\gembawalk_horizontal_animation_4b.flamenco.blend
|
||||
A:\1 Amazon_Active_Projects\260512_Agile_Gemba\Blends\animations\gembawalk_horizontal_animation_4a.blend
|
||||
F:\jobs\HamperSensorEye_animation 1C\HamperSensorEye_animation 1C.flamenco.blend
|
||||
P:\260423_MukBuddy\Blends\animations\Pak Buddy Animation_v2_red.blend
|
||||
P:\260423_MukBuddy\Blends\animations\Pak Buddy Animation_v2_vert.blend
|
||||
P:\260423_MukBuddy\Blends\animations\Pak Buddy Animation_v2.blend
|
||||
F:\jobs\FlowCSA_360_1aD-g64a\FlowCSA_360_1aD.blend
|
||||
F:\jobs\FlowCSA_360_1aD-80jq\FlowCSA_360_1aD.blend
|
||||
F:\jobs\FlowCSA_360_1aD-vxbv\FlowCSA_360_1aD.blend
|
||||
C:\Users\Nathan\Downloads\FlowCSA_360_1aD.blend
|
||||
A:\1 Amazon_Active_Projects\260420_FLOW_2026\Blends\animations\FlowCSA_360_1aD.blend
|
||||
A:\1 Amazon_Active_Projects\260420_FLOW_2026\Blends\animations\FlowCSA_360_1bA_opening_1.blend
|
||||
F:\jobs\FlowCSA_360_1bA_opening_1\FlowCSA_360_1bA_opening_1.flamenco.blend
|
||||
F:\jobs\FlowCSA_360_1bA_opening_1-zlpm\FlowCSA_360_1bA_opening_1.flamenco.blend
|
||||
A:\1 Amazon_Active_Projects\260420_FLOW_2026\Blends\animations\FlowCSA_360_1aC.blend
|
||||
A:\1 Amazon_Active_Projects\260420_FLOW_2026\Blends\animations\FlowCSA_360_1aB.blend
|
||||
A:\1 Amazon_Active_Projects\260420_FLOW_2026\Blends\animations\FlowCSA_360_1aA.blend
|
||||
A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Set-Dressing\Package_path_v1.0.blend
|
||||
A:\1 Amazon_Active_Projects\260420_FLOW_2026\Blends\animations\FlowCSA_360_2aB.blend
|
||||
F:\jobs\FlowCSA_360_2aB\FlowCSA_360_2aB.flamenco.blend
|
||||
F:\jobs\FlowCSA_360_1aC\FlowCSA_360_1aC.blend
|
||||
A:\1 Amazon_Active_Projects\260528_NonConSplitPickcart\Blends\stills\iNonConSplitPickcart_4b.blend
|
||||
A:\1 Amazon_Active_Projects\260528_NonConSplitPickcart\Blends\stills\iNonConSplitPickcart_4a.blend
|
||||
A:\1 Amazon_Active_Projects\260513_FSAF_edits_2026\Blends\stills\iAmbassadorM2_3.blend
|
||||
A:\1 Amazon_Active_Projects\260528_NonConSplitPickcart\Blends\stills\iNonConSplitPickcart_3b.blend
|
||||
A:\1 Amazon_Active_Projects\260528_NonConSplitPickcart\Blends\stills\iNonConSplitPickcart_3a.blend
|
||||
A:\1 Amazon_Active_Projects\260528_NonConSplitPickcart\Blends\stills\iNonConSplitPickcart_3.blend
|
||||
A:\1 Amazon_Active_Projects\260528_NonConSplitPickcart\Blends\stills\iNonConSplitPickcart_2.blend
|
||||
A:\1 Amazon_Active_Projects\260528_NonConSplitPickcart\Blends\stills\iNonConSplitPickcart_1.blend
|
||||
A:\1 Amazon_Active_Projects\260526_Walker_Leader\Assets\Blends\City_Sidewalk_1.blend
|
||||
A:\1 Amazon_Active_Projects\260526_Walker_Leader\Blends\stills\iWalker_CORTEX_1d.blend
|
||||
A:\1 Amazon_Active_Projects\260526_Walker_Leader\Blends\stills\iWalker_AP_1.blend
|
||||
A:\1 Amazon_Active_Projects\260526_Walker_Leader\Blends\animations\Walker_AP_animation 3.blend
|
||||
A:\1 Amazon_Active_Projects\260526_Walker_Leader\Blends\animations\Walker_LOADOUT_1.blend
|
||||
A:\1 Amazon_Active_Projects\260526_Walker_Leader\Blends\animations\Walker_AP_animation 4D.blend
|
||||
A:\1 Amazon_Active_Projects\260526_Walker_Leader\Blends\animations\Walker_AP_animation 4C.blend
|
||||
A:\1 Amazon_Active_Projects\260526_Walker_Leader\Blends\animations\Walker_AP_animation 4B.blend
|
||||
A:\1 Amazon_Active_Projects\260526_Walker_Leader\Blends\animations\Walker_AP_animation 4a.blend
|
||||
A:\1 Amazon_Active_Projects\260526_Walker_Leader\Blends\animations\Walker_AP_animation 2A.blend
|
||||
A:\1 Amazon_Active_Projects\260526_Walker_Leader\Blends\animations\Walker_AP_animation 2A_old.blend
|
||||
A:\1 Amazon_Active_Projects\260526_Walker_Leader\Blends\animations\Walker_AP_animation 2B.blend
|
||||
A:\1 Amazon_Active_Projects\260526_Walker_Leader\Blends\stills\iWalker_AP_17.blend
|
||||
A:\1 Amazon_Active_Projects\260526_Walker_Leader\Blends\stills\iWalker_AP_16.blend
|
||||
A:\1 Amazon_Active_Projects\260526_Walker_Leader\Blends\stills\iWalker_AP_11.blend
|
||||
A:\1 Amazon_Active_Projects\260526_Walker_Leader\Blends\stills\iWalker_AP_7.blend
|
||||
A:\1 Amazon_Active_Projects\260526_Walker_Leader\Assets\Blends\Traffic-Post.blend
|
||||
A:\1 Amazon_Active_Projects\260526_Walker_Leader\Blends\stills\iWalker_AP_9.blend
|
||||
A:\1 Amazon_Active_Projects\260526_Walker_Leader\Blends\stills\iWalker_AP_15.blend
|
||||
A:\1 Amazon_Active_Projects\260526_Walker_Leader\Blends\stills\iWalker_AP_12.blend
|
||||
A:\1 Amazon_Active_Projects\260526_Walker_Leader\Blends\stills\iWalker_AP_13.blend
|
||||
A:\1 Amazon_Active_Projects\260526_Walker_Leader\Blends\stills\iWalker_AP_14.blend
|
||||
A:\1 Amazon_Active_Projects\260526_Walker_Leader\Blends\stills\iWalker_AP_10.blend
|
||||
A:\1 Amazon_Active_Projects\260526_Walker_Leader\Blends\animations\Walker_AP_animation 1.blend
|
||||
A:\1 Amazon_Active_Projects\260526_Walker_Leader\Blends\animations\Walker_AP_animation 1A.blend
|
||||
A:\1 Amazon_Active_Projects\260526_Walker_Leader\Blends\animations\Walker_AP_animation 1B.blend
|
||||
A:\1 Amazon_Active_Projects\260526_Walker_Leader\Assets\Blends\Delivery_Truck_Openable_v1.1.blend
|
||||
A:\1 Amazon_Active_Projects\260526_Walker_Leader\Assets\Blends\Delivery_Truck_Openable.blend
|
||||
A:\1 Amazon_Active_Projects\260526_Walker_Leader\Blends\stills\iWalker_AP_4.blend
|
||||
A:\1 Amazon_Active_Projects\260420_FLOW_2026\Assets\Blends\lipsync.blend
|
||||
A:\1 Amazon_Active_Projects\260420_FLOW_2026\Blends\animations\FlowCSA_360_1a.blend
|
||||
A:\1 Amazon_Active_Projects\260506_Safety_School_Transformation_2026\Blends\animations\SS_2026_animation_5c.blend
|
||||
A:\1 Amazon_Active_Projects\260506_Safety_School_Transformation_2026\Blends\animations\SS_2026_animation_17bA.blend
|
||||
A:\1 Amazon_Active_Projects\260506_Safety_School_Transformation_2026\Blends\animations\SS_2026_animation_13aB.blend
|
||||
A:\1 Amazon_Active_Projects\260506_Safety_School_Transformation_2026\Assets\Blends\lipsync.blend
|
||||
A:\1 Amazon_Active_Projects\260506_Safety_School_Transformation_2026\Blends\animations\SS_2026_animation_16d.blend
|
||||
A:\1 Amazon_Active_Projects\260506_Safety_School_Transformation_2026\Blends\animations\SS_2026_animation_13a.blend
|
||||
A:\1 Amazon_Active_Projects\260506_Safety_School_Transformation_2026\Blends\animations\SS_2026_animation_13a_CM.blend
|
||||
A:\1 Amazon_Active_Projects\260506_Safety_School_Transformation_2026\Blends\animations\SS_2026_animation_5b.blend
|
||||
A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Char\Cartoon3\Kyle_v1.2.blend
|
||||
A:\1 Amazon_Active_Projects\260506_Safety_School_Transformation_2026\Blends\animations\SS_2026_animation_4c.blend
|
||||
A:\1 Amazon_Active_Projects\260506_Safety_School_Transformation_2026\Assets\Blends\lanyard_rigged.blend
|
||||
A:\1 Amazon_Active_Projects\260506_Safety_School_Transformation_2026\Blends\animations\SS_2026_animation_4d.blend
|
||||
A:\1 Amazon_Active_Projects\260506_Safety_School_Transformation_2026\Blends\animations\SS_2026_animation_16c_vertical.blend
|
||||
A:\1 Amazon_Active_Projects\260506_Safety_School_Transformation_2026\Blends\animations\SS_2026_animation_16c.blend
|
||||
A:\1 Amazon_Active_Projects\260506_Safety_School_Transformation_2026\Blends\animations\SS_2026_animation_16e.blend
|
||||
A:\1 Amazon_Active_Projects\260506_Safety_School_Transformation_2026\Blends\animations\SS_2026_animation_6a.blend
|
||||
T:\260506_Safety_School_Transformation_2026\Blends\animations\SS_2026_animation_13a.blend
|
||||
F:\jobs\SS_2026_animation_16d-vs40\_outside_project\A\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Char\Cartoon1\Regina_v4.4.blend
|
||||
A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Props\Tote_v2.0.blend
|
||||
A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Set-Dressing\P2B_filler.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
|
||||
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\Visual 9.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\Visual 8.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\Visual 8 insert2.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\Visual 8 insert.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\01_intro.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\Visual 7.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\Visual 6_INSERT.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\04_talking.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\04_stretching pattern.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\03_FT Shuffle_Intro.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\03_FT Shuffle.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\02_talking.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\05 Stretch Cut Nail\02_kicker insert.blend
|
||||
P:\260217_Jarvis-Defense\Blends\animations\Shot_Q.blend
|
||||
F:\jobs\Shot_5c\Shot_Q.flamenco.blend
|
||||
F:\jobs\Shot5c-e3zp\Shot_Q.flamenco.blend
|
||||
P:\260217_Jarvis-Defense\Blends\animations\Shot_4.blend
|
||||
P:\260217_Jarvis-Defense\Blends\animations\comp_RR\Shot_4a_holdout.blend
|
||||
P:\260217_Jarvis-Defense\Blends\animations\comp_RR\Shot_4_comp.blend
|
||||
F:\jobs\Shot_1b-qrzg\Shot_1.flamenco.blend
|
||||
C:\Users\Nathan\AppData\Local\Temp\2026-05-21_11-46_SS_2026_animation_16d.blend
|
||||
A:\1 Amazon_Active_Projects\260506_Safety_School_Transformation_2026\Blends\animations\SS_2026_animation_12A.blend
|
||||
A:\1 Amazon_Active_Projects\260506_Safety_School_Transformation_2026\Blends\animations\SS_2026_animation_13bB.blend
|
||||
A:\1 Amazon_Active_Projects\260506_Safety_School_Transformation_2026\Blends\animations\SS_2026_animation_4b.blend
|
||||
A:\1 Amazon_Active_Projects\260506_Safety_School_Transformation_2026\Blends\animations\SS_2026_animation_13bA.blend
|
||||
F:\jobs\SS_2026_animation_13bA\_outside_project\A\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Char\Cartoon3\Ren_v1.2.blend
|
||||
A:\1 Amazon_Active_Projects\260506_Safety_School_Transformation_2026\Blends\animations\SS_2026_animation_13b.blend
|
||||
F:\jobs\SS_2026_animation_13a\_outside_project\A\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Char\Cartoon3\Ren_v1.2.blend
|
||||
A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Char\Cartoon3\Ren_v1.2.blend
|
||||
A:\1 Amazon_Active_Projects\260506_Safety_School_Transformation_2026\Blends\animations\SS_2026_animation_8cB.blend
|
||||
A:\1 Amazon_Active_Projects\260506_Safety_School_Transformation_2026\Blends\animations\SS_2026_animation_15bB.blend
|
||||
A:\1 Amazon_Active_Projects\260506_Safety_School_Transformation_2026\Blends\animations\SS_2026_animation_8cB_vertical.blend
|
||||
C:\Users\Nathan\AppData\Local\Temp\bat-ss2026-2b-pack\_outside_project\A\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Char\Cartoon1\Martha_v4.4.blend
|
||||
F:\jobs\FlowCSA_360_1bD-7c6o\_outside_project\A\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Char\Cartoon3\Bebe_v1.2.blend
|
||||
A:\1 Amazon_Active_Projects\260420_FLOW_2026\Blends\animations\FlowCSA_360_1bD.blend
|
||||
F:\jobs\FlowCSA_360_1bD-to93\_outside_project\A\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Char\Cartoon3\Bebe_v1.2.blend
|
||||
A:\1 Amazon_Active_Projects\260420_FLOW_2026\Blends\animations\FlowCSA_360_1b_ADTA_D.blend
|
||||
F:\jobs\FlowCSA_360_1b_ADTA_D-ao45\_outside_project\A\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Char\Cartoon3\Bebe_v1.2.blend
|
||||
F:\jobs\FlowCSA_360_1b_ADTA_D-n9z0\_outside_project\A\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Char\Cartoon3\Bebe_v1.2.blend
|
||||
F:\jobs\FlowCSA_360_1b_ADTA_D\_outside_project\A\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Char\Cartoon3\Bebe_v1.2.blend
|
||||
F:\jobs\FlowCSA_360_1b_ADTA_D-jyur\_outside_project\A\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Char\Cartoon3\Bebe_v1.2.blend
|
||||
A:\1 Amazon_Active_Projects\260420_FLOW_2026\Blends\animations\FlowCSA_360_1b_ADTA_B.blend
|
||||
A:\1 Amazon_Active_Projects\260420_FLOW_2026\Blends\animations\FlowCSA_360_1bB.blend
|
||||
A:\1 Amazon_Active_Projects\260420_FLOW_2026\Blends\animations\FlowCSA_360_1b_ADTA_A.blend
|
||||
C:\Users\Nathan\AppData\Local\Temp\2026-05-13_13-27_FlowCSA_360_1b_ADTA_A.blend
|
||||
C:\Users\Nathan\AppData\Local\Temp\2026-05-13_12-55_FlowCSA_360_1b_ADTA_A.blend
|
||||
C:\Users\Nathan\AppData\Local\Temp\2026-05-13_13-03_FlowCSA_360_1b_ADTA_A.blend
|
||||
C:\Users\Nathan\AppData\Local\Temp\2026-05-13_13-11_FlowCSA_360_1b_ADTA_A.blend
|
||||
A:\1 Amazon_Active_Projects\260420_FLOW_2026\Blends\animations\FlowCSA_360_1bA.blend
|
||||
C:\Users\Nathan\AppData\Local\Temp\2026-05-12_17-26_FlowCSA_360_1b_ADTA_B.blend
|
||||
F:\jobs\FlowCSA_360_1b_ADTA_C-jh9z\_outside_project\A\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Char\Cartoon3\Bebe_v1.2.blend
|
||||
F:\jobs\FlowCSA_360_1b_ADTA_C\_outside_project\A\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Char\Cartoon3\Bebe_v1.2.blend
|
||||
A:\1 Amazon_Active_Projects\260420_FLOW_2026\Blends\animations\FlowCSA_360_1b_ADTA_C.blend
|
||||
F:\jobs\FlowCSA_360_1b_ADTA_C-sgxq\_outside_project\A\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Char\Cartoon3\Bebe_v1.2.blend
|
||||
F:\jobs\FlowCSA_360_1b_ADTA_C-n6rw\FlowCSA_360_1b_ADTA_C.blend
|
||||
F:\jobs\FlowCSA_360_1b_ADTA_C-n6rw\_outside_project\A\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Char\Cartoon3\Bebe_v1.2.blend
|
||||
C:\Users\Nathan\AppData\Local\Temp\bat-flow-not-relative-only\_outside_project\a\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Char\Cartoon3\Bebe_v1.2.blend
|
||||
A:\1 Amazon_Active_Projects\260420_FLOW_2026\Blends\animations\FlowCSA_360_1bC.blend
|
||||
A:\1 Amazon_Active_Projects\260512_Agile_Gemba\Assets\Blends\Marker.blend
|
||||
C:\Users\Nathan\Downloads\SS_2026_animation_2b.blend
|
||||
A:\1 Amazon_Active_Projects\260422_Upstream_edits_2026\Blends\stills\iUP_M6_13.blend
|
||||
A:\1 Amazon_Active_Projects\260422_Upstream_edits_2026\Blends\stills\iUP_M6_12.blend
|
||||
F:\jobs\Pak Buddy Animation_v2\Pak Buddy Animation_v2.flamenco.blend
|
||||
C:\Users\Nathan\Downloads\House.blend
|
||||
C:\Users\Nathan\Downloads\UP_M5_3.blend
|
||||
C:\Users\Nathan\Downloads\UP_M2_animation_d.blend
|
||||
G:\Amazon\2024\240402_Amazon_Coaching&Upstream\Upstream\blends\mod 2\animation 1\iUP_M2_6.blend
|
||||
A:\1 Amazon_Active_Projects\260422_Upstream_edits_2026\Blends\stills\iUP_M5_6.blend
|
||||
A:\1 Amazon_Active_Projects\260422_Upstream_edits_2026\Blends\stills\iUP_M2_5.blend
|
||||
A:\1 Amazon_Active_Projects\260422_Upstream_edits_2026\Blends\stills\iUP_M5_13.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\comp_RR\Visual 2B_SPA_comp_RR.blend
|
||||
|
||||
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,42 +1,36 @@
|
||||
if "bpy" in locals():
|
||||
import importlib
|
||||
for mod in [icons,
|
||||
operators,
|
||||
for mod in [operators,
|
||||
tools,
|
||||
ui,
|
||||
manual,
|
||||
preferences,
|
||||
properties,
|
||||
ui,
|
||||
versioning,
|
||||
]:
|
||||
importlib.reload(mod)
|
||||
print("Add-on Reloaded: Bool Tool")
|
||||
else:
|
||||
import bpy
|
||||
from . import (
|
||||
icons,
|
||||
operators,
|
||||
tools,
|
||||
ui,
|
||||
manual,
|
||||
preferences,
|
||||
properties,
|
||||
ui,
|
||||
versioning,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
modules = [
|
||||
icons,
|
||||
modules = (
|
||||
operators,
|
||||
tools,
|
||||
ui,
|
||||
manual,
|
||||
preferences,
|
||||
properties,
|
||||
ui,
|
||||
versioning,
|
||||
]
|
||||
)
|
||||
|
||||
def register():
|
||||
for module in modules:
|
||||
|
||||
@@ -2,18 +2,17 @@ schema_version = "1.0.0"
|
||||
|
||||
id = "bool_tool"
|
||||
name = "Bool Tool"
|
||||
version = "2.0.0"
|
||||
tagline = "Quick boolean operators and tools for hard surface modeling"
|
||||
version = "2.1.0"
|
||||
tagline = "Quick Boolean operators and tools for hard surface modeling"
|
||||
tags = ["Modeling", "Object"]
|
||||
type = "add-on"
|
||||
|
||||
maintainer = "Nika Kutsniashvili <nickberckley@gmail.com>"
|
||||
website = "https://github.com/nickberckley/bool_tool"
|
||||
tags = ["Modeling", "Object"]
|
||||
maintainer = "Nika Kutsniashvili"
|
||||
website = "https://nickberckley.github.io/bool_tool/"
|
||||
support = "https://github.com/nickberckley/bool_tool/issues/new?template=bug_report.md"
|
||||
|
||||
blender_version_min = "4.5.0"
|
||||
|
||||
# License conforming to https://spdx.org/licenses/ (use "SPDX: prefix)
|
||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html
|
||||
license = [
|
||||
"SPDX:GPL-3.0-or-later",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import os
|
||||
|
||||
|
||||
# Paths
|
||||
ICONS_PATH = os.path.join(os.path.dirname(__file__), "ui", "icons")
|
||||
|
||||
# Object types that can have evaluated mesh, and can be converted to Mesh.
|
||||
CONVERTABLE_TYPES = (
|
||||
'CURVE',
|
||||
'CURVES',
|
||||
'FONT',
|
||||
# 'POINTCLOUD', # Doesn't work in Blender 5.2
|
||||
# 'GREASEPENCIL' # Doesn't work in Blender 5.2
|
||||
'EMPTY',
|
||||
)
|
||||
@@ -0,0 +1,144 @@
|
||||
import bpy
|
||||
|
||||
from .modifier import (
|
||||
is_boolean_modifier,
|
||||
)
|
||||
from .object import (
|
||||
is_linked,
|
||||
change_parent,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ /poll/ ------------------------------ ####
|
||||
|
||||
def is_canvas(obj):
|
||||
"""Checks whether the object is a Boolean canvas (i.e. has Boolean cutters)."""
|
||||
|
||||
if obj.booleans.canvas == False:
|
||||
return False
|
||||
else:
|
||||
# Even if object is marked as a canvas, check if it actually has any cutters.
|
||||
cutters, __ = list_canvas_cutters([obj])
|
||||
if len(cutters) > 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /list/ ------------------------------ ####
|
||||
|
||||
def list_all_canvases(scene):
|
||||
"""Returns the list of all Boolean canvases in the scene."""
|
||||
|
||||
canvases = []
|
||||
|
||||
for obj in scene.objects:
|
||||
if is_canvas(obj):
|
||||
canvases.append(obj)
|
||||
|
||||
return canvases
|
||||
|
||||
|
||||
def list_selected_canvases(context):
|
||||
"""Returns the list of canvases in the selection."""
|
||||
|
||||
canvases = []
|
||||
active_object = context.active_object
|
||||
selected_objects = context.selected_objects
|
||||
|
||||
if selected_objects:
|
||||
for obj in selected_objects:
|
||||
if obj == active_object:
|
||||
continue
|
||||
if obj.type != 'MESH':
|
||||
continue
|
||||
if is_canvas(obj):
|
||||
canvases.append(obj)
|
||||
|
||||
if active_object:
|
||||
if is_canvas(active_object):
|
||||
canvases.append(active_object)
|
||||
|
||||
return canvases
|
||||
|
||||
|
||||
def list_canvas_cutters(canvases: list) -> tuple[list, dict]:
|
||||
"""List cutters (and their associated modifiers) that are used by specified canvases."""
|
||||
|
||||
cutters = []
|
||||
modifiers = {}
|
||||
for canvas in canvases:
|
||||
for mod in canvas.modifiers:
|
||||
if not is_boolean_modifier(mod):
|
||||
continue
|
||||
|
||||
if mod.object not in cutters:
|
||||
cutters.append(mod.object)
|
||||
modifiers.setdefault(canvas, []).append(mod)
|
||||
|
||||
return cutters, modifiers
|
||||
|
||||
|
||||
def list_canvas_slices(context, canvases: list):
|
||||
"""Returns the list of slices of specified canvases."""
|
||||
|
||||
slices = []
|
||||
for obj in context.scene.objects:
|
||||
if obj.booleans.slice:
|
||||
if obj.booleans.slice_of in canvases:
|
||||
slices.append(obj)
|
||||
|
||||
return slices
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /filter/ ------------------------------ ####
|
||||
|
||||
def filter_canvases(self, context, canvases: list) -> list:
|
||||
"""Filter out objects from the given list if they can't be cut."""
|
||||
|
||||
usable_canvases = []
|
||||
for canvas in canvases:
|
||||
# Exclude non-Mesh types.
|
||||
if canvas.type != 'MESH':
|
||||
self.report({'WARNING'}, f"{canvas.name} is not a Mesh type. Only Meshes can be cut")
|
||||
continue
|
||||
# Exclude linked objects.
|
||||
if is_linked(context, canvas):
|
||||
self.report({'WARNING'}, f"{canvas.name} is linked and can not be used as a cutter")
|
||||
continue
|
||||
|
||||
usable_canvases.append(canvas)
|
||||
|
||||
return usable_canvases
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /filter/ ------------------------------ ####
|
||||
|
||||
def create_slice(context, canvas, modifier=False):
|
||||
"""Creates copy of canvas to be used as slice."""
|
||||
|
||||
slice = canvas.copy()
|
||||
slice.data = canvas.data.copy()
|
||||
slice.name = slice.data.name = canvas.name + "_slice"
|
||||
|
||||
# Parent to canvas.
|
||||
change_parent(context, slice, canvas, inverse=True)
|
||||
|
||||
# Set Boolean properties.
|
||||
if modifier == True:
|
||||
slice.booleans.canvas = True
|
||||
slice.booleans.slice = True
|
||||
slice.booleans.slice_of = canvas
|
||||
|
||||
# Add to canvas collections.
|
||||
for coll in canvas.users_collection:
|
||||
coll.objects.link(slice)
|
||||
|
||||
# Add slices to local view.
|
||||
if context.space_data.local_view:
|
||||
slice.local_view_set(context.space_data, True)
|
||||
|
||||
return slice
|
||||
@@ -0,0 +1,188 @@
|
||||
import bpy
|
||||
from .. import __package__ as base_package
|
||||
|
||||
from ..constants import (
|
||||
CONVERTABLE_TYPES,
|
||||
)
|
||||
from .modifier import (
|
||||
is_boolean_modifier,
|
||||
)
|
||||
from .object import (
|
||||
is_linked,
|
||||
has_evaluated_mesh,
|
||||
object_visibility_set,
|
||||
convert_to_mesh,
|
||||
change_parent,
|
||||
delete_object,
|
||||
)
|
||||
from .scene import (
|
||||
ensure_collection,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ /list/ ------------------------------ ####
|
||||
|
||||
def list_selected_cutters(context):
|
||||
"""Returns the list of cutters in the selection."""
|
||||
|
||||
cutters = []
|
||||
active_object = context.active_object
|
||||
selected_objects = context.selected_objects
|
||||
|
||||
if selected_objects:
|
||||
for obj in selected_objects:
|
||||
if obj == active_object:
|
||||
continue
|
||||
if obj.type != 'MESH':
|
||||
continue
|
||||
if obj.booleans.cutter:
|
||||
cutters.append(obj)
|
||||
|
||||
if active_object:
|
||||
if active_object.booleans.cutter:
|
||||
cutters.append(active_object)
|
||||
|
||||
return cutters
|
||||
|
||||
|
||||
def list_cutter_users(cutters: list, exclude: list=None) -> dict:
|
||||
"""
|
||||
List canvases that use specified cutters.
|
||||
Canvases that should be excluded from the search can be specified with the `exclude` arg.
|
||||
Returns a dict of canvases (keys) and set of their Boolean modifiers that use cutters (values).
|
||||
"""
|
||||
|
||||
cutter_users = {}
|
||||
|
||||
for key, values in bpy.data.user_map(subset=cutters).items():
|
||||
for value in values:
|
||||
if value.id_type != 'OBJECT':
|
||||
continue
|
||||
if exclude and value in exclude:
|
||||
continue
|
||||
if len(value.modifiers) == 0:
|
||||
continue
|
||||
|
||||
for mod in value.modifiers:
|
||||
if not is_boolean_modifier(mod):
|
||||
continue
|
||||
if mod.object not in cutters:
|
||||
continue
|
||||
|
||||
cutter_users.setdefault(value, set()).add(mod)
|
||||
|
||||
return cutter_users
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /filter/ ------------------------------ ####
|
||||
|
||||
def filter_cutters(self, context, cutters: list, canvases: list) -> list:
|
||||
"""
|
||||
Filter out objects from the given list if they can't be used as a cutter.
|
||||
If non-mesh type object has evaluated mesh, and can be converted to mesh it will be.
|
||||
"""
|
||||
|
||||
usable_cutters = []
|
||||
for cutter in cutters:
|
||||
# Exclude object if it is in both lists.
|
||||
if cutter in canvases:
|
||||
continue
|
||||
# Exclude linked objects.
|
||||
if is_linked(context, cutter):
|
||||
self.report({'WARNING'}, f"{cutter.name} is linked and can not be used as a cutter")
|
||||
continue
|
||||
|
||||
if cutter.type == 'MESH':
|
||||
# Exclude if object is already a cutter for canvas.
|
||||
users = list_cutter_users([cutter]).keys()
|
||||
if any(canvas in users for canvas in canvases):
|
||||
continue
|
||||
# Exclude if canvas is cutting the object (avoid dependancy loop).
|
||||
users = list_cutter_users(canvases).keys()
|
||||
if cutter in users:
|
||||
self.report({'WARNING'}, f"{cutter.name} can not cut its own cutter (dependancy loop)")
|
||||
continue
|
||||
|
||||
usable_cutters.append(cutter)
|
||||
|
||||
elif cutter.type in CONVERTABLE_TYPES:
|
||||
if not has_evaluated_mesh(context, cutter):
|
||||
continue
|
||||
|
||||
convert_to_mesh(context, cutter)
|
||||
usable_cutters.append(cutter)
|
||||
|
||||
return usable_cutters
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /operate/ ------------------------------ ####
|
||||
|
||||
def make_cutter(context, cutter, mode: str, display='BOUNDS', collection=True):
|
||||
"""Ensures the cutter has the correct properties."""
|
||||
|
||||
# Hide Cutters
|
||||
cutter.hide_render = True
|
||||
cutter.display_type = display
|
||||
cutter.lineart.usage = 'EXCLUDE'
|
||||
object_visibility_set(cutter, value=False)
|
||||
|
||||
# Cutters Collection
|
||||
if collection:
|
||||
cutters_collection = ensure_collection(context)
|
||||
if cutters_collection not in cutter.users_collection:
|
||||
cutters_collection.objects.link(cutter)
|
||||
|
||||
# Set Boolean Property
|
||||
cutter.booleans.cutter = mode.capitalize()
|
||||
|
||||
|
||||
def restore_cutter(context, cutter, unparent=True, unlink_collection=True):
|
||||
"""Remove Boolean properties from a cutter object to restore it to a normal state."""
|
||||
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
|
||||
# Restore Unused Cutters
|
||||
cutter.hide_render = False
|
||||
cutter.display_type = 'TEXTURED'
|
||||
cutter.lineart.usage = 'INHERIT'
|
||||
object_visibility_set(cutter, value=True)
|
||||
cutter.booleans.cutter = ""
|
||||
|
||||
# Remove Parent & Collection
|
||||
if unparent:
|
||||
change_parent(context, cutter, None)
|
||||
|
||||
if unlink_collection:
|
||||
cutters_collection = bpy.data.collections.get(prefs.collection_name)
|
||||
if cutters_collection in cutter.users_collection:
|
||||
cutters_collection.objects.unlink(cutter)
|
||||
|
||||
|
||||
def handle_unused_cutters(context, cutters: list, canvases: list, delete=True):
|
||||
"""Deletes or restores cutters with no remaining users (besides given list of canvases)."""
|
||||
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
|
||||
for cutter in cutters:
|
||||
other_canvases = list_cutter_users([cutter], exclude=canvases).keys()
|
||||
|
||||
if len(other_canvases) == 0:
|
||||
if delete:
|
||||
# Delete unused cutters & transfer their children to canvas.
|
||||
for child in cutter.children:
|
||||
change_parent(context, child, cutter.parent, inverse=True)
|
||||
delete_object(cutter)
|
||||
|
||||
else:
|
||||
# Restore unused cutters.
|
||||
restore_cutter(context, cutter,
|
||||
unparent=prefs.parent and cutter.parent in canvases,
|
||||
unlink_collection=prefs.use_collection)
|
||||
|
||||
else:
|
||||
# Cutter has other users, parent it to one of them.
|
||||
if prefs.parent and cutter.parent in canvases:
|
||||
new_parent = next(c for c in other_canvases if not c.booleans.slice)
|
||||
change_parent(context, cutter, new_parent, inverse=True)
|
||||
@@ -10,7 +10,7 @@ from gpu_extras.batch import batch_for_shader
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def draw_shader(type, color, alpha, coords, size=1, indices=None):
|
||||
"""Creates a batch for a draw type"""
|
||||
"""Sets up and draws a batch for a given GPU shader type."""
|
||||
|
||||
gpu.state.blend_set('ALPHA')
|
||||
|
||||
@@ -48,9 +48,10 @@ def draw_shader(type, color, alpha, coords, size=1, indices=None):
|
||||
gpu.state.blend_set('NONE')
|
||||
|
||||
|
||||
def draw_bmesh_faces(faces, world_matrix):
|
||||
def draw_bmesh_faces(faces, world_matrix) -> tuple[list, list]:
|
||||
"""
|
||||
Get world-space vertex pairs and indices from `bmesh` face. To be used in GPU batch.
|
||||
Gets world-space vertex pairs and indices from `bmesh` faces.
|
||||
Returns the list of 3D vertices and the list of triangle indices to be used in GPU batch.
|
||||
Adapted from "Blockout" extension by niewinny (https://github.com/niewinny/blockout).
|
||||
"""
|
||||
|
||||
@@ -65,13 +66,13 @@ def draw_bmesh_faces(faces, world_matrix):
|
||||
for face in faces:
|
||||
face_indices = []
|
||||
|
||||
# Collect unique vertices only (avoid storing verts that are shared by faces multiple times).
|
||||
# (Iterating over face corners because unlike `face.verts` they're ordered).
|
||||
# Collect unique vertices only (avoid storing verts that are shared by multiple faces).
|
||||
# (NOTE: Iterating over face corners because unlike `face.verts` they're ordered).
|
||||
for loop in face.loops:
|
||||
vert = loop.vert
|
||||
co = world_matrix @ Vector(vert.co)
|
||||
|
||||
if vert not in vert_index_map:
|
||||
co = world_matrix @ Vector(vert.co)
|
||||
vertices.append(co)
|
||||
vert_index_map[vert] = vert_count
|
||||
face_indices.append(vert_count)
|
||||
@@ -94,46 +95,28 @@ def draw_bmesh_faces(faces, world_matrix):
|
||||
return vertices, indices
|
||||
|
||||
|
||||
def draw_bmesh_edges(edges, world_matrix):
|
||||
"""Convert bmesh edges into world-space vertex pairs to be used in GPU batch."""
|
||||
|
||||
if not edges:
|
||||
return None
|
||||
|
||||
vertices = []
|
||||
for edge in edges:
|
||||
v1 = world_matrix @ edge.verts[0].co
|
||||
v2 = world_matrix @ edge.verts[1].co
|
||||
vertices.append(v1)
|
||||
vertices.append(v2)
|
||||
|
||||
return vertices
|
||||
|
||||
|
||||
def draw_circle_around_point(context, obj, vert, radius, segments):
|
||||
def draw_circle_billboard(context, origin: Vector, radius: float, segments: int) -> list:
|
||||
"""
|
||||
Draws the screen-aligned circle around given vertex of the object.
|
||||
Returns the list of vertices for GPU batch.
|
||||
Draws a view-facing circle in the world-space around the given origin Vector.
|
||||
Returns the list of 3D vertices.
|
||||
"""
|
||||
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
vert_world = obj.matrix_world @ vert.co
|
||||
origin_2d = view3d_utils.location_3d_to_region_2d(region, rv3d, origin)
|
||||
radius = min(radius, 25)
|
||||
|
||||
vertices = []
|
||||
for i in range(segments + 1):
|
||||
angle = i * (2 * math.pi / segments)
|
||||
|
||||
# Calculate offset and vertex position in screen-space.
|
||||
# Add offset in screen-space.
|
||||
offset_x = radius * math.cos(angle)
|
||||
offset_y = radius * math.sin(angle)
|
||||
vert_screen = view3d_utils.location_3d_to_region_2d(region, rv3d, vert_world)
|
||||
vert_2d = Vector((origin_2d.x + offset_x, origin_2d.y + offset_y))
|
||||
|
||||
if vert_screen:
|
||||
# Add offset in screen-space and convert back to world-space.
|
||||
circle_screen = Vector((vert_screen.x + offset_x, vert_screen.y + offset_y))
|
||||
circle_3d = view3d_utils.region_2d_to_location_3d(region, rv3d, circle_screen, vert_world)
|
||||
vertices.append(circle_3d)
|
||||
# Convert back to world-space.
|
||||
vert_3d = view3d_utils.region_2d_to_location_3d(region, rv3d, vert_2d, origin)
|
||||
vertices.append(vert_3d)
|
||||
|
||||
return vertices
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
import bpy
|
||||
|
||||
|
||||
#### ------------------------------ /all/ ------------------------------ ####
|
||||
|
||||
def list_canvases():
|
||||
"""List all canvases in the scene"""
|
||||
|
||||
canvas = []
|
||||
for obj in bpy.context.scene.objects:
|
||||
if obj.booleans.canvas:
|
||||
canvas.append(obj)
|
||||
|
||||
return canvas
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /selected/ ------------------------------ ####
|
||||
|
||||
def list_selected_cutters(context):
|
||||
"""List selected cutters"""
|
||||
|
||||
cutters = []
|
||||
active_object = context.active_object
|
||||
selected_objects = context.selected_objects
|
||||
|
||||
if selected_objects:
|
||||
for obj in selected_objects:
|
||||
if obj != active_object and obj.type == 'MESH':
|
||||
if obj.booleans.cutter:
|
||||
cutters.append(obj)
|
||||
|
||||
if active_object:
|
||||
if active_object.booleans.cutter:
|
||||
cutters.append(active_object)
|
||||
|
||||
return cutters
|
||||
|
||||
|
||||
def list_selected_canvases(context):
|
||||
"""List selected canvases"""
|
||||
|
||||
canvases = []
|
||||
active_object = context.active_object
|
||||
selected_objects = context.selected_objects
|
||||
|
||||
if selected_objects:
|
||||
for obj in selected_objects:
|
||||
if obj != active_object and obj.type == 'MESH':
|
||||
if obj.booleans.canvas:
|
||||
canvases.append(obj)
|
||||
|
||||
if active_object:
|
||||
if active_object.booleans.canvas:
|
||||
canvases.append(active_object)
|
||||
|
||||
return canvases
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /users/ ------------------------------ ####
|
||||
|
||||
def list_canvas_cutters(canvases):
|
||||
"""List cutters that are used by specified canvases"""
|
||||
|
||||
cutters = []
|
||||
modifiers = []
|
||||
for canvas in canvases:
|
||||
for mod in canvas.modifiers:
|
||||
if mod.type == 'BOOLEAN' and "boolean_" in mod.name:
|
||||
if mod.object:
|
||||
cutters.append(mod.object)
|
||||
modifiers.append(mod)
|
||||
|
||||
return cutters, modifiers
|
||||
|
||||
|
||||
def list_canvas_slices(canvases):
|
||||
"""Returns list of slices for specified canvases"""
|
||||
|
||||
slices = []
|
||||
for obj in bpy.context.scene.objects:
|
||||
if obj.booleans.slice:
|
||||
if obj.booleans.slice_of in canvases:
|
||||
slices.append(obj)
|
||||
|
||||
return slices
|
||||
|
||||
|
||||
def list_cutter_users(cutters):
|
||||
"""List canvases that use specified cutters"""
|
||||
|
||||
cutter_users = []
|
||||
|
||||
for cutter in cutters:
|
||||
object = bpy.data.objects.get(cutter.name)
|
||||
|
||||
for key, values in bpy.data.user_map(subset=[object]).items():
|
||||
for value in values:
|
||||
# filter_only_object_type_users
|
||||
if value.id_type == 'OBJECT':
|
||||
for mod in value.modifiers:
|
||||
if mod.type == 'BOOLEAN':
|
||||
if mod.object and mod.object == cutter:
|
||||
cutter_users.append(value)
|
||||
|
||||
return cutter_users
|
||||
|
||||
|
||||
def list_cutter_modifiers(canvases, cutters):
|
||||
"""List modifiers on specified canvases that use specified cutters"""
|
||||
|
||||
if not canvases:
|
||||
canvases = list_canvases()
|
||||
|
||||
modifiers = []
|
||||
for canvas in canvases:
|
||||
for mod in canvas.modifiers:
|
||||
if mod.type == 'BOOLEAN':
|
||||
if mod.object in cutters:
|
||||
modifiers.append(mod)
|
||||
|
||||
return modifiers
|
||||
|
||||
|
||||
def list_unused_cutters(cutters, *canvases, do_leftovers=False):
|
||||
"""Takes in list of cutters and returns only those that have no other user besides specified canvas"""
|
||||
"""When `include_visible` is True it will return cutters that aren't used by any visible modifiers"""
|
||||
|
||||
other_canvases = list_canvases()
|
||||
original_cutters = cutters[:]
|
||||
|
||||
for obj in other_canvases:
|
||||
if obj in canvases:
|
||||
return
|
||||
|
||||
if any(mod.object in cutters for mod in obj.modifiers if mod.type == 'BOOLEAN'):
|
||||
cutters[:] = [cutter for cutter in cutters if cutter not in [mod.object for mod in obj.modifiers]]
|
||||
|
||||
leftovers = []
|
||||
# return_cutters_that_do_have_other_users_(so_that_parents_can_be_reassigned)
|
||||
if do_leftovers:
|
||||
leftovers = [cutter for cutter in original_cutters if cutter not in cutters]
|
||||
|
||||
return cutters, leftovers
|
||||
|
||||
|
||||
def list_pre_boolean_modifiers(obj) -> list:
|
||||
"""Returns a list of boolean modifiers & modifiers that come before last boolean modifier"""
|
||||
|
||||
# Find the index of a last boolean modifier
|
||||
last_boolean_index = -1
|
||||
for i in reversed(range(len(obj.modifiers))):
|
||||
if obj.modifiers[i].type == 'BOOLEAN':
|
||||
last_boolean_index = i
|
||||
break
|
||||
|
||||
# If boolean modifier is found, list all modifiers that come before it.
|
||||
if last_boolean_index != -1:
|
||||
return [mod for mod in obj.modifiers[:last_boolean_index + 1]]
|
||||
else:
|
||||
return []
|
||||
@@ -1,77 +0,0 @@
|
||||
import bpy
|
||||
import mathutils
|
||||
from mathutils import Vector
|
||||
from bpy_extras import view3d_utils
|
||||
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def distance_from_point_to_segment(point, start, end) -> float:
|
||||
"""
|
||||
Calculates the shortest distance between a point and a segment.
|
||||
All three inputs should be `mathutils.Vector` objects.
|
||||
This is an alternative to `mathutils.geometry.intersect_point_line`.
|
||||
Adapted from "Blockout" extension by niewinny (https://github.com/niewinny/blockout).
|
||||
"""
|
||||
|
||||
segment = end - start
|
||||
start_to_point = point - start
|
||||
|
||||
# projection_along_segment
|
||||
c1 = start_to_point.dot(segment)
|
||||
if c1 <= 0:
|
||||
return (point - start).length
|
||||
|
||||
# segment_length_squared
|
||||
c2 = segment.dot(segment)
|
||||
if c2 <= c1:
|
||||
return (point - end).length
|
||||
|
||||
t = c1 / c2
|
||||
closest_point = start + t * segment
|
||||
distance = (point - closest_point).length
|
||||
|
||||
return distance
|
||||
|
||||
|
||||
def region_2d_to_line_3d(region, rv3d, point_2d: Vector, line_origin: Vector, line_direction: Vector) -> tuple[Vector, Vector]:
|
||||
"""
|
||||
Converts a 2D screen-space point into a 3D ray and finds closest
|
||||
points between that ray and a given 3D line.
|
||||
"""
|
||||
|
||||
if line_origin is None or line_direction is None:
|
||||
return None, None
|
||||
|
||||
# Convert the screen-space 2D point Vector into a world-space 3D ray (origin + direction).
|
||||
ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, point_2d)
|
||||
ray_direction = view3d_utils.region_2d_to_vector_3d(region, rv3d, point_2d)
|
||||
|
||||
# Find closest points to each other on each line (second line being a ray).
|
||||
closest_points = mathutils.geometry.intersect_line_line(ray_origin,
|
||||
ray_origin + ray_direction,
|
||||
line_origin,
|
||||
line_origin + line_direction)
|
||||
|
||||
return closest_points
|
||||
|
||||
|
||||
def region_2d_to_plane_3d(region, rv3d, point_2d: Vector, plane: tuple[Vector]) -> Vector:
|
||||
"""
|
||||
Converts a 2D screen-space point into a 3D point on a plane in world-space.
|
||||
Adapted from "Blockout" extension by niewinny (https://github.com/niewinny/blockout).
|
||||
"""
|
||||
|
||||
location, normal = plane
|
||||
|
||||
# Convert the screen-space 2D point Vector into a world-space 3D ray (origin + direction).
|
||||
p3_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, point_2d)
|
||||
p3_direction = view3d_utils.region_2d_to_vector_3d(region, rv3d, point_2d)
|
||||
|
||||
# Intersect the point with the plane.
|
||||
p3_on_plane = mathutils.geometry.intersect_line_plane(p3_origin, # First point of line.
|
||||
p3_origin + p3_direction, # Second point of line.
|
||||
location, # `plane_co` (a point on the plane).
|
||||
normal) # `plane_no` (the direction the plane is facing).
|
||||
|
||||
return p3_on_plane
|
||||
@@ -1,17 +1,56 @@
|
||||
import bpy
|
||||
import bmesh
|
||||
import mathutils
|
||||
import math
|
||||
from bpy_extras import view3d_utils
|
||||
|
||||
from .object import hide_objects
|
||||
from .types import Ray
|
||||
from mathutils import Vector
|
||||
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
#### ------------------------------ /poll/ ------------------------------ ####
|
||||
|
||||
def extrude_face(bm, face):
|
||||
"""Extrudes cutter face (created by carve operation) along view vector to create a non-manifold mesh"""
|
||||
def is_instanced_mesh(data):
|
||||
"""
|
||||
Checks if `obj.data` has more than one users, i.e. is instanced.
|
||||
Function only considers object types as users, and excludes other pointers.
|
||||
"""
|
||||
|
||||
data = bpy.data.meshes.get(data.name)
|
||||
users = 0
|
||||
|
||||
for key, values in bpy.data.user_map(subset=[data]).items():
|
||||
for value in values:
|
||||
if value.id_type == 'OBJECT':
|
||||
users += 1
|
||||
|
||||
if users > 1:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def are_intersecting(obj_a, obj_b):
|
||||
"""Checks if bounding boxes of two given objects intersect."""
|
||||
|
||||
def world_bounds(obj):
|
||||
corners = [obj.matrix_world @ Vector(c) for c in obj.bound_box]
|
||||
xs = [c.x for c in corners]
|
||||
ys = [c.y for c in corners]
|
||||
zs = [c.z for c in corners]
|
||||
return (min(xs), max(xs)), (min(ys), max(ys)), (min(zs), max(zs))
|
||||
|
||||
(ax0, ax1), (ay0, ay1), (az0, az1) = world_bounds(obj_a)
|
||||
(bx0, bx1), (by0, by1), (bz0, bz1) = world_bounds(obj_b)
|
||||
|
||||
return (
|
||||
ax1 >= bx0 and ax0 <= bx1 and
|
||||
ay1 >= by0 and ay0 <= by1 and
|
||||
az1 >= bz0 and az0 <= bz1
|
||||
)
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /operate/ ------------------------------ ####
|
||||
|
||||
def extrude_face(bm, face) -> tuple[list[bmesh.types.BMVert], list[bmesh.types.BMEdge], list[bmesh.types.BMFace]]:
|
||||
"""Extrudes the `bmesh` face and returns tuple of lists of extruded vertices, edges and faces."""
|
||||
|
||||
bm.faces.ensure_lookup_table()
|
||||
|
||||
@@ -26,8 +65,8 @@ def extrude_face(bm, face):
|
||||
return extruded_verts, extruded_edges, extruded_faces
|
||||
|
||||
|
||||
def shade_smooth_by_angle(bm, mesh, angle=30):
|
||||
"""Replication of "Auto Smooth" functionality: Marks faces as smooth, sharp edges (by angle) as sharp"""
|
||||
def shade_smooth_by_angle(bm, mesh, angle: float=30):
|
||||
"""Replication of "Auto Smooth": Marks faces as smooth & edges above the angle as sharp."""
|
||||
|
||||
for f in bm.faces:
|
||||
f.smooth = True
|
||||
@@ -51,26 +90,6 @@ def shade_smooth_by_angle(bm, mesh, angle=30):
|
||||
bm.to_mesh(mesh)
|
||||
|
||||
|
||||
def are_intersecting(obj_a, obj_b):
|
||||
"""Checks if bounding boxes of two given objects intersect."""
|
||||
|
||||
def world_bounds(obj):
|
||||
corners = [obj.matrix_world @ mathutils.Vector(c) for c in obj.bound_box]
|
||||
xs = [c.x for c in corners]
|
||||
ys = [c.y for c in corners]
|
||||
zs = [c.z for c in corners]
|
||||
return (min(xs), max(xs)), (min(ys), max(ys)), (min(zs), max(zs))
|
||||
|
||||
(ax0, ax1), (ay0, ay1), (az0, az1) = world_bounds(obj_a)
|
||||
(bx0, bx1), (by0, by1), (bz0, bz1) = world_bounds(obj_b)
|
||||
|
||||
return (
|
||||
ax1 >= bx0 and ax0 <= bx1 and
|
||||
ay1 >= by0 and ay0 <= by1 and
|
||||
az1 >= bz0 and az0 <= bz1
|
||||
)
|
||||
|
||||
|
||||
def ensure_attribute(bm, name, domain):
|
||||
"""Ensure that the attribute with the given name and domain exists on mesh."""
|
||||
|
||||
@@ -85,21 +104,3 @@ def ensure_attribute(bm, name, domain):
|
||||
attr = bm.verts.layers.float.new(name)
|
||||
|
||||
return attr
|
||||
|
||||
|
||||
def raycast(context, position, objects):
|
||||
"""Cast a ray in the scene to get the surface on any of the given objects."""
|
||||
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
depsgraph = context.view_layer.depsgraph
|
||||
|
||||
origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, position)
|
||||
direction = view3d_utils.region_2d_to_vector_3d(region, rv3d, position)
|
||||
|
||||
# Cast Ray
|
||||
with hide_objects(context, exceptions=objects):
|
||||
hit, location, normal, index, object, matrix = context.scene.ray_cast(depsgraph, origin, direction)
|
||||
ray = Ray(hit, location, normal, index, object, matrix)
|
||||
|
||||
return ray
|
||||
|
||||
@@ -3,42 +3,102 @@ import bmesh
|
||||
from contextlib import contextmanager
|
||||
from .. import __package__ as base_package
|
||||
|
||||
from ..functions.list import (
|
||||
list_pre_boolean_modifiers,
|
||||
from .mesh import (
|
||||
is_instanced_mesh,
|
||||
)
|
||||
from .object import (
|
||||
convert_to_mesh,
|
||||
)
|
||||
from .poll import (
|
||||
is_instanced_data,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ /list/ ------------------------------ ####
|
||||
|
||||
def enumerate_boolean_modifiers(obj) -> int:
|
||||
"""Returns the number of Boolean modifiers on the object."""
|
||||
|
||||
num = 0
|
||||
for mod in obj.modifiers:
|
||||
if is_boolean_modifier(mod):
|
||||
num += 1
|
||||
|
||||
return num
|
||||
|
||||
|
||||
def get_modifiers_to_apply(context, obj, custom_list=None) -> list:
|
||||
"""Returns the list of modifiers that need to be applied based on add-on preferences."""
|
||||
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
|
||||
# Apply all modifiers.
|
||||
if prefs.apply_order == 'ALL':
|
||||
modifiers = list(obj.modifiers)
|
||||
|
||||
# Apply only Boolean modifiers.
|
||||
elif prefs.apply_order == 'BOOLEANS':
|
||||
if custom_list is None:
|
||||
modifiers = [mod for mod in obj.modifiers if is_boolean_modifier(mod)]
|
||||
else:
|
||||
modifiers = custom_list
|
||||
|
||||
# Apply all modifiers that come before last Boolean modifier.
|
||||
elif prefs.apply_order == 'BEFORE':
|
||||
# Find the index of a last Boolean modifier.
|
||||
last_boolean_index = -1
|
||||
for i in reversed(range(len(obj.modifiers))):
|
||||
if obj.modifiers[i].type == 'BOOLEAN':
|
||||
last_boolean_index = i
|
||||
break
|
||||
|
||||
# If a Boolean modifier is found, list all modifiers that come before it.
|
||||
if last_boolean_index != -1:
|
||||
modifiers = [mod for mod in obj.modifiers[:last_boolean_index + 1]]
|
||||
else:
|
||||
modifiers = []
|
||||
|
||||
return modifiers
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /poll/ ------------------------------ ####
|
||||
|
||||
def is_boolean_modifier(mod, check_cutter=True) -> bool:
|
||||
"""Checks if a modifier is a Boolean modifier (and optionally if it has a valid cutter)."""
|
||||
|
||||
if mod is None:
|
||||
return False
|
||||
if mod.type != 'BOOLEAN':
|
||||
return False
|
||||
if check_cutter and mod.object is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def add_boolean_modifier(self, context, obj, cutter, mode, solver, pin=False, redo=True):
|
||||
"Adds boolean modifier with specified cutter and properties to a single object"
|
||||
"""Adds the Boolean modifier with specified cutter and properties to a given object."""
|
||||
|
||||
if bpy.app.version < (5, 0, 0) and solver == 'FLOAT':
|
||||
solver = 'FAST'
|
||||
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
name = "boolean_" + cutter.name.replace("boolean_", "")
|
||||
|
||||
modifier = obj.modifiers.new("boolean_" + cutter.name.replace("boolean_", ""), 'BOOLEAN')
|
||||
modifier = obj.modifiers.new(name, 'BOOLEAN')
|
||||
modifier.operation = mode
|
||||
modifier.object = cutter
|
||||
modifier.solver = solver
|
||||
modifier.show_in_editmode = prefs.show_in_editmode
|
||||
|
||||
# Set solver options (inherited from operator properties).
|
||||
# Set solver options (inherited from operator properties, i.e. `self`).
|
||||
if redo:
|
||||
modifier.material_mode = self.material_mode
|
||||
modifier.use_self = self.use_self
|
||||
modifier.use_hole_tolerant = self.use_hole_tolerant
|
||||
modifier.double_threshold = self.double_threshold
|
||||
|
||||
if prefs.show_in_editmode:
|
||||
modifier.show_in_editmode = True
|
||||
|
||||
# Move modifier to the index 0 (make it first in the stack).
|
||||
if pin:
|
||||
index = obj.modifiers.find(modifier.name)
|
||||
@@ -50,19 +110,20 @@ def add_boolean_modifier(self, context, obj, cutter, mode, solver, pin=False, re
|
||||
def apply_modifiers(context, obj, modifiers: list, force_clean=False):
|
||||
"""
|
||||
Apply modifiers on object.
|
||||
Instead of using `bpy.ops.object.modifier_apply`, this function uses
|
||||
`bpy.data.meshes.new_from_object` built-in function to create a temporary
|
||||
mesh from the evaluated object (basically with visible modifiers applied).
|
||||
Temporary mesh is then transferred to objects mesh with `bmesh`.
|
||||
Instead of using `bpy.ops.object.modifier_apply`, by default this function uses
|
||||
`to_mesh` built-in function to create a temporary mesh from the evaluated object
|
||||
(basically with visible modifiers applied). Temporary mesh is then transferred
|
||||
to objects mesh using `bmesh`.
|
||||
|
||||
This method is up to 2x faster, although it's considered experimental
|
||||
and may fail in some cases, so a fallback to `bpy.ops.object.modifier_apply` is kept.
|
||||
"""
|
||||
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
_stored_active_obj = context.active_object
|
||||
|
||||
# Make object data unique if it's instanced.
|
||||
if is_instanced_data(obj):
|
||||
if is_instanced_mesh(obj.data):
|
||||
context.active_object.data = context.active_object.data.copy()
|
||||
|
||||
try:
|
||||
@@ -71,10 +132,13 @@ def apply_modifiers(context, obj, modifiers: list, force_clean=False):
|
||||
if not force_clean:
|
||||
raise Exception()
|
||||
|
||||
context.view_layer.objects.active = obj
|
||||
with hide_modifiers(obj, excluding=modifiers):
|
||||
# Create a temporary mesh from evaluated object.
|
||||
evaluated_obj = obj.evaluated_get(context.evaluated_depsgraph_get())
|
||||
temp_data = bpy.data.meshes.new_from_object(evaluated_obj)
|
||||
depsgraph = context.evaluated_depsgraph_get()
|
||||
evaluated_obj = obj.evaluated_get(depsgraph)
|
||||
temp_data = evaluated_obj.to_mesh(preserve_all_data_layers=True,
|
||||
depsgraph=depsgraph)
|
||||
|
||||
# Create `bmesh` from temporary mesh and update edit mesh.
|
||||
if context.mode == 'EDIT_MESH':
|
||||
@@ -86,11 +150,11 @@ def apply_modifiers(context, obj, modifiers: list, force_clean=False):
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(temp_data)
|
||||
bm.to_mesh(obj.data)
|
||||
|
||||
bm.free()
|
||||
evaluated_obj.to_mesh_clear()
|
||||
|
||||
# Remove modifiers and purge temporary mesh.
|
||||
bpy.data.meshes.remove(temp_data)
|
||||
# Remove modifiers.
|
||||
for mod in modifiers:
|
||||
obj.modifiers.remove(mod)
|
||||
|
||||
@@ -99,7 +163,6 @@ def apply_modifiers(context, obj, modifiers: list, force_clean=False):
|
||||
if obj.data.shape_keys:
|
||||
obj.shape_key_clear()
|
||||
|
||||
# Use `bpy.ops` operator to apply modifiers if above fails.
|
||||
except Exception as e:
|
||||
# print("Error applying modifiers with `bmesh` method:", e, "falling back to `bpy.ops` method")
|
||||
|
||||
@@ -118,10 +181,12 @@ def apply_modifiers(context, obj, modifiers: list, force_clean=False):
|
||||
for mod in modifiers:
|
||||
bpy.ops.object.modifier_apply(modifier=mod.name)
|
||||
|
||||
context.view_layer.objects.active = _stored_active_obj
|
||||
|
||||
|
||||
@contextmanager
|
||||
def hide_modifiers(obj, excluding: list):
|
||||
"""Hides all modifiers of a given object in viewport except those in excluding list"""
|
||||
"""Hides all modifiers of a given object in the viewport except those in `excluding` list."""
|
||||
|
||||
visible_modifiers = []
|
||||
for mod in obj.modifiers:
|
||||
@@ -139,10 +204,10 @@ def hide_modifiers(obj, excluding: list):
|
||||
|
||||
|
||||
def add_modifier_asset(obj, path: str, asset: str):
|
||||
"""Loads the node group asset and adds a Geometry Nodes modifier using it."""
|
||||
"""Loads in the node group asset and adds a Geometry Nodes modifier using it."""
|
||||
|
||||
try:
|
||||
# Load the node group.
|
||||
# Load in the node group.
|
||||
if bpy.app.version >= (5, 0, 0):
|
||||
with bpy.data.libraries.load(path, link=True, pack=True) as (data_from, data_to):
|
||||
if asset in data_from.node_groups:
|
||||
@@ -155,7 +220,7 @@ def add_modifier_asset(obj, path: str, asset: str):
|
||||
|
||||
node_group = bpy.data.node_groups[asset]
|
||||
|
||||
# Add modifier on the object.
|
||||
# Add modifier to the object.
|
||||
mod = obj.modifiers.new(asset, type='NODES')
|
||||
mod.node_group = node_group
|
||||
mod.show_group_selector = False
|
||||
@@ -168,16 +233,19 @@ def add_modifier_asset(obj, path: str, asset: str):
|
||||
return None
|
||||
|
||||
|
||||
def get_modifiers_to_apply(context, obj, new_modifiers) -> list:
|
||||
"""Returns the list of modifiers that need to be applied based on add-on preferences."""
|
||||
def update_modifier_input(modifier, socket: str, value):
|
||||
"""Change the value of the geometry nodes modifier input socket."""
|
||||
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
|
||||
if prefs.apply_order == 'ALL':
|
||||
modifiers = [mod for mod in obj.modifiers]
|
||||
elif prefs.apply_order == 'BOOLEANS':
|
||||
modifiers = new_modifiers
|
||||
elif prefs.apply_order == 'BEFORE':
|
||||
modifiers = list_pre_boolean_modifiers(obj)
|
||||
|
||||
return modifiers
|
||||
try:
|
||||
if bpy.app.version >= (5, 2, 0):
|
||||
socket = getattr(modifier.properties.inputs, socket)
|
||||
socket.value = value
|
||||
else:
|
||||
modifier[f"{socket}"] = value
|
||||
except:
|
||||
"""
|
||||
NOTE: There are plethora of reasons why this can fail, node trees are finicky.
|
||||
Accounting for all possible cases is borderline impossible, so this check is necessary
|
||||
to at least fail silently and not throw Python error to users.
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -1,152 +1,129 @@
|
||||
import bpy
|
||||
import bmesh
|
||||
import mathutils
|
||||
from mathutils import Vector, Matrix
|
||||
from contextlib import contextmanager
|
||||
from .. import __package__ as base_package
|
||||
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
#### ------------------------------ /poll/ ------------------------------ ####
|
||||
|
||||
def set_cutter_properties(context, cutter, mode, display='BOUNDS', collection=True):
|
||||
"""Ensures cutter is properly set: has right properties, is hidden, in a collection & parented"""
|
||||
def is_linked(context, obj) -> bool:
|
||||
"""Checks whether the object is linked from an external .blend file (including library-overrides)."""
|
||||
|
||||
# Hide Cutters
|
||||
cutter.hide_render = True
|
||||
cutter.display_type = display
|
||||
cutter.lineart.usage = 'EXCLUDE'
|
||||
object_visibility_set(cutter, value=False)
|
||||
if obj not in context.editable_objects:
|
||||
if obj.library:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
if obj.override_library:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
# Cutters Collection
|
||||
if collection:
|
||||
cutters_collection = ensure_collection(context)
|
||||
if cutters_collection not in cutter.users_collection:
|
||||
cutters_collection.objects.link(cutter)
|
||||
|
||||
# add_boolean_property
|
||||
cutter.booleans.cutter = mode.capitalize()
|
||||
def has_evaluated_mesh(context, obj):
|
||||
"""Checks if an evaluated object has mesh (created by Geometry Nodes modifiers)."""
|
||||
|
||||
# Exclude cases that return Python errors.
|
||||
if not obj:
|
||||
return False
|
||||
if bpy.app.version < (5, 2, 0) and obj.type == 'EMPTY':
|
||||
return False
|
||||
if obj.instance_type != 'NONE':
|
||||
return False
|
||||
|
||||
depsgraph = context.view_layer.depsgraph
|
||||
obj_eval = depsgraph.id_eval_get(obj)
|
||||
|
||||
geometry = None
|
||||
try:
|
||||
geometry = obj_eval.evaluated_geometry()
|
||||
except:
|
||||
pass
|
||||
|
||||
if not geometry or not geometry.mesh:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /operate/ ------------------------------ ####
|
||||
|
||||
def object_visibility_set(obj, value=False):
|
||||
"Sets object visibility properties to either True or False"
|
||||
"""Sets object visibility properties to either True or False."""
|
||||
|
||||
obj.visible_camera = value
|
||||
obj.visible_shadow = value
|
||||
obj.visible_diffuse = value
|
||||
obj.visible_glossy = value
|
||||
obj.visible_shadow = value
|
||||
obj.visible_transmission = value
|
||||
obj.visible_volume_scatter = value
|
||||
if bpy.app.version >= (5, 2, 0):
|
||||
obj.visible_raycast = value
|
||||
|
||||
obj.hide_probe_volume = not value
|
||||
obj.hide_probe_sphere = not value
|
||||
obj.hide_probe_plane = not value
|
||||
|
||||
|
||||
def convert_to_mesh(context, obj):
|
||||
"Converts active object into mesh (applying all modifiers and shape keys in process)"
|
||||
"""Converts active object into mesh (applying all modifiers and shape keys in the process)."""
|
||||
|
||||
# store_selection
|
||||
original_mode = obj.mode
|
||||
|
||||
if original_mode != 'OBJECT':
|
||||
edit_objects = context.objects_in_mode
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
# Store selection.
|
||||
stored_active = context.active_object
|
||||
stored_selection = context.selected_objects
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
for ob in context.scene.objects:
|
||||
ob.select_set(False)
|
||||
|
||||
# Convert
|
||||
# Make `obj` active and only one selected.
|
||||
obj.select_set(True)
|
||||
context.view_layer.objects.active = obj
|
||||
|
||||
# Convert.
|
||||
bpy.ops.object.convert(target='MESH')
|
||||
|
||||
# restore_selection
|
||||
for obj in stored_selection:
|
||||
obj.select_set(True)
|
||||
if original_mode != 'OBJECT':
|
||||
for ob in edit_objects:
|
||||
ob.select_set(True)
|
||||
bpy.ops.object.mode_set(mode=original_mode)
|
||||
|
||||
# Restore selection.
|
||||
for ob in stored_selection:
|
||||
ob.select_set(True)
|
||||
context.view_layer.objects.active = stored_active
|
||||
|
||||
|
||||
def ensure_collection(context):
|
||||
"""Checks the existance of boolean cutters collection and creates it if it doesn't exist"""
|
||||
def change_parent(context, obj, parent, inverse=False):
|
||||
"""Changes or removes parent from an object while keeping the transformation."""
|
||||
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
context.evaluated_depsgraph_get().update()
|
||||
|
||||
collection_name = prefs.collection_name
|
||||
cutters_collection = bpy.data.collections.get(collection_name)
|
||||
|
||||
if cutters_collection is None:
|
||||
cutters_collection = bpy.data.collections.new(collection_name)
|
||||
context.scene.collection.children.link(cutters_collection)
|
||||
cutters_collection.hide_render = True
|
||||
cutters_collection.color_tag = 'COLOR_01'
|
||||
# cutters_collection.hide_viewport = True
|
||||
# context.view_layer.layer_collection.children[collection_name].exclude = True
|
||||
|
||||
return cutters_collection
|
||||
|
||||
|
||||
def delete_empty_collection():
|
||||
"""Removes boolean cutters collection if it has no more objects in it"""
|
||||
|
||||
prefs = bpy.context.preferences.addons[base_package].preferences
|
||||
|
||||
collection = bpy.data.collections.get(prefs.collection_name)
|
||||
if collection and not collection.objects:
|
||||
bpy.data.collections.remove(collection)
|
||||
|
||||
|
||||
def delete_cutter(cutter):
|
||||
"""Deletes cutter object and purges it's mesh data"""
|
||||
|
||||
orphaned_mesh = cutter.data
|
||||
bpy.data.objects.remove(cutter)
|
||||
if orphaned_mesh.users == 0:
|
||||
bpy.data.meshes.remove(orphaned_mesh)
|
||||
|
||||
|
||||
def change_parent(obj, parent, force=False, inverse=False):
|
||||
"""Changes or removes parent from cutter object while keeping the transformation"""
|
||||
|
||||
if obj.parent is not None:
|
||||
if not force:
|
||||
return
|
||||
|
||||
matrix_copy = obj.matrix_world.copy()
|
||||
obj.parent = parent
|
||||
if inverse:
|
||||
if inverse and parent is not None:
|
||||
obj.matrix_parent_inverse = parent.matrix_world.inverted()
|
||||
obj.matrix_world = matrix_copy
|
||||
|
||||
|
||||
def create_slice(context, canvas, modifier=False):
|
||||
"""Creates copy of canvas to be used as slice"""
|
||||
|
||||
slice = canvas.copy()
|
||||
slice.data = canvas.data.copy()
|
||||
slice.name = slice.data.name = canvas.name + "_slice"
|
||||
change_parent(slice, canvas)
|
||||
|
||||
# Set Boolean Properties
|
||||
if modifier == True:
|
||||
slice.booleans.canvas = True
|
||||
slice.booleans.slice = True
|
||||
slice.booleans.slice_of = canvas
|
||||
|
||||
# Add to Canvas Collections
|
||||
for coll in canvas.users_collection:
|
||||
coll.objects.link(slice)
|
||||
|
||||
# add_slices_to_local_view
|
||||
if context.space_data.local_view:
|
||||
slice.local_view_set(context.space_data, True)
|
||||
|
||||
return slice
|
||||
|
||||
|
||||
def set_object_origin(obj, bm, point='CENTER', custom=None):
|
||||
"""Sets object origin to given position by shifting vertices"""
|
||||
def set_object_origin(obj, bm, point='CENTER', custom: Vector=None):
|
||||
"""Sets the origin of a mesh type object to given position by shifting vertices."""
|
||||
|
||||
# Center of the bounding box.
|
||||
if point == 'CENTER_OBJ':
|
||||
position_local = 0.125 * sum((mathutils.Vector(b) for b in obj.bound_box), mathutils.Vector())
|
||||
position_local = 0.125 * sum((Vector(b) for b in obj.bound_box), Vector())
|
||||
position_world = obj.matrix_world @ position_local
|
||||
|
||||
# Center of the geometry.
|
||||
elif point == 'CENTER_MESH':
|
||||
if len(bm.verts) > 0:
|
||||
position_local = sum((v.co for v in bm.verts), mathutils.Vector()) / len(bm.verts)
|
||||
position_local = sum((v.co for v in bm.verts), Vector()) / len(bm.verts)
|
||||
else:
|
||||
position_local = mathutils.Vector((0, 0, 0))
|
||||
position_local = Vector((0, 0, 0))
|
||||
position_world = obj.matrix_world @ position_local
|
||||
|
||||
# Custom origin point (should be local Vector).
|
||||
@@ -154,8 +131,8 @@ def set_object_origin(obj, bm, point='CENTER', custom=None):
|
||||
position_local = custom
|
||||
position_world = obj.matrix_world @ custom
|
||||
|
||||
mat = mathutils.Matrix.Translation(position_local)
|
||||
bmesh.ops.transform(bm, matrix=mat.inverted(), verts=bm.verts)
|
||||
matrix = Matrix.Translation(position_local)
|
||||
bmesh.ops.transform(bm, matrix=matrix.inverted(), verts=bm.verts)
|
||||
bm.to_mesh(obj.data)
|
||||
|
||||
obj.location = position_world
|
||||
@@ -179,3 +156,13 @@ def hide_objects(context, exceptions: list):
|
||||
finally:
|
||||
for obj in hidden_objects:
|
||||
obj.hide_set(False)
|
||||
|
||||
|
||||
def delete_object(cutter, purge_data=True):
|
||||
"""Deletes the object and optionally purges its data if it has no more users."""
|
||||
|
||||
orphaned_data = cutter.data
|
||||
bpy.data.objects.remove(cutter)
|
||||
|
||||
if purge_data and orphaned_data.users == 0:
|
||||
bpy.data.meshes.remove(orphaned_data)
|
||||
|
||||
@@ -1,153 +1,44 @@
|
||||
import bpy
|
||||
|
||||
from .list import (
|
||||
list_canvas_cutters,
|
||||
list_cutter_users,
|
||||
from ..constants import (
|
||||
CONVERTABLE_TYPES,
|
||||
)
|
||||
from .mesh import (
|
||||
is_instanced_mesh,
|
||||
)
|
||||
from .object import (
|
||||
convert_to_mesh,
|
||||
has_evaluated_mesh,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def basic_poll(cls, context, check_linked=False):
|
||||
"""Basic poll for boolean operators."""
|
||||
def basic_poll(cls, context, check_active=True):
|
||||
"""Basic poll for Boolean operators."""
|
||||
|
||||
if context.mode != 'OBJECT':
|
||||
cls.poll_message_set("Boolean operators can only be performed in Object Mode")
|
||||
return False
|
||||
|
||||
if check_active:
|
||||
if context.active_object is None:
|
||||
return False
|
||||
|
||||
obj = context.active_object
|
||||
if obj.type != 'MESH':
|
||||
cls.poll_message_set("Boolean operators can only be used for mesh objects")
|
||||
return False
|
||||
|
||||
if check_linked and is_linked(context, obj) == True:
|
||||
cls.poll_message_set("Boolean operators can not be executed on linked objects")
|
||||
cls.poll_message_set("No active object")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def is_linked(context, obj):
|
||||
"""Checks whether the object is linked from an external .blend file (including library-overrides)."""
|
||||
|
||||
if obj not in context.editable_objects:
|
||||
if obj.library:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
if obj.override_library:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def is_canvas(obj):
|
||||
"""Checks whether the object is a boolean canvas (i.e. has boolean cutters)."""
|
||||
|
||||
if obj.booleans.canvas == False:
|
||||
return False
|
||||
else:
|
||||
# Even if object is marked as canvas, check if it actually has any cutters
|
||||
cutters, __ = list_canvas_cutters([obj])
|
||||
if len(cutters) > 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def is_instanced_data(obj):
|
||||
"""Checks if `obj.data` has more than one users, i.e. is instanced."""
|
||||
"""Function only considers object types as users, and excludes pointers."""
|
||||
|
||||
data = bpy.data.meshes.get(obj.data.name)
|
||||
users = 0
|
||||
|
||||
for key, values in bpy.data.user_map(subset=[data]).items():
|
||||
for value in values:
|
||||
if value.id_type == 'OBJECT':
|
||||
users += 1
|
||||
|
||||
if users > 1:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def active_modifier_poll(obj):
|
||||
"""Checks whether the active modifier for active object is a boolean."""
|
||||
|
||||
# Check if active modifier exists.
|
||||
if len(obj.modifiers) == 0:
|
||||
return False
|
||||
if obj.modifiers.active is None:
|
||||
return False
|
||||
|
||||
# Check if active modifier is a boolean with a valid object.
|
||||
modifier = obj.modifiers.active
|
||||
if modifier.type != "BOOLEAN":
|
||||
return False
|
||||
if modifier.object is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def has_evaluated_mesh(context, obj):
|
||||
"""Checks if an object (non-mesh type) has an evaluated mesh created by Geometry Nodes modifiers."""
|
||||
|
||||
depsgraph = context.view_layer.depsgraph
|
||||
obj_eval = depsgraph.id_eval_get(obj)
|
||||
geometry = obj_eval.evaluated_geometry()
|
||||
|
||||
if geometry.mesh:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def list_candidate_objects(self, context, canvas):
|
||||
"""Filter out objects from the selection that can't be used as a cutter."""
|
||||
|
||||
cutters = []
|
||||
for obj in context.selected_objects:
|
||||
if obj == context.active_object:
|
||||
continue
|
||||
if is_linked(context, obj):
|
||||
self.report({'WARNING'}, f"{obj.name} is linked and can not be used as a cutter")
|
||||
continue
|
||||
|
||||
if obj.type == 'MESH':
|
||||
# Exclude if object is already a cutter for canvas.
|
||||
if canvas in list_cutter_users([obj]):
|
||||
continue
|
||||
# Exclude if canvas is cutting the object (avoid dependancy loop).
|
||||
if obj in list_cutter_users([canvas]):
|
||||
self.report({'WARNING'}, f"{obj.name} can not cut its own cutter (dependancy loop)")
|
||||
continue
|
||||
|
||||
cutters.append(obj)
|
||||
|
||||
elif obj.type in ('CURVE', 'FONT'):
|
||||
if has_evaluated_mesh(context, obj):
|
||||
convert_to_mesh(context, obj)
|
||||
cutters.append(obj)
|
||||
|
||||
return cutters
|
||||
|
||||
|
||||
def destructive_op_confirmation(self, context, event, canvases: list, title="Boolean Operation"):
|
||||
def destructive_op_confirmation(cls, context, event, canvases: list, title="Boolean Operation"):
|
||||
"""
|
||||
Creates & returns the confirmation pop-up window for destructive boolean operators.\n
|
||||
Confirmation window is triggered by canvas objects that have instanced object data or shape keys.\n
|
||||
If none of the canvas objects have them the operator is executed without any confirmation.
|
||||
Creates & returns the confirmation pop-up window for destructive Boolean operators.\n
|
||||
Confirmation window is triggered by canvases that have instanced data or shape keys.\n
|
||||
If none of the canvases have them the operator is executed without any confirmation.
|
||||
"""
|
||||
|
||||
has_instanced_data = any(obj for obj in canvases if is_instanced_data(obj))
|
||||
if len(canvases) == 0:
|
||||
return cls.execute(context)
|
||||
|
||||
has_instanced_data = any(obj for obj in canvases if is_instanced_mesh(obj.data))
|
||||
has_shape_keys = any(obj for obj in canvases if obj.data.shape_keys)
|
||||
|
||||
if has_instanced_data or has_shape_keys:
|
||||
@@ -166,15 +57,74 @@ def destructive_op_confirmation(self, context, event, canvases: list, title="Boo
|
||||
# Combined message.
|
||||
if has_instanced_data and has_shape_keys:
|
||||
message = ("Object(s) you're trying to cut have shape keys and instanced object data.\n"
|
||||
"In order to apply modifiers shape keys need to be applied, and object data made single user.\n"
|
||||
"In order to apply modifiers shape keys need to be applied & object data made single user.\n"
|
||||
"Do you proceed?")
|
||||
|
||||
popup = context.window_manager.invoke_confirm(self, event, title=title,
|
||||
popup = context.window_manager.invoke_confirm(cls, event, title=title,
|
||||
confirm_text="Yes", icon='WARNING',
|
||||
message=message)
|
||||
|
||||
cls._unflippable = True
|
||||
return popup
|
||||
|
||||
# Execute without confirmation window.
|
||||
else:
|
||||
return self.execute(context)
|
||||
return cls.execute(context)
|
||||
|
||||
|
||||
def convert_to_mesh_confirmation(cls, context, event, cutters: list, title="Boolean Operation"):
|
||||
"""
|
||||
Creates & returns the confirmation pop-up window when the object is
|
||||
about to be converted to mesh to be used as a cutter.
|
||||
|
||||
NOTE (1): Only triggers during brush Boolean operators,
|
||||
because object gets destroyed in the destructive one anyway.
|
||||
|
||||
NOTE (2): This is only required because of the limitation of legacy Boolean modifier.
|
||||
Geometry nodes implementation works with any object type. When the add-on is
|
||||
updated to work with custom modifiers this will not be necesary anymore.
|
||||
"""
|
||||
|
||||
if len(cutters) == 0:
|
||||
return cls.execute(context)
|
||||
|
||||
is_convertable = any(
|
||||
obj.type in CONVERTABLE_TYPES and has_evaluated_mesh(context, obj)
|
||||
for obj in cutters
|
||||
)
|
||||
|
||||
if is_convertable:
|
||||
message = ("Some of the selected objects are not of the Mesh type, but output mesh.\n"
|
||||
"In order to use them as cutters, they need to be converted to mesh.\n"
|
||||
"This is a destructive operator. Do you proceed?")
|
||||
|
||||
popup = context.window_manager.invoke_confirm(cls, event, title=title,
|
||||
confirm_text="Yes", icon='WARNING',
|
||||
message=message)
|
||||
|
||||
cls._unflippable = True
|
||||
return popup
|
||||
|
||||
# Execute without confirmation window.
|
||||
else:
|
||||
return cls.execute(context)
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /operator_helpers/ ------------------------------ ####
|
||||
|
||||
def _guess_toggle_state(modifiers):
|
||||
"""Guess whether cutters should be hidden or revealed."""
|
||||
|
||||
enabled = 0
|
||||
disabled = 0
|
||||
for mod in modifiers:
|
||||
if mod.show_viewport:
|
||||
enabled += 1
|
||||
else:
|
||||
disabled += 1
|
||||
|
||||
if enabled > disabled:
|
||||
return "On"
|
||||
else:
|
||||
return "Off"
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import bpy
|
||||
from .. import __package__ as base_package
|
||||
|
||||
from .object import (
|
||||
hide_objects,
|
||||
)
|
||||
from .types import (
|
||||
Ray,
|
||||
)
|
||||
from .view import (
|
||||
region_2d_to_ray_3d,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def ensure_collection(context) -> bpy.types.Collection:
|
||||
"""Returns the Boolean cutters collection and creates it if it doesn't exist."""
|
||||
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
collection_name = prefs.collection_name
|
||||
|
||||
coll = bpy.data.collections.get(collection_name)
|
||||
|
||||
# Create the collection if it doesn't exist.
|
||||
if coll is None:
|
||||
coll = bpy.data.collections.new(collection_name)
|
||||
coll.hide_render = True
|
||||
coll.color_tag = 'COLOR_01'
|
||||
context.scene.collection.children.link(coll)
|
||||
|
||||
return coll
|
||||
|
||||
|
||||
def delete_empty_collection(context):
|
||||
"""Removes Boolean cutters collection if it has no more objects in it."""
|
||||
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
|
||||
if not prefs.use_collection:
|
||||
return
|
||||
|
||||
collection_name = prefs.collection_name
|
||||
collection = bpy.data.collections.get(collection_name)
|
||||
|
||||
if not collection:
|
||||
return
|
||||
if collection.objects:
|
||||
return
|
||||
|
||||
bpy.data.collections.remove(collection)
|
||||
|
||||
|
||||
def raycast(context, position, objects):
|
||||
"""Cast a ray in the scene to get the surface on any of the given objects."""
|
||||
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
depsgraph = context.view_layer.depsgraph
|
||||
|
||||
origin, direction = region_2d_to_ray_3d(region, rv3d, position)
|
||||
|
||||
# Cast Ray
|
||||
with hide_objects(context, exceptions=objects):
|
||||
hit, location, normal, index, object, matrix = context.scene.ray_cast(depsgraph, origin, direction)
|
||||
ray = Ray(hit, location, normal, index, object, matrix)
|
||||
|
||||
return ray
|
||||
@@ -1,5 +1,4 @@
|
||||
import bpy
|
||||
import mathutils
|
||||
from mathutils import Vector, Matrix
|
||||
|
||||
|
||||
@@ -16,8 +15,8 @@ class Ray:
|
||||
obj,
|
||||
matrix: Matrix):
|
||||
self.hit = hit
|
||||
self.location = location if location is not None else mathutils.Vector()
|
||||
self.normal = normal if normal is not None else mathutils.Vector()
|
||||
self.location = location
|
||||
self.normal = normal
|
||||
self.index = index
|
||||
self.obj = obj
|
||||
self.matrix = matrix if matrix is not None else mathutils.Matrix()
|
||||
self.matrix = matrix
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import bpy
|
||||
import mathutils
|
||||
from mathutils import Vector
|
||||
from bpy_extras import view3d_utils
|
||||
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def redraw_regions(context):
|
||||
"""Redraw regions to find the limits of the 3D viewport."""
|
||||
|
||||
for area in context.window.screen.areas:
|
||||
if area.type != 'VIEW_3D':
|
||||
continue
|
||||
for region in area.regions:
|
||||
if region.type in {'WINDOW', 'UI'}:
|
||||
region.tag_redraw()
|
||||
|
||||
|
||||
def distance_from_point_to_segment(point: Vector, line_p1: Vector, line_p2: Vector) -> float:
|
||||
"""
|
||||
Calculates the shortest distance between the point and the finite segment.
|
||||
This is an alternative to `mathutils.geometry.intersect_point_line` (w/ clamping).
|
||||
Adapted from "Blockout" extension by niewinny (https://github.com/niewinny/blockout).
|
||||
"""
|
||||
|
||||
segment = line_p2 - line_p1
|
||||
start_to_point = point - line_p1
|
||||
|
||||
# Projection along segment.
|
||||
c1 = start_to_point.dot(segment)
|
||||
if c1 <= 0:
|
||||
return (point - line_p1).length
|
||||
|
||||
# Segment length squared.
|
||||
c2 = segment.dot(segment)
|
||||
if c2 <= c1:
|
||||
return (point - line_p2).length
|
||||
|
||||
t = c1 / c2
|
||||
closest_point = line_p1 + t * segment
|
||||
distance = (point - closest_point).length
|
||||
|
||||
return distance
|
||||
|
||||
|
||||
def region_2d_to_ray_3d(region, rv3d, point_2d: Vector) -> tuple[Vector, Vector]:
|
||||
"""
|
||||
Converts a 2D screen-space point into a 3D ray in the world-space.
|
||||
Returns a tuple of `ray_origin` and `ray_direction` Vectors.
|
||||
"""
|
||||
|
||||
origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, point_2d)
|
||||
direction = view3d_utils.region_2d_to_vector_3d(region, rv3d, point_2d)
|
||||
|
||||
return origin, direction
|
||||
|
||||
|
||||
def region_2d_to_plane_3d(region, rv3d, point_2d: Vector, plane: tuple[Vector]) -> Vector:
|
||||
"""
|
||||
Converts a 2D screen-space point into a 3D point on a plane in world-space.
|
||||
Adapted from "Blockout" extension by niewinny (https://github.com/niewinny/blockout).
|
||||
"""
|
||||
|
||||
location, normal = plane
|
||||
p3_origin, p3_direction = region_2d_to_ray_3d(region, rv3d, point_2d)
|
||||
|
||||
# Intersect the point with the plane.
|
||||
p3_on_plane = mathutils.geometry.intersect_line_plane(
|
||||
p3_origin, # First point of line.
|
||||
p3_origin + p3_direction, # Second point of line.
|
||||
location, # `plane_co` (a point on the plane).
|
||||
normal) # `plane_no` (the direction the plane is facing).
|
||||
|
||||
return p3_on_plane
|
||||
@@ -1,24 +0,0 @@
|
||||
import bpy
|
||||
import os
|
||||
import bpy.utils.previews
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
svg_icons = {}
|
||||
icons = bpy.utils.previews.new()
|
||||
dir = os.path.join(os.path.dirname(__file__))
|
||||
|
||||
icons.load("MEASURE", os.path.join(dir, "measure.svg"), 'IMAGE')
|
||||
icons.load("CPU", os.path.join(dir, "cpu.svg"), 'IMAGE')
|
||||
svg_icons["main"] = icons
|
||||
|
||||
|
||||
def register():
|
||||
...
|
||||
|
||||
def unregister():
|
||||
# ICONS
|
||||
for pcoll in svg_icons.values():
|
||||
bpy.utils.previews.remove(pcoll)
|
||||
svg_icons.clear()
|
||||
@@ -4,31 +4,34 @@ import bpy
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def bool_tool_manual_map():
|
||||
url_manual_prefix = "https://github.com/nickberckley/bool_tool/wiki/"
|
||||
url_manual_prefix = "https://nickberckley.github.io/bool_tool/"
|
||||
|
||||
# Carver
|
||||
url_manual_mapping = (("bpy.ops.object.carve", "Carver"),
|
||||
# Brush Boolean
|
||||
("bpy.ops.object.boolean_brush_union", "Boolean-Operators"),
|
||||
("bpy.ops.object.boolean_brush_intersect", "Boolean-Operators"),
|
||||
("bpy.ops.object.boolean_brush_difference", "Boolean-Operators"),
|
||||
("bpy.ops.object.boolean_brush_slice", "Boolean-Operators"),
|
||||
url_manual_mapping = (# Brush Boolean
|
||||
("bpy.ops.object.boolean_brush_union", "booleans/brush_booleans.html"),
|
||||
("bpy.ops.object.boolean_brush_intersect", "booleans/brush_booleans.html"),
|
||||
("bpy.ops.object.boolean_brush_difference", "booleans/brush_booleans.html"),
|
||||
("bpy.ops.object.boolean_brush_slice", "booleans/brush_booleans.html"),
|
||||
# Auto Boolean
|
||||
("bpy.ops.object.boolean_auto_union", "Boolean-Operators#auto-boolean-operators"),
|
||||
("bpy.ops.object.boolean_auto_intersect", "Boolean-Operators#auto-boolean-operators"),
|
||||
("bpy.ops.object.boolean_auto_difference", "Boolean-Operators#auto-boolean-operators"),
|
||||
("bpy.ops.object.boolean_auto_slice", "Boolean-Operators#auto-boolean-operators"),
|
||||
("bpy.ops.object.boolean_auto_union", "booleans/auto_booleans.html"),
|
||||
("bpy.ops.object.boolean_auto_intersect", "booleans/auto_booleans.html"),
|
||||
("bpy.ops.object.boolean_auto_difference", "booleans/auto_booleans.html"),
|
||||
("bpy.ops.object.boolean_auto_slice", "booleans/auto_booleans.html"),
|
||||
# Carver
|
||||
("bpy.ops.object.carve_box", "carver/index.html"),
|
||||
("bpy.ops.object.carve_circle", "carver/index.html"),
|
||||
("bpy.ops.object.carve_polyline", "carver/index.html"),
|
||||
# Cutter Utilities
|
||||
("bpy.ops.object.boolean_toggle_cutter", "Utility-Operators#toggle-cutter"),
|
||||
("bpy.ops.object.boolean_remove_cutter", "Utility-Operators#remove-cutter"),
|
||||
("bpy.ops.object.boolean_apply_cutter", "Utility-Operators#apply-cutter"),
|
||||
("bpy.ops.object.boolean_toggle_cutter", "utilities/toggle.html"),
|
||||
("bpy.ops.object.boolean_remove_cutter", "utilities/remove.html"),
|
||||
("bpy.ops.object.boolean_apply_cutter", "utilities/apply.html"),
|
||||
# Canvas Utilities
|
||||
("bpy.ops.object.boolean_toggle_all", "Utility-Operators#toggle-all-cutters"),
|
||||
("bpy.ops.object.boolean_remove_all", "Utility-Operators#remove-all-cutters"),
|
||||
("bpy.ops.object.boolean_apply_all", "Utility-Operators#apply-all-cutters"),
|
||||
("bpy.ops.object.boolean_toggle_all", "utilities/toggle.html"),
|
||||
("bpy.ops.object.boolean_remove_all", "utilities/remove.html"),
|
||||
("bpy.ops.object.boolean_apply_all", "utilities/apply.html"),
|
||||
# Select
|
||||
("bpy.ops.object.select_cutter_canvas", "Utility-Operators#select-operators"),
|
||||
("bpy.ops.object.boolean_select_all", "Utility-Operators#select-operators"),
|
||||
("bpy.ops.object.select_cutter_canvas", "utilities/select.html"),
|
||||
("bpy.ops.object.boolean_select_all", "utilities/select.html"),
|
||||
)
|
||||
|
||||
return url_manual_prefix, url_manual_mapping
|
||||
|
||||
@@ -18,12 +18,12 @@ else:
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
modules = [
|
||||
modules = (
|
||||
boolean,
|
||||
canvas,
|
||||
cutter,
|
||||
select,
|
||||
]
|
||||
)
|
||||
|
||||
def register():
|
||||
for module in modules:
|
||||
|
||||
@@ -2,12 +2,13 @@ import bpy
|
||||
from collections import defaultdict
|
||||
from .. import __package__ as base_package
|
||||
|
||||
from ..functions.poll import (
|
||||
basic_poll,
|
||||
is_linked,
|
||||
is_instanced_data,
|
||||
list_candidate_objects,
|
||||
destructive_op_confirmation,
|
||||
from ..functions.canvas import (
|
||||
filter_canvases,
|
||||
create_slice,
|
||||
)
|
||||
from ..functions.cutter import (
|
||||
filter_cutters,
|
||||
make_cutter,
|
||||
)
|
||||
from ..functions.modifier import (
|
||||
add_boolean_modifier,
|
||||
@@ -15,22 +16,35 @@ from ..functions.modifier import (
|
||||
get_modifiers_to_apply,
|
||||
)
|
||||
from ..functions.object import (
|
||||
set_cutter_properties,
|
||||
change_parent,
|
||||
create_slice,
|
||||
delete_cutter,
|
||||
delete_object,
|
||||
)
|
||||
from ..functions.poll import (
|
||||
basic_poll,
|
||||
convert_to_mesh_confirmation,
|
||||
destructive_op_confirmation,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ PROPERTIES ------------------------------ ####
|
||||
|
||||
class ModifierProperties():
|
||||
class BooleanBase():
|
||||
# Add-on properties.
|
||||
flip: bpy.props.BoolProperty(
|
||||
name = "Flip Canvas & Cutters",
|
||||
options = {'SKIP_SAVE'},
|
||||
default = False,
|
||||
)
|
||||
|
||||
# Boolean modifier properties.
|
||||
material_mode: bpy.props.EnumProperty(
|
||||
name = "Materials",
|
||||
description = "Method for setting materials on the new faces",
|
||||
items = (('INDEX', "Index Based", ("Set the material on new faces based on the order of the material slot lists. If a material doesn't exist on the\n"
|
||||
items = (('INDEX', "Index Based",
|
||||
("Set the material on new faces based on the order of the material slot lists. If a material doesn't exist on the\n"
|
||||
"modifier object, the face will use the same material slot or the first if the object doesn't have enough slots.")),
|
||||
('TRANSFER', "Transfer", ("Transfer materials from non-empty slots to the result mesh, adding new materials as necessary.\n"
|
||||
('TRANSFER', "Transfer",
|
||||
("Transfer materials from non-empty slots to the result mesh, adding new materials as necessary.\n"
|
||||
"For empty slots, fall back to using the same material index as the operand mesh."))),
|
||||
default = 'INDEX',
|
||||
)
|
||||
@@ -52,12 +66,24 @@ class ModifierProperties():
|
||||
default = 0.000001,
|
||||
)
|
||||
|
||||
# Built-in Methods.
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context)
|
||||
|
||||
|
||||
def draw(self, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
|
||||
col = layout.column()
|
||||
col.prop(self, "flip")
|
||||
if self._unflippable:
|
||||
col.enabled = False
|
||||
|
||||
layout.separator()
|
||||
if prefs.solver == 'EXACT':
|
||||
layout.prop(self, "material_mode")
|
||||
layout.prop(self, "use_self")
|
||||
@@ -66,52 +92,85 @@ class ModifierProperties():
|
||||
layout.prop(self, "double_threshold")
|
||||
|
||||
|
||||
# Custom Methods.
|
||||
def _filter_objects(self, context) -> tuple[list, list]:
|
||||
"""Returns lists of cutters & canvases."""
|
||||
|
||||
canvases = [context.active_object]
|
||||
cutters = [obj for obj in context.selected_objects if obj != context.active_object]
|
||||
|
||||
# Flip.
|
||||
if self.flip:
|
||||
canvases, cutters = cutters, canvases
|
||||
|
||||
# Filter Canvases.
|
||||
canvases = filter_canvases(self, context, canvases)
|
||||
if len(canvases) == 0:
|
||||
self.report({'WARNING'}, "No valid canvases selected")
|
||||
return None, None
|
||||
|
||||
# Filter Cutters.
|
||||
cutters = filter_cutters(self, context, cutters, canvases)
|
||||
if len(cutters) == 0:
|
||||
self.report({'WARNING'}, "No valid cutters selected")
|
||||
return None, None
|
||||
|
||||
return canvases, cutters
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /brush_boolean/ ------------------------------ ####
|
||||
|
||||
class BrushBoolean(ModifierProperties):
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context)
|
||||
|
||||
class BrushBoolean(BooleanBase):
|
||||
|
||||
def invoke(self, context, event):
|
||||
# Abort if there are less than 2 selected objects.
|
||||
# Abort if there are less than 2 objects selected.
|
||||
if len(context.selected_objects) < 2:
|
||||
self.report({'WARNING'}, "Boolean operator needs at least two selected objects")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Abort if active object is linked.
|
||||
if is_linked(context, context.active_object):
|
||||
self.report({'WARNING'}, "Boolean operators cannot be performed on linked objects")
|
||||
return {'CANCELLED'}
|
||||
if not self.flip:
|
||||
cutters = [obj for obj in context.selected_objects if obj != context.active_object]
|
||||
else:
|
||||
cutters = [context.active_object]
|
||||
|
||||
return self.execute(context)
|
||||
self._unflippable = False
|
||||
return convert_to_mesh_confirmation(self, context, event, cutters, "Brush Boolean")
|
||||
|
||||
|
||||
def execute(self, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
canvas = context.active_object
|
||||
cutters = list_candidate_objects(self, context, context.active_object)
|
||||
|
||||
if len(cutters) == 0:
|
||||
# Create lists of cutters & canvases.
|
||||
canvases, cutters = self._filter_objects(context)
|
||||
if canvases is None or cutters is None:
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Create slices.
|
||||
if self.mode == "SLICE":
|
||||
for cutter in cutters:
|
||||
"""NOTE: Slices need to be created in a separate loop to avoid inheriting boolean modifiers that the operator adds."""
|
||||
"""
|
||||
NOTE: Slices need to be created in a separate loop to avoid
|
||||
inheriting Boolean modifiers that the operator adds.
|
||||
"""
|
||||
for canvas in canvases:
|
||||
slice = create_slice(context, canvas, modifier=True)
|
||||
add_boolean_modifier(self, context, slice, cutter, "INTERSECT", prefs.solver, pin=prefs.pin)
|
||||
|
||||
for cutter in cutters:
|
||||
mode = "DIFFERENCE" if self.mode == "SLICE" else self.mode
|
||||
display = 'WIRE' if prefs.wireframe else 'BOUNDS'
|
||||
set_cutter_properties(context, cutter, self.mode, display=display, collection=prefs.use_collection)
|
||||
add_boolean_modifier(self, context, canvas, cutter, mode, prefs.solver, pin=prefs.pin)
|
||||
if prefs.parent:
|
||||
change_parent(cutter, canvas)
|
||||
make_cutter(context, cutter, self.mode,
|
||||
display=prefs.display,
|
||||
collection=prefs.use_collection)
|
||||
|
||||
mode = "DIFFERENCE" if self.mode == "SLICE" else self.mode
|
||||
for canvas in canvases:
|
||||
add_boolean_modifier(self, context, canvas, cutter, mode, prefs.solver, pin=prefs.pin)
|
||||
|
||||
if prefs.parent:
|
||||
change_parent(context, cutter, canvases[0], inverse=True)
|
||||
|
||||
# Set Boolean property to canvases.
|
||||
for canvas in canvases:
|
||||
canvas.booleans.canvas = True
|
||||
|
||||
return {'FINISHED'}
|
||||
@@ -120,29 +179,47 @@ class BrushBoolean(ModifierProperties):
|
||||
class OBJECT_OT_boolean_brush_union(bpy.types.Operator, BrushBoolean):
|
||||
bl_idname = "object.boolean_brush_union"
|
||||
bl_label = "Boolean Union (Brush)"
|
||||
bl_description = "Merge selected objects into active one"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
mode = "UNION"
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if not properties.flip:
|
||||
return "Merge selected objects into the active one"
|
||||
else:
|
||||
return "Merge the active object into selected ones"
|
||||
|
||||
|
||||
class OBJECT_OT_boolean_brush_intersect(bpy.types.Operator, BrushBoolean):
|
||||
bl_idname = "object.boolean_brush_intersect"
|
||||
bl_label = "Boolean Intersection (Brush)"
|
||||
bl_description = "Only keep the parts of the active object that are interesecting selected objects"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
mode = "INTERSECT"
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if not properties.flip:
|
||||
return "Only keep parts of the active object that are interesecting selected objects"
|
||||
else:
|
||||
return "Only keep parts of selected objects that are interesecting the active one"
|
||||
|
||||
|
||||
class OBJECT_OT_boolean_brush_difference(bpy.types.Operator, BrushBoolean):
|
||||
bl_idname = "object.boolean_brush_difference"
|
||||
bl_label = "Boolean Difference (Brush)"
|
||||
bl_description = "Subtract selected objects from active one"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
mode = "DIFFERENCE"
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if not properties.flip:
|
||||
return "Subtract selected objects from the active one"
|
||||
else:
|
||||
return "Subtract the active object from selected ones"
|
||||
|
||||
|
||||
class OBJECT_OT_boolean_brush_slice(bpy.types.Operator, BrushBoolean):
|
||||
bl_idname = "object.boolean_brush_slice"
|
||||
@@ -152,15 +229,18 @@ class OBJECT_OT_boolean_brush_slice(bpy.types.Operator, BrushBoolean):
|
||||
|
||||
mode = "SLICE"
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if not properties.flip:
|
||||
return "Slice the active object by selected ones. Will create slices as separate objects"
|
||||
else:
|
||||
return "Slice selected objects by the active one. Will create slices as separate objects"
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /auto_boolean/ ------------------------------ ####
|
||||
|
||||
class AutoBoolean(ModifierProperties):
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context)
|
||||
|
||||
class AutoBoolean(BooleanBase):
|
||||
|
||||
def invoke(self, context, event):
|
||||
# Abort if there are less than 2 selected objects.
|
||||
@@ -168,55 +248,66 @@ class AutoBoolean(ModifierProperties):
|
||||
self.report({'WARNING'}, "Boolean operator needs at least two selected objects")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Abort if active object is linked.
|
||||
if is_linked(context, context.active_object):
|
||||
self.report({'ERROR'}, "Modifiers cannot be applied to linked object")
|
||||
return {'CANCELLED'}
|
||||
if not self.flip:
|
||||
canvases = [context.active_object]
|
||||
else:
|
||||
canvases = [obj for obj in context.selected_objects if obj != context.active_object]
|
||||
|
||||
return destructive_op_confirmation(self, context, event, [context.active_object], title="Auto Boolean")
|
||||
for canvas in canvases:
|
||||
if canvas.type != 'MESH':
|
||||
canvases.remove(canvas)
|
||||
|
||||
self._unflippable = False
|
||||
return destructive_op_confirmation(self, context, event, canvases, "Auto Boolean")
|
||||
|
||||
|
||||
def execute(self, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
canvas = context.active_object
|
||||
cutters = list_candidate_objects(self, context, context.active_object)
|
||||
new_modifiers = defaultdict(list)
|
||||
|
||||
if len(cutters) == 0:
|
||||
# Create lists of cutters & canvases.
|
||||
canvases, cutters = self._filter_objects(context)
|
||||
if canvases is None or cutters is None:
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Create slices.
|
||||
if self.mode == "SLICE":
|
||||
for cutter in cutters:
|
||||
"""NOTE: Slices need to be created in a separate loop to avoid inheriting boolean modifiers that the operator adds."""
|
||||
"""
|
||||
NOTE: Slices need to be created in a separate loop to avoid
|
||||
inheriting Boolean modifiers that the operator adds.
|
||||
"""
|
||||
for canvas in canvases:
|
||||
slice = create_slice(context, canvas)
|
||||
modifier = add_boolean_modifier(self, context, slice, cutter, "INTERSECT", prefs.solver, pin=prefs.pin)
|
||||
modifier = add_boolean_modifier(self, context, slice, cutter, "INTERSECT",
|
||||
prefs.solver, pin=prefs.pin)
|
||||
new_modifiers[slice].append(modifier)
|
||||
slice.select_set(True)
|
||||
|
||||
for cutter in cutters:
|
||||
# Add boolean modifier on canvas.
|
||||
# Add Boolean modifier on canvases.
|
||||
mode = "DIFFERENCE" if self.mode == "SLICE" else self.mode
|
||||
for canvas in canvases:
|
||||
modifier = add_boolean_modifier(self, context, canvas, cutter, mode, prefs.solver, pin=prefs.pin)
|
||||
new_modifiers[canvas].append(modifier)
|
||||
|
||||
# Transfer cutters children to canvas.
|
||||
# Transfer cutters children to a canvas.
|
||||
for child in cutter.children:
|
||||
change_parent(child, canvas)
|
||||
change_parent(context, child, canvases[0])
|
||||
|
||||
# Select all faces of the cutter so that newly created faces in canvas
|
||||
# are also selected after applying the modifier.
|
||||
for face in cutter.data.polygons:
|
||||
face.select = True
|
||||
|
||||
# Apply modifiers on canvas & slices.
|
||||
# Apply modifiers on canvases & slices.
|
||||
for obj, modifiers in new_modifiers.items():
|
||||
modifiers = get_modifiers_to_apply(context, obj, modifiers)
|
||||
apply_modifiers(context, obj, modifiers)
|
||||
|
||||
# Delete cutters.
|
||||
for cutter in cutters:
|
||||
delete_cutter(cutter)
|
||||
delete_object(cutter)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -224,29 +315,47 @@ class AutoBoolean(ModifierProperties):
|
||||
class OBJECT_OT_boolean_auto_union(bpy.types.Operator, AutoBoolean):
|
||||
bl_idname = "object.boolean_auto_union"
|
||||
bl_label = "Boolean Union (Auto)"
|
||||
bl_description = "Merge selected objects into active one"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
mode = "UNION"
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if not properties.flip:
|
||||
return "Merge selected objects into the active one"
|
||||
else:
|
||||
return "Merge the active object into selected ones"
|
||||
|
||||
|
||||
class OBJECT_OT_boolean_auto_difference(bpy.types.Operator, AutoBoolean):
|
||||
bl_idname = "object.boolean_auto_difference"
|
||||
bl_label = "Boolean Difference (Auto)"
|
||||
bl_description = "Subtract selected objects from active one"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
mode = "DIFFERENCE"
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if not properties.flip:
|
||||
return "Subtract selected objects from the active one"
|
||||
else:
|
||||
return "Subtract the active object from selected ones"
|
||||
|
||||
|
||||
class OBJECT_OT_boolean_auto_intersect(bpy.types.Operator, AutoBoolean):
|
||||
bl_idname = "object.boolean_auto_intersect"
|
||||
bl_label = "Boolean Intersect (Auto)"
|
||||
bl_description = "Only keep the parts of the active object that are interesecting selected objects"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
mode = "INTERSECT"
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if not properties.flip:
|
||||
return "Only keep parts of the active object that are interesecting selected objects"
|
||||
else:
|
||||
return "Only keep parts of selected objects that are interesecting the active one"
|
||||
|
||||
|
||||
class OBJECT_OT_boolean_auto_slice(bpy.types.Operator, AutoBoolean):
|
||||
bl_idname = "object.boolean_auto_slice"
|
||||
@@ -256,13 +365,20 @@ class OBJECT_OT_boolean_auto_slice(bpy.types.Operator, AutoBoolean):
|
||||
|
||||
mode = "SLICE"
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if not properties.flip:
|
||||
return "Slice the active object by selected ones. Will create slices as separate objects"
|
||||
else:
|
||||
return "Slice selected objects by the active one. Will create slices as separate objects"
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
addon_keymaps = []
|
||||
|
||||
classes = [
|
||||
classes = (
|
||||
OBJECT_OT_boolean_brush_union,
|
||||
OBJECT_OT_boolean_brush_difference,
|
||||
OBJECT_OT_boolean_brush_intersect,
|
||||
@@ -272,7 +388,7 @@ classes = [
|
||||
OBJECT_OT_boolean_auto_difference,
|
||||
OBJECT_OT_boolean_auto_intersect,
|
||||
OBJECT_OT_boolean_auto_slice,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
|
||||
@@ -2,29 +2,30 @@ import bpy
|
||||
import itertools
|
||||
from .. import __package__ as base_package
|
||||
|
||||
from ..functions.poll import (
|
||||
basic_poll,
|
||||
is_canvas,
|
||||
is_instanced_data,
|
||||
destructive_op_confirmation,
|
||||
from ..functions.canvas import (
|
||||
list_selected_canvases,
|
||||
list_canvas_cutters,
|
||||
list_canvas_slices,
|
||||
)
|
||||
from ..functions.cutter import (
|
||||
list_cutter_users,
|
||||
handle_unused_cutters,
|
||||
)
|
||||
from ..functions.modifier import (
|
||||
apply_modifiers,
|
||||
get_modifiers_to_apply,
|
||||
is_boolean_modifier,
|
||||
)
|
||||
from ..functions.object import (
|
||||
object_visibility_set,
|
||||
delete_empty_collection,
|
||||
delete_cutter,
|
||||
change_parent,
|
||||
delete_object,
|
||||
)
|
||||
from ..functions.list import (
|
||||
list_canvases,
|
||||
list_canvas_slices,
|
||||
list_canvas_cutters,
|
||||
list_cutter_users,
|
||||
list_selected_canvases,
|
||||
list_unused_cutters,
|
||||
list_pre_boolean_modifiers,
|
||||
from ..functions.poll import (
|
||||
basic_poll,
|
||||
destructive_op_confirmation,
|
||||
_guess_toggle_state,
|
||||
)
|
||||
from ..functions.scene import (
|
||||
delete_empty_collection,
|
||||
)
|
||||
|
||||
|
||||
@@ -34,41 +35,50 @@ from ..functions.list import (
|
||||
class OBJECT_OT_boolean_toggle_all(bpy.types.Operator):
|
||||
bl_idname = "object.boolean_toggle_all"
|
||||
bl_label = "Toggle Boolean Cutters"
|
||||
bl_description = "Toggle all boolean cutters affecting selected canvases"
|
||||
bl_description = "Toggle all Boolean cutters affecting selected canvases"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context, check_linked=True) and is_canvas(context.active_object)
|
||||
return basic_poll(cls, context, check_active=False)
|
||||
|
||||
def execute(self, context):
|
||||
# Filter canvases.
|
||||
canvases = list_selected_canvases(context)
|
||||
if len(canvases) == 0:
|
||||
self.report({'WARNING'}, "No valid canvases selected")
|
||||
return {'CANCELLED'}
|
||||
|
||||
cutters, modifiers = list_canvas_cutters(canvases)
|
||||
slices = list_canvas_slices(canvases)
|
||||
modifiers = list(itertools.chain.from_iterable(modifiers.values()))
|
||||
slices = list_canvas_slices(context, canvases)
|
||||
|
||||
state = _guess_toggle_state(modifiers)
|
||||
state = True if state == "On" else False
|
||||
|
||||
# Toggle Modifiers
|
||||
for mod in modifiers:
|
||||
mod.show_viewport = not mod.show_viewport
|
||||
mod.show_render = not mod.show_render
|
||||
mod.show_viewport = not state
|
||||
mod.show_render = not state
|
||||
|
||||
# Hide Slices
|
||||
for slice in slices:
|
||||
slice.hide_viewport = not slice.hide_viewport
|
||||
slice.hide_render = not slice.hide_render
|
||||
slice.hide_viewport = state
|
||||
slice.hide_render = state
|
||||
slice.hide_set(state)
|
||||
for mod in slice.modifiers:
|
||||
if mod.type == 'BOOLEAN' and mod.object in cutters:
|
||||
mod.show_viewport = not mod.show_viewport
|
||||
mod.show_render = not mod.show_render
|
||||
if not is_boolean_modifier(mod):
|
||||
continue
|
||||
if mod.object in cutters:
|
||||
mod.show_viewport = not state
|
||||
mod.show_render = not state
|
||||
|
||||
# Hide Unused Cutters
|
||||
other_canvases = list_canvases()
|
||||
for obj in other_canvases:
|
||||
if obj not in canvases + slices:
|
||||
if any(mod.object in cutters and mod.show_viewport for mod in obj.modifiers if mod.type == 'BOOLEAN'):
|
||||
cutters[:] = [cutter for cutter in cutters if cutter not in [mod.object for mod in obj.modifiers]]
|
||||
|
||||
for cutter in cutters:
|
||||
cutter.hide_viewport = not cutter.hide_viewport
|
||||
other_canvases = list_cutter_users([cutter], exclude=canvases + slices).keys()
|
||||
if len(other_canvases) == 0:
|
||||
cutter.hide_viewport = state
|
||||
cutter.hide_set(state)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -77,72 +87,49 @@ class OBJECT_OT_boolean_toggle_all(bpy.types.Operator):
|
||||
class OBJECT_OT_boolean_remove_all(bpy.types.Operator):
|
||||
bl_idname = "object.boolean_remove_all"
|
||||
bl_label = "Remove Boolean Cutters"
|
||||
bl_description = "Remove all boolean cutters from selected canvases"
|
||||
bl_options = {'UNDO'}
|
||||
bl_description = "Remove all Boolean cutters from selected canvases"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
delete_cutters: bpy.props.BoolProperty(
|
||||
name = "Delete Unused Cutters",
|
||||
description = "Completely remove cutters if they're not used by any other remaining canvas",
|
||||
default = True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context, check_linked=True) and is_canvas(context.active_object)
|
||||
return basic_poll(cls, context, check_active=False)
|
||||
|
||||
def execute(self, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
|
||||
# Filter canvases.
|
||||
canvases = list_selected_canvases(context)
|
||||
cutters, __ = list_canvas_cutters(canvases)
|
||||
slices = list_canvas_slices(canvases)
|
||||
if len(canvases) == 0:
|
||||
self.report({'WARNING'}, "No valid canvases selected")
|
||||
return {'CANCELLED'}
|
||||
|
||||
cutters, modifiers = list_canvas_cutters(canvases)
|
||||
slices = list_canvas_slices(context, canvases)
|
||||
|
||||
# Remove Slices
|
||||
for slice in slices:
|
||||
if slice in canvases:
|
||||
canvases.remove(slice)
|
||||
delete_cutter(slice)
|
||||
delete_object(slice)
|
||||
|
||||
for canvas in canvases:
|
||||
# Remove Modifiers
|
||||
for mod in canvas.modifiers:
|
||||
if mod.type == 'BOOLEAN' and "boolean_" in mod.name:
|
||||
if mod.object in cutters:
|
||||
for canvas, mods in modifiers.items():
|
||||
for mod in mods:
|
||||
canvas.modifiers.remove(mod)
|
||||
|
||||
# remove_boolean_properties
|
||||
if canvas.booleans.canvas == True:
|
||||
canvas.booleans.canvas = False
|
||||
|
||||
# Handle Unused Cutters
|
||||
handle_unused_cutters(context, list(cutters), canvases + slices,
|
||||
delete=self.delete_cutters)
|
||||
|
||||
# Restore Orphaned Cutters
|
||||
unused_cutters, leftovers = list_unused_cutters(cutters, canvases, slices, do_leftovers=True)
|
||||
|
||||
for cutter in unused_cutters:
|
||||
if cutter.booleans.carver:
|
||||
delete_cutter(cutter)
|
||||
else:
|
||||
# restore_visibility
|
||||
cutter.hide_render = False
|
||||
cutter.display_type = 'TEXTURED'
|
||||
cutter.lineart.usage = 'INHERIT'
|
||||
object_visibility_set(cutter, value=True)
|
||||
cutter.booleans.cutter = ""
|
||||
|
||||
# remove_parent_&_collection
|
||||
if prefs.parent and cutter.parent in canvases:
|
||||
change_parent(cutter, None)
|
||||
|
||||
if prefs.use_collection:
|
||||
cutters_collection = bpy.data.collections.get(prefs.collection_name)
|
||||
if cutters_collection in cutter.users_collection:
|
||||
bpy.data.collections.get(prefs.collection_name).objects.unlink(cutter)
|
||||
|
||||
# purge_empty_collection
|
||||
if prefs.use_collection:
|
||||
delete_empty_collection()
|
||||
|
||||
|
||||
# Change Leftover Cutter Parent
|
||||
if prefs.parent:
|
||||
for cutter in leftovers:
|
||||
if cutter.parent in canvases:
|
||||
other_canvases = list_cutter_users([cutter])
|
||||
change_parent(cutter, other_canvases[0])
|
||||
# Purge Empty Collection
|
||||
delete_empty_collection(context)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -151,74 +138,56 @@ class OBJECT_OT_boolean_remove_all(bpy.types.Operator):
|
||||
class OBJECT_OT_boolean_apply_all(bpy.types.Operator):
|
||||
bl_idname = "object.boolean_apply_all"
|
||||
bl_label = "Apply All Boolean Cutters"
|
||||
bl_description = "Apply all boolean cutters on selected canvases"
|
||||
bl_options = {'UNDO'}
|
||||
bl_description = "Apply all Boolean cutters to selected canvases"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
delete_cutters: bpy.props.BoolProperty(
|
||||
name = "Delete Unused Cutters",
|
||||
description = "Completely remove cutters if they're not used by any other remaining canvas",
|
||||
default = True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context, check_linked=True) and is_canvas(context.active_object)
|
||||
|
||||
return basic_poll(cls, context, check_active=False)
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.canvases = list_selected_canvases(context)
|
||||
return destructive_op_confirmation(self, context, event, self.canvases, title="Apply Boolean Cutters")
|
||||
# Filter canvases.
|
||||
canvases = list_selected_canvases(context)
|
||||
if len(canvases) == 0:
|
||||
self.report({'WARNING'}, "No valid canvases selected")
|
||||
return {'CANCELLED'}
|
||||
|
||||
return destructive_op_confirmation(self, context, event, canvases, title="Apply Boolean Cutters")
|
||||
|
||||
def execute(self, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
|
||||
cutters, __ = list_canvas_cutters(self.canvases)
|
||||
slices = list_canvas_slices(self.canvases)
|
||||
canvases = list_selected_canvases(context)
|
||||
cutters, __ = list_canvas_cutters(canvases)
|
||||
slices = list_canvas_slices(context, canvases)
|
||||
|
||||
# Select all faces of the cutter so that newly created faces in canvas
|
||||
# are also selected after applying the modifier.
|
||||
for cutter in cutters:
|
||||
for cutter in list(cutters):
|
||||
for face in cutter.data.polygons:
|
||||
face.select = True
|
||||
|
||||
for canvas in itertools.chain(self.canvases, slices):
|
||||
context.view_layer.objects.active = canvas
|
||||
|
||||
for canvas in itertools.chain(canvases, slices):
|
||||
# Apply Modifiers
|
||||
if prefs.apply_order == 'ALL':
|
||||
modifiers = [mod for mod in canvas.modifiers]
|
||||
elif prefs.apply_order == 'BEFORE':
|
||||
modifiers = list_pre_boolean_modifiers(canvas)
|
||||
elif prefs.apply_order == 'BOOLEANS':
|
||||
modifiers = [mod for mod in canvas.modifiers if mod.type == 'BOOLEAN' and "boolean_" in mod.name]
|
||||
|
||||
modifiers = get_modifiers_to_apply(context, canvas)
|
||||
apply_modifiers(context, canvas, modifiers)
|
||||
|
||||
# remove_boolean_properties
|
||||
# Remove Boolean Properties
|
||||
canvas.booleans.canvas = False
|
||||
canvas.booleans.slice = False
|
||||
canvas.booleans.slice_of = None
|
||||
|
||||
# Handle Unused Cutters
|
||||
handle_unused_cutters(context, list(cutters), canvases, delete=self.delete_cutters)
|
||||
|
||||
# Purge Orphaned Cutters
|
||||
unused_cutters, leftovers = list_unused_cutters(cutters, self.canvases, slices, do_leftovers=True)
|
||||
|
||||
purged_cutters = []
|
||||
for cutter in unused_cutters:
|
||||
if cutter not in purged_cutters:
|
||||
# Transfer Children
|
||||
for child in cutter.children:
|
||||
change_parent(child, cutter.parent)
|
||||
|
||||
# Purge
|
||||
delete_cutter(cutter)
|
||||
purged_cutters.append(cutter)
|
||||
|
||||
# purge_empty_collection
|
||||
if prefs.use_collection:
|
||||
delete_empty_collection()
|
||||
|
||||
|
||||
# Change Leftover Cutter Parent
|
||||
if prefs.parent:
|
||||
for cutter in leftovers:
|
||||
if cutter.parent in self.canvases:
|
||||
other_canvases = list_cutter_users([cutter])
|
||||
change_parent(cutter, other_canvases[0])
|
||||
# Purge Empty Collection
|
||||
delete_empty_collection(context)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -228,11 +197,11 @@ class OBJECT_OT_boolean_apply_all(bpy.types.Operator):
|
||||
|
||||
addon_keymaps = []
|
||||
|
||||
classes = [
|
||||
classes = (
|
||||
OBJECT_OT_boolean_toggle_all,
|
||||
OBJECT_OT_boolean_remove_all,
|
||||
OBJECT_OT_boolean_apply_all,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
|
||||
@@ -1,27 +1,35 @@
|
||||
import bpy
|
||||
import itertools
|
||||
from .. import __package__ as base_package
|
||||
|
||||
from ..functions.poll import (
|
||||
basic_poll,
|
||||
is_instanced_data,
|
||||
destructive_op_confirmation,
|
||||
from ..functions.canvas import (
|
||||
list_canvas_cutters,
|
||||
list_canvas_slices,
|
||||
)
|
||||
from ..functions.cutter import (
|
||||
list_selected_cutters,
|
||||
list_cutter_users,
|
||||
restore_cutter,
|
||||
handle_unused_cutters,
|
||||
)
|
||||
from ..functions.modifier import (
|
||||
apply_modifiers,
|
||||
is_boolean_modifier,
|
||||
)
|
||||
from ..functions.object import (
|
||||
object_visibility_set,
|
||||
delete_empty_collection,
|
||||
delete_cutter,
|
||||
change_parent,
|
||||
delete_object,
|
||||
)
|
||||
from ..functions.list import (
|
||||
list_canvases,
|
||||
list_selected_cutters,
|
||||
list_canvas_cutters,
|
||||
list_canvas_slices,
|
||||
list_cutter_users,
|
||||
list_unused_cutters,
|
||||
from ..functions.poll import (
|
||||
basic_poll,
|
||||
destructive_op_confirmation,
|
||||
_guess_toggle_state,
|
||||
)
|
||||
from ..functions.scene import (
|
||||
delete_empty_collection,
|
||||
)
|
||||
from ..ui.common import (
|
||||
preserve_list_index,
|
||||
)
|
||||
|
||||
|
||||
@@ -31,13 +39,19 @@ from ..functions.list import (
|
||||
class OBJECT_OT_boolean_toggle_cutter(bpy.types.Operator):
|
||||
bl_idname = "object.boolean_toggle_cutter"
|
||||
bl_label = "Toggle Boolean Cutter"
|
||||
bl_description = "Toggle this boolean cutter. If cutter is the active object it will be toggled for every canvas that uses it"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if properties.specified_cutter:
|
||||
return ("Toggle selected cutter and its effect")
|
||||
else:
|
||||
return ("Toggle all selected cutters and their effects")
|
||||
|
||||
method: bpy.props.EnumProperty(
|
||||
name = "Method",
|
||||
items = (('ALL', "All", "Remove cutter from all canvases that use it"),
|
||||
('SPECIFIED', "Specified", "Remove cutter from specified canvas")),
|
||||
items = (('ALL', "All", "Toggle all selected cutters"),
|
||||
('SPECIFIED', "Specified", "Toggle selected cutter")),
|
||||
default = 'ALL',
|
||||
)
|
||||
|
||||
@@ -45,57 +59,71 @@ class OBJECT_OT_boolean_toggle_cutter(bpy.types.Operator):
|
||||
)
|
||||
specified_canvas: bpy.props.StringProperty(
|
||||
)
|
||||
specified_modifier: bpy.props.StringProperty(
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context, check_linked=True)
|
||||
return basic_poll(cls, context, check_active=False)
|
||||
|
||||
def execute(self, context):
|
||||
# Create lists of cutters & canvases.
|
||||
if self.method == 'SPECIFIED':
|
||||
canvases = [context.scene.objects[self.specified_canvas]]
|
||||
cutters = [context.scene.objects[self.specified_cutter]]
|
||||
slices = list_canvas_slices(canvases)
|
||||
cutters: list = [context.scene.objects[self.specified_cutter]]
|
||||
canvases: list = [context.scene.objects[self.specified_canvas]]
|
||||
modifiers: list = [canvases[0].modifiers.get(self.specified_modifier)]
|
||||
slices: list = list_canvas_slices(context, canvases)
|
||||
elif self.method == 'ALL':
|
||||
cutters = list_selected_cutters(context)
|
||||
canvases = list_cutter_users(cutters)
|
||||
cutters: list = list_selected_cutters(context)
|
||||
canvases: dict = list_cutter_users(cutters)
|
||||
modifiers: list = list(itertools.chain.from_iterable(canvases.values()))
|
||||
|
||||
if cutters:
|
||||
for canvas in canvases:
|
||||
# toggle_slices_visibility (for_all_canvases)
|
||||
if canvas.booleans.slice == True:
|
||||
if any(modifier.object in cutters for modifier in canvas.modifiers):
|
||||
canvas.hide_viewport = not canvas.hide_viewport
|
||||
canvas.hide_render = not canvas.hide_render
|
||||
if len(cutters) == 0:
|
||||
self.report({'INFO'}, "Boolean cutters are not selected")
|
||||
return {'CANCELLED'}
|
||||
|
||||
if not canvases:
|
||||
return {'CANCELLED'}
|
||||
|
||||
state = _guess_toggle_state(modifiers)
|
||||
state = True if state == "On" else False
|
||||
|
||||
# Toggle Modifiers
|
||||
for mod in canvas.modifiers:
|
||||
if mod.type == 'BOOLEAN' and mod.object in cutters:
|
||||
mod.show_viewport = not mod.show_viewport
|
||||
mod.show_render = not mod.show_render
|
||||
for mod in modifiers:
|
||||
mod.show_viewport = not state
|
||||
mod.show_render = not state
|
||||
|
||||
# Hide Slices
|
||||
if self.method == 'ALL':
|
||||
for canvas in canvases.keys():
|
||||
if not canvas.booleans.slice:
|
||||
continue
|
||||
|
||||
if self.method == 'SPECIFIED':
|
||||
# toggle_slices_visibility (for_specified_canvas)
|
||||
slice = canvas
|
||||
if any(modifier.object in cutters for modifier in slice.modifiers):
|
||||
slice.hide_viewport = state
|
||||
slice.hide_render = state
|
||||
slice.hide_set(state)
|
||||
|
||||
elif self.method == 'SPECIFIED':
|
||||
for slice in slices:
|
||||
for mod in slice.modifiers:
|
||||
if mod.type == 'BOOLEAN' and mod.object in cutters:
|
||||
slice.hide_viewport = not slice.hide_viewport
|
||||
slice.hide_render = not slice.hide_render
|
||||
mod.show_viewport = not mod.show_viewport
|
||||
mod.show_render = not mod.show_render
|
||||
if not is_boolean_modifier(mod):
|
||||
continue
|
||||
if mod.object in cutters:
|
||||
mod.show_viewport = not state
|
||||
mod.show_render = not state
|
||||
|
||||
# hide_cutter_if_not_used_by_any_visible_modifiers
|
||||
other_canvases = list_canvases()
|
||||
for obj in other_canvases:
|
||||
if obj not in canvases + slices:
|
||||
if any(mod.object in cutters and mod.show_viewport for mod in obj.modifiers if mod.type == 'BOOLEAN'):
|
||||
cutters[:] = [cutter for cutter in cutters if cutter not in [mod.object for mod in obj.modifiers]]
|
||||
slice.hide_viewport = state
|
||||
slice.hide_render = state
|
||||
slice.hide_set(state)
|
||||
|
||||
# Hide Unused Cutters
|
||||
for cutter in cutters:
|
||||
cutter.hide_viewport = not cutter.hide_viewport
|
||||
|
||||
else:
|
||||
self.report({'INFO'}, "Boolean cutters are not selected")
|
||||
other_canvases = list_cutter_users([cutter], exclude=canvases + slices).keys()
|
||||
if len(other_canvases) == 0:
|
||||
cutter.hide_viewport = state
|
||||
cutter.hide_set(state)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -104,9 +132,16 @@ class OBJECT_OT_boolean_toggle_cutter(bpy.types.Operator):
|
||||
class OBJECT_OT_boolean_remove_cutter(bpy.types.Operator):
|
||||
bl_idname = "object.boolean_remove_cutter"
|
||||
bl_label = "Remove Boolean Cutter"
|
||||
bl_description = "Remove this boolean cutter. If cutter is the active object it will be removed from every canvas that uses it"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if properties.specified_cutter:
|
||||
return ("Remove this cutter and the Boolean modifier that uses it.\n"
|
||||
"If the cutter is not used by any other canvas it will be deleted from the scene")
|
||||
else:
|
||||
return ("Remove selected Boolean cutters from all canvases that uses them; restore their visibility")
|
||||
|
||||
method: bpy.props.EnumProperty(
|
||||
name = "Method",
|
||||
items = (('ALL', "All", "Remove cutter from all canvases that use it"),
|
||||
@@ -118,86 +153,70 @@ class OBJECT_OT_boolean_remove_cutter(bpy.types.Operator):
|
||||
)
|
||||
specified_canvas: bpy.props.StringProperty(
|
||||
)
|
||||
specified_modifier: bpy.props.StringProperty(
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context, check_linked=True)
|
||||
return basic_poll(cls, context, check_active=False)
|
||||
|
||||
def execute(self, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
leftovers = []
|
||||
|
||||
# Create lists of cutters & canvases.
|
||||
if self.method == 'SPECIFIED':
|
||||
canvases = [context.scene.objects[self.specified_canvas]]
|
||||
cutters = [context.scene.objects[self.specified_cutter]]
|
||||
slices = list_canvas_slices(canvases)
|
||||
cutters: list = [context.scene.objects[self.specified_cutter]]
|
||||
canvases: dict = {
|
||||
context.scene.objects[self.specified_canvas]:
|
||||
[context.scene.objects[self.specified_canvas].modifiers.get(self.specified_modifier)]
|
||||
}
|
||||
elif self.method == 'ALL':
|
||||
cutters = list_selected_cutters(context)
|
||||
canvases = list_cutter_users(cutters)
|
||||
cutters: list = list_selected_cutters(context)
|
||||
canvases: dict = list_cutter_users(cutters)
|
||||
|
||||
if cutters:
|
||||
# Remove Modifiers
|
||||
for canvas in canvases:
|
||||
for mod in canvas.modifiers:
|
||||
if "boolean_" in mod.name:
|
||||
if len(cutters) == 0:
|
||||
self.report({'INFO'}, "Boolean cutters are not selected")
|
||||
return {'CANCELLED'}
|
||||
|
||||
if not canvases:
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Remove Slices
|
||||
slices = list_canvas_slices(context, canvases.keys())
|
||||
for slice in slices:
|
||||
for mod in slice.modifiers:
|
||||
if not is_boolean_modifier(mod):
|
||||
continue
|
||||
if mod.object in cutters:
|
||||
if slice in canvases:
|
||||
del canvases[slice]
|
||||
delete_object(slice)
|
||||
continue
|
||||
|
||||
# Remove Modifiers
|
||||
for canvas, modifiers in canvases.items():
|
||||
for mod in modifiers:
|
||||
with preserve_list_index(canvas.booleans, "modifiers_list_index"):
|
||||
canvas.modifiers.remove(mod)
|
||||
|
||||
# remove_canvas_property_if_needed
|
||||
# Unset canvas property if it's no longer needed.
|
||||
other_cutters, __ = list_canvas_cutters([canvas])
|
||||
if len(other_cutters) == 0:
|
||||
canvas.booleans.canvas = False
|
||||
|
||||
# Remove Slices (for_all_method)
|
||||
if canvas.booleans.slice == True:
|
||||
delete_cutter(canvas)
|
||||
|
||||
|
||||
# Handle Unused Cutters
|
||||
if self.method == 'SPECIFIED':
|
||||
# Remove Slices (for_specified_method)
|
||||
for slice in slices:
|
||||
for mod in slice.modifiers:
|
||||
if mod.type == 'BOOLEAN' and mod.object in cutters:
|
||||
delete_cutter(slice)
|
||||
handle_unused_cutters(context, cutters, list(canvases.keys()), delete=True)
|
||||
|
||||
cutters, leftovers = list_unused_cutters(cutters, canvases, do_leftovers=True)
|
||||
|
||||
|
||||
# Restore Orphaned Cutters
|
||||
elif self.method == 'ALL':
|
||||
for cutter in cutters:
|
||||
if self.method == 'SPECIFIED' and cutter.booleans.carver:
|
||||
delete_cutter(cutter)
|
||||
else:
|
||||
# restore_visibility
|
||||
cutter.hide_render = False
|
||||
cutter.display_type = 'TEXTURED'
|
||||
cutter.lineart.usage = 'INHERIT'
|
||||
object_visibility_set(cutter, value=True)
|
||||
cutter.booleans.cutter = ""
|
||||
# Restore unused cutters, even if they don't have other users.
|
||||
restore_cutter(context, cutter,
|
||||
unparent=prefs.parent and cutter.parent in canvases,
|
||||
unlink_collection=prefs.use_collection)
|
||||
|
||||
# remove_parent_&_collection
|
||||
if prefs.parent and cutter.parent in canvases:
|
||||
change_parent(cutter, None)
|
||||
|
||||
if prefs.use_collection:
|
||||
cutters_collection = bpy.data.collections.get(prefs.collection_name)
|
||||
if cutters_collection in cutter.users_collection:
|
||||
bpy.data.collections.get(prefs.collection_name).objects.unlink(cutter)
|
||||
|
||||
# purge_empty_collection
|
||||
if prefs.use_collection:
|
||||
delete_empty_collection()
|
||||
|
||||
|
||||
# Change Leftover Cutter Parent
|
||||
if prefs.parent and leftovers != None:
|
||||
for cutter in leftovers:
|
||||
if cutter.parent in canvases:
|
||||
other_canvases = list_cutter_users([cutter])
|
||||
change_parent(cutter, other_canvases[0])
|
||||
|
||||
else:
|
||||
self.report({'INFO'}, "Boolean cutters are not selected")
|
||||
# Purge Empty Collection
|
||||
delete_empty_collection(context)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -206,104 +225,100 @@ class OBJECT_OT_boolean_remove_cutter(bpy.types.Operator):
|
||||
class OBJECT_OT_boolean_apply_cutter(bpy.types.Operator):
|
||||
bl_idname = "object.boolean_apply_cutter"
|
||||
bl_label = "Apply Boolean Cutter"
|
||||
bl_description = "Apply this boolean cutter. If cutter is the active object it will be applied to every canvas that uses it"
|
||||
bl_options = {'UNDO'}
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if properties.specified_cutter:
|
||||
return ("Apply the modifier using this Boolean cutter")
|
||||
else:
|
||||
return ("Apply this Boolean cutter to every canvas object that uses it")
|
||||
|
||||
method: bpy.props.EnumProperty(
|
||||
name = "Method",
|
||||
items = (('ALL', "All", "Remove cutter from all canvases that use it"),
|
||||
('SPECIFIED', "Specified", "Remove cutter from specified canvas")),
|
||||
items = (('ALL', "All", "Apply all selected cutter to all canvases that use them"),
|
||||
('SPECIFIED', "Specified", "Apply selected cutter to a specified canvas")),
|
||||
options = {'SKIP_SAVE', 'HIDDEN'},
|
||||
default = 'ALL',
|
||||
)
|
||||
|
||||
specified_cutter: bpy.props.StringProperty(
|
||||
options = {'SKIP_SAVE', 'HIDDEN'},
|
||||
)
|
||||
specified_canvas: bpy.props.StringProperty(
|
||||
options = {'SKIP_SAVE', 'HIDDEN'},
|
||||
)
|
||||
|
||||
delete: bpy.props.BoolProperty(
|
||||
name = "Delete Unused Cutter",
|
||||
description = "Completely remove the cutter object if it's not used by any other canvas",
|
||||
default = True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context, check_linked=True)
|
||||
|
||||
return basic_poll(cls, context, check_active=False)
|
||||
|
||||
def invoke(self, context, event):
|
||||
# Filter Objects
|
||||
if self.method == 'SPECIFIED':
|
||||
self.cutters = [context.scene.objects[self.specified_cutter]]
|
||||
self.canvases = [context.scene.objects[self.specified_canvas]]
|
||||
self.slices = list_canvas_slices(self.canvases)
|
||||
|
||||
canvases = [context.scene.objects[self.specified_canvas]]
|
||||
elif self.method == 'ALL':
|
||||
self.cutters = list_selected_cutters(context)
|
||||
self.canvases = list_cutter_users(self.cutters)
|
||||
|
||||
return destructive_op_confirmation(self, context, event, self.canvases, title="Apply Boolean Cutter")
|
||||
cutters = list_selected_cutters(context)
|
||||
canvases = list_cutter_users(cutters).keys()
|
||||
|
||||
return destructive_op_confirmation(self, context, event, canvases, title="Apply Boolean Cutter")
|
||||
|
||||
def execute(self, context):
|
||||
prefs = bpy.context.preferences.addons[base_package].preferences
|
||||
leftovers = []
|
||||
|
||||
if self.cutters:
|
||||
# Create lists of cutters & canvases.
|
||||
if self.method == 'SPECIFIED':
|
||||
cutters = [context.scene.objects[self.specified_cutter]]
|
||||
canvases = [context.scene.objects[self.specified_canvas]]
|
||||
slices = list_canvas_slices(context, canvases)
|
||||
elif self.method == 'ALL':
|
||||
cutters = list_selected_cutters(context)
|
||||
canvases = list_cutter_users(cutters).keys()
|
||||
slices = []
|
||||
|
||||
if len(cutters) == 0:
|
||||
self.report({'INFO'}, "Boolean cutters are not selected")
|
||||
return {'CANCELLED'}
|
||||
|
||||
if not canvases:
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Select all faces of the cutter so that newly created faces in canvas
|
||||
# are also selected after applying the modifier.
|
||||
for cutter in self.cutters:
|
||||
for cutter in cutters:
|
||||
for face in cutter.data.polygons:
|
||||
face.select = True
|
||||
|
||||
# Apply Modifiers
|
||||
for canvas in self.canvases:
|
||||
context.view_layer.objects.active = canvas
|
||||
|
||||
for canvas in itertools.chain(canvases, slices):
|
||||
boolean_mods = []
|
||||
for mod in canvas.modifiers:
|
||||
if "boolean_" in mod.name:
|
||||
if mod.object in self.cutters:
|
||||
if not is_boolean_modifier(mod):
|
||||
continue
|
||||
if mod.object in cutters:
|
||||
boolean_mods.append(mod)
|
||||
|
||||
if boolean_mods:
|
||||
with preserve_list_index(canvas.booleans, "modifiers_list_index"):
|
||||
apply_modifiers(context, canvas, boolean_mods)
|
||||
|
||||
# remove_canvas_property_if_needed
|
||||
# Unset canvas property if it's no longer needed.
|
||||
other_cutters, __ = list_canvas_cutters([canvas])
|
||||
if len(other_cutters) == 0:
|
||||
canvas.booleans.canvas = False
|
||||
canvas.booleans.slice = False
|
||||
canvas.booleans.slice_of = None
|
||||
|
||||
# Handle Unused Cutters
|
||||
handle_unused_cutters(context, cutters, canvases, delete=self.delete)
|
||||
|
||||
if self.method == 'SPECIFIED':
|
||||
# Apply Modifier for Slices (for_specified_method)
|
||||
for slice in self.slices:
|
||||
boolean_mods = []
|
||||
for mod in slice.modifiers:
|
||||
if mod.type == 'BOOLEAN' and mod.object in self.cutters:
|
||||
boolean_mods.append(mod)
|
||||
apply_modifiers(context, slice, boolean_mods)
|
||||
|
||||
|
||||
unused_cutters, leftovers = list_unused_cutters(self.cutters, self.canvases, do_leftovers=True)
|
||||
|
||||
|
||||
for cutter in unused_cutters:
|
||||
# Transfer Children
|
||||
for child in cutter.children:
|
||||
change_parent(child, cutter.parent)
|
||||
|
||||
# Purge Orphaned Cutters
|
||||
delete_cutter(cutter)
|
||||
|
||||
# purge_empty_collection
|
||||
if prefs.use_collection:
|
||||
delete_empty_collection()
|
||||
|
||||
|
||||
# Change Leftover Cutter Parent
|
||||
if prefs.parent and leftovers != None:
|
||||
for cutter in leftovers:
|
||||
if cutter.parent in self.canvases:
|
||||
other_canvases = list_cutter_users([cutter])
|
||||
change_parent(cutter, other_canvases[0])
|
||||
|
||||
else:
|
||||
self.report({'INFO'}, "Boolean cutters are not selected")
|
||||
# Purge Empty Collection
|
||||
delete_empty_collection(context)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -313,11 +328,11 @@ class OBJECT_OT_boolean_apply_cutter(bpy.types.Operator):
|
||||
|
||||
addon_keymaps = []
|
||||
|
||||
classes = [
|
||||
classes = (
|
||||
OBJECT_OT_boolean_toggle_cutter,
|
||||
OBJECT_OT_boolean_remove_cutter,
|
||||
OBJECT_OT_boolean_apply_cutter,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import bpy
|
||||
from .. import __package__ as base_package
|
||||
|
||||
from ..functions.poll import (
|
||||
basic_poll,
|
||||
active_modifier_poll,
|
||||
from ..functions.canvas import (
|
||||
is_canvas,
|
||||
)
|
||||
from ..functions.list import (
|
||||
list_selected_cutters,
|
||||
list_selected_canvases,
|
||||
list_canvas_cutters,
|
||||
)
|
||||
from ..functions.cutter import (
|
||||
list_selected_cutters,
|
||||
list_cutter_users,
|
||||
)
|
||||
from ..functions.poll import (
|
||||
basic_poll,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ OPERATORS ------------------------------ ####
|
||||
@@ -20,20 +20,23 @@ from ..functions.list import (
|
||||
class OBJECT_OT_select_cutter_canvas(bpy.types.Operator):
|
||||
bl_idname = "object.select_cutter_canvas"
|
||||
bl_label = "Select Boolean Canvas"
|
||||
bl_description = "Select all the objects that use selected objects as boolean cutters"
|
||||
bl_description = "Select all canvases that use selected objects as Boolean cutters"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context) and context.active_object.booleans.cutter
|
||||
return basic_poll(cls, context, check_active=True) and context.active_object.booleans.cutter
|
||||
|
||||
def execute(self, context):
|
||||
cutters = list_selected_cutters(context)
|
||||
canvases = list_cutter_users(cutters)
|
||||
canvases = list_cutter_users(cutters).keys()
|
||||
|
||||
# Select Canvases
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
for obj in context.scene.objects:
|
||||
obj.select_set(False)
|
||||
|
||||
# Select canvases.
|
||||
for canvas in canvases:
|
||||
if not canvas.booleans.slice:
|
||||
canvas.select_set(True)
|
||||
|
||||
return {'FINISHED'}
|
||||
@@ -43,19 +46,21 @@ class OBJECT_OT_select_cutter_canvas(bpy.types.Operator):
|
||||
class OBJECT_OT_boolean_select_all(bpy.types.Operator):
|
||||
bl_idname = "object.boolean_select_all"
|
||||
bl_label = "Select Boolean Cutters"
|
||||
bl_description = "Select all boolean cutters affecting active object"
|
||||
bl_description = "Select all Boolean cutters affecting selected canvases"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context) and is_canvas(context.active_object)
|
||||
return basic_poll(cls, context, check_active=True) and is_canvas(context.active_object)
|
||||
|
||||
def execute(self, context):
|
||||
canvases = list_selected_canvases(context)
|
||||
cutters, __ = list_canvas_cutters(canvases)
|
||||
|
||||
# select_cutters
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
for obj in context.scene.objects:
|
||||
obj.select_set(False)
|
||||
|
||||
# Select cutters.
|
||||
for cutter in cutters:
|
||||
cutter.select_set(True)
|
||||
|
||||
@@ -66,24 +71,24 @@ class OBJECT_OT_boolean_select_all(bpy.types.Operator):
|
||||
class OBJECT_OT_boolean_select_cutter(bpy.types.Operator):
|
||||
bl_idname = "object.boolean_select_cutter"
|
||||
bl_label = "Select Boolean Cutter"
|
||||
bl_description = "Select object that is used as boolean cutter by this modifier"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
return (basic_poll(cls, context) and active_modifier_poll(context.active_object) and
|
||||
context.area.type == 'PROPERTIES' and context.space_data.context == 'MODIFIER' and
|
||||
prefs.double_click)
|
||||
cutter: bpy.props.StringProperty()
|
||||
extend: bpy.props.BoolProperty()
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.extend = event.shift
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
modifier = context.object.modifiers.active
|
||||
if modifier and modifier.type == "BOOLEAN":
|
||||
cutter = modifier.object
|
||||
cutter = bpy.data.objects.get(self.cutter)
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
if not self.extend:
|
||||
for obj in context.scene.objects:
|
||||
obj.select_set(False)
|
||||
|
||||
if cutter:
|
||||
cutter.select_set(True)
|
||||
context.view_layer.objects.active = cutter
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -93,31 +98,16 @@ class OBJECT_OT_boolean_select_cutter(bpy.types.Operator):
|
||||
|
||||
addon_keymaps = []
|
||||
|
||||
classes = [
|
||||
classes = (
|
||||
OBJECT_OT_select_cutter_canvas,
|
||||
OBJECT_OT_boolean_select_all,
|
||||
OBJECT_OT_boolean_select_cutter,
|
||||
]
|
||||
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
# KEYMAP
|
||||
addon = bpy.context.window_manager.keyconfigs.addon
|
||||
km = addon.keymaps.new(name="Property Editor", space_type='PROPERTIES')
|
||||
|
||||
kmi = km.keymap_items.new("object.boolean_select_cutter", type='LEFTMOUSE', value='DOUBLE_CLICK')
|
||||
kmi.active = True
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
# KEYMAP
|
||||
for km, kmi in addon_keymaps:
|
||||
km.keymap_items.remove(kmi)
|
||||
addon_keymaps.clear()
|
||||
|
||||
@@ -8,9 +8,9 @@ def update_sidebar_category(self, context):
|
||||
"""Change sidebar category of add-ons panel."""
|
||||
|
||||
panel_classes = [
|
||||
ui.VIEW3D_PT_boolean,
|
||||
ui.VIEW3D_PT_boolean_properties,
|
||||
ui.VIEW3D_PT_boolean_cutters,
|
||||
ui.panels.VIEW3D_PT_boolean,
|
||||
ui.panels.VIEW3D_PT_boolean_helpers,
|
||||
ui.panels.VIEW3D_PT_boolean_cutters,
|
||||
]
|
||||
|
||||
for cls in panel_classes:
|
||||
@@ -29,14 +29,22 @@ class BoolToolPreferences(bpy.types.AddonPreferences):
|
||||
bl_idname = __package__
|
||||
|
||||
# UI
|
||||
category: bpy.props.EnumProperty(
|
||||
name = "Category",
|
||||
items = (('ADDON', "Add-on", "General add-on features"),
|
||||
('SHARED', "Shared", "Features shared by all of add-ons operators and tools"),
|
||||
('OPERATORS', "Boolean Operators", "Features for brush and auto Boolean operators")),
|
||||
default = 'OPERATORS'
|
||||
)
|
||||
|
||||
show_in_sidebar: bpy.props.BoolProperty(
|
||||
name = "Show Addon Panel in Sidebar",
|
||||
name = "Show Add-on Panel in Sidebar",
|
||||
description = "Add a sidebar panel in 3D Viewport with add-ons operators and properties",
|
||||
default = True,
|
||||
)
|
||||
sidebar_category: bpy.props.StringProperty(
|
||||
name = "Category Name",
|
||||
description = "Sidebar category name. Using the name of the existing category will add panel there",
|
||||
description = "Sidebar category name. Using the name of the existing category will add the panel there",
|
||||
default = "Edit",
|
||||
update = update_sidebar_category,
|
||||
)
|
||||
@@ -44,87 +52,67 @@ class BoolToolPreferences(bpy.types.AddonPreferences):
|
||||
# Defaults
|
||||
solver: bpy.props.EnumProperty(
|
||||
name = "Boolean Solver",
|
||||
description = "Which solver to use for automatic and brush booleans",
|
||||
description = "Which solver to use for automatic and brush Boolean operators",
|
||||
items = [('FLOAT', "Float", ""),
|
||||
('EXACT', "Exact", ""),
|
||||
('MANIFOLD', "Manifold", "")],
|
||||
default = 'FLOAT',
|
||||
)
|
||||
wireframe: bpy.props.BoolProperty(
|
||||
name = "Display Cutters as Wireframe",
|
||||
description = ("When enabled cutters will be displayed as wireframes, instead of bounding boxes.\n"
|
||||
"It's better for visualizating the shape, but might be harder to see and have performance cost"),
|
||||
default = False,
|
||||
display: bpy.props.EnumProperty(
|
||||
name = "Cutter Display",
|
||||
items = (('WIRE', "Wire", "Display the cutter object as a wireframe"),
|
||||
('BOUNDS', "Bounds", "Display only the bounds of the cutter object")),
|
||||
default = 'BOUNDS'
|
||||
)
|
||||
show_in_editmode: bpy.props.BoolProperty(
|
||||
name = "Enable 'Show in Edit Mode' by Default",
|
||||
description = "Every new boolean modifier created with brush boolean wil have 'Show in Edit Mode' enabled by default",
|
||||
description = "Added Boolean modifiers will have 'Show in Edit Mode' enabled by default",
|
||||
default = True,
|
||||
)
|
||||
|
||||
# Advanced
|
||||
use_collection: bpy.props.BoolProperty(
|
||||
name = "Put Cutters in Collection",
|
||||
description = ("Brush boolean operators will put all cutters in same collection, and create one if it doesn't exist.\n"
|
||||
"Useful for scene management, and quickly selecting and removing all clutter when needed"),
|
||||
description = ("Put all cutters in the same collection, and create one if it doesn't exist"),
|
||||
default = True,
|
||||
)
|
||||
collection_name: bpy.props.StringProperty(
|
||||
name = "Collection Name",
|
||||
description = "Name of the collection where cutters will be added",
|
||||
default = "boolean_cutters",
|
||||
)
|
||||
parent: bpy.props.BoolProperty(
|
||||
name = "Parent Cutters to Object",
|
||||
description = ("Cutters will be parented to first canvas they're applied to. Works best when one cutter is used one canvas.\n"
|
||||
"NOTE: This doesn't affect Carver tool, which has its own property for this"),
|
||||
description = ("Cutters will be parented to first canvas they're applied to"),
|
||||
default = True,
|
||||
)
|
||||
apply_order: bpy.props.EnumProperty(
|
||||
name = "When Applying Cutters...",
|
||||
description = ("What happens when boolean cutters are applied on object.\n"
|
||||
"Either when performing auto-boolean, using 'Apply All Cutters' operator.\n"
|
||||
"NOTE: This doesn't apply to Carver tool on 'Destructive' mode; or when applying individual cutters"),
|
||||
items = (('ALL', "Apply All Modifiers", "All modifiers on object will be applied (this includes shape keys as well)"),
|
||||
('BEFORE', "Apply Booleans & Everything Before", "Alongside boolean modifiers all modifiers will be applied that come before the last boolean"),
|
||||
('BOOLEANS', "Only Apply Booleans", "Only apply boolean modifiers. This method will fail if object has shape keys")),
|
||||
name = "Apply Modifiers",
|
||||
description = ("Which modifiers to apply when using add-ons destructive operators.\n"
|
||||
"NOTE: This option is not used when applying individual cutters"),
|
||||
items = (('ALL', "All",
|
||||
"All modifiers on object (and shape keys) will be applied during destructive operators"),
|
||||
('BEFORE', "Booleans & Everything Before",
|
||||
"Apply all modifiers that come before the last Boolean modifier in the stack"),
|
||||
('BOOLEANS', "Booleans",
|
||||
"Only apply Boolean modifiers")),
|
||||
default = 'ALL',
|
||||
)
|
||||
pin: bpy.props.BoolProperty(
|
||||
name = "Pin Boolean Modifiers",
|
||||
description = ("When enabled boolean modifiers will be placed above every other modifier on the object (if there are any).\n"
|
||||
"Order of modifiers can drastically affect the result (especially when performing auto boolean).\n"
|
||||
"NOTE: This doesn't affect Carver tool, which has its own property for this"),
|
||||
description = ("Always make new Boolean modifiers first in the modifier stack.\n"
|
||||
"NOTE: Order of modifiers can drastically affect the final result"),
|
||||
default = False,
|
||||
)
|
||||
|
||||
# Features
|
||||
fast_modifier_apply: bpy.props.BoolProperty(
|
||||
name = "Faster Destructive Booleans",
|
||||
description = ("Experimental method of applying modifiers that results in 30-50% faster destructive booleans.\n"
|
||||
"Performance improvements also affect the add-ons operators that apply cutters.\n"
|
||||
description = ("Experimental method of applying modifiers that results in 30-50% faster destructive Booleans.\n"
|
||||
"However, changing modifier properties in the redo panel (like material transfer)\n"
|
||||
"is not available for this method yet."),
|
||||
default = False,
|
||||
)
|
||||
double_click: bpy.props.BoolProperty(
|
||||
name = "Double-click Select",
|
||||
description = ("Select boolean cutters by dbl-clicking on the boolean modifier.\n"
|
||||
"This feature works in entire modifier properties area, not just on boolean modifier header,\n"
|
||||
"therefore can result in lot of misclicks and unintended selections."),
|
||||
default = False,
|
||||
)
|
||||
|
||||
# Debug
|
||||
versioning: bpy.props.BoolProperty(
|
||||
name = "Versioning",
|
||||
description = ("Because of the drastic changes in add-on data, it's necessary to do versioning when loading old files\n"
|
||||
"where Bool Tool cutters(brushes) are not applied. If you don't have files like that, you can ignore this")
|
||||
)
|
||||
experimental: bpy.props.BoolProperty(
|
||||
name = "Experimental",
|
||||
description = "Enable experimental features",
|
||||
default = False,
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
@@ -132,6 +120,12 @@ class BoolToolPreferences(bpy.types.AddonPreferences):
|
||||
layout.use_property_decorate = False
|
||||
|
||||
# UI
|
||||
row = layout.row()
|
||||
row.prop(self, "category", expand=True)
|
||||
layout.separator()
|
||||
|
||||
# Add-on Properties
|
||||
if self.category == 'ADDON':
|
||||
col = layout.column(align=True, heading="Show in Sidebar")
|
||||
row = col.row(align=True)
|
||||
sub = row.row(align=True)
|
||||
@@ -140,47 +134,43 @@ class BoolToolPreferences(bpy.types.AddonPreferences):
|
||||
sub.active = self.show_in_sidebar
|
||||
sub.prop(self, "sidebar_category", text="")
|
||||
|
||||
# Defaults
|
||||
layout.separator()
|
||||
col = layout.column(align=True)
|
||||
row = col.row(align=True)
|
||||
row.prop(self, "solver", text="Solver", expand=True)
|
||||
col.prop(self, "wireframe")
|
||||
col.prop(self, "show_in_editmode")
|
||||
col.separator()
|
||||
col = layout.column(align=True, heading="Features")
|
||||
col.prop(self, "fast_modifier_apply")
|
||||
|
||||
# Advanced
|
||||
layout.separator()
|
||||
# Shared Properties
|
||||
if self.category == 'SHARED':
|
||||
col = layout.column()
|
||||
col.prop(self, "show_in_editmode")
|
||||
col.prop(self, "apply_order")
|
||||
|
||||
# Boolean Operator Properties
|
||||
if self.category == 'OPERATORS':
|
||||
col = layout.column(align=True, heading="Put Cutters in Collection")
|
||||
row = col.row(align=True)
|
||||
sub = row.row(align=True)
|
||||
sub.prop(self, "use_collection", text="")
|
||||
sub = sub.row(align=True)
|
||||
sub.active = self.show_in_sidebar
|
||||
sub.prop(self, "collection_name", text="")
|
||||
sub.prop(self, "collection_name", text="", placeholder="boolean_cutters")
|
||||
|
||||
col = layout.column()
|
||||
row = col.row(align=True)
|
||||
row.prop(self, "solver", text="Solver", expand=True)
|
||||
row = col.row(align=True)
|
||||
row.prop(self, "display", expand=True)
|
||||
|
||||
col = layout.column()
|
||||
col.prop(self, "parent")
|
||||
col.prop(self, "apply_order")
|
||||
col.prop(self, "pin")
|
||||
|
||||
# Features
|
||||
layout.separator()
|
||||
col = layout.column(align=True, heading="Features")
|
||||
col.prop(self, "fast_modifier_apply")
|
||||
col.prop(self, "double_click")
|
||||
|
||||
# Experimentals
|
||||
layout.separator()
|
||||
col = layout.column(align=True)
|
||||
col.prop(self, "versioning", text="⚠ Versioning")
|
||||
col.prop(self, "experimental", text="⚠ Experimental")
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
classes = [
|
||||
classes = (
|
||||
BoolToolPreferences,
|
||||
]
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
|
||||
@@ -32,13 +32,15 @@ class OBJECT_PG_booleans(bpy.types.PropertyGroup):
|
||||
default = False,
|
||||
)
|
||||
|
||||
modifiers_list_index: bpy.props.IntProperty()
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
classes = [
|
||||
classes = (
|
||||
OBJECT_PG_booleans,
|
||||
]
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
|
||||
@@ -21,12 +21,12 @@ else:
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
"""NOTE: Order of modules is important because of dependancies. Don't change without a reason."""
|
||||
modules = [
|
||||
modules = (
|
||||
carver_circle,
|
||||
carver_box,
|
||||
carver_polyline,
|
||||
ui,
|
||||
]
|
||||
)
|
||||
|
||||
main_tools = [
|
||||
carver_box.OBJECT_WT_carve_box,
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import bpy
|
||||
import os
|
||||
from mathutils import Vector
|
||||
from .. import __file__ as base_file
|
||||
|
||||
from ..constants import (
|
||||
ICONS_PATH,
|
||||
)
|
||||
from ..functions.view import (
|
||||
redraw_regions,
|
||||
)
|
||||
|
||||
from .common.base import (
|
||||
CarverBase,
|
||||
)
|
||||
from .common.properties import (
|
||||
CarverPropsArray,
|
||||
CarverPropsBevel,
|
||||
)
|
||||
from .common.types import (
|
||||
@@ -34,7 +39,7 @@ class OBJECT_WT_carve_box(bpy.types.WorkSpaceTool):
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_context_mode = 'OBJECT'
|
||||
|
||||
bl_icon = os.path.join(os.path.dirname(base_file), "icons", "tool_icons", "ops.object.carver_box")
|
||||
bl_icon = os.path.join(ICONS_PATH, "dat", "ops.object.carver_box")
|
||||
bl_keymap = (
|
||||
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, {"properties": None}),
|
||||
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True}, {"properties": None}),
|
||||
@@ -58,9 +63,7 @@ class MESH_WT_carve_box(OBJECT_WT_carve_box):
|
||||
|
||||
#### ------------------------------ OPERATORS ------------------------------ ####
|
||||
|
||||
class OBJECT_OT_carve_box(CarverBase,
|
||||
CarverPropsArray,
|
||||
CarverPropsBevel):
|
||||
class OBJECT_OT_carve_box(CarverBase, CarverPropsBevel):
|
||||
bl_idname = "object.carve_box"
|
||||
bl_label = "Box Carve"
|
||||
bl_description = description
|
||||
@@ -70,6 +73,7 @@ class OBJECT_OT_carve_box(CarverBase,
|
||||
# SHAPE-properties
|
||||
shape = 'BOX'
|
||||
|
||||
# NOTE: There are registered on operator level because they need to be overriden or hidden per-tool.
|
||||
aspect: bpy.props.EnumProperty(
|
||||
name = "Aspect",
|
||||
description = "The initial aspect",
|
||||
@@ -115,8 +119,8 @@ class OBJECT_OT_carve_box(CarverBase,
|
||||
# cached_variables
|
||||
"""Important for storing context as it was when operator was invoked (untouched by the modal)."""
|
||||
self.phase = "DRAW"
|
||||
self.initial_origin = self.origin # Initial shape origin.
|
||||
self.initial_aspect = self.aspect # Initial shape aspect.
|
||||
self._initial_origin = self.origin # Initial shape origin.
|
||||
self._initial_aspect = self.aspect # Initial shape aspect.
|
||||
self._stored_phase = "DRAW"
|
||||
|
||||
# Add Draw Handler
|
||||
@@ -130,12 +134,11 @@ class OBJECT_OT_carve_box(CarverBase,
|
||||
|
||||
|
||||
def modal(self, context, event):
|
||||
redraw_regions(context)
|
||||
|
||||
# Status Bar Text
|
||||
self.status(context)
|
||||
|
||||
# find_the_limit_of_the_3d_viewport_region
|
||||
self.redraw_region(context)
|
||||
|
||||
# Modifier Keys
|
||||
self.event_aspect(context, event)
|
||||
self.event_origin(context, event)
|
||||
@@ -151,7 +154,6 @@ class OBJECT_OT_carve_box(CarverBase,
|
||||
if self.phase != "BEVEL":
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
|
||||
# Mouse Move
|
||||
if event.type == 'MOUSEMOVE':
|
||||
self.mouse.current = Vector((event.mouse_region_x, event.mouse_region_y))
|
||||
@@ -164,7 +166,6 @@ class OBJECT_OT_carve_box(CarverBase,
|
||||
elif self.phase == "EXTRUDE":
|
||||
self.set_extrusion_depth(context)
|
||||
|
||||
|
||||
# Confirm
|
||||
elif event.type == 'LEFTMOUSE':
|
||||
# Confirm Shape
|
||||
@@ -178,7 +179,7 @@ class OBJECT_OT_carve_box(CarverBase,
|
||||
min_distance = 5
|
||||
|
||||
if delta_x < min_distance or delta_y < min_distance:
|
||||
self.finalize(context, clean_up=True, abort=True)
|
||||
self.finalize(context, abort=True)
|
||||
return {'FINISHED'}
|
||||
|
||||
self.extrude_cutter(context)
|
||||
@@ -188,18 +189,15 @@ class OBJECT_OT_carve_box(CarverBase,
|
||||
if self.depth != 'MANUAL':
|
||||
self.confirm(context)
|
||||
return {'FINISHED'}
|
||||
else:
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
# Confirm Depth
|
||||
if self.phase == "EXTRUDE" and event.value == 'PRESS':
|
||||
self.confirm(context)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
# Cancel
|
||||
elif event.type in {'RIGHTMOUSE', 'ESC'}:
|
||||
self.finalize(context, clean_up=True, abort=True)
|
||||
self.finalize(context, abort=True)
|
||||
return {'FINISHED'}
|
||||
|
||||
return {'RUNNING_MODAL'}
|
||||
@@ -305,9 +303,9 @@ class OBJECT_OT_carve_box(CarverBase,
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
classes = [
|
||||
classes = (
|
||||
OBJECT_OT_carve_box,
|
||||
]
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import bpy
|
||||
import os
|
||||
from .. import __file__ as base_file
|
||||
|
||||
from ..constants import (
|
||||
ICONS_PATH,
|
||||
)
|
||||
|
||||
from .common.ui import (
|
||||
carver_ui_common,
|
||||
@@ -20,7 +23,7 @@ class OBJECT_WT_carve_circle(bpy.types.WorkSpaceTool):
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_context_mode = 'OBJECT'
|
||||
|
||||
bl_icon = os.path.join(os.path.dirname(base_file), "icons", "tool_icons", "ops.object.carver_circle")
|
||||
bl_icon = os.path.join(ICONS_PATH, "dat", "ops.object.carver_circle")
|
||||
bl_keymap = (
|
||||
("object.carve_circle", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, {"properties": None}),
|
||||
("object.carve_circle", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True}, {"properties": None}),
|
||||
@@ -51,6 +54,7 @@ class OBJECT_OT_carve_circle(OBJECT_OT_carve_box):
|
||||
# SHAPE-properties
|
||||
shape = 'CIRCLE'
|
||||
|
||||
# NOTE: There are registered on operator level because they need to be overriden or hidden per-tool.
|
||||
subdivision: bpy.props.IntProperty(
|
||||
name = "Circle Subdivisions",
|
||||
description = "Number of vertices that will make up the circular shape that will be extruded into a cylinder",
|
||||
@@ -76,9 +80,9 @@ class OBJECT_OT_carve_circle(OBJECT_OT_carve_box):
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
classes = [
|
||||
classes = (
|
||||
OBJECT_OT_carve_circle,
|
||||
]
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import bpy
|
||||
import math
|
||||
import os
|
||||
from mathutils import Vector
|
||||
from bpy_extras import view3d_utils
|
||||
from .. import __file__ as base_file
|
||||
|
||||
from ..constants import (
|
||||
ICONS_PATH,
|
||||
)
|
||||
from ..functions.view import (
|
||||
redraw_regions,
|
||||
)
|
||||
|
||||
from .common.base import (
|
||||
CarverBase,
|
||||
)
|
||||
from .common.properties import (
|
||||
CarverPropsArray,
|
||||
)
|
||||
from .common.types import (
|
||||
Selection,
|
||||
Mouse,
|
||||
@@ -35,7 +37,7 @@ class OBJECT_WT_carve_polyline(bpy.types.WorkSpaceTool):
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_context_mode = 'OBJECT'
|
||||
|
||||
bl_icon = os.path.join(os.path.dirname(base_file), "icons", "tool_icons", "ops.object.carver_polyline")
|
||||
bl_icon = os.path.join(ICONS_PATH, "dat", "ops.object.carver_polyline")
|
||||
bl_keymap = (
|
||||
("object.carve_polyline", {"type": 'LEFTMOUSE', "value": 'CLICK'}, None),
|
||||
("object.carve_polyline", {"type": 'LEFTMOUSE', "value": 'CLICK', "ctrl": True}, None),
|
||||
@@ -57,8 +59,7 @@ class MESH_WT_carve_polyline(OBJECT_WT_carve_polyline):
|
||||
|
||||
#### ------------------------------ OPERATORS ------------------------------ ####
|
||||
|
||||
class OBJECT_OT_carve_polyline(CarverBase,
|
||||
CarverPropsArray):
|
||||
class OBJECT_OT_carve_polyline(CarverBase):
|
||||
bl_idname = "object.carve_polyline"
|
||||
bl_label = "Polyline Carve"
|
||||
bl_description = description
|
||||
@@ -108,12 +109,11 @@ class OBJECT_OT_carve_polyline(CarverBase,
|
||||
|
||||
|
||||
def modal(self, context, event):
|
||||
redraw_regions(context)
|
||||
|
||||
# Status Bar Text
|
||||
self.status(context)
|
||||
|
||||
# find_the_limit_of_the_3d_viewport_region
|
||||
self.redraw_region(context)
|
||||
|
||||
# Modifier Keys
|
||||
self.event_array(context, event)
|
||||
self.event_move(context, event)
|
||||
@@ -124,7 +124,6 @@ class OBJECT_OT_carve_polyline(CarverBase,
|
||||
if self.phase != "BEVEL":
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
|
||||
# Mouse Move
|
||||
if event.type == 'MOUSEMOVE':
|
||||
self.mouse.current = Vector((event.mouse_region_x, event.mouse_region_y))
|
||||
@@ -146,12 +145,15 @@ class OBJECT_OT_carve_polyline(CarverBase,
|
||||
elif self.phase == "EXTRUDE":
|
||||
self.set_extrusion_depth(context)
|
||||
|
||||
|
||||
# Add Points & Confirm
|
||||
elif event.type == 'LEFTMOUSE' and event.value == 'PRESS':
|
||||
if self.phase == "DRAW":
|
||||
# Confirm Shape (if clicked on the first vert)
|
||||
if self._distance_from_first > 75:
|
||||
# Add Point
|
||||
if self._distance_from_first < 75:
|
||||
self._insert_polyline_point()
|
||||
|
||||
# Confirm Shape (if clicked near the first vert).
|
||||
else:
|
||||
verts = self.cutter.verts
|
||||
if len(verts) > 3:
|
||||
self._remove_polyline_point(context, jump_mouse=False)
|
||||
@@ -165,17 +167,12 @@ class OBJECT_OT_carve_polyline(CarverBase,
|
||||
else:
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
# Add Point
|
||||
else:
|
||||
self._insert_polyline_point()
|
||||
|
||||
# Confirm Depth
|
||||
if self.phase == "EXTRUDE":
|
||||
self.confirm(context)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
# Confirm
|
||||
# Enter (Confirm)
|
||||
elif event.type == 'RET':
|
||||
verts = self.cutter.verts
|
||||
if len(verts) > 2:
|
||||
@@ -195,18 +192,14 @@ class OBJECT_OT_carve_polyline(CarverBase,
|
||||
if self.phase == "EXTRUDE" and event.value == 'PRESS':
|
||||
self.confirm(context)
|
||||
return {'FINISHED'}
|
||||
else:
|
||||
self.report({'WARNING'}, "At least three points are required to make a polygonal shape")
|
||||
|
||||
|
||||
# Remove Last Point
|
||||
if event.type == 'BACK_SPACE' and event.value == 'PRESS':
|
||||
self._remove_polyline_point(context)
|
||||
|
||||
|
||||
# Cancel
|
||||
elif event.type in {'RIGHTMOUSE', 'ESC'}:
|
||||
self.finalize(context, clean_up=True, abort=True)
|
||||
self.finalize(context, abort=True)
|
||||
return {'FINISHED'}
|
||||
|
||||
return {'RUNNING_MODAL'}
|
||||
@@ -301,7 +294,7 @@ class OBJECT_OT_carve_polyline(CarverBase,
|
||||
context.workspace.status_text_set(modal_keys_extrude)
|
||||
|
||||
|
||||
# Polyline-specific features.
|
||||
# Polyline-specific methods.
|
||||
def _insert_polyline_point(self):
|
||||
"""Inserts a new vertex in the cutter geometry and connects it to the previous last one."""
|
||||
|
||||
@@ -383,15 +376,16 @@ class OBJECT_OT_carve_polyline(CarverBase,
|
||||
context.region_data,
|
||||
vert_world)
|
||||
if screen_pos:
|
||||
context.window.cursor_warp(int(screen_pos.x), int(screen_pos.y))
|
||||
x, y = int(screen_pos.x), int(screen_pos.y)
|
||||
context.window.cursor_warp(x, y)
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
classes = [
|
||||
classes = (
|
||||
OBJECT_OT_carve_polyline,
|
||||
]
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
|
||||
@@ -1,33 +1,22 @@
|
||||
import bpy
|
||||
import bmesh
|
||||
import math
|
||||
import mathutils
|
||||
from bpy_extras import view3d_utils
|
||||
from mathutils import Vector, Matrix
|
||||
|
||||
from .types import (
|
||||
Effects,
|
||||
from ...functions.cutter import (
|
||||
make_cutter,
|
||||
)
|
||||
from .properties import (
|
||||
CarverPropsOperator,
|
||||
CarverPropsShape,
|
||||
CarverPropsModifier,
|
||||
CarverPropsCutter,
|
||||
)
|
||||
|
||||
from ...functions.draw import (
|
||||
draw_shader,
|
||||
draw_bmesh_faces,
|
||||
draw_circle_around_point,
|
||||
)
|
||||
from ...functions.math import (
|
||||
distance_from_point_to_segment,
|
||||
region_2d_to_plane_3d,
|
||||
region_2d_to_line_3d,
|
||||
draw_circle_billboard,
|
||||
)
|
||||
from ...functions.mesh import (
|
||||
is_instanced_mesh,
|
||||
extrude_face,
|
||||
are_intersecting,
|
||||
raycast,
|
||||
)
|
||||
from ...functions.modifier import (
|
||||
add_boolean_modifier,
|
||||
@@ -35,300 +24,44 @@ from ...functions.modifier import (
|
||||
get_modifiers_to_apply,
|
||||
)
|
||||
from ...functions.object import (
|
||||
set_cutter_properties,
|
||||
delete_empty_collection,
|
||||
delete_cutter,
|
||||
set_object_origin,
|
||||
)
|
||||
from ...functions.poll import (
|
||||
is_linked,
|
||||
is_instanced_data,
|
||||
set_object_origin,
|
||||
delete_object,
|
||||
)
|
||||
from ...functions.scene import (
|
||||
delete_empty_collection,
|
||||
raycast,
|
||||
)
|
||||
from ...functions.view import (
|
||||
distance_from_point_to_segment,
|
||||
region_2d_to_ray_3d,
|
||||
region_2d_to_plane_3d,
|
||||
)
|
||||
|
||||
from .events import (
|
||||
CarverEvents,
|
||||
)
|
||||
from .properties import (
|
||||
CarverPropsOperator,
|
||||
CarverPropsShape,
|
||||
CarverPropsModifier,
|
||||
CarverPropsCutter,
|
||||
CarverPropsArray,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ /base/ ------------------------------ ####
|
||||
|
||||
class CarverEvents():
|
||||
|
||||
def _custom_modifier_event(self, context, event, modifier,
|
||||
cursor='NONE', store_values=False, restore_mouse=True,
|
||||
postprocess=None):
|
||||
"""Creates custom modifier event when key is held and hides cursor until it's released"""
|
||||
|
||||
# Initialize Modifier Phase
|
||||
if event.value == 'PRESS':
|
||||
if self.phase in ("DRAW", "EXTRUDE"):
|
||||
self._stored_phase = self.phase
|
||||
self.phase = modifier
|
||||
self.mouse.cached = self.mouse.current
|
||||
context.window.cursor_set(cursor)
|
||||
|
||||
if store_values:
|
||||
# Store center of the geometry as a Vector.
|
||||
verts = [v.co for v in self.cutter.bm.verts]
|
||||
center = sum(verts, Vector()) / len(verts)
|
||||
self.cutter.center = self.cutter.obj.matrix_world @ center
|
||||
|
||||
# End Modifier Phase
|
||||
elif event.value == 'RELEASE':
|
||||
if self.phase == modifier:
|
||||
context.window.cursor_set('MUTE')
|
||||
if restore_mouse:
|
||||
context.window.cursor_warp(int(self.mouse.cached[0]), int(self.mouse.cached[1]) + 100)
|
||||
self.mouse.current = self.mouse.cached
|
||||
self.phase = self._stored_phase
|
||||
|
||||
if postprocess is not None:
|
||||
postprocess(self)
|
||||
|
||||
return self._stored_phase
|
||||
|
||||
|
||||
# Individual Events
|
||||
def event_aspect(self, context, event):
|
||||
"""Modifier keys for changing aspect of the shape"""
|
||||
|
||||
if event.shift and self.phase == "DRAW":
|
||||
if self.initial_aspect == 'FREE':
|
||||
self.aspect = 'FIXED'
|
||||
elif self.initial_aspect == 'FIXED':
|
||||
self.aspect = 'FREE'
|
||||
else:
|
||||
self.aspect = self.initial_aspect
|
||||
|
||||
|
||||
def event_origin(self, context, event):
|
||||
"""Modifier keys for changing the origin of the shape"""
|
||||
|
||||
if event.alt and self.phase == "DRAW":
|
||||
if self.initial_origin == 'EDGE':
|
||||
self.origin = 'CENTER'
|
||||
elif self.initial_origin == 'CENTER':
|
||||
self.origin = 'EDGE'
|
||||
else:
|
||||
self.origin = self.initial_origin
|
||||
|
||||
|
||||
def event_rotate(self, context, event):
|
||||
"""Modifier keys for rotating the shape"""
|
||||
|
||||
def _remove_rotate_phase_properties(self):
|
||||
del self._stored_mouse_pos_3d
|
||||
del self._stored_rotation
|
||||
del self._stored_cutter_center
|
||||
del self._stored_cutter_euler
|
||||
|
||||
# Restore origin at edge (first vertex).
|
||||
if self.origin == 'EDGE':
|
||||
point = self.cutter.bm.verts[0].co
|
||||
set_object_origin(self.cutter.obj, self.cutter.bm, point='CUSTOM', custom=point)
|
||||
|
||||
|
||||
# Set correct phase.
|
||||
if event.type == 'R':
|
||||
stored_phase = self._custom_modifier_event(context, event, "ROTATE",
|
||||
cursor='MOVE_X', store_values=True, restore_mouse=False,
|
||||
postprocess=_remove_rotate_phase_properties)
|
||||
|
||||
if self.phase == "ROTATE":
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
|
||||
# Project current mouse position onto the workplane.
|
||||
current_mouse_pos_3d = region_2d_to_plane_3d(region, rv3d,
|
||||
self.mouse.current,
|
||||
(self.workplane.location, self.workplane.normal))
|
||||
if current_mouse_pos_3d is not None:
|
||||
# Store values.
|
||||
obj = self.cutter.obj
|
||||
if not hasattr(self, '_stored_mouse_pos_3d'):
|
||||
self._stored_mouse_pos_3d = current_mouse_pos_3d.copy()
|
||||
self._stored_rotation = self.rotation
|
||||
self._stored_cutter_center = self.cutter.center
|
||||
self._stored_cutter_euler = obj.rotation_euler.copy()
|
||||
|
||||
# Calculate angle and direction.
|
||||
to_start = (self._stored_mouse_pos_3d - self._stored_cutter_center).normalized()
|
||||
to_current = (current_mouse_pos_3d - self._stored_cutter_center).normalized()
|
||||
|
||||
angle = to_start.angle(to_current)
|
||||
cross = to_start.cross(to_current)
|
||||
if cross.dot(self.workplane.normal) < 0:
|
||||
angle = -angle
|
||||
|
||||
if abs(angle) > 0.0001:
|
||||
self.rotation = self._stored_rotation + angle
|
||||
|
||||
# Offset the object location when drawing from edge to move rotation pivot to center.
|
||||
if self.origin == 'EDGE':
|
||||
set_object_origin(obj, self.cutter.bm, point='CENTER_OBJ')
|
||||
|
||||
# Calculate rotation amount.
|
||||
rotation_total = Matrix.Rotation(self.rotation, 4, self.workplane.normal)
|
||||
rotation_stored = Matrix.Rotation(self._stored_rotation, 4, self.workplane.normal)
|
||||
rotation_matrix = rotation_total @ rotation_stored.inverted()
|
||||
new_rot = rotation_matrix @ self._stored_cutter_euler.to_matrix().to_4x4()
|
||||
|
||||
# Rotate.
|
||||
obj.rotation_euler = new_rot.to_euler()
|
||||
|
||||
|
||||
def event_bevel(self, context, event):
|
||||
"""Modifier keys for beveling the shape"""
|
||||
|
||||
def _remove_empty_bevel_modifier(self):
|
||||
bevel = self.effects.bevel
|
||||
if bevel.width == 0:
|
||||
self.cutter.obj.modifiers.remove(bevel)
|
||||
self.effects.bevel = None
|
||||
|
||||
if self.effects.weld is not None:
|
||||
self.cutter.obj.modifiers.remove(self.effects.weld)
|
||||
self.effects.weld = None
|
||||
|
||||
|
||||
if self.shape != 'BOX':
|
||||
return
|
||||
|
||||
# Set correct phase.
|
||||
if event.type == 'B':
|
||||
stored_phase = self._custom_modifier_event(context, event, "BEVEL",
|
||||
cursor='PICK_AREA', store_values=True,
|
||||
postprocess=_remove_empty_bevel_modifier)
|
||||
|
||||
if self.phase == "BEVEL":
|
||||
self.use_bevel = True
|
||||
|
||||
# Initialize bevel effect if it doesn't exist.
|
||||
if self.effects.bevel is None:
|
||||
self.bevel_width = 0
|
||||
affect = 'VERTICES' if stored_phase == "DRAW" else 'EDGES'
|
||||
self.effects.add_bevel_modifier(self, affect=affect)
|
||||
|
||||
# Force the geometry to update.
|
||||
if stored_phase == "DRAW":
|
||||
self.update_cutter_shape(context)
|
||||
elif stored_phase == "EXTRUDE":
|
||||
self.set_extrusion_depth(context)
|
||||
|
||||
# Calculate bevel width.
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
|
||||
self.mouse.cached_3d = view3d_utils.region_2d_to_location_3d(region, rv3d, self.mouse.cached, self.cutter.center)
|
||||
self.mouse.current_3d = view3d_utils.region_2d_to_location_3d(region, rv3d, self.mouse.current, self.cutter.center)
|
||||
d = (self.cutter.center - self.mouse.current_3d).length - (self.cutter.center - self.mouse.cached_3d).length
|
||||
self.bevel_width = d * 0.2
|
||||
|
||||
# Adjust bevel segments.
|
||||
if event.type == 'WHEELUPMOUSE':
|
||||
self.bevel_segments += 1
|
||||
elif event.type == 'WHEELDOWNMOUSE':
|
||||
self.bevel_segments -= 1
|
||||
|
||||
# Update modifier.
|
||||
self.effects.update(self, 'BEVEL')
|
||||
|
||||
|
||||
def event_array(self, context, event):
|
||||
"""Modifier keys for creating the array of the shape"""
|
||||
|
||||
# Add duplicates.
|
||||
if event.type == 'LEFT_ARROW' and event.value == 'PRESS':
|
||||
self.columns -= 1
|
||||
if event.type == 'RIGHT_ARROW' and event.value == 'PRESS':
|
||||
self.columns += 1
|
||||
if event.type == 'DOWN_ARROW' and event.value == 'PRESS':
|
||||
self.rows -= 1
|
||||
if event.type == 'UP_ARROW' and event.value == 'PRESS':
|
||||
self.rows += 1
|
||||
|
||||
if event.type in ['LEFT_ARROW',
|
||||
'RIGHT_ARROW',
|
||||
'DOWN_ARROW',
|
||||
'UP_ARROW',] and event.value == 'PRESS':
|
||||
self.effects.update(self, 'ARRAY_COUNT')
|
||||
|
||||
# Force the geometry to update.
|
||||
if self.phase == "DRAW":
|
||||
self.update_cutter_shape(context)
|
||||
elif self.phase == "EXTRUDE":
|
||||
self.set_extrusion_depth(context)
|
||||
|
||||
# Adjust gap.
|
||||
if (self.rows > 1 or self.columns > 1) and (event.type == 'A'):
|
||||
stored_phase = self._custom_modifier_event(context, event, "ARRAY", cursor='MUTE',
|
||||
store_values=True, restore_mouse=True)
|
||||
|
||||
if self.phase == "ARRAY":
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
|
||||
self.mouse.cached_3d = view3d_utils.region_2d_to_location_3d(region, rv3d, self.mouse.cached, self.cutter.center)
|
||||
self.mouse.current_3d = view3d_utils.region_2d_to_location_3d(region, rv3d, self.mouse.current, self.cutter.center)
|
||||
d = (self.cutter.center - self.mouse.current_3d).length - (self.cutter.center - self.mouse.cached_3d).length
|
||||
self.gap = 1 + (d * 0.2)
|
||||
|
||||
self.effects.update(self, 'ARRAY_GAP')
|
||||
|
||||
|
||||
def event_flip(self, context, event):
|
||||
"""Modifier keys for flipping the direction of extrusion."""
|
||||
|
||||
if event.type == 'F' and event.value == 'PRESS':
|
||||
if self.phase == 'EXTRUDE':
|
||||
self.flip_direction = not self.flip_direction
|
||||
|
||||
|
||||
def event_move(self, context, event):
|
||||
"""Modifier keys for moving the cutter shape."""
|
||||
|
||||
def _remove_move_phase_properties(self):
|
||||
del self._stored_cutter_location
|
||||
self.mouse.cached_3d = None
|
||||
|
||||
|
||||
if event.type == 'SPACE':
|
||||
stored_phase = self._custom_modifier_event(context, event, "MOVE",
|
||||
cursor='SCROLL_XY', restore_mouse=False,
|
||||
postprocess=_remove_move_phase_properties)
|
||||
|
||||
if self.phase == "MOVE":
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
|
||||
# Project current mouse position onto the workplane.
|
||||
current_mouse_pos_3d = region_2d_to_plane_3d(region, rv3d,
|
||||
self.mouse.current,
|
||||
(self.workplane.location, self.workplane.normal))
|
||||
if current_mouse_pos_3d is not None:
|
||||
if not hasattr(self, '_stored_cutter_location'):
|
||||
self.mouse.cached_3d = current_mouse_pos_3d.copy()
|
||||
self._stored_cutter_location = self.cutter.obj.location.copy()
|
||||
|
||||
offset = current_mouse_pos_3d - self.mouse.cached_3d
|
||||
self.cutter.obj.location = self._stored_cutter_location + offset
|
||||
|
||||
#### ------------------------------ CLASSES ------------------------------ ####
|
||||
|
||||
class CarverBase(bpy.types.Operator,
|
||||
CarverEvents,
|
||||
CarverPropsOperator,
|
||||
CarverPropsShape,
|
||||
CarverPropsModifier,
|
||||
CarverPropsCutter):
|
||||
CarverPropsCutter,
|
||||
CarverPropsArray):
|
||||
"""Base class for Carver operators."""
|
||||
|
||||
def redraw_region(self, context):
|
||||
"""Redraw the region to find the limits of the 3D viewport."""
|
||||
|
||||
region_types = {'WINDOW', 'UI'}
|
||||
for area in context.window.screen.areas:
|
||||
if area.type == 'VIEW_3D':
|
||||
for region in area.regions:
|
||||
if not region_types or region.type in region_types:
|
||||
region.tag_redraw()
|
||||
|
||||
|
||||
# Core Methods
|
||||
def validate_selection(self, context):
|
||||
"""Filter out selection to get the list of viable canvases."""
|
||||
|
||||
@@ -349,10 +82,14 @@ class CarverBase(bpy.types.Operator,
|
||||
continue
|
||||
|
||||
if self.mode == 'DESTRUCTIVE':
|
||||
if is_instanced_data(obj):
|
||||
if is_instanced_mesh(obj.data):
|
||||
self.report({'WARNING'}, f"Modifiers cannot be applied to {obj.name} because it has instanced object data")
|
||||
continue
|
||||
|
||||
if obj.data.shape_keys:
|
||||
self.report({'WARNING'}, f"Modifiers cannot be applied to {obj.name} because it has shape keys")
|
||||
continue
|
||||
|
||||
selected.append(obj)
|
||||
|
||||
# Ensure the active object.
|
||||
@@ -367,11 +104,10 @@ class CarverBase(bpy.types.Operator,
|
||||
return selected, active
|
||||
|
||||
|
||||
# Core Methods
|
||||
def calculate_workplane(self, context):
|
||||
"""
|
||||
Calculates matrix, location (origin point), and normal (direction)
|
||||
of the workplane based on the active alignment method.
|
||||
of the workplane based on the chosen alignment method.
|
||||
"""
|
||||
|
||||
if self.alignment == 'SURFACE':
|
||||
@@ -393,7 +129,7 @@ class CarverBase(bpy.types.Operator,
|
||||
|
||||
|
||||
def create_cutter(self, context):
|
||||
"""Creates a cutter object with correct properties & initializes `bmesh` geometry."""
|
||||
"""Creates a cutter object with correct properties & initializes `bmesh`."""
|
||||
|
||||
# Create the Mesh & bmesh
|
||||
mesh = bpy.data.meshes.new(name="boolean_cutter")
|
||||
@@ -402,15 +138,15 @@ class CarverBase(bpy.types.Operator,
|
||||
# Create the Object
|
||||
obj = bpy.data.objects.new("boolean_cutter", mesh)
|
||||
obj.matrix_world = self.workplane.matrix
|
||||
set_cutter_properties(context, obj, "Difference", collection=True,
|
||||
make_cutter(context, obj, "Difference", collection=True,
|
||||
display='WIRE' if self.shape == 'POLYLINE' else self.display)
|
||||
obj.booleans.carver = True
|
||||
|
||||
# Initial Rotation
|
||||
if self.rotation != 0:
|
||||
rotation_matrix = Matrix.Rotation(self.rotation, 4, self.workplane.normal)
|
||||
temp_matrix_world = rotation_matrix @ obj.matrix_world
|
||||
obj.rotation_euler = temp_matrix_world.to_euler()
|
||||
rotation_matrix = rotation_matrix @ obj.matrix_world
|
||||
obj.rotation_euler = rotation_matrix.to_euler()
|
||||
|
||||
# Create Verts
|
||||
if self.shape == 'BOX':
|
||||
@@ -478,11 +214,14 @@ class CarverBase(bpy.types.Operator,
|
||||
if vertices is not None:
|
||||
draw_shader('LINES', (0.00, 0.00, 0.00), 1.0, vertices)
|
||||
|
||||
# Draw Circle around First Vertex
|
||||
# Draw circle around first vertex.
|
||||
if self.shape == 'POLYLINE' and self.phase == 'DRAW':
|
||||
verts = self.cutter.verts
|
||||
if len(verts) > 3:
|
||||
vertices = draw_circle_around_point(context, obj, verts[0], self._distance_from_first, 4)
|
||||
vertices = draw_circle_billboard(context,
|
||||
obj.matrix_world @ verts[0].co,
|
||||
radius=self._distance_from_first,
|
||||
segments=4)
|
||||
if len(vertices) > 0:
|
||||
draw_shader('LINE_LOOP', (0, 0, 0), 1.0, vertices)
|
||||
|
||||
@@ -497,7 +236,7 @@ class CarverBase(bpy.types.Operator,
|
||||
bm = self.cutter.bm
|
||||
face = self.cutter.faces[0]
|
||||
|
||||
# Get the mouse positon on the workplane.
|
||||
# Get the mouse positon x, y on the workplane.
|
||||
current_mouse_pos_3d = region_2d_to_plane_3d(region, rv3d,
|
||||
self.mouse.current,
|
||||
(self.workplane.location, self.workplane.normal))
|
||||
@@ -582,7 +321,6 @@ class CarverBase(bpy.types.Operator,
|
||||
if self.alignment == 'CURSOR' and self.depth == 'CURSOR':
|
||||
self.depth = 'AUTO'
|
||||
|
||||
|
||||
# Push the extruded face towards the furthest point of the collective bounding box.
|
||||
if self.depth == 'AUTO':
|
||||
corners = []
|
||||
@@ -599,10 +337,9 @@ class CarverBase(bpy.types.Operator,
|
||||
offset = self.offset if self.alignment == 'SURFACE' else 0.1
|
||||
|
||||
for v in face.verts:
|
||||
if self.depth == 'AUTO':
|
||||
v.co += normal * (furthest_corner - offset)
|
||||
|
||||
# Push the extruded face towards the plane of 3D cursor.
|
||||
# Push the extruded face towards the plane of the 3D cursor.
|
||||
elif self.depth == 'CURSOR':
|
||||
local_cursor = self.cutter.obj.matrix_world.inverted() @ context.scene.cursor.location
|
||||
for v in extruded_verts:
|
||||
@@ -647,20 +384,24 @@ class CarverBase(bpy.types.Operator,
|
||||
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
normal = self.cutter.obj.matrix_world.to_3x3().inverted() @ self.workplane.normal
|
||||
|
||||
# Find the point on 3D line (along the normal) closest to the cursor.
|
||||
# and calculate the distance between that and the extrude origin.
|
||||
closest_points = region_2d_to_line_3d(region, rv3d,
|
||||
self.mouse.current,
|
||||
self._extrude_origin, self.workplane.normal)
|
||||
# Convert the mouse position into a 3D ray and find closest points between...
|
||||
# that ray and the line coming from the extrude origin along the workplane normal.
|
||||
ray_origin, ray_direction = region_2d_to_ray_3d(region, rv3d, self.mouse.current)
|
||||
closest_points = mathutils.geometry.intersect_line_line(ray_origin,
|
||||
ray_origin + ray_direction,
|
||||
self._extrude_origin,
|
||||
self._extrude_origin + self.workplane.normal)
|
||||
|
||||
if closest_points is None:
|
||||
return
|
||||
|
||||
# Calculate the distance between the extrude origin and mouse position.
|
||||
offset_vector = closest_points[1] - self._extrude_origin
|
||||
distance = offset_vector.dot(self.workplane.normal)
|
||||
|
||||
# Offset vertices of the extruded face from their their original coordinates.
|
||||
normal = self.cutter.obj.matrix_world.to_3x3().inverted() @ self.workplane.normal
|
||||
if distance is not None:
|
||||
for v, vert_co in zip(self._extrude_faces[-1].verts, self._extrude_verts_co):
|
||||
offset = normal * distance
|
||||
@@ -695,8 +436,11 @@ class CarverBase(bpy.types.Operator,
|
||||
ray_obj_matrix = ray.obj.matrix_world
|
||||
mesh = ray.obj.evaluated_get(context.view_layer.depsgraph).to_mesh()
|
||||
|
||||
if mesh is not None:
|
||||
# create_temporary_bmesh
|
||||
if mesh is None:
|
||||
self.alignment = 'VIEW'
|
||||
return None, None, None
|
||||
|
||||
# Create temporary `bmesh`.
|
||||
temp_bm = bmesh.new()
|
||||
temp_bm.from_mesh(mesh)
|
||||
temp_bm.faces.ensure_lookup_table()
|
||||
@@ -776,8 +520,7 @@ class CarverBase(bpy.types.Operator,
|
||||
context.scene.cursor.location)
|
||||
else:
|
||||
# Put the location at the closest point of the bounding box of all selected objects.
|
||||
ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, self.mouse.initial)
|
||||
ray_direction = view3d_utils.region_2d_to_vector_3d(region, rv3d, self.mouse.initial)
|
||||
ray_origin, ray_direction = region_2d_to_ray_3d(region, rv3d, self.mouse.initial)
|
||||
|
||||
corners = []
|
||||
for obj in self.objects.selected:
|
||||
@@ -803,8 +546,7 @@ class CarverBase(bpy.types.Operator,
|
||||
matrix = Matrix.Identity(4)
|
||||
normal = matrix.col[2].xyz
|
||||
|
||||
ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, self.mouse.initial)
|
||||
ray_direction = view3d_utils.region_2d_to_vector_3d(region, rv3d, self.mouse.initial)
|
||||
ray_origin, ray_direction = region_2d_to_ray_3d(region, rv3d, self.mouse.initial)
|
||||
t = (matrix.translation.z - ray_origin.z) / ray_direction.z
|
||||
location = ray_origin + ray_direction * t
|
||||
matrix.translation = location
|
||||
@@ -851,7 +593,6 @@ class CarverBase(bpy.types.Operator,
|
||||
|
||||
cutter = self.cutter.obj
|
||||
|
||||
# Add Modifier(s)
|
||||
for obj in self.objects.selected:
|
||||
mod = add_boolean_modifier(self, context, obj,
|
||||
cutter, "DIFFERENCE",
|
||||
@@ -877,7 +618,7 @@ class CarverBase(bpy.types.Operator,
|
||||
obj.modifiers.remove(mod)
|
||||
|
||||
if not intersecting_canvases:
|
||||
self.finalize(context, clean_up=True)
|
||||
self.finalize(context)
|
||||
return
|
||||
|
||||
# Select all faces of the cutter so that newly created faces in canvas
|
||||
@@ -920,7 +661,7 @@ class CarverBase(bpy.types.Operator,
|
||||
for obj in intersecting_canvases:
|
||||
obj.booleans.canvas = True
|
||||
|
||||
self.finalize(context)
|
||||
self.finalize(context, clean_up=False)
|
||||
return
|
||||
|
||||
elif self.mode == 'DESTRUCTIVE':
|
||||
@@ -930,11 +671,11 @@ class CarverBase(bpy.types.Operator,
|
||||
modifiers = get_modifiers_to_apply(context, obj, [modifiers])
|
||||
apply_modifiers(context, obj, modifiers, force_clean=True)
|
||||
|
||||
self.finalize(context, clean_up=True)
|
||||
self.finalize(context)
|
||||
return
|
||||
|
||||
|
||||
def finalize(self, context, clean_up=False, abort=False):
|
||||
def finalize(self, context, clean_up=True, abort=False):
|
||||
"""
|
||||
Finalize and clean-up after the operation ends.
|
||||
Regardless of whether it was confirmed or cancelled.
|
||||
@@ -943,15 +684,19 @@ class CarverBase(bpy.types.Operator,
|
||||
# Operation was aborted, or successfully finished in the Destructive mode.
|
||||
# Delete everything created by the operator (i.e. cutter).
|
||||
if clean_up:
|
||||
delete_cutter(self.cutter.obj)
|
||||
delete_object(self.cutter.obj)
|
||||
self.cutter.bm.free()
|
||||
delete_empty_collection()
|
||||
delete_empty_collection(context)
|
||||
|
||||
# Remove modifiers added by the operator.
|
||||
if abort:
|
||||
for obj, mod in self.objects.modifiers.items():
|
||||
obj.modifiers.remove(mod)
|
||||
|
||||
# Clean-up temporary changes made by operator.
|
||||
if self.effects.array:
|
||||
self.effects.array.use_pin_to_last = False
|
||||
|
||||
bpy.types.SpaceView3D.draw_handler_remove(self._handler, 'WINDOW')
|
||||
context.workspace.status_text_set(None)
|
||||
context.window.cursor_set('DEFAULT' if context.mode == 'OBJECT' else 'CROSSHAIR')
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
import bpy
|
||||
from bpy_extras import view3d_utils
|
||||
from mathutils import Vector, Matrix
|
||||
|
||||
from ...functions.object import (
|
||||
set_object_origin,
|
||||
)
|
||||
from ...functions.view import (
|
||||
region_2d_to_plane_3d,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ CLASSES ------------------------------ ####
|
||||
|
||||
class CarverEvents():
|
||||
|
||||
# Private Methods.
|
||||
def _custom_modifier_event(self, context, event, modifier,
|
||||
cursor='NONE', store_values=False, restore_mouse=True,
|
||||
postprocess=None):
|
||||
"""Creates a custom modifier event when the key is held down."""
|
||||
|
||||
# Initialize Modifier Phase
|
||||
if event.value == 'PRESS':
|
||||
if self.phase in ("DRAW", "EXTRUDE"):
|
||||
self._stored_phase = self.phase
|
||||
self.phase = modifier
|
||||
self.mouse.cached = self.mouse.current
|
||||
context.window.cursor_set(cursor)
|
||||
|
||||
if store_values:
|
||||
# Store center of the geometry as a Vector.
|
||||
verts = [v.co for v in self.cutter.bm.verts]
|
||||
center = sum(verts, Vector()) / len(verts)
|
||||
self.cutter.center = self.cutter.obj.matrix_world @ center
|
||||
|
||||
# End Modifier Phase
|
||||
elif event.value == 'RELEASE':
|
||||
if self.phase == modifier:
|
||||
context.window.cursor_set('MUTE')
|
||||
if restore_mouse:
|
||||
context.window.cursor_warp(int(self.mouse.cached[0]), int(self.mouse.cached[1]) + 100)
|
||||
self.mouse.current = self.mouse.cached
|
||||
self.phase = self._stored_phase
|
||||
|
||||
if postprocess is not None:
|
||||
postprocess(self)
|
||||
|
||||
return self._stored_phase
|
||||
|
||||
|
||||
# Public Methods.
|
||||
def event_aspect(self, context, event):
|
||||
"""Modifier key for changing the aspect of the shape."""
|
||||
|
||||
if event.shift and self.phase == "DRAW":
|
||||
if self._initial_aspect == 'FREE':
|
||||
self.aspect = 'FIXED'
|
||||
elif self._initial_aspect == 'FIXED':
|
||||
self.aspect = 'FREE'
|
||||
else:
|
||||
self.aspect = self._initial_aspect
|
||||
|
||||
|
||||
def event_origin(self, context, event):
|
||||
"""Modifier key for changing the origin of the shape."""
|
||||
|
||||
if event.alt and self.phase == "DRAW":
|
||||
if self._initial_origin == 'EDGE':
|
||||
self.origin = 'CENTER'
|
||||
elif self._initial_origin == 'CENTER':
|
||||
self.origin = 'EDGE'
|
||||
else:
|
||||
self.origin = self._initial_origin
|
||||
|
||||
|
||||
def event_rotate(self, context, event):
|
||||
"""Modifier key for rotating the shape."""
|
||||
|
||||
def _remove_rotate_phase_properties(self):
|
||||
del self._stored_mouse_pos_3d
|
||||
del self._stored_rotation
|
||||
del self._stored_cutter_center
|
||||
del self._stored_cutter_euler
|
||||
|
||||
# Restore origin at edge (first vertex).
|
||||
if self.origin == 'EDGE':
|
||||
self.cutter.bm.verts.ensure_lookup_table()
|
||||
point = self.cutter.bm.verts[0].co
|
||||
set_object_origin(self.cutter.obj, self.cutter.bm, point='CUSTOM', custom=point)
|
||||
|
||||
|
||||
# Set correct phase.
|
||||
if event.type == 'R':
|
||||
_rm = False if self._stored_phase == "DRAW" else True
|
||||
stored_phase = self._custom_modifier_event(context, event, "ROTATE",
|
||||
cursor='MOVE_X', store_values=True, restore_mouse=_rm,
|
||||
postprocess=_remove_rotate_phase_properties)
|
||||
|
||||
if self.phase == "ROTATE":
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
|
||||
# Project current mouse position onto the workplane.
|
||||
current_mouse_pos_3d = region_2d_to_plane_3d(region, rv3d,
|
||||
self.mouse.current,
|
||||
(self.workplane.location, self.workplane.normal))
|
||||
if current_mouse_pos_3d is not None:
|
||||
# Store values.
|
||||
obj = self.cutter.obj
|
||||
if not hasattr(self, '_stored_mouse_pos_3d'):
|
||||
self._stored_mouse_pos_3d = current_mouse_pos_3d.copy()
|
||||
self._stored_rotation = self.rotation
|
||||
self._stored_cutter_center = self.cutter.center
|
||||
self._stored_cutter_euler = obj.rotation_euler.copy()
|
||||
|
||||
# Calculate angle and direction.
|
||||
to_start = (self._stored_mouse_pos_3d - self._stored_cutter_center).normalized()
|
||||
to_current = (current_mouse_pos_3d - self._stored_cutter_center).normalized()
|
||||
|
||||
angle = to_start.angle(to_current)
|
||||
cross = to_start.cross(to_current)
|
||||
if cross.dot(self.workplane.normal) < 0:
|
||||
angle = -angle
|
||||
|
||||
if abs(angle) > 0.0001:
|
||||
self.rotation = self._stored_rotation + angle
|
||||
|
||||
# Offset the object location when drawing from edge to move rotation pivot to center.
|
||||
if self.origin == 'EDGE':
|
||||
set_object_origin(obj, self.cutter.bm, point='CENTER_OBJ')
|
||||
|
||||
# Calculate rotation amount.
|
||||
rotation_total = Matrix.Rotation(self.rotation, 4, self.workplane.normal)
|
||||
rotation_stored = Matrix.Rotation(self._stored_rotation, 4, self.workplane.normal)
|
||||
rotation_matrix = rotation_total @ rotation_stored.inverted()
|
||||
new_rot = rotation_matrix @ self._stored_cutter_euler.to_matrix().to_4x4()
|
||||
|
||||
# Rotate.
|
||||
obj.rotation_euler = new_rot.to_euler()
|
||||
|
||||
|
||||
def event_bevel(self, context, event):
|
||||
"""Modifier key for beveling the shape."""
|
||||
|
||||
def _remove_empty_bevel_modifier(self):
|
||||
bevel = self.effects.bevel
|
||||
if bevel.width == 0:
|
||||
self.cutter.obj.modifiers.remove(bevel)
|
||||
self.effects.bevel = None
|
||||
|
||||
if self.effects.weld is not None:
|
||||
self.cutter.obj.modifiers.remove(self.effects.weld)
|
||||
self.effects.weld = None
|
||||
|
||||
|
||||
if self.shape != 'BOX':
|
||||
return
|
||||
|
||||
# Set correct phase.
|
||||
if event.type == 'B':
|
||||
stored_phase = self._custom_modifier_event(context, event, "BEVEL",
|
||||
cursor='PICK_AREA', store_values=True,
|
||||
postprocess=_remove_empty_bevel_modifier)
|
||||
|
||||
if self.phase == "BEVEL":
|
||||
self.use_bevel = True
|
||||
|
||||
# Initialize bevel effect if it doesn't exist.
|
||||
if self.effects.bevel is None:
|
||||
self.bevel_width = 0
|
||||
affect = 'VERTICES' if stored_phase == "DRAW" else 'EDGES'
|
||||
self.effects.add_bevel_modifier(self, affect=affect)
|
||||
|
||||
# Force the geometry to update.
|
||||
if stored_phase == "DRAW":
|
||||
self.update_cutter_shape(context)
|
||||
elif stored_phase == "EXTRUDE":
|
||||
self.set_extrusion_depth(context)
|
||||
|
||||
# Calculate bevel width.
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
|
||||
self.mouse.cached_3d = view3d_utils.region_2d_to_location_3d(region, rv3d,
|
||||
self.mouse.cached,
|
||||
self.cutter.center)
|
||||
self.mouse.current_3d = view3d_utils.region_2d_to_location_3d(region, rv3d,
|
||||
self.mouse.current,
|
||||
self.cutter.center)
|
||||
d = (self.cutter.center - self.mouse.current_3d).length - (self.cutter.center - self.mouse.cached_3d).length
|
||||
self.bevel_width = d * 0.2
|
||||
|
||||
# Adjust bevel segments.
|
||||
if event.type == 'WHEELUPMOUSE':
|
||||
self.bevel_segments += 1
|
||||
elif event.type == 'WHEELDOWNMOUSE':
|
||||
self.bevel_segments -= 1
|
||||
|
||||
# Update modifier.
|
||||
self.effects.update(self, "BEVEL")
|
||||
|
||||
|
||||
def event_array(self, context, event):
|
||||
"""Modifier key for arraying the shape."""
|
||||
|
||||
# Add duplicates.
|
||||
if event.type == 'LEFT_ARROW' and event.value == 'PRESS':
|
||||
self.columns -= 1
|
||||
if event.type == 'RIGHT_ARROW' and event.value == 'PRESS':
|
||||
self.columns += 1
|
||||
if event.type == 'DOWN_ARROW' and event.value == 'PRESS':
|
||||
self.rows -= 1
|
||||
if event.type == 'UP_ARROW' and event.value == 'PRESS':
|
||||
self.rows += 1
|
||||
|
||||
if event.type in ['LEFT_ARROW',
|
||||
'RIGHT_ARROW',
|
||||
'DOWN_ARROW',
|
||||
'UP_ARROW',] and event.value == 'PRESS':
|
||||
self.effects.update(self, "ARRAY_COUNT")
|
||||
|
||||
# Force the geometry to update.
|
||||
if self.phase == "DRAW":
|
||||
self.update_cutter_shape(context)
|
||||
elif self.phase == "EXTRUDE":
|
||||
self.set_extrusion_depth(context)
|
||||
|
||||
# Adjust gap.
|
||||
if (self.rows > 1 or self.columns > 1) and (event.type == 'A'):
|
||||
stored_phase = self._custom_modifier_event(context, event, "ARRAY",
|
||||
cursor='MUTE', store_values=True)
|
||||
|
||||
if self.phase == "ARRAY":
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
|
||||
self.mouse.cached_3d = view3d_utils.region_2d_to_location_3d(region, rv3d,
|
||||
self.mouse.cached,
|
||||
self.cutter.center)
|
||||
self.mouse.current_3d = view3d_utils.region_2d_to_location_3d(region, rv3d,
|
||||
self.mouse.current,
|
||||
self.cutter.center)
|
||||
d = (self.cutter.center - self.mouse.current_3d).length - (self.cutter.center - self.mouse.cached_3d).length
|
||||
self.gap = 1 + (d * 0.2)
|
||||
|
||||
self.effects.update(self, "ARRAY_GAP")
|
||||
|
||||
|
||||
def event_flip(self, context, event):
|
||||
"""Modifier key for flipping the direction of extrusion."""
|
||||
|
||||
if event.type == 'F' and event.value == 'PRESS':
|
||||
if self.phase == 'EXTRUDE':
|
||||
self.flip_direction = not self.flip_direction
|
||||
|
||||
|
||||
def event_move(self, context, event):
|
||||
"""Modifier key for moving the shape."""
|
||||
|
||||
def _remove_move_phase_properties(self):
|
||||
del self._stored_cutter_location
|
||||
self.mouse.cached_3d = None
|
||||
|
||||
|
||||
if event.type == 'SPACE':
|
||||
stored_phase = self._custom_modifier_event(context, event, "MOVE",
|
||||
cursor='SCROLL_XY', restore_mouse=False,
|
||||
postprocess=_remove_move_phase_properties)
|
||||
|
||||
if self.phase == "MOVE":
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
|
||||
# Project current mouse position onto the workplane.
|
||||
current_mouse_pos_3d = region_2d_to_plane_3d(region, rv3d,
|
||||
self.mouse.current,
|
||||
(self.workplane.location, self.workplane.normal))
|
||||
if current_mouse_pos_3d is not None:
|
||||
if not hasattr(self, '_stored_cutter_location'):
|
||||
self.mouse.cached_3d = current_mouse_pos_3d.copy()
|
||||
self._stored_cutter_location = self.cutter.obj.location.copy()
|
||||
|
||||
offset = current_mouse_pos_3d - self.mouse.cached_3d
|
||||
self.cutter.obj.location = self._stored_cutter_location + offset
|
||||
@@ -1,40 +1,59 @@
|
||||
import bpy
|
||||
import math
|
||||
|
||||
|
||||
# Import Custom Icons
|
||||
from ... import icons
|
||||
svg_icons = icons.svg_icons["main"]
|
||||
icon_measure = svg_icons["MEASURE"].icon_id
|
||||
icon_cpu = svg_icons["CPU"].icon_id
|
||||
from ...ui.icons import (
|
||||
get_custom_icon,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ PROPERTIES ------------------------------ ####
|
||||
|
||||
class CarverPropsOperator():
|
||||
|
||||
# NOTE: Defining enum items inside a function is necessary to allow correct import of custom icons.
|
||||
# Without this, classes (and their properties) are registered before `register` function of modules
|
||||
# are called, resulting in empty preview collections.
|
||||
def get_depth_items(self, context):
|
||||
icon_measure = get_custom_icon("MEASURE")
|
||||
icon_cpu = get_custom_icon("CPU")
|
||||
|
||||
items = [
|
||||
('MANUAL', "Manual", "Depth can be manually set after creating a cutter shape", icon_measure, 0),
|
||||
('AUTO', "Auto", "Depth is set automatically to cover selected objects entirely", icon_cpu, 1),
|
||||
('CURSOR', "3D Cursor", "Depth is set to 3D cursors location", 'PIVOT_CURSOR', 2),
|
||||
]
|
||||
return items
|
||||
|
||||
# OPERATOR-properties
|
||||
mode: bpy.props.EnumProperty(
|
||||
name = "Mode",
|
||||
items = (('DESTRUCTIVE', "Destructive",
|
||||
"Boolean cutters are immediatelly applied and removed after the cut", 'MESH_DATA', 0),
|
||||
"Boolean cutters are immediatelly applied and removed after the cut",
|
||||
'MESH_DATA', 0),
|
||||
('MODIFIER', "Modifier",
|
||||
"Cuts are stored as boolean modifiers and cutters are placed inside the collection", 'MODIFIER_DATA', 1)),
|
||||
"Cuts are stored as Boolean modifiers and cutters are placed inside the collection", 'MODIFIER_DATA', 1)),
|
||||
default = 'MODIFIER',
|
||||
)
|
||||
alignment: bpy.props.EnumProperty(
|
||||
name = "Alignment",
|
||||
items = (('SURFACE', "Surface", "Align cutters to the surface normal of the mesh under the mouse", 'SNAP_NORMAL', 0),
|
||||
('VIEW', "View", "Align cutters to the current view", 'VIEW_CAMERA_UNSELECTED', 1),
|
||||
('CURSOR', "3D Cursor", "Align cutters to the 3D cursor orientation", 'ORIENTATION_CURSOR', 2),
|
||||
('GRID', "Grid", "Align cutters to the world grid", 'GRID', 3)),
|
||||
items = (('SURFACE', "Surface",
|
||||
"Align cutters to the surface normal of the mesh under the mouse",
|
||||
'SNAP_NORMAL', 0),
|
||||
('VIEW', "View",
|
||||
"Align cutters to the current view",
|
||||
'VIEW_CAMERA_UNSELECTED', 1),
|
||||
('CURSOR', "3D Cursor",
|
||||
"Align cutters to the 3D cursor orientation",
|
||||
'ORIENTATION_CURSOR', 2),
|
||||
('GRID', "Grid",
|
||||
"Align cutters to the world grid",
|
||||
'GRID', 3)),
|
||||
default = 'SURFACE',
|
||||
)
|
||||
depth: bpy.props.EnumProperty(
|
||||
name = "Depth",
|
||||
items = (('MANUAL', "Manual", "Depth can be manually set after creating a cutter shape", icon_measure, 0),
|
||||
('AUTO', "Auto", "Depth is set automatically to cover selected objects entirely", icon_cpu, 1),
|
||||
('CURSOR', "3D Cursor", "Depth is set to 3D cursors location", 'PIVOT_CURSOR', 2)),
|
||||
default = 'MANUAL',
|
||||
items = get_depth_items,
|
||||
default = 0,
|
||||
)
|
||||
|
||||
|
||||
@@ -72,7 +91,7 @@ class CarverPropsShape():
|
||||
flip_direction: bpy.props.BoolProperty(
|
||||
name = "Flip Direction",
|
||||
description = "Change which way the geometry is extruded",
|
||||
options = {'SKIP_SAVE', 'HIDDEN', 'SKIP_PRESET', },
|
||||
options = {'SKIP_SAVE', 'HIDDEN'},
|
||||
default = False,
|
||||
)
|
||||
|
||||
@@ -88,7 +107,7 @@ class CarverPropsModifier():
|
||||
)
|
||||
pin: bpy.props.BoolProperty(
|
||||
name = "Pin Boolean Modifier",
|
||||
description = ("Boolean modifier will be placed first in modifier stack, above other modifier (if there are any).\n"
|
||||
description = ("Always make new Boolean modifiers first in the modifier stack.\n"
|
||||
"NOTE: Order of modifiers can drastically affect the result (especially in destructive mode)"),
|
||||
default = True,
|
||||
)
|
||||
@@ -98,13 +117,13 @@ class CarverPropsCutter():
|
||||
# CUTTER-properties
|
||||
hide: bpy.props.BoolProperty(
|
||||
name = "Hide Cutter",
|
||||
description = ("Hide cutter objects in the viewport after they're created."),
|
||||
description = ("Hide cutter objects in the viewport after they are created."),
|
||||
default = True,
|
||||
)
|
||||
parent: bpy.props.BoolProperty(
|
||||
name = "Parent to Canvas",
|
||||
description = ("Cutters will be parented to active object being cut, even if cutting multiple objects.\n"
|
||||
"If there is no active object in selection cutters parent might be chosen seemingly randomly"),
|
||||
description = ("Parent new cutters to active object (even if they are cutting multiple objects).\n"
|
||||
"If there is no active object in the selection, parent might be chosen randomly"),
|
||||
default = True,
|
||||
)
|
||||
display: bpy.props.EnumProperty(
|
||||
@@ -115,24 +134,29 @@ class CarverPropsCutter():
|
||||
)
|
||||
cutter_origin: bpy.props.EnumProperty(
|
||||
name = "Cutter Origin Point",
|
||||
items = (('CENTER_OBJ', "Bounding Box", "Put the object origin at the center of the cutters bounding box"),
|
||||
('CENTER_MESH', "Geometry", "Put the object origin at the center of the cutters geometry (not including effects)"),
|
||||
('FACE_CENTER', "First Face", "Put the object origin at the center of cutters first face (i.e. shape)"),
|
||||
('MOUSE_INITIAL', "Mouse Click", "Put the object origin at the point where mouse was first clicked"),
|
||||
('CANVAS', "Same as Canvas", "Put the object origin of the cutter to the origin point of the cutter")),
|
||||
items = (('CENTER_OBJ', "Bounding Box",
|
||||
"Put the object origin at the center of the cutters bounding box"),
|
||||
('CENTER_MESH', "Geometry",
|
||||
"Put the object origin at the center of the cutters geometry (not including effects)"),
|
||||
('FACE_CENTER', "First Face",
|
||||
"Put the object origin at the center of cutters first face (i.e. shape)"),
|
||||
('MOUSE_INITIAL', "Mouse Click",
|
||||
"Put the object origin at the point where mouse was first clicked"),
|
||||
('CANVAS', "Same as Canvas",
|
||||
"Put the object origin of the cutter to the origin point of the cutter")),
|
||||
default = 'CENTER_MESH',
|
||||
)
|
||||
|
||||
auto_smooth: bpy.props.BoolProperty(
|
||||
name = "Shade Auto Smooth",
|
||||
description = ("Cutter object will be shaded smooth with sharp edges (above specified degrees) marked as sharp\n"
|
||||
"NOTE: This is a one time operator. 'Smooth by Angle' modifier will not be added on cutter"),
|
||||
description = ("Add 'Smooth by Angle' modifier to cutter object"),
|
||||
default = True,
|
||||
)
|
||||
sharp_angle: bpy.props.FloatProperty(
|
||||
name = "Angle",
|
||||
description = "Maximum face angle for sharp edges",
|
||||
subtype = "ANGLE",
|
||||
precision = 3,
|
||||
min = 0, max = math.pi,
|
||||
default = 0.523599,
|
||||
)
|
||||
@@ -142,13 +166,13 @@ class CarverPropsArray():
|
||||
# ARRAY-properties
|
||||
rows: bpy.props.IntProperty(
|
||||
name = "Rows",
|
||||
description = "Number of times shape is duplicated horizontally",
|
||||
description = "Number of times the shape is duplicated horizontally",
|
||||
min = 1, soft_max = 16,
|
||||
default = 1,
|
||||
)
|
||||
columns: bpy.props.IntProperty(
|
||||
name = "Columns",
|
||||
description = "Number of times shape is duplicated vertically",
|
||||
description = "Number of times the shape is duplicated vertically",
|
||||
min = 1, soft_max = 16,
|
||||
default = 1,
|
||||
)
|
||||
|
||||
@@ -9,13 +9,14 @@ from ...functions.mesh import (
|
||||
)
|
||||
from ...functions.modifier import (
|
||||
add_modifier_asset,
|
||||
update_modifier_input,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ CLASSES ------------------------------ ####
|
||||
|
||||
class Selection:
|
||||
"""Storage of viable selected and active object(s) throughout the modal."""
|
||||
"""Storage of viable cannvas objects and their Boolean modifiers."""
|
||||
|
||||
def __init__(self, selected, active):
|
||||
self.selected: list = selected
|
||||
@@ -24,10 +25,7 @@ class Selection:
|
||||
|
||||
|
||||
class Mouse:
|
||||
"""
|
||||
Mouse positions throughout different phases of the modal operator.
|
||||
Each class variable is a 2D vector in screen space (x, y).
|
||||
"""
|
||||
"""Mouse positions throughout different phases of the modal operator."""
|
||||
|
||||
def __init__(self):
|
||||
self.initial = Vector()
|
||||
@@ -99,17 +97,17 @@ class Effects:
|
||||
return self
|
||||
|
||||
def update(self, cls, effect):
|
||||
"""Update bevel modifier during modal."""
|
||||
"""Update cutter modifiers during modal."""
|
||||
|
||||
# Update array count.
|
||||
if effect == 'ARRAY_COUNT':
|
||||
if effect == "ARRAY_COUNT":
|
||||
if self.array is None:
|
||||
self.add_array_modifier(cls)
|
||||
|
||||
else:
|
||||
if cls.columns > 1 or cls.rows > 1:
|
||||
self.array["Socket_2"] = cls.columns
|
||||
self.array["Socket_3"] = cls.rows
|
||||
update_modifier_input(self.array, "Socket_2", cls.columns)
|
||||
update_modifier_input(self.array, "Socket_3", cls.rows)
|
||||
|
||||
# Remove modifier if it's no longer needed.
|
||||
if cls.columns == 1 and cls.rows == 1:
|
||||
@@ -117,12 +115,12 @@ class Effects:
|
||||
self.array = None
|
||||
|
||||
# Update array gap.
|
||||
if effect == 'ARRAY_GAP':
|
||||
if cls.columns > 1 or cls.row > 1:
|
||||
if effect == "ARRAY_GAP":
|
||||
if cls.columns > 1 or cls.rows > 1:
|
||||
if self.array is not None:
|
||||
self.array["Socket_4"] = cls.gap
|
||||
update_modifier_input(self.array, "Socket_4", cls.gap)
|
||||
|
||||
# Force the modifier to update in viewport.
|
||||
# NOTE: Force the modifier to update in viewport when key is pressed.
|
||||
self.array.show_viewport = False
|
||||
self.array.show_viewport = True
|
||||
|
||||
@@ -150,15 +148,16 @@ class Effects:
|
||||
|
||||
# Columns
|
||||
if cls.columns > 1:
|
||||
mod["Socket_2"] = cls.columns
|
||||
update_modifier_input(mod, "Socket_2", cls.columns)
|
||||
|
||||
# Rows
|
||||
if cls.rows > 1:
|
||||
mod["Socket_3"] = cls.rows
|
||||
update_modifier_input(mod, "Socket_3", cls.rows)
|
||||
|
||||
# Gap
|
||||
mod["Socket_4"] = cls.gap
|
||||
update_modifier_input(mod, "Socket_4", cls.gap)
|
||||
|
||||
mod.use_pin_to_last = True
|
||||
self.array = mod
|
||||
|
||||
|
||||
@@ -233,7 +232,7 @@ class Effects:
|
||||
|
||||
# Smooth by Angle
|
||||
def add_auto_smooth_modifier(self, cls, context):
|
||||
"""Adds a 'Smooth by Angle' modifier on cutter object, a.k.a. Auto Smooth."""
|
||||
"""Adds a 'Smooth by Angle' modifier on the cutter object (a.k.a. Auto Smooth)."""
|
||||
|
||||
obj = cls.cutter.obj
|
||||
mesh = cls.cutter.mesh
|
||||
@@ -262,7 +261,7 @@ class Effects:
|
||||
|
||||
mod = obj.modifiers.active
|
||||
|
||||
# Try loading the node group manually if `bpy.ops` operators fail.
|
||||
# Try loading the node group manually if `bpy.ops` operators failed.
|
||||
if mod is None:
|
||||
dir = os.path.join(os.path.dirname(bpy.app.binary_path), "5.0", "datafiles", "assets")
|
||||
assets_path = os.path.join(dir, modifier_asset_file)
|
||||
@@ -274,13 +273,13 @@ class Effects:
|
||||
print("Destructively marking sharp edges and smooth faces in the mesh")
|
||||
shade_smooth_by_angle(bm, mesh, angle=math.degrees(cls.sharp_angle))
|
||||
else:
|
||||
# Set smoothing angle.
|
||||
# Set smoothing angle (necessary for all methods except `shade_auto_smooth()`).
|
||||
for face in bm.faces:
|
||||
face.smooth = True
|
||||
bm.to_mesh(mesh)
|
||||
|
||||
mod.use_pin_to_last = True
|
||||
mod["Input_1"] = cls.sharp_angle
|
||||
update_modifier_input(mod, "Input_1", cls.sharp_angle)
|
||||
|
||||
self.smooth = mod
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import bpy
|
||||
from ... import __package__ as base_package
|
||||
|
||||
|
||||
#### ------------------------------ /toolbar/ ------------------------------ ####
|
||||
@@ -14,7 +13,7 @@ def carver_ui_common(context, layout, props):
|
||||
layout.prop(props, "solver", expand=True)
|
||||
|
||||
else:
|
||||
# Use labels for Properties editor/sidebar.
|
||||
# NOTE: Use labels for Properties editor/sidebar.
|
||||
layout.prop(props, "mode", text="Mode")
|
||||
layout.prop(props, "alignment", text="Alignment")
|
||||
layout.prop(props, "depth", text="Depth")
|
||||
@@ -43,20 +42,17 @@ class TOPBAR_PT_carver_shape(bpy.types.Panel):
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
|
||||
tool = context.workspace.tools.from_space_view3d_mode('OBJECT' if context.mode == 'OBJECT' else 'EDIT_MESH')
|
||||
mode = 'OBJECT' if context.mode == 'OBJECT' else 'EDIT_MESH'
|
||||
tool = context.workspace.tools.from_space_view3d_mode(mode)
|
||||
props = tool.operator_properties(tool.idname)
|
||||
|
||||
# Box & Circle
|
||||
if tool.idname == "object.carve_box" or tool.idname == "object.carve_circle":
|
||||
if tool.idname == "object.carve_box":
|
||||
props = tool.operator_properties("object.carve_box")
|
||||
else:
|
||||
props = tool.operator_properties("object.carve_circle")
|
||||
|
||||
if tool.idname == "object.carve_circle":
|
||||
layout.prop(props, "subdivision", text="Vertices")
|
||||
layout.prop(props, "rotation")
|
||||
layout.prop(props, "aspect", expand=True)
|
||||
layout.prop(props, "origin", expand=True)
|
||||
layout.prop(props, "aspect", expand=True)
|
||||
|
||||
if props.alignment == 'SURFACE':
|
||||
layout.prop(props, "orientation")
|
||||
@@ -67,7 +63,6 @@ class TOPBAR_PT_carver_shape(bpy.types.Panel):
|
||||
|
||||
# Polyline
|
||||
elif tool.idname == "object.carve_polyline":
|
||||
props = tool.operator_properties("object.carve_polyline")
|
||||
if props.alignment == 'SURFACE':
|
||||
layout.prop(props, "offset", text="Offset")
|
||||
layout.prop(props, "align_to_all")
|
||||
@@ -85,13 +80,9 @@ class TOPBAR_PT_carver_effects(bpy.types.Panel):
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
|
||||
tool = context.workspace.tools.from_space_view3d_mode('OBJECT' if context.mode == 'OBJECT' else 'EDIT_MESH')
|
||||
if tool.idname == "object.carve_box":
|
||||
props = tool.operator_properties("object.carve_box")
|
||||
elif tool.idname == "object.carve_circle":
|
||||
props = tool.operator_properties("object.carve_circle")
|
||||
elif tool.idname == "object.carve_polyline":
|
||||
props = tool.operator_properties("object.carve_polyline")
|
||||
mode = 'OBJECT' if context.mode == 'OBJECT' else 'EDIT_MESH'
|
||||
tool = context.workspace.tools.from_space_view3d_mode(mode)
|
||||
props = tool.operator_properties(tool.idname)
|
||||
|
||||
# Bevel
|
||||
if tool.idname == 'object.carve_box':
|
||||
@@ -116,6 +107,7 @@ class TOPBAR_PT_carver_effects(bpy.types.Panel):
|
||||
col.prop(props, "rows")
|
||||
col.prop(props, "gap")
|
||||
|
||||
|
||||
class TOPBAR_PT_carver_cutter(bpy.types.Panel):
|
||||
bl_label = "Carver Cutter"
|
||||
bl_idname = "TOPBAR_PT_carver_cutter"
|
||||
@@ -128,25 +120,25 @@ class TOPBAR_PT_carver_cutter(bpy.types.Panel):
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
|
||||
tool = context.workspace.tools.from_space_view3d_mode('OBJECT' if context.mode == 'OBJECT' else 'EDIT_MESH')
|
||||
if tool.idname == "object.carve_box":
|
||||
props = tool.operator_properties("object.carve_box")
|
||||
elif tool.idname == "object.carve_circle":
|
||||
props = tool.operator_properties("object.carve_circle")
|
||||
elif tool.idname == "object.carve_polyline":
|
||||
props = tool.operator_properties("object.carve_polyline")
|
||||
mode = 'OBJECT' if context.mode == 'OBJECT' else 'EDIT_MESH'
|
||||
tool = context.workspace.tools.from_space_view3d_mode(mode)
|
||||
props = tool.operator_properties(tool.idname)
|
||||
|
||||
# modifier_&_cutter
|
||||
# Modifier properties.
|
||||
col = layout.column()
|
||||
col.prop(props, "pin", text="Pin Modifier")
|
||||
col.separator()
|
||||
|
||||
# Cutter object properties.
|
||||
col = layout.column()
|
||||
row = col.row()
|
||||
row.prop(props, "display", text="Display", expand=True)
|
||||
col.prop(props, "pin", text="Pin Modifier")
|
||||
if props.mode == 'MODIFIER':
|
||||
col.prop(props, "parent")
|
||||
col.prop(props, "hide")
|
||||
col.prop(props, "cutter_origin", text="Origin")
|
||||
|
||||
# auto_smooth
|
||||
# Auto Smooth.
|
||||
layout.separator()
|
||||
col = layout.column(align=True)
|
||||
col.prop(props, "auto_smooth", text="Auto Smooth")
|
||||
@@ -159,11 +151,11 @@ class TOPBAR_PT_carver_cutter(bpy.types.Panel):
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
classes = [
|
||||
classes = (
|
||||
TOPBAR_PT_carver_shape,
|
||||
TOPBAR_PT_carver_effects,
|
||||
TOPBAR_PT_carver_cutter,
|
||||
]
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
|
||||
@@ -1,270 +0,0 @@
|
||||
import bpy
|
||||
from .functions.poll import is_canvas
|
||||
from .functions.list import list_canvas_cutters
|
||||
|
||||
|
||||
#### ------------------------------ /ui/ ------------------------------ ####
|
||||
|
||||
def carve_menu(self, context):
|
||||
layout = self.layout
|
||||
layout.operator("object.carve_box", text="Box Carve").shape='BOX'
|
||||
layout.operator("object.carve_box", text="Circle Carve").shape='CIRCLE'
|
||||
layout.operator("object.carve_polyline", text="Polyline Carve")
|
||||
|
||||
|
||||
def boolean_operators_menu(self, context):
|
||||
layout = self.layout
|
||||
layout.operator_context = 'INVOKE_DEFAULT'
|
||||
col = layout.column(align=True)
|
||||
|
||||
col.label(text="Auto Boolean")
|
||||
col.operator("object.boolean_auto_difference", text="Difference", icon='SELECT_SUBTRACT')
|
||||
col.operator("object.boolean_auto_union", text="Union", icon='SELECT_EXTEND')
|
||||
col.operator("object.boolean_auto_intersect", text="Intersect", icon='SELECT_INTERSECT')
|
||||
col.operator("object.boolean_auto_slice", text="Slice", icon='SELECT_DIFFERENCE')
|
||||
|
||||
col.separator()
|
||||
col.label(text="Brush Boolean")
|
||||
col.operator("object.boolean_brush_difference", text="Difference", icon='SELECT_SUBTRACT')
|
||||
col.operator("object.boolean_brush_union", text="Union", icon='SELECT_EXTEND')
|
||||
col.operator("object.boolean_brush_intersect", text="Intersect", icon='SELECT_INTERSECT')
|
||||
col.operator("object.boolean_brush_slice", text="Slice", icon='SELECT_DIFFERENCE')
|
||||
|
||||
|
||||
def boolean_extras_menu(self, context):
|
||||
layout = self.layout
|
||||
layout.operator_context = 'INVOKE_DEFAULT'
|
||||
col = layout.column(align=True)
|
||||
|
||||
if context.active_object:
|
||||
# Canvas operators
|
||||
active_object = context.active_object
|
||||
if active_object.booleans.canvas == True and any(mod.name.startswith("boolean_") for mod in active_object.modifiers):
|
||||
col.separator()
|
||||
col.operator("object.boolean_toggle_all", text="Toggle All Cuters")
|
||||
col.operator("object.boolean_apply_all", text="Apply All Cutters")
|
||||
col.operator("object.boolean_remove_all", text="Remove All Cutters")
|
||||
|
||||
# Cutter operators
|
||||
if active_object.booleans.cutter:
|
||||
col.separator()
|
||||
col.operator("object.boolean_toggle_cutter", text="Toggle Cutter").method='ALL'
|
||||
col.operator("object.boolean_apply_cutter", text="Apply Cutter").method='ALL'
|
||||
col.operator("object.boolean_remove_cutter", text="Remove Cutter").method='ALL'
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ PANELS ------------------------------ ####
|
||||
|
||||
# Boolean Operators Panel
|
||||
class VIEW3D_PT_boolean(bpy.types.Panel):
|
||||
bl_label = "Boolean"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Edit"
|
||||
bl_context = "objectmode"
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
prefs = context.preferences.addons[__package__].preferences
|
||||
return prefs.show_in_sidebar
|
||||
|
||||
def draw(self, context):
|
||||
boolean_operators_menu(self, context)
|
||||
|
||||
|
||||
# Properties Panel
|
||||
class VIEW3D_PT_boolean_properties(bpy.types.Panel):
|
||||
bl_label = "Properties"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Edit"
|
||||
bl_context = "objectmode"
|
||||
bl_parent_id = "VIEW3D_PT_boolean"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
prefs = context.preferences.addons[__package__].preferences
|
||||
if prefs.show_in_sidebar:
|
||||
if context.active_object:
|
||||
if is_canvas(context.active_object) or context.active_object.booleans.cutter:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
def draw(self, context):
|
||||
boolean_extras_menu(self, context)
|
||||
|
||||
|
||||
# Cutters Panel
|
||||
class VIEW3D_PT_boolean_cutters(bpy.types.Panel):
|
||||
bl_label = "Cutters"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Edit"
|
||||
bl_context = "objectmode"
|
||||
bl_parent_id = "VIEW3D_PT_boolean"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
prefs = context.preferences.addons[__package__].preferences
|
||||
if prefs.show_in_sidebar:
|
||||
if context.active_object:
|
||||
if is_canvas(context.active_object):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
canvas = context.active_object
|
||||
__, modifiers = list_canvas_cutters([canvas])
|
||||
|
||||
for mod in modifiers:
|
||||
col = layout.column(align=True)
|
||||
row = col.row(align=True)
|
||||
|
||||
# icon
|
||||
if mod.operation == 'DIFFERENCE':
|
||||
icon = 'SELECT_SUBTRACT'
|
||||
elif mod.operation == 'UNION':
|
||||
icon = 'SELECT_EXTEND'
|
||||
elif mod.operation == 'INTERSECT':
|
||||
icon = 'SELECT_INTERSECT'
|
||||
|
||||
row.prop(mod.object, "name", text="", icon=icon)
|
||||
|
||||
# Toggle
|
||||
op_toggle = row.operator("object.boolean_toggle_cutter", text="", icon='HIDE_OFF' if mod.show_viewport else 'HIDE_ON')
|
||||
op_toggle.method = 'SPECIFIED'
|
||||
op_toggle.specified_cutter = mod.object.name
|
||||
op_toggle.specified_canvas = canvas.name
|
||||
|
||||
# Apply
|
||||
op_apply = row.operator("object.boolean_apply_cutter", text="", icon='CHECKMARK')
|
||||
op_apply.method = 'SPECIFIED'
|
||||
op_apply.specified_cutter = mod.object.name
|
||||
op_apply.specified_canvas = canvas.name
|
||||
|
||||
# Remove
|
||||
op_remove = row.operator("object.boolean_remove_cutter", text="", icon='X')
|
||||
op_remove.method = 'SPECIFIED'
|
||||
op_remove.specified_cutter = mod.object.name
|
||||
op_remove.specified_canvas = canvas.name
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ MENUS ------------------------------ ####
|
||||
|
||||
# Carve Menu
|
||||
class VIEW3D_MT_carve(bpy.types.Menu):
|
||||
bl_label = "Carve"
|
||||
bl_idname = "VIEW3D_MT_carve"
|
||||
|
||||
def draw(self, context):
|
||||
carve_menu(self, context)
|
||||
|
||||
|
||||
# 3D Viewport (Object Mode) -> Object
|
||||
class VIEW3D_MT_boolean(bpy.types.Menu):
|
||||
bl_label = "Boolean"
|
||||
bl_idname = "VIEW3D_MT_boolean"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.menu("VIEW3D_MT_carve")
|
||||
layout.separator()
|
||||
boolean_operators_menu(self, context)
|
||||
boolean_extras_menu(self, context)
|
||||
|
||||
|
||||
# Shift-Ctrl-B Menu
|
||||
class VIEW3D_MT_boolean_popup(bpy.types.Menu):
|
||||
bl_label = "Boolean"
|
||||
bl_idname = "VIEW3D_MT_boolean_popup"
|
||||
|
||||
def draw(self, context):
|
||||
boolean_operators_menu(self, context)
|
||||
boolean_extras_menu(self, context)
|
||||
|
||||
|
||||
def object_mode_menu(self, context):
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
layout.menu("VIEW3D_MT_boolean")
|
||||
|
||||
|
||||
def edit_mode_menu(self, context):
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
layout.menu("VIEW3D_MT_carve")
|
||||
|
||||
|
||||
def boolean_select_menu(self, context):
|
||||
layout = self.layout
|
||||
active_obj = context.active_object
|
||||
if active_obj:
|
||||
if active_obj.booleans.canvas == True or active_obj.booleans.cutter:
|
||||
layout.separator()
|
||||
|
||||
if active_obj.booleans.canvas == True:
|
||||
layout.operator("object.boolean_select_all", text="Boolean Cutters")
|
||||
if active_obj.booleans.cutter:
|
||||
layout.operator("object.select_cutter_canvas", text="Boolean Canvases")
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
addon_keymaps = []
|
||||
|
||||
classes = [
|
||||
VIEW3D_MT_carve,
|
||||
VIEW3D_MT_boolean,
|
||||
VIEW3D_MT_boolean_popup,
|
||||
VIEW3D_PT_boolean,
|
||||
VIEW3D_PT_boolean_properties,
|
||||
VIEW3D_PT_boolean_cutters,
|
||||
]
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
# MENU
|
||||
bpy.types.VIEW3D_MT_object.append(object_mode_menu)
|
||||
bpy.types.VIEW3D_MT_select_object.append(boolean_select_menu)
|
||||
bpy.types.VIEW3D_MT_edit_mesh.append(edit_mode_menu)
|
||||
|
||||
# KEYMAP
|
||||
addon = bpy.context.window_manager.keyconfigs.addon
|
||||
km = addon.keymaps.new(name="Object Mode")
|
||||
|
||||
kmi = km.keymap_items.new("wm.call_menu", 'B', 'PRESS', ctrl=True, shift=True)
|
||||
kmi.properties.name = "VIEW3D_MT_boolean_popup"
|
||||
kmi.active = True
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
# MENU
|
||||
bpy.types.VIEW3D_MT_object.remove(object_mode_menu)
|
||||
bpy.types.VIEW3D_MT_select_object.remove(boolean_select_menu)
|
||||
bpy.types.VIEW3D_MT_edit_mesh.remove(edit_mode_menu)
|
||||
|
||||
# KEYMAP
|
||||
for km, kmi in addon_keymaps:
|
||||
km.keymap_items.remove(kmi)
|
||||
addon_keymaps.clear()
|
||||
@@ -0,0 +1,34 @@
|
||||
if "bpy" in locals():
|
||||
import importlib
|
||||
for mod in [icons,
|
||||
lists,
|
||||
menus,
|
||||
panels,
|
||||
]:
|
||||
importlib.reload(mod)
|
||||
else:
|
||||
import bpy
|
||||
from . import (
|
||||
icons,
|
||||
lists,
|
||||
menus,
|
||||
panels,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
modules = (
|
||||
icons,
|
||||
lists,
|
||||
menus,
|
||||
panels,
|
||||
)
|
||||
|
||||
def register():
|
||||
for module in modules:
|
||||
module.register()
|
||||
|
||||
def unregister():
|
||||
for module in reversed(modules):
|
||||
module.unregister()
|
||||
@@ -0,0 +1,96 @@
|
||||
import bpy
|
||||
from contextlib import contextmanager
|
||||
|
||||
from ..functions.canvas import (
|
||||
is_canvas,
|
||||
)
|
||||
from ..functions.modifier import (
|
||||
enumerate_boolean_modifiers,
|
||||
is_boolean_modifier,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def get_modifier_from_list_index(obj, index: int):
|
||||
"""
|
||||
Returns the modifier of an object based on Cutters list index.
|
||||
Filters out non-Boolean modifiers to leave a list that matches Cutters one in length.
|
||||
"""
|
||||
|
||||
# Create a list of only Boolean modifiers.
|
||||
boolean_modifiers = []
|
||||
for mod in obj.modifiers:
|
||||
if is_boolean_modifier(mod):
|
||||
boolean_modifiers.append(mod)
|
||||
|
||||
if 0 <= index < len(boolean_modifiers):
|
||||
return boolean_modifiers[index]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@contextmanager
|
||||
def preserve_list_index(prop_owner, index_prop: str):
|
||||
"""Preserves index of active item in UI list."""
|
||||
|
||||
index = getattr(prop_owner, index_prop, 0)
|
||||
stored_index = index
|
||||
|
||||
try:
|
||||
yield
|
||||
|
||||
finally:
|
||||
new_index = stored_index - 1
|
||||
|
||||
if new_index < 0:
|
||||
new_index = 0
|
||||
|
||||
setattr(prop_owner, index_prop, new_index)
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /ui/ ------------------------------ ####
|
||||
|
||||
def boolean_operators_menu(self, context):
|
||||
layout = self.layout
|
||||
layout.operator_context = 'INVOKE_DEFAULT'
|
||||
col = layout.column(align=True)
|
||||
|
||||
col.label(text="Auto Boolean")
|
||||
col.operator("object.boolean_auto_difference", text="Difference", icon='SELECT_SUBTRACT')
|
||||
col.operator("object.boolean_auto_union", text="Union", icon='SELECT_EXTEND')
|
||||
col.operator("object.boolean_auto_intersect", text="Intersect", icon='SELECT_INTERSECT')
|
||||
col.operator("object.boolean_auto_slice", text="Slice", icon='SELECT_DIFFERENCE')
|
||||
|
||||
col.separator()
|
||||
col.label(text="Brush Boolean")
|
||||
col.operator("object.boolean_brush_difference", text="Difference", icon='SELECT_SUBTRACT')
|
||||
col.operator("object.boolean_brush_union", text="Union", icon='SELECT_EXTEND')
|
||||
col.operator("object.boolean_brush_intersect", text="Intersect", icon='SELECT_INTERSECT')
|
||||
col.operator("object.boolean_brush_slice", text="Slice", icon='SELECT_DIFFERENCE')
|
||||
|
||||
|
||||
def boolean_extras_menu(self, context, cutter_only=False):
|
||||
layout = self.layout
|
||||
layout.operator_context = 'INVOKE_DEFAULT'
|
||||
col = layout.column(align=True)
|
||||
obj = context.active_object
|
||||
|
||||
if not obj:
|
||||
return
|
||||
|
||||
# Canvas operators
|
||||
if is_canvas(obj) and not cutter_only:
|
||||
col.separator()
|
||||
col.operator("object.boolean_toggle_all", text="Toggle All Cuters")
|
||||
col.operator("object.boolean_apply_all", text="Apply All Cutters")
|
||||
col.operator("object.boolean_remove_all", text="Remove All Cutters")
|
||||
|
||||
# Cutter operators
|
||||
if obj.booleans.cutter:
|
||||
if not cutter_only:
|
||||
col.separator()
|
||||
col.operator("object.boolean_toggle_cutter", text="Toggle Cutter").method='ALL'
|
||||
col.operator("object.boolean_apply_cutter", text="Apply Cutter").method='ALL'
|
||||
col.operator("object.boolean_remove_cutter", text="Remove Cutter").method='ALL'
|
||||
@@ -0,0 +1,46 @@
|
||||
import bpy
|
||||
import os
|
||||
import bpy.utils.previews
|
||||
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def get_custom_icon(icon_name: str):
|
||||
"""Returns the ID of a custom icon registered by add-on."""
|
||||
|
||||
svg_icons = preview_collections["main"]
|
||||
if not svg_icons:
|
||||
return 0
|
||||
|
||||
icon = svg_icons[icon_name].icon_id
|
||||
if not icon:
|
||||
return 0
|
||||
|
||||
return icon
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
preview_collections = {}
|
||||
svg_icons_dir = os.path.join(os.path.dirname(__file__), "svg")
|
||||
|
||||
icons = {
|
||||
"MEASURE": "measure.svg",
|
||||
"CPU": "cpu.svg",
|
||||
}
|
||||
|
||||
|
||||
def register():
|
||||
svg_icons = bpy.utils.previews.new()
|
||||
|
||||
for key, file in icons.items():
|
||||
svg_icons.load(key, os.path.join(svg_icons_dir, file), 'IMAGE')
|
||||
|
||||
preview_collections["main"] = svg_icons
|
||||
|
||||
|
||||
def unregister():
|
||||
for pcoll in preview_collections.values():
|
||||
bpy.utils.previews.remove(pcoll)
|
||||
preview_collections.clear()
|
||||
|
Before Width: | Height: | Size: 772 B After Width: | Height: | Size: 772 B |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,92 @@
|
||||
import bpy
|
||||
|
||||
from ..functions.modifier import (
|
||||
is_boolean_modifier,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ /cutters_list/ ------------------------------ ####
|
||||
|
||||
class VIEW3D_UL_boolean_cutters(bpy.types.UIList):
|
||||
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
||||
canvas = context.active_object
|
||||
mod = item
|
||||
|
||||
# Pick Icon
|
||||
if mod.operation == 'DIFFERENCE':
|
||||
icon = 'SELECT_SUBTRACT'
|
||||
elif mod.operation == 'UNION':
|
||||
icon = 'SELECT_EXTEND'
|
||||
elif mod.operation == 'INTERSECT':
|
||||
icon = 'SELECT_INTERSECT'
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(mod.object, "name", text="", icon=icon, emboss=False)
|
||||
|
||||
# Select Cutter
|
||||
op_select = row.operator("object.boolean_select_cutter", text="", icon='RESTRICT_SELECT_OFF', emboss=False)
|
||||
op_select.cutter = mod.object.name
|
||||
|
||||
# Toggle Cutter
|
||||
icon = 'HIDE_OFF' if mod.show_viewport else 'HIDE_ON'
|
||||
op_toggle = row.operator("object.boolean_toggle_cutter", text="", icon=icon, emboss=False)
|
||||
op_toggle.method = 'SPECIFIED'
|
||||
op_toggle.specified_cutter = mod.object.name
|
||||
op_toggle.specified_canvas = canvas.name
|
||||
op_toggle.specified_modifier = mod.name
|
||||
|
||||
|
||||
def filter_items(self, context, data, propname):
|
||||
flags = []
|
||||
indices = []
|
||||
|
||||
modifiers = getattr(data, propname)
|
||||
for mod in modifiers:
|
||||
if is_boolean_modifier(mod):
|
||||
flags.append(self.bitflag_filter_item)
|
||||
else:
|
||||
flags.append(0)
|
||||
|
||||
# Search Filter
|
||||
if self.filter_name:
|
||||
filter_name = self.filter_name.lower()
|
||||
for i, mod in enumerate(modifiers):
|
||||
if flags[i] != self.bitflag_filter_item:
|
||||
continue
|
||||
if filter_name not in mod.object.name.lower():
|
||||
flags[i] = 0
|
||||
|
||||
# Invert
|
||||
if self.use_filter_invert:
|
||||
for i, mod in enumerate(modifiers):
|
||||
if not is_boolean_modifier(mod):
|
||||
continue
|
||||
flags[i] ^= self.bitflag_filter_item
|
||||
|
||||
# Sort by Name
|
||||
indices = list(range(len(modifiers)))
|
||||
if self.use_filter_sort_alpha:
|
||||
sorted_indices = sorted(range(len(modifiers)),
|
||||
key=lambda i: modifiers[i].object.name if modifiers[i].object else "")
|
||||
indices = [0] * len(modifiers)
|
||||
for rank, original_i in enumerate(sorted_indices):
|
||||
indices[original_i] = rank
|
||||
|
||||
return flags, indices
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
classes = (
|
||||
VIEW3D_UL_boolean_cutters,
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -0,0 +1,129 @@
|
||||
import bpy
|
||||
|
||||
from ..functions.canvas import (
|
||||
is_canvas,
|
||||
)
|
||||
|
||||
from .common import (
|
||||
boolean_operators_menu,
|
||||
boolean_extras_menu,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ /boolean_menu/ ------------------------------ ####
|
||||
|
||||
# 3D Viewport (Object Mode) -> Object
|
||||
class VIEW3D_MT_boolean(bpy.types.Menu):
|
||||
bl_label = "Boolean"
|
||||
bl_idname = "VIEW3D_MT_boolean"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.menu("VIEW3D_MT_carve")
|
||||
layout.separator()
|
||||
boolean_operators_menu(self, context)
|
||||
boolean_extras_menu(self, context)
|
||||
|
||||
|
||||
def object_mode_menu(self, context):
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
layout.menu("VIEW3D_MT_boolean")
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /popup_menu/ ------------------------------ ####
|
||||
|
||||
# Shift-Ctrl-B Menu
|
||||
class VIEW3D_MT_boolean_popup(bpy.types.Menu):
|
||||
bl_label = "Boolean"
|
||||
bl_idname = "VIEW3D_MT_boolean_popup"
|
||||
|
||||
def draw(self, context):
|
||||
boolean_operators_menu(self, context)
|
||||
boolean_extras_menu(self, context)
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /carve_menu/ ------------------------------ ####
|
||||
|
||||
class VIEW3D_MT_carve(bpy.types.Menu):
|
||||
bl_label = "Carve"
|
||||
bl_idname = "VIEW3D_MT_carve"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator("object.carve_box", text="Box Carve")
|
||||
layout.operator("object.carve_circle", text="Circle Carve")
|
||||
layout.operator("object.carve_polyline", text="Polyline Carve")
|
||||
|
||||
|
||||
# Separate "Carve" menu in Edit Mode (where we don't have "Boolean" menu).
|
||||
def edit_mode_menu(self, context):
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
layout.menu("VIEW3D_MT_carve")
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /select_menu/ ------------------------------ ####
|
||||
|
||||
def select_menu(self, context):
|
||||
layout = self.layout
|
||||
obj = context.active_object
|
||||
|
||||
if not obj:
|
||||
return
|
||||
|
||||
if is_canvas(obj) or obj.booleans.cutter:
|
||||
layout.separator()
|
||||
|
||||
if is_canvas(obj):
|
||||
layout.operator("object.boolean_select_all", text="Boolean Cutters")
|
||||
if obj.booleans.cutter:
|
||||
layout.operator("object.select_cutter_canvas", text="Boolean Canvases")
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
addon_keymaps = []
|
||||
|
||||
classes = (
|
||||
VIEW3D_MT_boolean,
|
||||
VIEW3D_MT_boolean_popup,
|
||||
VIEW3D_MT_carve,
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
# MENU
|
||||
bpy.types.VIEW3D_MT_object.append(object_mode_menu)
|
||||
bpy.types.VIEW3D_MT_edit_mesh.append(edit_mode_menu)
|
||||
bpy.types.VIEW3D_MT_select_object.append(select_menu)
|
||||
|
||||
# KEYMAP
|
||||
addon = bpy.context.window_manager.keyconfigs.addon
|
||||
km = addon.keymaps.new(name="Object Mode")
|
||||
|
||||
kmi = km.keymap_items.new("wm.call_menu", 'B', 'PRESS', ctrl=True, shift=True)
|
||||
kmi.properties.name = "VIEW3D_MT_boolean_popup"
|
||||
kmi.active = True
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
# MENU
|
||||
bpy.types.VIEW3D_MT_object.remove(object_mode_menu)
|
||||
bpy.types.VIEW3D_MT_edit_mesh.remove(edit_mode_menu)
|
||||
bpy.types.VIEW3D_MT_select_object.remove(select_menu)
|
||||
|
||||
# KEYMAP
|
||||
for km, kmi in addon_keymaps:
|
||||
km.keymap_items.remove(kmi)
|
||||
addon_keymaps.clear()
|
||||
@@ -0,0 +1,182 @@
|
||||
import bpy
|
||||
from .. import __package__ as base_package
|
||||
|
||||
from ..functions.canvas import (
|
||||
is_canvas,
|
||||
)
|
||||
|
||||
from .common import (
|
||||
get_modifier_from_list_index,
|
||||
boolean_extras_menu,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ PANELS ------------------------------ ####
|
||||
|
||||
# Boolean Operators Panel
|
||||
class VIEW3D_PT_boolean(bpy.types.Panel):
|
||||
bl_label = "Boolean"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Edit"
|
||||
bl_context = "objectmode"
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
return prefs.show_in_sidebar
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator_context = 'INVOKE_DEFAULT'
|
||||
col = layout.column(align=True)
|
||||
|
||||
col.label(text="Auto Boolean")
|
||||
row = col.row(align=False)
|
||||
row.operator("object.boolean_auto_difference", text="Difference", icon='SELECT_SUBTRACT')
|
||||
row.operator("object.boolean_auto_difference", text="", icon='UV_SYNC_SELECT').flip=True
|
||||
row = col.row(align=False)
|
||||
row.operator("object.boolean_auto_union", text="Union", icon='SELECT_EXTEND')
|
||||
row.operator("object.boolean_auto_union", text="", icon='UV_SYNC_SELECT').flip=True
|
||||
row = col.row(align=False)
|
||||
row.operator("object.boolean_auto_intersect", text="Intersect", icon='SELECT_INTERSECT')
|
||||
row.operator("object.boolean_auto_intersect", text="", icon='UV_SYNC_SELECT').flip=True
|
||||
row = col.row(align=False)
|
||||
row.operator("object.boolean_auto_slice", text="Slice", icon='SELECT_DIFFERENCE')
|
||||
row.operator("object.boolean_auto_slice", text="", icon='UV_SYNC_SELECT').flip=True
|
||||
|
||||
col.separator()
|
||||
col.label(text="Brush Boolean")
|
||||
row = col.row(align=False)
|
||||
row.operator("object.boolean_brush_difference", text="Difference", icon='SELECT_SUBTRACT')
|
||||
row.operator("object.boolean_brush_difference", text="", icon='UV_SYNC_SELECT').flip=True
|
||||
row = col.row(align=False)
|
||||
row.operator("object.boolean_brush_union", text="Union", icon='SELECT_EXTEND')
|
||||
row.operator("object.boolean_brush_union", text="", icon='UV_SYNC_SELECT').flip=True
|
||||
row = col.row(align=False)
|
||||
row.operator("object.boolean_brush_intersect", text="Intersect", icon='SELECT_INTERSECT')
|
||||
row.operator("object.boolean_brush_intersect", text="", icon='UV_SYNC_SELECT').flip=True
|
||||
row = col.row(align=False)
|
||||
row.operator("object.boolean_brush_slice", text="Slice", icon='SELECT_DIFFERENCE')
|
||||
row.operator("object.boolean_brush_slice", text="", icon='UV_SYNC_SELECT').flip=True
|
||||
|
||||
|
||||
# Cutters Panel
|
||||
class VIEW3D_PT_boolean_cutters(bpy.types.Panel):
|
||||
bl_label = "Cutters"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Edit"
|
||||
bl_context = "objectmode"
|
||||
bl_parent_id = "VIEW3D_PT_boolean"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
if prefs.show_in_sidebar:
|
||||
if context.active_object:
|
||||
if is_canvas(context.active_object):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
canvas = context.active_object
|
||||
|
||||
# Cutters List
|
||||
row = layout.row()
|
||||
col = row.column()
|
||||
col.template_list(
|
||||
"VIEW3D_UL_boolean_cutters",
|
||||
"",
|
||||
canvas, "modifiers",
|
||||
canvas.booleans, "modifiers_list_index",
|
||||
rows=5,
|
||||
)
|
||||
|
||||
# Filter & Operators
|
||||
col = row.column(align=True)
|
||||
cutters_list_index = canvas.booleans.modifiers_list_index
|
||||
mod = get_modifier_from_list_index(canvas, cutters_list_index)
|
||||
sub = col.column(align=True)
|
||||
|
||||
# Apply Cutter
|
||||
op_apply = sub.operator("object.boolean_apply_cutter", text="", icon='CHECKMARK')
|
||||
op_apply.method = 'SPECIFIED'
|
||||
op_apply.specified_cutter = mod.object.name if mod else ""
|
||||
op_apply.specified_canvas = canvas.name
|
||||
|
||||
# Remove Cutter
|
||||
op_remove = sub.operator("object.boolean_remove_cutter", text="", icon='X')
|
||||
op_remove.method = 'SPECIFIED'
|
||||
op_remove.specified_cutter = mod.object.name if mod else ""
|
||||
op_remove.specified_canvas = canvas.name
|
||||
op_remove.specified_modifier = mod.name if mod else ""
|
||||
|
||||
if cutters_list_index < 0 or not mod:
|
||||
sub.enabled = False
|
||||
|
||||
col.separator()
|
||||
col.menu("VIEW3D_MT_boolean_specials", icon='DOWNARROW_HLT', text="")
|
||||
|
||||
|
||||
# Helpers Panel
|
||||
class VIEW3D_PT_boolean_helpers(bpy.types.Panel):
|
||||
bl_label = "Helpers"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Edit"
|
||||
bl_context = "objectmode"
|
||||
bl_parent_id = "VIEW3D_PT_boolean"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
if not prefs.show_in_sidebar:
|
||||
return False
|
||||
if not context.active_object:
|
||||
return False
|
||||
if context.active_object.booleans.cutter:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def draw(self, context):
|
||||
boolean_extras_menu(self, context, cutter_only=True)
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ MENUS ------------------------------ ####
|
||||
|
||||
# Specials
|
||||
class VIEW3D_MT_boolean_specials(bpy.types.Menu):
|
||||
bl_label = "Boolean Operators"
|
||||
bl_idname = "VIEW3D_MT_boolean_specials"
|
||||
|
||||
def draw(self, context):
|
||||
boolean_extras_menu(self, context)
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
classes = (
|
||||
VIEW3D_PT_boolean,
|
||||
VIEW3D_PT_boolean_cutters,
|
||||
VIEW3D_PT_boolean_helpers,
|
||||
VIEW3D_MT_boolean_specials,
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -1,43 +0,0 @@
|
||||
import bpy
|
||||
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
@bpy.app.handlers.persistent
|
||||
def populate_boolean_properties(scene):
|
||||
prefs = bpy.context.preferences.addons[__package__].preferences
|
||||
if prefs.versioning:
|
||||
for obj in bpy.data.objects:
|
||||
if not obj.get("BoolToolRoot"):
|
||||
continue
|
||||
|
||||
# Convert Canvas
|
||||
if obj.get("BoolToolRoot"):
|
||||
obj.booleans.canvas = True
|
||||
del obj["BoolToolRoot"]
|
||||
if obj.get("BoolTool_FTransform"):
|
||||
del obj["BoolTool_FTransform"]
|
||||
|
||||
for mod in obj.modifiers:
|
||||
if mod.type == 'BOOLEAN' and "BTool_" in mod.name:
|
||||
mod.name = "boolean_" + mod.object.name
|
||||
cutter = mod.object
|
||||
|
||||
# Convert Canvases
|
||||
if cutter.get("BoolToolBrush"):
|
||||
cutter.booleans.cutter = cutter.get("BoolToolBrush")
|
||||
del cutter["BoolToolBrush"]
|
||||
if cutter.get("BoolTool_FTransform"):
|
||||
del cutter["BoolTool_FTransform"]
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
def register():
|
||||
# HANDLERS
|
||||
bpy.app.handlers.load_post.append(populate_boolean_properties)
|
||||
|
||||
def unregister():
|
||||
# HANDLERS
|
||||
bpy.app.handlers.load_post.remove(populate_boolean_properties)
|
||||
@@ -2,14 +2,17 @@
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from . import utils, icons, settings, preferences, ui, draw_tool, ops
|
||||
import importlib
|
||||
from . import geomod, utils, icons, settings, preferences, ui, draw_tool, ops
|
||||
import tomllib as toml
|
||||
|
||||
modules = [utils, icons, settings, preferences, ui, draw_tool, ops]
|
||||
modules = [geomod, utils, icons, settings, preferences, ui, draw_tool, ops]
|
||||
|
||||
def register():
|
||||
# register modules
|
||||
for m in modules:
|
||||
importlib.reload(m)
|
||||
if hasattr(m, 'register'):
|
||||
m.register()
|
||||
|
||||
# read addon meta-data
|
||||
@@ -26,6 +29,7 @@ def register():
|
||||
def unregister():
|
||||
# un-register modules
|
||||
for m in reversed(modules):
|
||||
if hasattr(m, 'unregister'):
|
||||
m.unregister()
|
||||
|
||||
# Remove addon asset library
|
||||
|
||||
BIN
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
schema_version = "1.0.0"
|
||||
|
||||
id = "brushstroke_tools"
|
||||
version = "1.2.1"
|
||||
version = "1.2.3"
|
||||
name = "Brushstroke Tools"
|
||||
tagline = "Brushstroke painting tools by the Blender Studio"
|
||||
maintainer = "Simon Thommes <simon@blender.org>"
|
||||
|
||||
@@ -97,9 +97,7 @@ class BSBST_OT_pre_process_brushstroke(bpy.types.Operator):
|
||||
if bpy.app.version >= (5,1):
|
||||
utils.ensure_resources()
|
||||
ng_process = bpy.data.node_groups['.brushstroke_tools.draw_processing']
|
||||
ng_process.asset_mark() # workaround to let Blender register the node tool as an operator with the automatically generated id
|
||||
ng_process.asset_clear()
|
||||
|
||||
ng_process.is_tool = True # workaround to let Blender register the node tool as an operator with the automatically generated id
|
||||
return {'FINISHED'}
|
||||
|
||||
class BSBST_OT_post_process_brushstroke(bpy.types.Operator):
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
"""
|
||||
Compatibility wrappers for the GeoNodes modifier input API.
|
||||
|
||||
Blender 5.2 replaced dict-style custom-property access on GeoNodes modifiers
|
||||
with proper RNA properties. All modifier input reads and writes in this add-on
|
||||
go through these helpers so version branching stays in one place.
|
||||
|
||||
Before 5.2: modifier["identifier"] = value
|
||||
5.2+: modifier.properties.inputs.identifier.value = value
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
import bpy
|
||||
from bpy.types import NodesModifier, UILayout, NodeTreeInterfaceSocket
|
||||
|
||||
|
||||
_ICON_FOR_SOCKET = {
|
||||
bpy.types.NodeTreeInterfaceSocketObject: 'OBJECT_DATA',
|
||||
bpy.types.NodeTreeInterfaceSocketMaterial: 'MATERIAL',
|
||||
bpy.types.NodeTreeInterfaceSocketImage: 'IMAGE_DATA',
|
||||
bpy.types.NodeTreeInterfaceSocketCollection: 'OUTLINER_COLLECTION',
|
||||
}
|
||||
|
||||
_DATA_COLLECTION_FOR_SOCKET = {
|
||||
bpy.types.NodeTreeInterfaceSocketMaterial: 'materials',
|
||||
bpy.types.NodeTreeInterfaceSocketImage: 'images',
|
||||
bpy.types.NodeTreeInterfaceSocketCollection: 'collections',
|
||||
}
|
||||
|
||||
|
||||
def get_value(mod: NodesModifier, identifier: str) -> Any:
|
||||
if bpy.app.version >= (5, 2, 0):
|
||||
return getattr(mod.properties.inputs, identifier).value
|
||||
return mod[identifier]
|
||||
|
||||
|
||||
def set_value(mod: NodesModifier, identifier: str, value: Any):
|
||||
if bpy.app.version >= (5, 2, 0):
|
||||
getattr(mod.properties.inputs, identifier).value = value
|
||||
else:
|
||||
prop_ui = mod.id_properties_ui(identifier).as_dict()
|
||||
if 'items' in prop_ui.keys():
|
||||
for item in prop_ui['items']:
|
||||
if item[0] == value:
|
||||
mod[identifier] = item[4]
|
||||
return
|
||||
print(f"Error: Item '{value}' not found in menu '{identifier}' of modifier '{mod.name}'")
|
||||
else:
|
||||
mod[identifier] = value
|
||||
|
||||
|
||||
def is_input_value_settable(mod: NodesModifier, identifier: str) -> bool:
|
||||
"""Return True if the modifier exposes a settable input with this identifier."""
|
||||
if bpy.app.version >= (5, 2, 0):
|
||||
inp = getattr(mod.properties.inputs, identifier, None)
|
||||
return inp is not None and hasattr(inp, 'value')
|
||||
return identifier in mod.keys()
|
||||
|
||||
|
||||
def can_input_be_attribute(mod: NodesModifier, identifier: str) -> bool:
|
||||
"""Return True if this input can be driven by an attribute field."""
|
||||
if bpy.app.version >= (5, 2, 0):
|
||||
inp = getattr(mod.properties.inputs, identifier, None)
|
||||
if inp is None or not hasattr(inp, 'type'):
|
||||
return False
|
||||
return any(item.identifier == 'ATTRIBUTE'
|
||||
for item in inp.bl_rna.properties['type'].enum_items)
|
||||
return f'{identifier}_use_attribute' in mod.keys()
|
||||
|
||||
|
||||
def get_input_is_attribute(mod: NodesModifier, identifier: str) -> bool:
|
||||
"""Return True if this input is currently being driven by an attribute field."""
|
||||
if bpy.app.version >= (5, 2, 0):
|
||||
return getattr(mod.properties.inputs, identifier).type == 'ATTRIBUTE'
|
||||
return mod[f'{identifier}_use_attribute']
|
||||
|
||||
|
||||
def set_input_is_attribute(mod: NodesModifier, identifier: str, value: bool):
|
||||
if bpy.app.version >= (5, 2, 0):
|
||||
getattr(mod.properties.inputs, identifier).type = 'ATTRIBUTE' if value else 'VALUE'
|
||||
else:
|
||||
mod[f'{identifier}_use_attribute'] = value
|
||||
|
||||
|
||||
def set_attribute_name(mod: NodesModifier, identifier: str, name: str):
|
||||
if bpy.app.version >= (5, 2, 0):
|
||||
getattr(mod.properties.inputs, identifier).attribute_name = name
|
||||
else:
|
||||
mod[f'{identifier}_attribute_name'] = name
|
||||
|
||||
|
||||
def get_enum_value_to_compare(mod: NodesModifier, identifier: str) -> list[tuple[str, str | int]]:
|
||||
"""Return list of (identifier_str, comparable_value) pairs for a Menu socket.
|
||||
The comparable_value matches whatever get_value() returns for the same socket:
|
||||
an identifier string on 5.2+ (where .value is a string), an integer on older versions."""
|
||||
if bpy.app.version >= (5, 2, 0):
|
||||
inp = getattr(mod.properties.inputs, identifier)
|
||||
return [(item.identifier, item.identifier)
|
||||
for item in inp.bl_rna.properties['value'].enum_items]
|
||||
return [(item[0], item[4])
|
||||
for item in mod.id_properties_ui(identifier).as_dict()['items']]
|
||||
|
||||
|
||||
def copy_inputs(source_mod: NodesModifier, target_mod: NodesModifier):
|
||||
"""Copy all GeoNodes input values (and attribute settings) between modifiers."""
|
||||
if bpy.app.version >= (5, 2, 0):
|
||||
props = source_mod.properties
|
||||
if not props:
|
||||
return
|
||||
target_props = target_mod.properties
|
||||
for socket_name in props.inputs.keys():
|
||||
source_socket = getattr(props.inputs, socket_name, None)
|
||||
target_socket = getattr(target_props.inputs, socket_name, None)
|
||||
if source_socket and target_socket and hasattr(source_socket, "value"):
|
||||
target_socket.value = source_socket.value
|
||||
else:
|
||||
for key, value in source_mod.items():
|
||||
try:
|
||||
target_mod[key] = value
|
||||
except (TypeError, ValueError):
|
||||
target_mod[key] = type(target_mod[key])(value)
|
||||
|
||||
|
||||
def draw_socket(
|
||||
layout: UILayout,
|
||||
mod: NodesModifier,
|
||||
v: NodeTreeInterfaceSocket,
|
||||
label: str,
|
||||
is_preset: bool,
|
||||
):
|
||||
"""Draw a GeoNodes input socket: value widget plus attribute toggle if supported."""
|
||||
if can_input_be_attribute(mod, v.identifier) and not v.force_non_field:
|
||||
if get_input_is_attribute(mod, v.identifier):
|
||||
_draw_attribute_name(layout, mod, v.identifier, text=label)
|
||||
else:
|
||||
_draw_input_simple(layout, mod, v.identifier, text=label)
|
||||
operator_id = ('brushstroke_tools.preset_toggle_attribute' if is_preset
|
||||
else 'brushstroke_tools.brushstrokes_toggle_attribute')
|
||||
toggle = layout.operator(operator_id, text='',
|
||||
depress=get_input_is_attribute(mod, v.identifier),
|
||||
icon='SPREADSHEET')
|
||||
toggle.modifier_name = mod.name
|
||||
toggle.input_name = v.identifier
|
||||
else:
|
||||
socket_type = type(v)
|
||||
icon = _ICON_FOR_SOCKET.get(socket_type, 'NONE')
|
||||
search_prop = _DATA_COLLECTION_FOR_SOCKET.get(socket_type)
|
||||
_draw_input_simple(
|
||||
layout, mod, v.identifier,
|
||||
search_prop=search_prop,
|
||||
text=label, icon=icon,
|
||||
)
|
||||
|
||||
|
||||
def _draw_attribute_name(
|
||||
layout: UILayout,
|
||||
mod: NodesModifier,
|
||||
identifier: str,
|
||||
**kwargs,
|
||||
):
|
||||
"""Draw a modifier input's attribute-name field."""
|
||||
if bpy.app.version >= (5, 2, 0):
|
||||
layout.prop(getattr(mod.properties.inputs, identifier), 'attribute_name', **kwargs)
|
||||
else:
|
||||
layout.prop(mod, f'["{identifier}_attribute_name"]', **kwargs)
|
||||
|
||||
|
||||
def _draw_input_simple(
|
||||
layout: UILayout,
|
||||
mod: NodesModifier,
|
||||
identifier: str,
|
||||
search_prop: str | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Draw a modifier input value widget.
|
||||
Pass search_prop for prop_search on Blender < 4.2.
|
||||
Later versions use pointers, so no need for prop_search.
|
||||
"""
|
||||
if bpy.app.version >= (5, 2, 0):
|
||||
layout.prop(getattr(mod.properties.inputs, identifier), 'value', **kwargs)
|
||||
elif search_prop:
|
||||
layout.prop_search(mod, f'["{identifier}"]', bpy.data, search_prop, **kwargs)
|
||||
else:
|
||||
layout.prop(mod, f'["{identifier}"]', **kwargs)
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import bpy
|
||||
import random
|
||||
from . import utils, settings
|
||||
from . import utils, settings, geomod
|
||||
import mathutils
|
||||
|
||||
class BSBST_OT_new_brushstrokes(bpy.types.Operator):
|
||||
@@ -85,8 +85,8 @@ class BSBST_OT_new_brushstrokes(bpy.types.Operator):
|
||||
|
||||
utils.mark_socket_context_type(mod_info, 'Socket_2', 'SURFACE_OBJECT')
|
||||
|
||||
mod['Socket_2'] = surface_object
|
||||
mod['Socket_3'] = False
|
||||
geomod.set_value(mod, 'Socket_2', surface_object)
|
||||
geomod.set_value(mod, 'Socket_3', False)
|
||||
|
||||
utils.set_deformable(flow_object, settings.deforming_surface)
|
||||
utils.set_animated(flow_object, settings.animated)
|
||||
@@ -161,24 +161,25 @@ class BSBST_OT_new_brushstrokes(bpy.types.Operator):
|
||||
continue
|
||||
# initialize linked context parameters
|
||||
link_context_type = settings.preset_object.modifier_info[mod.name].socket_info[v.identifier].link_context_type
|
||||
bs_mod = brushstrokes_object.modifiers[mod.name]
|
||||
if link_context_type=='SURFACE_OBJECT':
|
||||
brushstrokes_object.modifiers[mod.name][f'{v.identifier}'] = surface_object
|
||||
geomod.set_value(bs_mod, v.identifier, surface_object)
|
||||
elif link_context_type=='FLOW_OBJECT':
|
||||
brushstrokes_object.modifiers[mod.name][f'{v.identifier}'] = flow_object
|
||||
geomod.set_value(bs_mod, v.identifier, flow_object)
|
||||
elif link_context_type=='MATERIAL':
|
||||
brushstrokes_object.modifiers[mod.name][f'{v.identifier}'] = settings.context_material
|
||||
geomod.set_value(bs_mod, v.identifier, settings.context_material)
|
||||
elif link_context_type=='UVMAP':
|
||||
if type(brushstrokes_object.modifiers[mod.name][f'{v.identifier}']) == str:
|
||||
brushstrokes_object.modifiers[mod.name][f'{v.identifier}'] = surface_object.data.uv_layers.active.name
|
||||
if isinstance(geomod.get_value(bs_mod, v.identifier), str):
|
||||
geomod.set_value(bs_mod, v.identifier, surface_object.data.uv_layers.active.name)
|
||||
else:
|
||||
brushstrokes_object.modifiers[mod.name][f'{v.identifier}_use_attribute'] = True
|
||||
brushstrokes_object.modifiers[mod.name][f'{v.identifier}_attribute_name'] = surface_object.data.uv_layers.active.name
|
||||
geomod.set_input_is_attribute(bs_mod, v.identifier, True)
|
||||
geomod.set_attribute_name(bs_mod, v.identifier, surface_object.data.uv_layers.active.name)
|
||||
elif link_context_type=='RANDOM':
|
||||
vmin = v.min_value
|
||||
vmax = v.max_value
|
||||
val = vmin + random.random() * (vmax - vmin)
|
||||
brushstrokes_object.modifiers[mod.name][f'{v.identifier}_use_attribute'] = False
|
||||
brushstrokes_object.modifiers[mod.name][f'{v.identifier}'] = type(brushstrokes_object.modifiers[mod.name][f'{v.identifier}'])(val)
|
||||
geomod.set_input_is_attribute(bs_mod, v.identifier, False)
|
||||
geomod.set_value(bs_mod, v.identifier, type(geomod.get_value(bs_mod, v.identifier))(val))
|
||||
|
||||
# transfer modifier info data from preset to brush strokes
|
||||
utils.deep_copy_mod_info(settings.preset_object, brushstrokes_object)
|
||||
@@ -195,14 +196,14 @@ class BSBST_OT_new_brushstrokes(bpy.types.Operator):
|
||||
mod = brushstrokes_object.modifiers['Brushstrokes']
|
||||
if settings.brushstroke_method == 'SURFACE_FILL':
|
||||
# set density
|
||||
mod['Socket_7'] = utils.round_n((1000 / surf_est) ** 0.5, 2)
|
||||
geomod.set_value(mod, 'Socket_7', utils.round_n((1000 / surf_est) ** 0.5, 2))
|
||||
# set length
|
||||
mod['Socket_11'] = utils.round_n(bb_radius * 0.5, 2)
|
||||
geomod.set_value(mod, 'Socket_11', utils.round_n(bb_radius * 0.5, 2))
|
||||
# set width
|
||||
mod['Socket_13'] = utils.round_n(bb_radius * 0.05, 2)
|
||||
geomod.set_value(mod, 'Socket_13', utils.round_n(bb_radius * 0.05, 2))
|
||||
elif settings.brushstroke_method == 'SURFACE_DRAW':
|
||||
# set width
|
||||
mod['Socket_5'] = utils.round_n(bb_radius * 0.05, 2)
|
||||
geomod.set_value(mod, 'Socket_5', utils.round_n(bb_radius * 0.05, 2))
|
||||
|
||||
# refresh UI
|
||||
for mod in brushstrokes_object.modifiers:
|
||||
@@ -213,7 +214,7 @@ class BSBST_OT_new_brushstrokes(bpy.types.Operator):
|
||||
for v in mod.node_group.interface.items_tree.values():
|
||||
if type(v) != bpy.types.NodeTreeInterfaceSocketMaterial:
|
||||
continue
|
||||
mat = mod[v.identifier]
|
||||
mat = geomod.get_value(mod, v.identifier)
|
||||
if not mat:
|
||||
continue
|
||||
if mat in [m_slot.material for m_slot in brushstrokes_object.material_slots]:
|
||||
@@ -383,7 +384,7 @@ class BSBST_OT_duplicate_brushstrokes(bpy.types.Operator):
|
||||
vmin = v.min_value
|
||||
vmax = v.max_value
|
||||
val = vmin + random.random() * (vmax - vmin)
|
||||
mod[f'{v.identifier}'] = type(ob.modifiers[mod.name][f'{v.identifier}'])(val)
|
||||
geomod.set_value(mod, v.identifier, type(geomod.get_value(mod, v.identifier))(val))
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -475,20 +476,21 @@ class BSBST_OT_copy_brushstrokes(bpy.types.Operator):
|
||||
continue
|
||||
# re-initialize linked context parameters
|
||||
link_context_type = ob.modifier_info[mod.name].socket_info[v.identifier].link_context_type
|
||||
ob_mod = ob.modifiers[mod.name]
|
||||
if link_context_type=='SURFACE_OBJECT':
|
||||
ob.modifiers[mod.name][f'{v.identifier}'] = surface_object
|
||||
geomod.set_value(ob_mod, v.identifier, surface_object)
|
||||
elif link_context_type=='UVMAP':
|
||||
if type(ob.modifiers[mod.name][f'{v.identifier}']) == str:
|
||||
ob.modifiers[mod.name][f'{v.identifier}'] = surface_object.data.uv_layers.active.name
|
||||
if isinstance(geomod.get_value(ob_mod, v.identifier), str):
|
||||
geomod.set_value(ob_mod, v.identifier, surface_object.data.uv_layers.active.name)
|
||||
else:
|
||||
ob.modifiers[mod.name][f'{v.identifier}_use_attribute'] = True
|
||||
ob.modifiers[mod.name][f'{v.identifier}_attribute_name'] = surface_object.data.uv_layers.active.name
|
||||
geomod.set_input_is_attribute(ob_mod, v.identifier, True)
|
||||
geomod.set_attribute_name(ob_mod, v.identifier, surface_object.data.uv_layers.active.name)
|
||||
elif link_context_type=='RANDOM':
|
||||
vmin = v.min_value
|
||||
vmax = v.max_value
|
||||
val = vmin + random.random() * (vmax - vmin)
|
||||
ob.modifiers[mod.name][f'{v.identifier}_use_attribute'] = False
|
||||
ob.modifiers[mod.name][f'{v.identifier}'] = type(ob.modifiers[mod.name][f'{v.identifier}'])(val)
|
||||
geomod.set_input_is_attribute(ob_mod, v.identifier, False)
|
||||
geomod.set_value(ob_mod, v.identifier, type(geomod.get_value(ob_mod, v.identifier))(val))
|
||||
|
||||
|
||||
# enable rest position
|
||||
@@ -577,11 +579,11 @@ def set_brushstrokes_deformable(bs_ob, deformable):
|
||||
if not mod.node_group:
|
||||
continue
|
||||
if mod.node_group.name == '.brushstroke_tools.pre_processing':
|
||||
mod['Socket_3'] = deformable
|
||||
geomod.set_value(mod, 'Socket_3', deformable)
|
||||
elif mod.node_group.name == '.brushstroke_tools.surface_fill':
|
||||
mod['Socket_27'] = deformable
|
||||
geomod.set_value(mod, 'Socket_27', deformable)
|
||||
elif mod.node_group.name == '.brushstroke_tools.surface_draw':
|
||||
mod['Socket_15'] = deformable
|
||||
geomod.set_value(mod, 'Socket_15', deformable)
|
||||
|
||||
mod.node_group.interface_update(bpy.context)
|
||||
utils.set_deformable(bs_ob, deformable)
|
||||
@@ -610,7 +612,7 @@ def set_brushstrokes_animated(bs_ob, animated):
|
||||
# ui visibility settings
|
||||
mod_info.hide_ui = True
|
||||
else:
|
||||
mod['Socket_5'] = True
|
||||
geomod.set_value(mod, 'Socket_5', True)
|
||||
mod.node_group.interface_update(bpy.context)
|
||||
with bpy.context.temp_override(object=ob):
|
||||
bpy.ops.object.modifier_move_to_index(modifier=mod.name, index=0)
|
||||
@@ -846,7 +848,7 @@ class BSBST_OT_init_preset(bpy.types.Operator):
|
||||
|
||||
# set fill custom defaults
|
||||
|
||||
mod['Socket_59'] = 1 # color method: curves
|
||||
geomod.set_value(mod, 'Socket_59', 'Curves') # color method: curves
|
||||
|
||||
# context link settings
|
||||
utils.mark_socket_context_type(mod_info, 'Socket_2', 'FLOW_OBJECT')
|
||||
@@ -1002,8 +1004,8 @@ class BSBST_OT_make_preset(bpy.types.Operator):
|
||||
if not settings.preset_object.modifier_info[mod.name].socket_info[v.identifier]:
|
||||
continue
|
||||
if type(v) == bpy.types.NodeTreeInterfaceSocketObject:
|
||||
if bs_ob['BSBST_surface_object']==mod[v.identifier]:
|
||||
mod[v.identifier] = None
|
||||
if bs_ob['BSBST_surface_object']==geomod.get_value(mod, v.identifier):
|
||||
geomod.set_value(mod, v.identifier, None)
|
||||
settings.preset_object.modifier_info[mod.name].socket_info[v.identifier].link_context = True
|
||||
elif type(v) == bpy.types.NodeTreeInterfaceSocketMaterial:
|
||||
pass # TODO: figure out material preset linking
|
||||
|
||||
@@ -3,8 +3,15 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from . import utils
|
||||
from . import utils, geomod
|
||||
from . import settings as settings_py
|
||||
from bpy.types import (
|
||||
UILayout,
|
||||
NodesModifier,
|
||||
NodeTreeInterfaceItem,
|
||||
NodeTreeInterfacePanel,
|
||||
NodeTreeInterfaceSocketMenu,
|
||||
)
|
||||
|
||||
warning_icons_dict = {
|
||||
'ERROR': 'CANCEL',
|
||||
@@ -12,133 +19,92 @@ warning_icons_dict = {
|
||||
'INFO': 'INFO',
|
||||
}
|
||||
|
||||
def draw_panel_ui_recursive(panel, panel_name, mod, items, display_mode, hide_panel=False):
|
||||
|
||||
scene = bpy.context.scene
|
||||
settings = scene.BSBST_settings
|
||||
|
||||
is_preset = mod.id_data == settings.preset_object and mod.id_data
|
||||
|
||||
def draw_panel_ui_recursive(
|
||||
panel: UILayout | None,
|
||||
panel_name: str,
|
||||
mod: NodesModifier,
|
||||
interface_items: tuple[str, NodeTreeInterfaceItem],
|
||||
display_mode: int,
|
||||
hide_panel: bool = False,
|
||||
):
|
||||
if not panel:
|
||||
return
|
||||
|
||||
mod_info = mod.id_data.modifier_info.get(mod.name)
|
||||
|
||||
icon_dict = {
|
||||
bpy.types.NodeTreeInterfaceSocketObject: 'OBJECT_DATA',
|
||||
bpy.types.NodeTreeInterfaceSocketMaterial: 'MATERIAL',
|
||||
bpy.types.NodeTreeInterfaceSocketImage: 'IMAGE_DATA',
|
||||
bpy.types.NodeTreeInterfaceSocketCollection: 'OUTLINER_COLLECTION',
|
||||
}
|
||||
|
||||
data_dict = {
|
||||
bpy.types.NodeTreeInterfaceSocketMaterial: 'materials',
|
||||
bpy.types.NodeTreeInterfaceSocketImage: 'images',
|
||||
bpy.types.NodeTreeInterfaceSocketCollection: 'collections',
|
||||
}
|
||||
|
||||
mode_compare = []
|
||||
for k, v in items:
|
||||
if not v.parent.name == panel_name:
|
||||
continue
|
||||
if type(v) == bpy.types.NodeTreeInterfacePanel:
|
||||
|
||||
v_id = f'Panel_{v.index}' # TODO: replace with panel identifier once that is exposed in Blender 4.3
|
||||
|
||||
if not mod_info:
|
||||
continue
|
||||
s = mod_info.socket_info.get(v_id)
|
||||
if not s:
|
||||
continue
|
||||
if display_mode == 0:
|
||||
if s.hide_ui:
|
||||
return
|
||||
|
||||
panel_active = not (mod_info.hide_ui or hide_panel)
|
||||
inactive_mode_names: list[str] = []
|
||||
|
||||
for name, interface_item in interface_items:
|
||||
if interface_item.parent.name != panel_name:
|
||||
continue
|
||||
|
||||
subpanel_header, subpanel = panel.panel(k, default_closed = v.default_closed)
|
||||
subpanel_header.label(text=k)
|
||||
if isinstance(interface_item, NodeTreeInterfacePanel):
|
||||
panel_id = f'Panel_{interface_item.index}' # TODO: replace with panel identifier once exposed in Blender 4.3
|
||||
socket_info = mod_info.socket_info.get(panel_id)
|
||||
if not socket_info:
|
||||
continue
|
||||
if display_mode == 0 and socket_info.hide_ui:
|
||||
continue
|
||||
|
||||
subpanel_header, subpanel = panel.panel(name, default_closed=interface_item.default_closed)
|
||||
subpanel_header.label(text=name)
|
||||
if display_mode != 0:
|
||||
col = subpanel_header.column()
|
||||
col.active = not (mod_info.hide_ui or hide_panel)
|
||||
col.prop(s, 'hide_ui', icon_only=True, icon='UNPINNED' if s.hide_ui else 'PINNED', emboss=False)
|
||||
draw_panel_ui_recursive(subpanel, k, mod, v.interface_items.items(), display_mode, s.hide_ui)
|
||||
mode_compare = []
|
||||
col.active = panel_active
|
||||
col.prop(socket_info, 'hide_ui', icon_only=True,
|
||||
icon='UNPINNED' if socket_info.hide_ui else 'PINNED', emboss=False)
|
||||
draw_panel_ui_recursive(subpanel, name, mod, interface_item.interface_items.items(), display_mode, socket_info.hide_ui)
|
||||
inactive_mode_names = []
|
||||
else:
|
||||
if v.parent.name != panel_name:
|
||||
if not geomod.is_input_value_settable(mod, interface_item.identifier):
|
||||
continue
|
||||
if f'{v.identifier}' not in mod.keys():
|
||||
continue
|
||||
if not mod_info:
|
||||
socket_info = mod_info.socket_info.get(interface_item.identifier)
|
||||
if not socket_info:
|
||||
continue
|
||||
|
||||
if type(v) == bpy.types.NodeTreeInterfaceSocketMenu:
|
||||
for item in mod.id_properties_ui(f'{v.identifier}').as_dict()['items']:
|
||||
if item[4] == mod[f'{v.identifier}']:
|
||||
continue
|
||||
mode_compare += [item[0]]
|
||||
if isinstance(interface_item, NodeTreeInterfaceSocketMenu):
|
||||
current_value = geomod.get_value(mod, interface_item.identifier)
|
||||
inactive_mode_names += [
|
||||
mode_id for mode_id, mode_value
|
||||
in geomod.get_enum_value_to_compare(mod, interface_item.identifier)
|
||||
if mode_value != current_value
|
||||
]
|
||||
|
||||
s = mod_info.socket_info.get(v.identifier)
|
||||
if not s:
|
||||
continue
|
||||
if display_mode == 0:
|
||||
comp_match = False
|
||||
for c in mode_compare:
|
||||
comp_match = c in v.name
|
||||
if comp_match:
|
||||
break
|
||||
if comp_match:
|
||||
if socket_info.hide_ui:
|
||||
continue
|
||||
if s.hide_ui:
|
||||
if any(mode_name in interface_item.name for mode_name in inactive_mode_names):
|
||||
continue
|
||||
|
||||
row = panel.row(align=True)
|
||||
row.active = not (mod_info.hide_ui or hide_panel or s.hide_ui)
|
||||
row.active = not (mod_info.hide_ui or hide_panel or socket_info.hide_ui)
|
||||
|
||||
col = row.column()
|
||||
input_row = col.row(align=True)
|
||||
attribute_toggle = False
|
||||
if f'{v.identifier}_use_attribute' in mod.keys() and not v.force_non_field:
|
||||
attribute_toggle = mod[f'{v.identifier}_use_attribute']
|
||||
if attribute_toggle:
|
||||
input_row.prop(mod, f'["{v.identifier}_attribute_name"]', text=k)
|
||||
else:
|
||||
input_row.prop(mod, f'["{v.identifier}"]', text=k)
|
||||
if is_preset:
|
||||
toggle = input_row.operator('brushstroke_tools.preset_toggle_attribute',
|
||||
text='',
|
||||
depress=mod[f'{v.identifier}_use_attribute'],
|
||||
icon='SPREADSHEET')
|
||||
else:
|
||||
toggle = input_row.operator('brushstroke_tools.brushstrokes_toggle_attribute',
|
||||
text='',
|
||||
depress=mod[f'{v.identifier}_use_attribute'],
|
||||
icon='SPREADSHEET')
|
||||
toggle.modifier_name = mod.name
|
||||
toggle.input_name = v.identifier
|
||||
else:
|
||||
if type(v) in icon_dict.keys():
|
||||
icon = icon_dict[type(v)]
|
||||
else:
|
||||
icon='NONE'
|
||||
if type(v) in data_dict.keys():
|
||||
input_row.prop_search(mod, f'["{v.identifier}"]', bpy.data, data_dict[type(v)], text=k, icon=icon)
|
||||
else:
|
||||
input_row.prop(mod, f'["{v.identifier}"]', text=k, icon=icon)
|
||||
if type(v) in utils.linkable_sockets:
|
||||
col.active = not s.link_context
|
||||
icon = settings_py.icon_from_link_type(s.link_context_type)
|
||||
settings = bpy.context.scene.BSBST_settings
|
||||
is_preset = bool(mod.id_data and mod.id_data == settings.preset_object)
|
||||
geomod.draw_socket(col.row(align=True), mod, interface_item, name, is_preset)
|
||||
|
||||
if type(interface_item) in utils.linkable_sockets:
|
||||
col.active = not socket_info.link_context
|
||||
row.alignment = 'EXPAND'
|
||||
if s.link_context:
|
||||
row.prop(s, 'link_context', text='', icon_value=icon)
|
||||
else:
|
||||
if display_mode == -1:
|
||||
row.prop(s, 'link_context_type', text='', emboss=True, icon='LINKED', icon_only=True)
|
||||
if socket_info.link_context:
|
||||
row.prop(socket_info, 'link_context', text='',
|
||||
icon_value=settings_py.icon_from_link_type(socket_info.link_context_type))
|
||||
elif display_mode == -1:
|
||||
row.prop(socket_info, 'link_context_type', text='', emboss=True, icon='LINKED', icon_only=True)
|
||||
|
||||
if display_mode != 0:
|
||||
col = row.column()
|
||||
col.active = not (mod_info.hide_ui or hide_panel)
|
||||
col.prop(s, 'hide_ui', icon_only=True, icon='UNPINNED' if s.hide_ui else 'PINNED', emboss=False)
|
||||
col.active = panel_active
|
||||
icon = 'UNPINNED' if socket_info.hide_ui else 'PINNED'
|
||||
col.prop(socket_info, 'hide_ui', icon_only=True, icon=icon, emboss=False)
|
||||
|
||||
def draw_material_settings(layout, material, surface_object=None):
|
||||
addon_prefs = bpy.context.preferences.addons[__package__].preferences
|
||||
settings = bpy.context.scene.BSBST_settings
|
||||
def draw_material_settings(context, layout, material, surface_object=None):
|
||||
addon_prefs = context.preferences.addons[__package__].preferences
|
||||
settings = context.scene.BSBST_settings
|
||||
|
||||
material_row = layout.row(align=True)
|
||||
material_row.template_ID(settings, 'context_material')
|
||||
@@ -365,9 +331,9 @@ def draw_shape_properties(layout, settings, style_object, is_preset, display_mod
|
||||
|
||||
draw_mod_warnings(layout, mod)
|
||||
|
||||
def draw_material_properties(layout, settings, surface_object):
|
||||
def draw_material_properties(context, layout, settings, surface_object):
|
||||
if settings.context_material:
|
||||
draw_material_settings(layout, settings.context_material, surface_object=surface_object)
|
||||
draw_material_settings(context, layout, settings.context_material, surface_object=surface_object)
|
||||
else:
|
||||
material_row = layout.row(align=True)
|
||||
material_row.template_ID(settings, 'context_material', new='brushstroke_tools.new_material')
|
||||
@@ -383,7 +349,7 @@ def draw_settings_properties(layout, settings, style_object):
|
||||
|
||||
layout.prop(style_object, 'visible_shadow', icon='LIGHT', emboss=True)
|
||||
|
||||
def draw_properties_panel(layout, settings, style_object, surface_object, is_preset, display_mode):
|
||||
def draw_properties_panel(context, layout, settings, style_object, surface_object, is_preset, display_mode):
|
||||
|
||||
layout.separator(type='LINE')
|
||||
row = layout.row(align=True)
|
||||
@@ -391,7 +357,7 @@ def draw_properties_panel(layout, settings, style_object, surface_object, is_pre
|
||||
layout.separator(factor=.0, type='SPACE')
|
||||
|
||||
if settings.view_tab == 'MATERIAL':
|
||||
draw_material_properties(layout, settings, surface_object)
|
||||
draw_material_properties(context, layout, settings, surface_object)
|
||||
elif settings.view_tab == 'SHAPE':
|
||||
draw_shape_properties(layout, settings, style_object, is_preset, display_mode)
|
||||
|
||||
@@ -545,7 +511,7 @@ class BSBST_PT_brushstroke_tools_panel(bpy.types.Panel):
|
||||
if not settings.preset_object and is_preset:
|
||||
layout.operator("brushstroke_tools.init_preset", icon='MODIFIER')
|
||||
else:
|
||||
draw_properties_panel(style_panel, settings, style_object, surface_object, is_preset, display_mode)
|
||||
draw_properties_panel(context, style_panel, settings, style_object, surface_object, is_preset, display_mode)
|
||||
|
||||
class BSBST_MT_PIE_brushstroke_data_marking(bpy.types.Menu):
|
||||
bl_idname= "BSBST_MT_PIE_brushstroke_data_marking"
|
||||
|
||||
@@ -10,6 +10,7 @@ from bpy.app.handlers import persistent
|
||||
import math, shutil, errno, numpy
|
||||
from bpy.app.handlers import persistent
|
||||
from mathutils import Vector
|
||||
from . import geomod
|
||||
|
||||
addon_version = (0,0,0)
|
||||
|
||||
@@ -36,6 +37,7 @@ linkable_sockets = [
|
||||
|
||||
asset_lib_name = 'Brushstroke Tools Library'
|
||||
|
||||
|
||||
@persistent
|
||||
def find_context_brushstrokes(scene, depsgraph):
|
||||
settings = scene.BSBST_settings
|
||||
@@ -178,7 +180,7 @@ def set_brushstroke_material(ob, material):
|
||||
continue
|
||||
if not s.link_context_type == 'MATERIAL':
|
||||
continue
|
||||
mod[s.name] = material
|
||||
geomod.set_value(mod, s.name, material)
|
||||
ob.update_tag()
|
||||
|
||||
if ob.type == 'EMPTY':
|
||||
@@ -655,11 +657,7 @@ def transfer_modifier(modifier_name, target_obj, source_obj):
|
||||
|
||||
if source_mod.type == 'NODES':
|
||||
# Transfer geo node attributes
|
||||
for key, value in source_mod.items():
|
||||
try:
|
||||
target_mod[key] = value
|
||||
except (TypeError, ValueError) as e:
|
||||
target_mod[key] = type(target_mod[key])(value)
|
||||
geomod.copy_inputs(source_mod, target_mod)
|
||||
|
||||
# Transfer geo node bake settings
|
||||
target_mod.bake_directory = source_mod.bake_directory
|
||||
@@ -744,7 +742,7 @@ def set_surface_object(bs, surf_ob):
|
||||
continue
|
||||
if not s.link_context_type == 'SURFACE_OBJECT':
|
||||
continue
|
||||
mod[s.name] = surf_ob
|
||||
geomod.set_value(mod, s.name, surf_ob)
|
||||
ob.parent = surf_ob
|
||||
ob.parent_type = 'OBJECT'
|
||||
surf_ob.update_tag()
|
||||
@@ -775,7 +773,7 @@ def set_flow_object(bs, ob):
|
||||
continue
|
||||
if not s.link_context_type == 'FLOW_OBJECT':
|
||||
continue
|
||||
mod[s.name] = ob
|
||||
geomod.set_value(mod, s.name, ob)
|
||||
ob.update_tag()
|
||||
|
||||
bs['BSBST_flow_object'] = ob
|
||||
@@ -1106,8 +1104,18 @@ def match_mat(tgt_mat, src_mat):
|
||||
|
||||
for attr_path in path_list:
|
||||
try:
|
||||
exec(f'tgt_mat.{attr_path} = src_mat.{attr_path}')
|
||||
path_parts = attr_path.replace("'", '"').split('.')
|
||||
attr_name = path_parts[-1]
|
||||
item_path = ".".join(path_parts[:-1])
|
||||
if item_path:
|
||||
tgt_item = tgt_mat.path_resolve(item_path)
|
||||
src_item = src_mat.path_resolve(item_path)
|
||||
else:
|
||||
tgt_item = tgt_mat
|
||||
src_item = src_mat
|
||||
setattr(tgt_item, attr_name, getattr(src_item, attr_name))
|
||||
except:
|
||||
print(f"Could not copy attribute '{attr_path}' on material '{tgt_mat.name}'")
|
||||
pass
|
||||
|
||||
tgt_curve_node = tgt_mat.node_tree.nodes.get('Brush Curve')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
schema_version = "1.0.0"
|
||||
id = "datablock_utils"
|
||||
version = "1.3.0"
|
||||
version = "1.3.2"
|
||||
name = "Data-Block Utilities"
|
||||
tagline = "Show users, merge duplicates, find similar, and more"
|
||||
maintainer = "Leonardo Pike-Excell <leonardopike.excell@gmail.com>"
|
||||
|
||||
@@ -49,6 +49,8 @@ def _generate_id_types() -> dict[str, IDType]:
|
||||
if bpy.app.version >= (4, 3, 0):
|
||||
if bpy.app.version >= (5, 0, 0):
|
||||
collections.remove(next(c for c in collections if c.identifier == 'annotations'))
|
||||
if bpy.app.version >= (5, 2, 0):
|
||||
collections.remove(next(c for c in collections if c.identifier == 'all_ids'))
|
||||
_assign('CURVES', 'hair_curves', enums, collections)
|
||||
if bpy.app.version >= (5, 0, 0):
|
||||
_assign('GREASEPENCIL', 'grease_pencils', enums, collections, remove=False)
|
||||
|
||||
@@ -24,6 +24,10 @@ def get_settings() -> DBU_PG_FindSimilarSettings:
|
||||
return bpy.context.scene.dbu_similar_settings # type: ignore
|
||||
|
||||
|
||||
def is_local_id(id_data: bpy.types.ID) -> bool:
|
||||
return not id_data.library and not id_data.override_library
|
||||
|
||||
|
||||
def get_invalid_nodes(ntree: NodeTree) -> set[Node]:
|
||||
settings = get_settings()
|
||||
invalid_nodes = set()
|
||||
@@ -100,6 +104,24 @@ def get_image_props(img: bpy.types.Image) -> tuple[Any, ...]:
|
||||
)
|
||||
|
||||
|
||||
def get_light_props(light: bpy.types.Light) -> tuple[Any, ...]:
|
||||
props: list[Any] = [
|
||||
tuple(light.color),
|
||||
light.energy,
|
||||
light.exposure,
|
||||
light.use_shadow,
|
||||
light.normalize,
|
||||
]
|
||||
match light.type:
|
||||
case 'AREA':
|
||||
props.extend((light.size, light.size_y, light.shape))
|
||||
case 'SPOT':
|
||||
props.extend((light.spot_size, light.spot_blend))
|
||||
case 'SUN':
|
||||
props.append(light.angle)
|
||||
return tuple(props)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class NodeProperties:
|
||||
id_data: Node | NodeTree
|
||||
@@ -218,7 +240,7 @@ def contents_of_ntrees(
|
||||
) -> defaultdict[str, list[NodeProperties]]:
|
||||
content_map = defaultdict(list)
|
||||
for id_data in bl_data:
|
||||
if id_data.library or (not isinstance(id_data, NodeTree) and not id_data.use_nodes):
|
||||
if not is_local_id(id_data) or (not isinstance(id_data, NodeTree) and not id_data.use_nodes):
|
||||
continue
|
||||
|
||||
ntree = id_data if isinstance(id_data, NodeTree) else id_data.node_tree
|
||||
@@ -238,6 +260,9 @@ def contents_of_ntrees(
|
||||
tuple(p) if isinstance(p, bpy.types.bpy_prop_array) else p for p in props]
|
||||
contents.append(props)
|
||||
|
||||
if isinstance(id_data, bpy.types.Light) and bpy.app.version >= (5, 1, 0):
|
||||
contents.append(NodeProperties(id_data, ['LIGHT PROPS', *get_light_props(id_data)]))
|
||||
|
||||
if not isinstance(id_data, NodeTree):
|
||||
continue
|
||||
|
||||
@@ -403,7 +428,7 @@ def find_similar_and_duplicate_ntrees(id_type: str) -> None:
|
||||
|
||||
def find_duplicate_images() -> None:
|
||||
duplicates = []
|
||||
images = [i for i in bpy.data.images if not i.library]
|
||||
images = [i for i in bpy.data.images if is_local_id(i)]
|
||||
images.sort(key=get_image_props)
|
||||
for _, raw_group in groupby(images, get_image_props):
|
||||
group = [i.name for i in raw_group]
|
||||
@@ -414,7 +439,7 @@ def find_duplicate_images() -> None:
|
||||
|
||||
|
||||
def find_duplicate_meshes() -> None:
|
||||
meshes = [m for m in bpy.data.meshes if not m.library]
|
||||
meshes = [m for m in bpy.data.meshes if is_local_id(m)]
|
||||
seen = set()
|
||||
results = []
|
||||
for m1 in meshes:
|
||||
@@ -493,6 +518,8 @@ class DBU_OT_SimilarAndDuplicatesClearResults(Operator):
|
||||
def merge_ids(duplicate_ids: Iterable[Iterable[bpy.types.ID]]) -> int:
|
||||
to_remove = []
|
||||
for target, *junk in duplicate_ids:
|
||||
if not is_local_id(target) or any(not is_local_id(id_data) for id_data in junk):
|
||||
continue
|
||||
to_remove.extend(junk)
|
||||
for id_data in junk:
|
||||
id_data.user_remap(target)
|
||||
|
||||
@@ -154,19 +154,44 @@ class GOVIE_Quick_Export_GLB_Operator(bpy.types.Operator):
|
||||
|
||||
# read preset parameters from file
|
||||
if preset_filepath:
|
||||
|
||||
class Container(object):
|
||||
__slots__ = ("__dict__",)
|
||||
|
||||
op = Container()
|
||||
preset_file = open(preset_filepath, "r")
|
||||
|
||||
# storing the values from the preset on the class
|
||||
for line in preset_file.readlines()[3::]:
|
||||
exec(line, globals(), locals())
|
||||
preset_key = line.split("op.")[1].split(" = ")[0]
|
||||
preset_value = line.split("op.")[1].split(" = ")[1].rstrip()
|
||||
|
||||
if preset_value.startswith("'") and preset_value.endswith("'"):
|
||||
# value is a string
|
||||
gltf_export_param[preset_key] = preset_value[1:-1]
|
||||
continue
|
||||
|
||||
if preset_value == "True":
|
||||
# value is a bool
|
||||
gltf_export_param[preset_key] = True
|
||||
continue
|
||||
|
||||
if preset_value == "False":
|
||||
# value is a bool
|
||||
gltf_export_param[preset_key] = False
|
||||
continue
|
||||
|
||||
try:
|
||||
preset_value_converted = int(preset_value)
|
||||
# if no error is thrown its an int
|
||||
gltf_export_param[preset_key] = preset_value_converted
|
||||
continue
|
||||
except ValueError:
|
||||
try:
|
||||
# should be a float
|
||||
preset_value_converted = float(preset_value)
|
||||
gltf_export_param[preset_key] = preset_value_converted
|
||||
except ValueError:
|
||||
print(
|
||||
"Could not convert preset parameter from gltf preset at line: {}".format(
|
||||
line
|
||||
)
|
||||
)
|
||||
|
||||
# pass class dictionary to the operator
|
||||
gltf_export_param = op.__dict__
|
||||
else:
|
||||
gltf_export_param["export_extras"] = True
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ bl_info = {
|
||||
"author": "Lorenz Wieseke, 3D Interaction Technologies GmbH (contact@govie.de)",
|
||||
"description": "Transform your model into a web-optimized GLB file for use in the Govie Editor.",
|
||||
"blender": (4, 2, 0),
|
||||
"version": (1, 0, 20),
|
||||
"version": (1, 0, 22),
|
||||
"location": "View 3D > Property Panel (N-Key in 3D View)",
|
||||
"warning": "",
|
||||
"category": "Scene",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
schema_version = "1.0.0"
|
||||
|
||||
id = "govietools"
|
||||
version = "1.0.20"
|
||||
version = "1.0.22"
|
||||
name = "Govie Tools"
|
||||
tagline = "Optimize your model for use in the Govie Editor"
|
||||
maintainer = "Stefan Bernstein, 3D Interaction Technologies GmbH <contact@govie.de>"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "govietools"
|
||||
version = "1.0.20"
|
||||
version = "1.0.21"
|
||||
description = "Optimize your model for use in the Govie Editor"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11.13"
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
version = 1
|
||||
revision = 2
|
||||
requires-python = ">=3.11.13"
|
||||
|
||||
[[package]]
|
||||
name = "govietools"
|
||||
version = "1.0.20"
|
||||
source = { virtual = "." }
|
||||
@@ -1,3 +1,5 @@
|
||||
### Incremental Auto-Save
|
||||
|
||||
[Incremental Auto-Save](https://extensions.blender.org/add-ons/incremental-auto-save/) makes Blender's autosaves more configurable and reliable.
|
||||
|
||||

|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import bpy
|
||||
from bl_ui.generic_ui_list import draw_ui_list
|
||||
@@ -15,15 +18,23 @@ from bpy.props import (
|
||||
IntProperty,
|
||||
StringProperty,
|
||||
)
|
||||
from bpy.types import AddonPreferences, PropertyGroup, UIList
|
||||
from bpy.types import (
|
||||
AddonPreferences,
|
||||
Context,
|
||||
PropertyGroup,
|
||||
UILayout,
|
||||
UIList,
|
||||
bpy_prop_collection,
|
||||
)
|
||||
from rna_prop_ui import IDPropertyGroup
|
||||
|
||||
|
||||
# The add-on is confirmed to work with Blender 3.5 in 2026.
|
||||
# As long as that remains true, there's no point removing this bl_info block.
|
||||
bl_info = {
|
||||
"name": "Incremental Autosave",
|
||||
"author": "Demeter Dzadik",
|
||||
"version": (1, 1, 1),
|
||||
"blender": (2, 90, 0),
|
||||
"version": (1, 1, 2),
|
||||
"blender": (3, 5, 0),
|
||||
"location": "blender",
|
||||
"description": "Autosaves in a way where subsequent autosaves don't overwrite previous ones",
|
||||
"category": "System",
|
||||
@@ -38,7 +49,14 @@ LAUNCH_TIME = datetime.now()
|
||||
|
||||
class INCSAVE_UL_file_paths(UIList):
|
||||
def draw_item(
|
||||
self, context, layout, data, item, icon_value, active_data, active_propname
|
||||
self,
|
||||
_context: Context,
|
||||
layout: UILayout,
|
||||
_data: object,
|
||||
item: AutoSavePath,
|
||||
_icon_value: int,
|
||||
_active_data: object,
|
||||
_active_propname: str,
|
||||
):
|
||||
filepath: AutoSavePath = item
|
||||
|
||||
@@ -51,12 +69,14 @@ class INCSAVE_UL_file_paths(UIList):
|
||||
row.prop(filepath, "path", text="")
|
||||
|
||||
|
||||
def get_addon_prefs(context=None):
|
||||
def get_addon_prefs(
|
||||
context: Context | None = None,
|
||||
) -> IncrementalAutoSavePreferences:
|
||||
context = context or bpy.context
|
||||
return context.preferences.addons[__name__].preferences
|
||||
|
||||
|
||||
def update_prefs_on_file(self=None, context=None):
|
||||
def update_prefs_on_file(_self=None, context: Context | None = None):
|
||||
prefs = get_addon_prefs(context)
|
||||
if not type(prefs).loading:
|
||||
prefs.save_prefs_to_file()
|
||||
@@ -71,7 +91,7 @@ class PrefsFileSaveLoadMixin:
|
||||
import bpy, json
|
||||
from pathlib import Path
|
||||
|
||||
class MyAddonPrefs(PrefsFileSaveLoadMixin, bpy.types.AddonPreferences):
|
||||
class MyAddonPrefs(PrefsFileSaveLoadMixin, AddonPreferences):
|
||||
some_prop: bpy.props.IntProperty(update=update_prefs_on_file)
|
||||
|
||||
def register():
|
||||
@@ -91,7 +111,7 @@ class PrefsFileSaveLoadMixin:
|
||||
|
||||
@staticmethod
|
||||
def register_autoload_from_file(delay=0.1):
|
||||
def timer_func(_scene=None):
|
||||
def timer_func(_scene: object = None):
|
||||
prefs = get_addon_prefs()
|
||||
prefs.load_prefs_from_file()
|
||||
|
||||
@@ -105,7 +125,7 @@ class PrefsFileSaveLoadMixin:
|
||||
ret = {}
|
||||
|
||||
rna_class = None
|
||||
if isinstance(propgroup, bpy.types.AddonPreferences):
|
||||
if isinstance(propgroup, AddonPreferences):
|
||||
prop_dict = {
|
||||
key: getattr(propgroup, key)
|
||||
for key in propgroup.bl_rna.properties.keys()
|
||||
@@ -113,9 +133,7 @@ class PrefsFileSaveLoadMixin:
|
||||
}
|
||||
else:
|
||||
property_group_class_name = type(propgroup).__name__
|
||||
rna_class = bpy.types.PropertyGroup.bl_rna_get_subclass_py(
|
||||
property_group_class_name
|
||||
)
|
||||
rna_class = PropertyGroup.bl_rna_get_subclass_py(property_group_class_name)
|
||||
if not hasattr(rna_class, "properties"):
|
||||
rna_class = None
|
||||
prop_dict = {
|
||||
@@ -127,7 +145,7 @@ class PrefsFileSaveLoadMixin:
|
||||
for key, value in prop_dict.items():
|
||||
if key in type(self).omit_from_disk:
|
||||
continue
|
||||
if type(value) in (list, bpy.types.bpy_prop_collection_idprop):
|
||||
if type(value) is list or isinstance(value, bpy_prop_collection):
|
||||
ret[key] = [self.prefs_to_dict_recursive(elem) for elem in value]
|
||||
elif type(value) is IDPropertyGroup:
|
||||
ret[key] = self.prefs_to_dict_recursive(value)
|
||||
@@ -143,7 +161,11 @@ class PrefsFileSaveLoadMixin:
|
||||
ret[key] = value
|
||||
return ret
|
||||
|
||||
def apply_prefs_from_dict_recursive(self, propgroup, data):
|
||||
def apply_prefs_from_dict_recursive(
|
||||
self,
|
||||
propgroup: IDPropertyGroup,
|
||||
data: dict[str, Any],
|
||||
):
|
||||
for key, value in data.items():
|
||||
if not hasattr(propgroup, key):
|
||||
# Property got removed or renamed in the implementation.
|
||||
@@ -165,7 +187,7 @@ class PrefsFileSaveLoadMixin:
|
||||
addon_name = __package__.split(".")[-1]
|
||||
return Path(bpy.utils.user_resource("CONFIG")) / Path(addon_name + ".txt")
|
||||
|
||||
def save_prefs_to_file(self, _context=None):
|
||||
def save_prefs_to_file(self, _context: Context | None = None):
|
||||
data_dict = self.prefs_to_dict_recursive(propgroup=self)
|
||||
|
||||
with open(self.get_prefs_filepath(), "w") as f:
|
||||
@@ -190,7 +212,11 @@ class PrefsFileSaveLoadMixin:
|
||||
|
||||
|
||||
class AutoSavePath(PropertyGroup):
|
||||
name: StringProperty(update=update_prefs_on_file)
|
||||
name: StringProperty(
|
||||
name="Autosave Path Identifier",
|
||||
description="Name of this autosave path",
|
||||
update=update_prefs_on_file,
|
||||
)
|
||||
path: StringProperty(
|
||||
name="Autosave Path",
|
||||
description="An autosave path. If this filepath doesn't exist, the next existing one will be used. If none are valid, the Blender auto-save path will be used. If that's not valid either, the OS default temp folder will be used",
|
||||
@@ -221,7 +247,7 @@ class IncrementalAutoSavePreferences(PrefsFileSaveLoadMixin, AddonPreferences):
|
||||
update=update_prefs_on_file,
|
||||
)
|
||||
|
||||
max_save_files: bpy.props.IntProperty(
|
||||
max_save_files: IntProperty(
|
||||
name="Max Backups Per File",
|
||||
description="Maximum number of backups to save for each file, 0 means unlimited. Otherwise, the oldest file will be deleted after reaching the limit",
|
||||
default=10,
|
||||
@@ -229,7 +255,7 @@ class IncrementalAutoSavePreferences(PrefsFileSaveLoadMixin, AddonPreferences):
|
||||
max=100,
|
||||
update=update_prefs_on_file,
|
||||
)
|
||||
compress_files: bpy.props.BoolProperty(
|
||||
compress_files: BoolProperty(
|
||||
name="Compress Files",
|
||||
description="Save backups with compression enabled",
|
||||
default=True,
|
||||
@@ -263,7 +289,7 @@ class IncrementalAutoSavePreferences(PrefsFileSaveLoadMixin, AddonPreferences):
|
||||
assert bpy.data.filepath
|
||||
return os.sep.join((os.path.dirname(bpy.data.filepath), "Autosaves"))
|
||||
|
||||
def get_valid_autosave_path(self, context) -> str:
|
||||
def get_valid_autosave_path(self, context: Context) -> str:
|
||||
"""Return an autosave path that will always actually exist, no matter how desperate."""
|
||||
prefs = get_addon_prefs(context)
|
||||
if bpy.data.filepath and prefs.relative_to_blend:
|
||||
@@ -287,12 +313,19 @@ class IncrementalAutoSavePreferences(PrefsFileSaveLoadMixin, AddonPreferences):
|
||||
sys_temp = tempfile.gettempdir()
|
||||
return sys_temp
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout.column()
|
||||
layout.use_property_decorate = False
|
||||
layout.use_property_split = True
|
||||
|
||||
header, panel = layout.panel("Incremental Autosave: Paths", default_closed=True)
|
||||
if hasattr(layout, "panel"):
|
||||
header, panel = layout.panel(
|
||||
"Incremental Autosave: Paths", default_closed=True
|
||||
)
|
||||
else:
|
||||
header = layout.row()
|
||||
panel = layout
|
||||
|
||||
header.label(text="Autosave Paths")
|
||||
if panel:
|
||||
split = panel.split(factor=0.4)
|
||||
@@ -384,7 +417,7 @@ def save_file():
|
||||
if addon_prefs.save_images:
|
||||
save_images()
|
||||
if addon_prefs.save_sculpt:
|
||||
save_sculpt()
|
||||
realize_sculpt_or_edit_mesh()
|
||||
bpy.ops.wm.save_as_mainfile(
|
||||
filepath=backup_file, copy=True, compress=addon_prefs.compress_files
|
||||
)
|
||||
@@ -409,25 +442,36 @@ def save_images():
|
||||
print(f"Incremental Autosave: Failed to save dirty image: {img.name}")
|
||||
|
||||
|
||||
def save_sculpt():
|
||||
"""We just need to enter object mode and then go back to the original mode."""
|
||||
def realize_sculpt_or_edit_mesh():
|
||||
"""We just need to enter object mode and then go back to the original mode,
|
||||
to write the mesh data to the object.
|
||||
"""
|
||||
context = bpy.context
|
||||
try:
|
||||
if context.mode == "EDIT_MESH":
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
if context.mode == "SCULPT":
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
bpy.ops.object.mode_set(mode="SCULPT")
|
||||
except RuntimeError:
|
||||
# This can happen if we happen to catch user during some modal operator that's
|
||||
# not accounted for by save_after_modal_operation().
|
||||
print(
|
||||
"Incremental Autosave Error: Mesh could not be written to Object due to running modal operators: ",
|
||||
[op.bl_idname for op in context.window.modal_operators],
|
||||
)
|
||||
print("You could report this as a bug!")
|
||||
|
||||
|
||||
@persistent
|
||||
def save_before_close(_dummy=None):
|
||||
def save_before_close(_dummy: object = None):
|
||||
# is_dirty means there are changes that haven't been saved to disk
|
||||
if bpy.data.is_dirty and get_addon_prefs().save_before_close:
|
||||
save_file()
|
||||
|
||||
|
||||
def create_autosave_on_timer():
|
||||
def create_autosave_on_timer() -> int:
|
||||
now = datetime.now()
|
||||
delta = now - LAUNCH_TIME
|
||||
prefs = get_addon_prefs()
|
||||
@@ -439,7 +483,7 @@ def create_autosave_on_timer():
|
||||
return prefs.save_interval * 60
|
||||
|
||||
|
||||
def save_after_modal_operation():
|
||||
def save_after_modal_operation() -> float | None:
|
||||
"""Mesh edits, sculpt strokes, and texture paint strokes trigger a depsgraph update,
|
||||
so it's better to make a save after one of those operations, rather than on a timer.
|
||||
"""
|
||||
@@ -462,7 +506,7 @@ def save_after_modal_operation():
|
||||
|
||||
|
||||
@persistent
|
||||
def register_autosave_timer(_dummy=None):
|
||||
def register_autosave_timer(_dummy: object = None):
|
||||
bpy.app.timers.register(create_autosave_on_timer)
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
schema_version = "1.0.0"
|
||||
|
||||
id = "incremental_auto_save"
|
||||
version = "1.1.1"
|
||||
version = "1.1.2"
|
||||
name = "Incremental Auto-Save"
|
||||
tagline = "Improvements to Blender's Autosave"
|
||||
maintainer = "Demeter Dzadik <demeter@blender.org>"
|
||||
type = "add-on"
|
||||
website = "https://projects.blender.org/Mets/incremental-autosave"
|
||||
website = "https://projects.blender.org/Mets/incremental-autosave#incremental-auto-save"
|
||||
tags = ["System"]
|
||||
|
||||
blender_version_min = "4.2.0"
|
||||
@@ -15,7 +15,7 @@ license = [
|
||||
"SPDX:GPL-3.0-or-later"
|
||||
]
|
||||
copyright = [
|
||||
"2019-2024 Demeter Dzadik"
|
||||
"2019-2026 Demeter Dzadik"
|
||||
]
|
||||
[permissions]
|
||||
files = "Save preferences & .blends in chosen directories"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Documentation: https://weisl.github.io/renaming_overview/
|
||||
Documentation: https://weisl.github.io/simple_renaming/renaming_overview/
|
||||
|
||||
[](https://www.paypal.com/donate?hosted_button_id=JV7KRF77TY78A)
|
||||
|
||||
|
||||
@@ -18,22 +18,11 @@ Created by Matthias Patscheider
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
# support reloading sub-modules
|
||||
if "bpy" in locals():
|
||||
import importlib
|
||||
|
||||
importlib.reload(add_suffix_panel)
|
||||
importlib.reload(operators)
|
||||
importlib.reload(preferences)
|
||||
importlib.reload(ui)
|
||||
importlib.reload(variable_replacer)
|
||||
|
||||
else:
|
||||
from . import add_suffix_panel
|
||||
from . import operators
|
||||
from . import preferences
|
||||
from . import ui
|
||||
from . import variable_replacer
|
||||
from . import add_suffix_panel
|
||||
from . import operators
|
||||
from . import preferences
|
||||
from . import ui
|
||||
from . import variable_replacer
|
||||
|
||||
files = [
|
||||
add_suffix_panel,
|
||||
|
||||
+1
-1
@@ -245,7 +245,7 @@ class VIEW3D_OT_add_type_suf_pre(bpy.types.Operator):
|
||||
obj_list = []
|
||||
|
||||
for obj in self.get_selection_all():
|
||||
if obj.type == 'GPENCIL':
|
||||
if obj.type in ('GPENCIL', 'GREASEPENCIL'):
|
||||
obj_list.append(obj)
|
||||
self.rename_suffix_prefix(obj_list, pre_suffix=wm.renaming_suffix_prefix_gpencil, object_type='GPENCIL',
|
||||
icon='OUTLINER_OB_GREASEPENCIL')
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
schema_version = "1.0.0"
|
||||
|
||||
id = "simple_renaming_panel"
|
||||
version = "2.1.5"
|
||||
version = "2.1.8"
|
||||
name = "Simple Renaming"
|
||||
tagline = "Effortlessly rename multiple objects with this simple addon"
|
||||
maintainer = "Matthias Patscheider <patscheider.matthias@gmail.com>"
|
||||
type = "add-on"
|
||||
|
||||
website = "https://weisl.github.io/renaming_overview/"
|
||||
website = "https://weisl.github.io/simple_renaming/renaming_overview/"
|
||||
|
||||
tags = ["3D View", "Scene", "User Interface"]
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import bpy
|
||||
from bpy.app.handlers import persistent
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
EnumProperty,
|
||||
@@ -10,15 +9,12 @@ 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
|
||||
from . import search_replace
|
||||
from . import search_select
|
||||
from . import trim_string
|
||||
from .renaming_utilities import update_selection_order
|
||||
|
||||
enumObjectTypes = [('EMPTY', "", "Rename empty objects", 'OUTLINER_OB_EMPTY', 1),
|
||||
('MESH', "", "Rename mesh objects", 'OUTLINER_OB_MESH', 2),
|
||||
@@ -29,7 +25,7 @@ enumObjectTypes = [('EMPTY', "", "Rename empty objects", 'OUTLINER_OB_EMPTY', 1)
|
||||
('CURVE', "", "Rename curve objects", 'OUTLINER_OB_CURVE', 64),
|
||||
('SURFACE', "", "Rename surface objects", 'OUTLINER_OB_SURFACE', 128),
|
||||
('FONT', "", "Rename font objects", 'OUTLINER_OB_FONT', 256),
|
||||
('GPENCIL', "", "Rename greace pencil objects", 'OUTLINER_OB_GREASEPENCIL', 512),
|
||||
('GPENCIL', "", "Rename grease pencil objects", 'OUTLINER_OB_GREASEPENCIL', 512),
|
||||
('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),
|
||||
@@ -83,36 +79,17 @@ classes = (
|
||||
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."),
|
||||
('Y', "Y Axis", "Sort the object based on the Y axis."),
|
||||
('Z', "Z Axis", "Sort the object based on the Z axis."),
|
||||
('SELECTION', "Selection", "Sort the objects based on the selection order"), ]
|
||||
('Z', "Z Axis", "Sort the object based on the Z axis."), ]
|
||||
|
||||
enum_sort_bone_items = [('X', "X Axis", "Sort the object based on the X axis."),
|
||||
('Y', "Y Axis", "Sort the object based on the Y axis."),
|
||||
('Z', "Z Axis", "Sort the object based on the Z axis."), ]
|
||||
|
||||
|
||||
# persistent is needed for handler to work in addons https://docs.blender.org/api/current/bpy.app.handlers.html
|
||||
@persistent
|
||||
def PostChange(scene):
|
||||
if bpy.context.mode != "OBJECT":
|
||||
return
|
||||
|
||||
# Any() function returns True if any element of an iterable is True. If not, it returns False.
|
||||
is_selection_update = any(
|
||||
not u.is_updated_geometry
|
||||
and not u.is_updated_transform
|
||||
and not u.is_updated_shading
|
||||
for u in bpy.context.view_layer.depsgraph.updates
|
||||
)
|
||||
if is_selection_update:
|
||||
update_selection_order()
|
||||
|
||||
|
||||
def register():
|
||||
id_store = bpy.types.Scene
|
||||
|
||||
@@ -150,12 +127,20 @@ def register():
|
||||
default='X', # Set a default value
|
||||
)
|
||||
|
||||
id_store.renaming_new_name = StringProperty(name="New Name", default='')
|
||||
id_store.renaming_new_name = StringProperty(
|
||||
name="New Name",
|
||||
description="Name pattern for renaming. Use @n for a numbered variable (configure its digits in Preferences). Use # in the Numerate field (right) to set the auto-suffix digit count",
|
||||
default='',
|
||||
)
|
||||
id_store.renaming_search = StringProperty(name='Search', default='')
|
||||
id_store.renaming_replace = StringProperty(name='Replace', default='')
|
||||
id_store.renaming_suffix = StringProperty(name="Suffix", default='')
|
||||
id_store.renaming_prefix = StringProperty(name="Prefix", default='')
|
||||
id_store.renaming_numerate = StringProperty(name="Numerate", default='###')
|
||||
id_store.renaming_numerate = StringProperty(
|
||||
name="Numerate Pattern",
|
||||
description="Pattern for the auto-numeration suffix appended when Numerate is enabled. Each # represents one digit (e.g. ### → 001, 002, …). This does not affect the @n variable",
|
||||
default='###',
|
||||
)
|
||||
|
||||
id_store.renaming_sorting = bpy.props.BoolProperty(
|
||||
name="Sort Target Objects",
|
||||
@@ -169,10 +154,9 @@ def register():
|
||||
|
||||
id_store.renaming_matchcase = BoolProperty(name="Match Case", description="", default=True)
|
||||
id_store.renaming_useRegex = BoolProperty(name="Use Regex", description="", default=False)
|
||||
id_store.renaming_use_enumerate = BoolProperty(name="Numerate",
|
||||
description="Enable and Disable the numeration of objects. This can "
|
||||
"be especially useful in combination with the numeration "
|
||||
"variable @n",
|
||||
id_store.renaming_use_enumerate = BoolProperty(
|
||||
name="Numerate",
|
||||
description="Automatically appends an incrementing numeric suffix to each renamed object. Configure the suffix format with # in the Numerate field, and the digit count for the @n variable in Preferences",
|
||||
default=True,
|
||||
)
|
||||
id_store.renaming_base_numerate = IntProperty(name="Step Size", default=1)
|
||||
@@ -203,8 +187,6 @@ def register():
|
||||
for cls in classes:
|
||||
register_class(cls)
|
||||
|
||||
bpy.app.handlers.depsgraph_update_post.append(PostChange)
|
||||
start_version_check()
|
||||
|
||||
|
||||
def unregister():
|
||||
@@ -230,4 +212,3 @@ def unregister():
|
||||
del IDStore.renaming_filter_by_index
|
||||
del IDStore.renaming_index_target
|
||||
|
||||
bpy.app.handlers.depsgraph_update_post.remove(PostChange)
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
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'}
|
||||
@@ -37,9 +37,7 @@ def get_renaming_list(context):
|
||||
else:
|
||||
sort_enum = scene.renaming_sort_enum
|
||||
|
||||
if sort_enum == 'SELECTION':
|
||||
obj_list = get_ordered_selection_objects()
|
||||
elif sort_enum == 'X':
|
||||
if sort_enum == 'X':
|
||||
obj_list = get_sorted_objects_x(obj_list)
|
||||
elif sort_enum == 'Y':
|
||||
obj_list = get_sorted_objects_y(obj_list)
|
||||
@@ -51,7 +49,8 @@ def get_renaming_list(context):
|
||||
|
||||
if scene.renaming_object_types == 'OBJECT':
|
||||
for obj in obj_list:
|
||||
if obj.type in scene.renaming_object_types_specified:
|
||||
effective_type = 'GPENCIL' if obj.type == 'GREASEPENCIL' else obj.type
|
||||
if effective_type in scene.renaming_object_types_specified:
|
||||
renaming_list.append(obj)
|
||||
|
||||
elif scene.renaming_object_types == 'DATA':
|
||||
@@ -65,8 +64,8 @@ def get_renaming_list(context):
|
||||
if selection_only:
|
||||
for obj in context.selected_objects:
|
||||
for mat in obj.material_slots:
|
||||
if mat is not None and mat.name != '':
|
||||
renaming_list.append(bpy.data.materials[mat.name])
|
||||
if mat.material is not None:
|
||||
renaming_list.append(mat.material)
|
||||
else:
|
||||
renaming_list = list(bpy.data.materials)
|
||||
|
||||
@@ -124,7 +123,7 @@ def get_renaming_list(context):
|
||||
renaming_list.append(new_bone)
|
||||
|
||||
elif scene.renaming_object_types == 'COLLECTION':
|
||||
if bpy.context.space_data.type == 'OUTLINER' and selection_only is True:
|
||||
if bpy.context.space_data and bpy.context.space_data.type == 'OUTLINER' and selection_only is True:
|
||||
selected_collections = [c for c in context.selected_ids if c.bl_rna.identifier == "Collection"]
|
||||
for col in selected_collections:
|
||||
renaming_list.append(col)
|
||||
@@ -300,16 +299,6 @@ def get_global_z(obj):
|
||||
return obj.matrix_world.to_translation().z
|
||||
|
||||
|
||||
def get_ordered_selection_objects():
|
||||
tagged_objects = []
|
||||
for o in bpy.data.objects:
|
||||
order_index = o.get("selection_order", -1)
|
||||
if order_index >= 0:
|
||||
tagged_objects.append((order_index, o))
|
||||
tagged_objects = sorted(tagged_objects, key=lambda item: item[0])
|
||||
return [o for i, o in tagged_objects]
|
||||
|
||||
|
||||
def get_sorted_objects_x(objects):
|
||||
# Sort objects by their global X location
|
||||
sorted_objects = sorted(objects, key=get_global_x)
|
||||
@@ -368,44 +357,3 @@ def update_bone_drivers(old_name, new_name):
|
||||
target.data_path = target.data_path.replace(old_token, new_token)
|
||||
|
||||
|
||||
def clear_order_flag(obj):
|
||||
try:
|
||||
del obj["selection_order"]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
def update_selection_order():
|
||||
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
|
||||
selection_order = get_ordered_selection_objects()
|
||||
idx = 0
|
||||
|
||||
for o in selection_order:
|
||||
if not o.select_get():
|
||||
selection_order.remove(o)
|
||||
clear_order_flag(o)
|
||||
else:
|
||||
o["selection_order"] = idx
|
||||
|
||||
# Remove any existing fcurves for 'selection_order'
|
||||
if o.animation_data and o.animation_data.action:
|
||||
action = o.animation_data.action
|
||||
|
||||
import bpy_extras.anim_utils as anim_utils
|
||||
# Get the default channelbag for the action (slot 0)
|
||||
channelbag = anim_utils.action_get_channelbag_for_slot(action, 0)
|
||||
if channelbag:
|
||||
fcurves = channelbag.fcurves
|
||||
for fcurve in fcurves:
|
||||
if fcurve.data_path == '["selection_order"]':
|
||||
fcurves.remove(fcurve)
|
||||
|
||||
|
||||
idx += 1
|
||||
for o in bpy.context.selected_objects:
|
||||
if o not in selection_order:
|
||||
o["selection_order"] = len(selection_order)
|
||||
selection_order.append(o)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user