update addons (for 5.2)
This commit is contained in:
@@ -10,13 +10,13 @@ D:\Work\9 iClone\Amazon\
|
||||
D:\Amazon\00_external-files\
|
||||
N:\1. CHARACTERS\remapping\
|
||||
[Recent]
|
||||
A:\1 Amazon_Active_Projects\1 BlenderAssets\
|
||||
A:\1 Amazon_Active_Projects\260127_Pick-and-Stage_2026_edits\Blends\stills\2026_new\
|
||||
A:\1 Amazon_Active_Projects\260706_Leader_Turbotote\Blends\stills\
|
||||
A:\1 Amazon_Active_Projects\260512_Agile_Gemba\Blends\animations\
|
||||
A:\1 Amazon_Active_Projects\260622_Stow_and_Go\Blends\animations\
|
||||
N:\1. CHARACTERS\remapping\
|
||||
A:\1 Amazon_Active_Projects\260622_Stow_and_Go\Assets\Blends\
|
||||
F:\renders\
|
||||
A:\1 Amazon_Active_Projects\260512_Agile_Gemba\Deliverable\Stills\
|
||||
A:\1 Amazon_Active_Projects\260512_Agile_Gemba\Blends\stills\
|
||||
C:\Users\Nathan\Downloads\
|
||||
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,9 +1,9 @@
|
||||
{
|
||||
"flamenco_version": {
|
||||
"version": "3.10-alpha0 (b5e531ae)",
|
||||
"version": "3.10-alpha0 (340bb57b)",
|
||||
"shortversion": "3.10-alpha0",
|
||||
"name": "Flamenco",
|
||||
"git": "b5e531ae"
|
||||
"git": "340bb57b"
|
||||
},
|
||||
"shared_storage": {
|
||||
"location": "F:\\jobs",
|
||||
|
||||
@@ -19,3 +19,4 @@
|
||||
{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
|
||||
|
||||
+13
-13
@@ -1,6 +1,18 @@
|
||||
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\260706_Leader_Turbotote\Blends\stills\iTurboTote_Reuse_8_v2.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
|
||||
@@ -186,15 +198,3 @@ A:\1 Amazon_Active_Projects\260422_Upstream_edits_2026\Blends\stills\iUP_M5_6.bl
|
||||
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
|
||||
P:\250827_FestivalTurf\Blends\animations\06 Infill And Powerbrooming\Visual 2B_SPA.blend
|
||||
C:\Users\Nathan\Downloads\Visual 2B_SPA\Visual 2B_SPA.blend
|
||||
F:\jobs\Visual 2B_SPA-393g\Visual 2B_SPA.blend
|
||||
F:\jobs\Visual_5_SPA-ii75\Visual_5_SPA.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\07 Final Touches And Maintenance\Visual_5_SPA.blend
|
||||
F:\jobs\Visual_5_SPA-ki6s\Visual_5_SPA.blend
|
||||
C:\Users\Nathan\Downloads\Visual_5_SPA.blend
|
||||
C:\Users\Nathan\Downloads\Visual_5_SPA_106-109.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\07 Final Touches And Maintenance\Visual_2_broom_SPA.blend
|
||||
C:\Users\Nathan\Downloads\Visual_new_final_SPA\Visual_new_final_SPA.blend
|
||||
P:\250827_FestivalTurf\Blends\animations\07 Final Touches And Maintenance\Visual_new_final_SPA.blend
|
||||
C:\Users\Nathan\Downloads\Visual_new_final\Visual_new_final.blend
|
||||
|
||||
Binary file not shown.
BIN
Binary file not shown.
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':
|
||||
return False
|
||||
if context.active_object is None:
|
||||
cls.poll_message_set("Boolean operators can only be performed in Object Mode")
|
||||
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")
|
||||
return False
|
||||
if check_active:
|
||||
if context.active_object is None:
|
||||
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,23 +16,36 @@ 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"
|
||||
"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"
|
||||
"For empty slots, fall back to using the same material index as the operand mesh."))),
|
||||
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"
|
||||
"For empty slots, fall back to using the same material index as the operand mesh."))),
|
||||
default = 'INDEX',
|
||||
)
|
||||
use_self: bpy.props.BoolProperty(
|
||||
@@ -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,53 +92,86 @@ 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."""
|
||||
slice = create_slice(context, canvas, modifier=True)
|
||||
add_boolean_modifier(self, context, slice, cutter, "INTERSECT", prefs.solver, pin=prefs.pin)
|
||||
"""
|
||||
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)
|
||||
|
||||
canvas.booleans.canvas = True
|
||||
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."""
|
||||
slice = create_slice(context, canvas)
|
||||
modifier = add_boolean_modifier(self, context, slice, cutter, "INTERSECT", prefs.solver, pin=prefs.pin)
|
||||
new_modifiers[slice].append(modifier)
|
||||
slice.select_set(True)
|
||||
"""
|
||||
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)
|
||||
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
|
||||
modifier = add_boolean_modifier(self, context, canvas, cutter, mode, prefs.solver, pin=prefs.pin)
|
||||
new_modifiers[canvas].append(modifier)
|
||||
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:
|
||||
canvas.modifiers.remove(mod)
|
||||
# Remove Modifiers
|
||||
for canvas, mods in modifiers.items():
|
||||
for mod in mods:
|
||||
canvas.modifiers.remove(mod)
|
||||
canvas.booleans.canvas = False
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
if self.method == 'SPECIFIED':
|
||||
# toggle_slices_visibility (for_specified_canvas)
|
||||
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
|
||||
|
||||
# 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]]
|
||||
|
||||
for cutter in cutters:
|
||||
cutter.hide_viewport = not cutter.hide_viewport
|
||||
|
||||
else:
|
||||
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 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
|
||||
|
||||
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 not is_boolean_modifier(mod):
|
||||
continue
|
||||
if mod.object in cutters:
|
||||
mod.show_viewport = not state
|
||||
mod.show_render = not state
|
||||
|
||||
slice.hide_viewport = state
|
||||
slice.hide_render = state
|
||||
slice.hide_set(state)
|
||||
|
||||
# Hide Unused Cutters
|
||||
for cutter in cutters:
|
||||
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 mod.object in cutters:
|
||||
canvas.modifiers.remove(mod)
|
||||
|
||||
# remove_canvas_property_if_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)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
cutters, leftovers = list_unused_cutters(cutters, canvases, do_leftovers=True)
|
||||
|
||||
|
||||
# Restore Orphaned Cutters
|
||||
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 = ""
|
||||
|
||||
# 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:
|
||||
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)
|
||||
|
||||
# Unset canvas property if it's no longer needed.
|
||||
other_cutters, __ = list_canvas_cutters([canvas])
|
||||
if len(other_cutters) == 0:
|
||||
canvas.booleans.canvas = False
|
||||
|
||||
# Handle Unused Cutters
|
||||
if self.method == 'SPECIFIED':
|
||||
handle_unused_cutters(context, cutters, list(canvases.keys()), delete=True)
|
||||
|
||||
elif self.method == 'ALL':
|
||||
for cutter in cutters:
|
||||
# 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)
|
||||
|
||||
# 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:
|
||||
# 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 face in cutter.data.polygons:
|
||||
face.select = True
|
||||
# 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 = []
|
||||
|
||||
# Apply Modifiers
|
||||
for canvas in self.canvases:
|
||||
context.view_layer.objects.active = canvas
|
||||
if len(cutters) == 0:
|
||||
self.report({'INFO'}, "Boolean cutters are not selected")
|
||||
return {'CANCELLED'}
|
||||
|
||||
boolean_mods = []
|
||||
for mod in canvas.modifiers:
|
||||
if "boolean_" in mod.name:
|
||||
if mod.object in self.cutters:
|
||||
boolean_mods.append(mod)
|
||||
apply_modifiers(context, canvas, boolean_mods)
|
||||
if not canvases:
|
||||
return {'CANCELLED'}
|
||||
|
||||
# remove_canvas_property_if_needed
|
||||
# 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 face in cutter.data.polygons:
|
||||
face.select = True
|
||||
|
||||
# Apply Modifiers
|
||||
for canvas in itertools.chain(canvases, slices):
|
||||
boolean_mods = []
|
||||
for mod in canvas.modifiers:
|
||||
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)
|
||||
|
||||
# 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,21 +20,24 @@ 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:
|
||||
canvas.select_set(True)
|
||||
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,55 +120,57 @@ class BoolToolPreferences(bpy.types.AddonPreferences):
|
||||
layout.use_property_decorate = False
|
||||
|
||||
# UI
|
||||
col = layout.column(align=True, heading="Show in Sidebar")
|
||||
row = col.row(align=True)
|
||||
sub = row.row(align=True)
|
||||
sub.prop(self, "show_in_sidebar", text="")
|
||||
sub = sub.row(align=True)
|
||||
sub.active = self.show_in_sidebar
|
||||
sub.prop(self, "sidebar_category", text="")
|
||||
|
||||
# Defaults
|
||||
row = layout.row()
|
||||
row.prop(self, "category", expand=True)
|
||||
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")
|
||||
|
||||
# Advanced
|
||||
layout.separator()
|
||||
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="")
|
||||
# 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)
|
||||
sub.prop(self, "show_in_sidebar", text="")
|
||||
sub = sub.row(align=True)
|
||||
sub.active = self.show_in_sidebar
|
||||
sub.prop(self, "sidebar_category", text="")
|
||||
|
||||
col.prop(self, "parent")
|
||||
col.prop(self, "apply_order")
|
||||
col.prop(self, "pin")
|
||||
col.separator()
|
||||
col = layout.column(align=True, heading="Features")
|
||||
col.prop(self, "fast_modifier_apply")
|
||||
|
||||
# Features
|
||||
layout.separator()
|
||||
col = layout.column(align=True, heading="Features")
|
||||
col.prop(self, "fast_modifier_apply")
|
||||
col.prop(self, "double_click")
|
||||
# Shared Properties
|
||||
if self.category == 'SHARED':
|
||||
col = layout.column()
|
||||
col.prop(self, "show_in_editmode")
|
||||
col.prop(self, "apply_order")
|
||||
|
||||
# Experimentals
|
||||
layout.separator()
|
||||
col = layout.column(align=True)
|
||||
col.prop(self, "versioning", text="⚠ Versioning")
|
||||
col.prop(self, "experimental", text="⚠ Experimental")
|
||||
# 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="", 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, "pin")
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ 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,
|
||||
display='WIRE' if self.shape == 'POLYLINE' else self.display)
|
||||
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':
|
||||
@@ -471,18 +207,21 @@ class CarverBase(bpy.types.Operator,
|
||||
# Draw Line
|
||||
if self.phase in ("BEVEL", "ROTATE", "ARRAY"):
|
||||
current_mouse_pos_3d = region_2d_to_plane_3d(context.region, context.region_data,
|
||||
self.mouse.current,
|
||||
(self.workplane.location, self.workplane.normal))
|
||||
self.mouse.current,
|
||||
(self.workplane.location, self.workplane.normal))
|
||||
if current_mouse_pos_3d is not None:
|
||||
vertices = [self.cutter.center, current_mouse_pos_3d]
|
||||
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)
|
||||
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,68 +436,71 @@ 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
|
||||
temp_bm = bmesh.new()
|
||||
temp_bm.from_mesh(mesh)
|
||||
temp_bm.faces.ensure_lookup_table()
|
||||
if mesh is None:
|
||||
self.alignment = 'VIEW'
|
||||
return None, None, None
|
||||
|
||||
face = temp_bm.faces[ray.index]
|
||||
# Create temporary `bmesh`.
|
||||
temp_bm = bmesh.new()
|
||||
temp_bm.from_mesh(mesh)
|
||||
temp_bm.faces.ensure_lookup_table()
|
||||
|
||||
if self.orientation == 'FACE':
|
||||
# Get the tangent, normal, and bitangent from the face normal.
|
||||
tangent = face.calc_tangent_edge()
|
||||
normal = face.normal
|
||||
bitangent = normal.cross(tangent)
|
||||
face = temp_bm.faces[ray.index]
|
||||
|
||||
elif self.orientation in ('CLOSEST_EDGE', 'LONGEST_EDGE'):
|
||||
# Get the tangent, normal, and bitangent from the longest or the closest edge of the face.
|
||||
if self.orientation == 'FACE':
|
||||
# Get the tangent, normal, and bitangent from the face normal.
|
||||
tangent = face.calc_tangent_edge()
|
||||
normal = face.normal
|
||||
bitangent = normal.cross(tangent)
|
||||
|
||||
if self.orientation == 'LONGEST_EDGE':
|
||||
lengths = [
|
||||
(ray_obj_matrix @ edge.verts[0].co - ray_obj_matrix @ edge.verts[1].co).length
|
||||
for edge in face.edges
|
||||
]
|
||||
longest_edge = sorted(zip(lengths, face.edges), key=lambda x: x[0], reverse=True)[0][1]
|
||||
edge = longest_edge
|
||||
elif self.orientation in ('CLOSEST_EDGE', 'LONGEST_EDGE'):
|
||||
# Get the tangent, normal, and bitangent from the longest or the closest edge of the face.
|
||||
|
||||
elif self.orientation == 'CLOSEST_EDGE':
|
||||
distances = [
|
||||
distance_from_point_to_segment(
|
||||
ray.location,
|
||||
ray_obj_matrix @ edge.verts[0].co,
|
||||
ray_obj_matrix @ edge.verts[1].co,
|
||||
)
|
||||
for edge in face.edges
|
||||
]
|
||||
closest_edge = sorted(zip(distances, face.edges), key=lambda x: x[0])[0][1]
|
||||
edge = closest_edge
|
||||
if self.orientation == 'LONGEST_EDGE':
|
||||
lengths = [
|
||||
(ray_obj_matrix @ edge.verts[0].co - ray_obj_matrix @ edge.verts[1].co).length
|
||||
for edge in face.edges
|
||||
]
|
||||
longest_edge = sorted(zip(lengths, face.edges), key=lambda x: x[0], reverse=True)[0][1]
|
||||
edge = longest_edge
|
||||
|
||||
# Get the loop (face corner) for the edge that is also in the face.
|
||||
face_corner = next(loop for loop in edge.link_loops if loop.face == face)
|
||||
elif self.orientation == 'CLOSEST_EDGE':
|
||||
distances = [
|
||||
distance_from_point_to_segment(
|
||||
ray.location,
|
||||
ray_obj_matrix @ edge.verts[0].co,
|
||||
ray_obj_matrix @ edge.verts[1].co,
|
||||
)
|
||||
for edge in face.edges
|
||||
]
|
||||
closest_edge = sorted(zip(distances, face.edges), key=lambda x: x[0])[0][1]
|
||||
edge = closest_edge
|
||||
|
||||
start = face_corner.vert
|
||||
end = face_corner.link_loop_next.vert
|
||||
direction = (end.co - start.co)
|
||||
# Get the loop (face corner) for the edge that is also in the face.
|
||||
face_corner = next(loop for loop in edge.link_loops if loop.face == face)
|
||||
|
||||
tangent = edge.calc_tangent(face_corner)
|
||||
normal = direction.cross(tangent)
|
||||
bitangent = normal.cross(tangent)
|
||||
start = face_corner.vert
|
||||
end = face_corner.link_loop_next.vert
|
||||
direction = (end.co - start.co)
|
||||
|
||||
# Construct Matrix
|
||||
matrix = Matrix.Identity(4)
|
||||
matrix[0].xyz = (ray_obj_matrix.to_3x3() @ tangent).normalized()
|
||||
matrix[1].xyz = (ray_obj_matrix.to_3x3() @ bitangent).normalized()
|
||||
matrix[2].xyz = (ray_obj_matrix.to_3x3() @ normal).normalized()
|
||||
matrix[3].xyz = ray.location + (ray.normal * self.offset)
|
||||
tangent = edge.calc_tangent(face_corner)
|
||||
normal = direction.cross(tangent)
|
||||
bitangent = normal.cross(tangent)
|
||||
|
||||
# destroy_temporary_bmesh
|
||||
temp_bm.free()
|
||||
del mesh
|
||||
# Construct Matrix
|
||||
matrix = Matrix.Identity(4)
|
||||
matrix[0].xyz = (ray_obj_matrix.to_3x3() @ tangent).normalized()
|
||||
matrix[1].xyz = (ray_obj_matrix.to_3x3() @ bitangent).normalized()
|
||||
matrix[2].xyz = (ray_obj_matrix.to_3x3() @ normal).normalized()
|
||||
matrix[3].xyz = ray.location + (ray.normal * self.offset)
|
||||
|
||||
matrix = matrix.transposed()
|
||||
location = ray.location + (ray.normal * self.offset)
|
||||
normal = ray.normal
|
||||
# destroy_temporary_bmesh
|
||||
temp_bm.free()
|
||||
del mesh
|
||||
|
||||
matrix = matrix.transposed()
|
||||
location = ray.location + (ray.normal * self.offset)
|
||||
normal = ray.normal
|
||||
|
||||
return matrix, location, normal
|
||||
|
||||
@@ -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,15 +2,18 @@
|
||||
#
|
||||
# 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:
|
||||
m.register()
|
||||
importlib.reload(m)
|
||||
if hasattr(m, 'register'):
|
||||
m.register()
|
||||
|
||||
# read addon meta-data
|
||||
with open(f"{utils.get_addon_directory()}/blender_manifest.toml", 'rb') as f:
|
||||
@@ -26,7 +29,8 @@ def register():
|
||||
def unregister():
|
||||
# un-register modules
|
||||
for m in reversed(modules):
|
||||
m.unregister()
|
||||
if hasattr(m, 'unregister'):
|
||||
m.unregister()
|
||||
|
||||
# Remove addon asset library
|
||||
utils.unregister_asset_lib()
|
||||
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):
|
||||
@@ -64,7 +64,7 @@ class BSBST_OT_new_brushstrokes(bpy.types.Operator):
|
||||
|
||||
# link to surface object's collections (fall back to active collection if all are linked data)
|
||||
utils.link_to_collections_by_ref(flow_object, surface_object, unlink=False)
|
||||
|
||||
|
||||
visibility_options = [
|
||||
'visible_camera',
|
||||
'visible_diffuse',
|
||||
@@ -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)
|
||||
@@ -94,7 +94,7 @@ class BSBST_OT_new_brushstrokes(bpy.types.Operator):
|
||||
|
||||
def main(self, context):
|
||||
settings = context.scene.BSBST_settings
|
||||
|
||||
|
||||
utils.ensure_resources()
|
||||
if not context.mode == 'OBJECT':
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
@@ -135,7 +135,7 @@ class BSBST_OT_new_brushstrokes(bpy.types.Operator):
|
||||
|
||||
if not settings.preset_object:
|
||||
bpy.ops.brushstroke_tools.init_preset()
|
||||
|
||||
|
||||
# assign preset material
|
||||
preset_material = getattr(settings.preset_object, '["BSBST_material"]', None)
|
||||
|
||||
@@ -153,7 +153,7 @@ class BSBST_OT_new_brushstrokes(bpy.types.Operator):
|
||||
# transfer preset modifiers to new brushstrokes TODO: refactor to deduplicate
|
||||
for mod in settings.preset_object.modifiers:
|
||||
utils.transfer_modifier(mod.name, brushstrokes_object, settings.preset_object)
|
||||
|
||||
|
||||
for v in mod.node_group.interface.items_tree.values():
|
||||
if type(v) not in utils.linkable_sockets:
|
||||
continue
|
||||
@@ -161,25 +161,26 @@ 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]:
|
||||
@@ -223,13 +224,13 @@ class BSBST_OT_new_brushstrokes(bpy.types.Operator):
|
||||
with context.temp_override(**override):
|
||||
bpy.ops.object.material_slot_add()
|
||||
brushstrokes_object.material_slots[-1].material = mat
|
||||
|
||||
|
||||
# set deformable
|
||||
set_brushstrokes_deformable(brushstrokes_object, settings.deforming_surface)
|
||||
|
||||
|
||||
# set animated
|
||||
set_brushstrokes_animated(brushstrokes_object, settings.animated)
|
||||
|
||||
|
||||
for mod in brushstrokes_object.modifiers:
|
||||
mod.show_group_selector = False
|
||||
|
||||
@@ -239,7 +240,7 @@ class BSBST_OT_new_brushstrokes(bpy.types.Operator):
|
||||
if name == brushstrokes_object.name:
|
||||
settings.active_context_brushstrokes_index = i
|
||||
break
|
||||
|
||||
|
||||
utils.edit_active_brushstrokes(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -247,10 +248,10 @@ class BSBST_OT_new_brushstrokes(bpy.types.Operator):
|
||||
settings = context.scene.BSBST_settings
|
||||
settings.silent_switch = True
|
||||
state = self.main(context)
|
||||
|
||||
|
||||
settings.silent_switch = False
|
||||
return state
|
||||
|
||||
|
||||
class BSBST_OT_edit_brushstrokes(bpy.types.Operator):
|
||||
"""
|
||||
Enter the editing context for the active context brushstrokes.
|
||||
@@ -269,10 +270,10 @@ class BSBST_OT_edit_brushstrokes(bpy.types.Operator):
|
||||
settings = context.scene.BSBST_settings
|
||||
settings.silent_switch = True
|
||||
state = utils.edit_active_brushstrokes(context)
|
||||
|
||||
|
||||
settings.silent_switch = False
|
||||
return state
|
||||
|
||||
|
||||
class BSBST_OT_delete_brushstrokes(bpy.types.Operator):
|
||||
"""
|
||||
Delete the active context brushstrokes
|
||||
@@ -324,7 +325,7 @@ class BSBST_OT_delete_brushstrokes(bpy.types.Operator):
|
||||
|
||||
settings.edit_toggle = edit_toggle
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class BSBST_OT_duplicate_brushstrokes(bpy.types.Operator):
|
||||
"""
|
||||
Duplicate the active context brushstrokes
|
||||
@@ -345,7 +346,7 @@ class BSBST_OT_duplicate_brushstrokes(bpy.types.Operator):
|
||||
bs_ob = utils.get_active_context_brushstrokes_object(context.scene)
|
||||
if not bs_ob:
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
if not bs_ob.visible_get(view_layer=context.view_layer):
|
||||
self.report({"WARNING"}, f"Skipped Brushstroke layer '{bs_ob.name}' because it is invisible in this context")
|
||||
return {"CANCELLED"}
|
||||
@@ -354,7 +355,7 @@ class BSBST_OT_duplicate_brushstrokes(bpy.types.Operator):
|
||||
|
||||
if context.mode != 'OBJECT':
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
|
||||
bpy.context.view_layer.objects.active = bs_ob
|
||||
if context.mode != 'OBJECT':
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
@@ -371,7 +372,7 @@ class BSBST_OT_duplicate_brushstrokes(bpy.types.Operator):
|
||||
if not mod.type=='NODES':
|
||||
continue
|
||||
if not mod.node_group:
|
||||
continue
|
||||
continue
|
||||
for v in mod.node_group.interface.items_tree.values():
|
||||
if type(v) not in utils.linkable_sockets:
|
||||
continue
|
||||
@@ -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'}
|
||||
|
||||
@@ -459,7 +460,7 @@ class BSBST_OT_copy_brushstrokes(bpy.types.Operator):
|
||||
|
||||
ob.parent = surface_object
|
||||
utils.set_surface_object(ob, surface_object)
|
||||
|
||||
|
||||
for mod in ob.modifiers:
|
||||
if not mod.type:
|
||||
continue
|
||||
@@ -475,27 +476,28 @@ 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
|
||||
surface_object.add_rest_position_attribute = True
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class BSBST_OT_select_surface(bpy.types.Operator):
|
||||
"""
|
||||
Select the surface object for the active context brushstrokes.
|
||||
@@ -547,7 +549,7 @@ class BSBST_OT_assign_surface(bpy.types.Operator):
|
||||
bs_ob = utils.get_active_context_brushstrokes_object(context.scene)
|
||||
if not bs_ob:
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
surface_object = bpy.data.objects.get(self.surface_object)
|
||||
|
||||
if not surface_object:
|
||||
@@ -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)
|
||||
@@ -601,7 +603,7 @@ def set_brushstrokes_animated(bs_ob, animated):
|
||||
if not mod:
|
||||
mod = ob.modifiers.new('Animation', 'NODES')
|
||||
mod.node_group = utils.ensure_node_group('.brushstroke_tools.animation')
|
||||
|
||||
|
||||
mod_info = ob.modifier_info.get(mod.name)
|
||||
if not mod_info:
|
||||
mod_info = ob.modifier_info.add()
|
||||
@@ -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)
|
||||
@@ -632,7 +634,7 @@ class BSBST_OT_copy_flow(bpy.types.Operator):
|
||||
|
||||
source_bs: bpy.props.StringProperty(name="Brushstroke Layers")
|
||||
bs_list: bpy.props.CollectionProperty(type=settings.BSBST_context_brushstrokes)
|
||||
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
settings = context.scene.BSBST_settings
|
||||
@@ -643,7 +645,7 @@ class BSBST_OT_copy_flow(bpy.types.Operator):
|
||||
flow_ob_old = utils.get_flow_object(bs_ob)
|
||||
if not bs_ob:
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
# get source
|
||||
|
||||
source_ob = bpy.data.objects.get(self.source_bs)
|
||||
@@ -738,7 +740,7 @@ class BSBST_OT_switch_deformable(bpy.types.Operator):
|
||||
if ob.type == 'GREASEPENCIL':
|
||||
self.report({"WARNING"}, "Grease Pencil does not currently support drawing on deformable surface geometry.")
|
||||
set_brushstrokes_deformable(ob, self.deformable)
|
||||
|
||||
|
||||
context.view_layer.depsgraph.update()
|
||||
|
||||
return {"FINISHED"}
|
||||
@@ -775,11 +777,11 @@ class BSBST_OT_switch_animated(bpy.types.Operator):
|
||||
|
||||
for ob in bs_objects:
|
||||
set_brushstrokes_animated(ob, self.animated)
|
||||
|
||||
|
||||
context.view_layer.depsgraph.update()
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BSBST_OT_init_preset(bpy.types.Operator):
|
||||
"""
|
||||
Initialize the preset to define a modifier stack applied to new brushstrokess.
|
||||
@@ -793,7 +795,7 @@ class BSBST_OT_init_preset(bpy.types.Operator):
|
||||
def poll(cls, context):
|
||||
settings = context.scene.BSBST_settings
|
||||
return settings.preset_object is None
|
||||
|
||||
|
||||
def init_fill(self, context):
|
||||
settings = context.scene.BSBST_settings
|
||||
|
||||
@@ -803,7 +805,7 @@ class BSBST_OT_init_preset(bpy.types.Operator):
|
||||
## input
|
||||
mod = preset_object.modifiers.new('Surface Input', 'NODES')
|
||||
mod.node_group = bpy.data.node_groups['.brushstroke_tools.geometry_input']
|
||||
|
||||
|
||||
mod_info = settings.preset_object.modifier_info.get(mod.name)
|
||||
if not mod_info:
|
||||
mod_info = settings.preset_object.modifier_info.add()
|
||||
@@ -820,7 +822,7 @@ class BSBST_OT_init_preset(bpy.types.Operator):
|
||||
## masking
|
||||
mod = preset_object.modifiers.new('Masking', 'NODES')
|
||||
mod.node_group = bpy.data.node_groups['.brushstroke_tools.mask_surface']
|
||||
|
||||
|
||||
mod_info = settings.preset_object.modifier_info.get(mod.name)
|
||||
if not mod_info:
|
||||
mod_info = settings.preset_object.modifier_info.add()
|
||||
@@ -838,7 +840,7 @@ class BSBST_OT_init_preset(bpy.types.Operator):
|
||||
## brushstrokes
|
||||
mod = preset_object.modifiers.new('Brushstrokes', 'NODES')
|
||||
mod.node_group = bpy.data.node_groups['.brushstroke_tools.surface_fill']
|
||||
|
||||
|
||||
mod_info = settings.preset_object.modifier_info.get(mod.name)
|
||||
if not mod_info:
|
||||
mod_info = settings.preset_object.modifier_info.add()
|
||||
@@ -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')
|
||||
@@ -879,7 +881,7 @@ class BSBST_OT_init_preset(bpy.types.Operator):
|
||||
]
|
||||
for p in hide_panels:
|
||||
utils.mark_panel_hidden(mod_info, p)
|
||||
|
||||
|
||||
def init_draw(self, context):
|
||||
settings = context.scene.BSBST_settings
|
||||
|
||||
@@ -889,7 +891,7 @@ class BSBST_OT_init_preset(bpy.types.Operator):
|
||||
## add pre-processing modifier
|
||||
mod = preset_object.modifiers.new('Pre-Processing', 'NODES')
|
||||
mod.node_group = bpy.data.node_groups['.brushstroke_tools.pre_processing']
|
||||
|
||||
|
||||
mod_info = settings.preset_object.modifier_info.get(mod.name)
|
||||
if not mod_info:
|
||||
mod_info = settings.preset_object.modifier_info.add()
|
||||
@@ -903,7 +905,7 @@ class BSBST_OT_init_preset(bpy.types.Operator):
|
||||
## brushstrokes
|
||||
mod = preset_object.modifiers.new('Brushstrokes', 'NODES')
|
||||
mod.node_group = bpy.data.node_groups['.brushstroke_tools.surface_draw']
|
||||
|
||||
|
||||
mod_info = settings.preset_object.modifier_info.get(mod.name)
|
||||
if not mod_info:
|
||||
mod_info = settings.preset_object.modifier_info.add()
|
||||
@@ -937,7 +939,7 @@ class BSBST_OT_init_preset(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
|
||||
settings = context.scene.BSBST_settings
|
||||
|
||||
|
||||
utils.ensure_resources()
|
||||
preset_name = f'BSBST-PRESET_{settings.brushstroke_method}'
|
||||
preset_object = bpy.data.objects.new(preset_name, bpy.data.hair_curves.new(preset_name))
|
||||
@@ -947,7 +949,7 @@ class BSBST_OT_init_preset(bpy.types.Operator):
|
||||
self.init_fill(context)
|
||||
elif settings.brushstroke_method == "SURFACE_DRAW":
|
||||
self.init_draw(context)
|
||||
|
||||
|
||||
# select preset material
|
||||
mat = bpy.data.materials.get('Brush Material')
|
||||
if not mat:
|
||||
@@ -982,7 +984,7 @@ class BSBST_OT_make_preset(bpy.types.Operator):
|
||||
else:
|
||||
for mod in settings.preset_object.modifiers[:]:
|
||||
settings.preset_object.modifiers.remove(mod)
|
||||
|
||||
|
||||
# transfer brushstrokes modifiers to preset
|
||||
bs_ob = utils.get_active_context_brushstrokes_object(context.scene)
|
||||
if not bs_ob:
|
||||
@@ -994,7 +996,7 @@ class BSBST_OT_make_preset(bpy.types.Operator):
|
||||
for mod in settings.preset_object.modifiers:
|
||||
# refresh UI
|
||||
mod.node_group.interface_update(context)
|
||||
|
||||
|
||||
# identify linked sockets
|
||||
for v in mod.node_group.interface.items_tree.values():
|
||||
if type(v) not in utils.linkable_sockets:
|
||||
@@ -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
|
||||
@@ -1109,14 +1111,14 @@ class BSBST_OT_brushstrokes_toggle_attribute(bpy.types.Operator):
|
||||
if not bs_ob:
|
||||
settings.edit_toggle = edit_toggle
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
override = context.copy()
|
||||
override['object'] = bs_ob
|
||||
with context.temp_override(**override):
|
||||
bpy.ops.object.geometry_nodes_input_attribute_toggle(input_name=self.input_name,
|
||||
modifier_name=self.modifier_name)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BSBST_OT_render_setup(bpy.types.Operator):
|
||||
"""
|
||||
Set up render settings.
|
||||
@@ -1165,7 +1167,7 @@ class BSBST_OT_render_setup(bpy.types.Operator):
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
|
||||
class BSBST_OT_view_all(bpy.types.Operator):
|
||||
"""
|
||||
Enable/disable all brushstrokes for the viewport.
|
||||
@@ -1299,11 +1301,11 @@ class BSBST_OT_select_brush_style(bpy.types.Operator):
|
||||
if bs.name == settings.context_material.brush_style:
|
||||
self.brush_styles_filtered_active_index = i
|
||||
break
|
||||
|
||||
|
||||
self.name_filter = ''
|
||||
|
||||
return context.window_manager.invoke_props_dialog(self, width=450)
|
||||
|
||||
|
||||
class BSBST_OT_upgrade_resources(bpy.types.Operator):
|
||||
""" Upgrade all local BST assets to available addon resources.
|
||||
"""
|
||||
@@ -1331,12 +1333,12 @@ class BSBST_OT_upgrade_resources(bpy.types.Operator):
|
||||
layout = self.layout
|
||||
|
||||
split = layout.split(factor=.6)
|
||||
|
||||
|
||||
col = split.column()
|
||||
col.prop(self, 'upgrade_shape_modifiers', icon='MODIFIER')
|
||||
col.prop(self, 'upgrade_materials', icon='MATERIAL')
|
||||
col.prop(self, 'upgrade_brush_styles', icon='BRUSHES_ALL')
|
||||
|
||||
|
||||
col = split.column()
|
||||
row = col.row()
|
||||
row.active = self.upgrade_shape_modifiers
|
||||
@@ -1422,8 +1424,8 @@ classes = [
|
||||
|
||||
def register():
|
||||
for c in classes:
|
||||
bpy.utils.register_class(c)
|
||||
bpy.utils.register_class(c)
|
||||
|
||||
def unregister():
|
||||
for c in reversed(classes):
|
||||
bpy.utils.unregister_class(c)
|
||||
bpy.utils.unregister_class(c)
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
if not mod_info:
|
||||
return
|
||||
|
||||
data_dict = {
|
||||
bpy.types.NodeTreeInterfaceSocketMaterial: 'materials',
|
||||
bpy.types.NodeTreeInterfaceSocketImage: 'images',
|
||||
bpy.types.NodeTreeInterfaceSocketCollection: 'collections',
|
||||
}
|
||||
panel_active = not (mod_info.hide_ui or hide_panel)
|
||||
inactive_mode_names: list[str] = []
|
||||
|
||||
mode_compare = []
|
||||
for k, v in items:
|
||||
if not v.parent.name == panel_name:
|
||||
for name, interface_item in interface_items:
|
||||
if interface_item.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:
|
||||
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
|
||||
s = mod_info.socket_info.get(v_id)
|
||||
if not s:
|
||||
if display_mode == 0 and socket_info.hide_ui:
|
||||
continue
|
||||
if display_mode == 0:
|
||||
if s.hide_ui:
|
||||
continue
|
||||
|
||||
subpanel_header, subpanel = panel.panel(k, default_closed = v.default_closed)
|
||||
subpanel_header.label(text=k)
|
||||
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')
|
||||
@@ -298,7 +264,7 @@ def draw_effect_panel_recursive(effects_panel, material, prev_node):
|
||||
panel.active = False
|
||||
for input in node.inputs[1:]:
|
||||
panel.prop(input, 'default_value', text=input.name)
|
||||
|
||||
|
||||
draw_effect_panel_recursive(effects_panel, material, node)
|
||||
|
||||
def draw_advanced_settings(layout, settings):
|
||||
@@ -309,7 +275,7 @@ def draw_advanced_settings(layout, settings):
|
||||
new_advanced_panel.row().prop(settings, 'curve_mode', expand=True)
|
||||
if settings.curve_mode in ['CURVE', 'GP']:
|
||||
new_advanced_panel.label(text='Curve mode does not support drawing on deformed geometry', icon='ERROR')
|
||||
|
||||
|
||||
new_advanced_panel.prop(settings, 'animated')
|
||||
new_advanced_panel.prop(settings, 'deforming_surface')
|
||||
new_advanced_panel.prop(settings, 'assign_materials')
|
||||
@@ -329,7 +295,7 @@ def draw_shape_properties(layout, settings, style_object, is_preset, display_mod
|
||||
if display_mode == 0:
|
||||
if mod_info.hide_ui:
|
||||
continue
|
||||
|
||||
|
||||
mod_header, mod_panel = layout.panel(mod.name, default_closed = mod_info.default_closed)
|
||||
row = mod_header.row(align=True)
|
||||
row.label(text='', icon='GEOMETRY_NODES')
|
||||
@@ -362,12 +328,12 @@ def draw_shape_properties(layout, settings, style_object, is_preset, display_mod
|
||||
mod,
|
||||
mod.node_group.interface.items_tree.items(),
|
||||
display_mode)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
@@ -441,7 +407,7 @@ class BSBST_MT_bs_context_menu(bpy.types.Menu):
|
||||
|
||||
def draw(self, _context):
|
||||
layout = self.layout
|
||||
|
||||
|
||||
op = layout.operator('brushstroke_tools.copy_brushstrokes', text='Copy to Selected Objects')
|
||||
op.copy_all = False
|
||||
|
||||
@@ -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"
|
||||
@@ -607,7 +573,7 @@ def register():
|
||||
wm = bpy.context.window_manager
|
||||
if wm.keyconfigs.addon is not None:
|
||||
km = wm.keyconfigs.addon.keymaps.new(name="Mesh")
|
||||
kmi = km.keymap_items.new("brushstroke_tools.data_marking","F", "PRESS",shift=False, ctrl=True, alt=True)
|
||||
kmi = km.keymap_items.new("brushstroke_tools.data_marking","F", "PRESS",shift=False, ctrl=True, alt=True)
|
||||
|
||||
def unregister():
|
||||
for c in reversed(classes):
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
@@ -13,4 +15,4 @@
|
||||
- Save modified images: For texture painting workflows, this will auto-save your images as well as the .blend file.
|
||||
- Save Mesh/Sculpt: If you spend a long time in Edit/Sculpt mode and Blender crashes, you will lose your work, because the mesh data is not written back into the object until you go to Object Mode. The add-on will periodically enter object mode for you, without interrupting any Sculpt/Paint/Transform operations.
|
||||
|
||||
It goes without saying that this add-on can introduce periodic lag. This is simply the price of saving data to a disk. A slow disk or a large file will exacerbate this, and all you can do is adjust your save interval according to what you can put up with.
|
||||
It goes without saying that this add-on can introduce periodic lag. This is simply the price of saving data to a disk. A slow disk or a large file will exacerbate this, and all you can do is adjust your save interval according to what you can put up with.
|
||||
|
||||
@@ -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
|
||||
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")
|
||||
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.6"
|
||||
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
|
||||
|
||||
@@ -210,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():
|
||||
@@ -237,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)
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import threading
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import json
|
||||
|
||||
# Module-level state — read by the panel draw function
|
||||
update_available = False
|
||||
latest_version_str = ""
|
||||
|
||||
_RELEASES_URL = "https://api.github.com/repos/Weisl/simple_renaming/releases/latest"
|
||||
|
||||
|
||||
def _parse_version(version_str):
|
||||
"""Convert '2.1.4' or 'v2.1.4' to (2, 1, 4)."""
|
||||
return tuple(int(x) for x in version_str.lstrip("v").split("."))
|
||||
|
||||
|
||||
def _fetch():
|
||||
global update_available, latest_version_str
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
_RELEASES_URL,
|
||||
headers={"User-Agent": "simple-renaming-addon"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=5) as response:
|
||||
data = json.loads(response.read().decode())
|
||||
|
||||
tag = data.get("tag_name", "")
|
||||
if not tag:
|
||||
return
|
||||
|
||||
latest = _parse_version(tag)
|
||||
|
||||
# Read current version from blender_manifest.toml at the addon root
|
||||
import os
|
||||
manifest_path = os.path.join(os.path.dirname(__file__), "..", "blender_manifest.toml")
|
||||
current_str = ""
|
||||
with open(manifest_path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
if line.startswith("version"):
|
||||
current_str = line.split("=")[1].strip().strip('"')
|
||||
break
|
||||
|
||||
if not current_str:
|
||||
return
|
||||
|
||||
current = _parse_version(current_str)
|
||||
|
||||
if latest > current:
|
||||
update_available = True
|
||||
latest_version_str = tag.lstrip("v")
|
||||
else:
|
||||
print(f"[RENAMING] Addon is up to date (v{current_str})")
|
||||
|
||||
except Exception as exc:
|
||||
print(f"[RENAMING] version check failed: {exc}")
|
||||
|
||||
|
||||
def start_version_check():
|
||||
"""Fire a background thread to check for a newer release on GitHub."""
|
||||
t = threading.Thread(target=_fetch, daemon=True)
|
||||
t.start()
|
||||
@@ -92,8 +92,7 @@ class VIEW3D_OT_renaming_preferences(bpy.types.AddonPreferences):
|
||||
bl_options = {'REGISTER'}
|
||||
|
||||
prefs_tabs: EnumProperty(items=(('UI', "General", "General Settings"),
|
||||
('KEYMAPS', "Keymaps", "Keymaps"),
|
||||
('SUPPORT', "Support & Donation", "Support")),
|
||||
('KEYMAPS', "Keymaps", "Keymaps")),
|
||||
default='UI')
|
||||
|
||||
renaming_category: StringProperty(name="Category",
|
||||
@@ -382,52 +381,3 @@ class VIEW3D_OT_renaming_preferences(bpy.types.AddonPreferences):
|
||||
self.keymap_ui(layout, 'Renaming Sub/Prefix', 'renaming_suf_pre',
|
||||
'wm.call_panel', "VIEW3D_PT_tools_type_suffix")
|
||||
|
||||
|
||||
elif self.prefs_tabs == 'SUPPORT':
|
||||
|
||||
text = "Support me developing great tools!"
|
||||
label_multiline(
|
||||
context=context,
|
||||
text=text,
|
||||
parent=layout
|
||||
)
|
||||
|
||||
# Donations
|
||||
box = layout.box()
|
||||
text = "Consider supporting the development of this addon with a donation!"
|
||||
label_multiline(
|
||||
context=context,
|
||||
text=text,
|
||||
parent=box
|
||||
)
|
||||
col = box.column(align=True)
|
||||
row = col.row()
|
||||
row.operator("wm.url_open", text="Gumroad",
|
||||
icon="URL").url = "https://weisl.gumroad.com/l/simple_renaming"
|
||||
row = col.row()
|
||||
row.operator("wm.url_open", text="Superhive Market",
|
||||
icon="URL").url = "https://superhivemarket.com/products/simple-renaming"
|
||||
row = col.row()
|
||||
row.operator("wm.url_open", text="PayPal Donation",
|
||||
icon="URL").url = "https://www.paypal.com/donate?hosted_button_id=JV7KRF77TY78A"
|
||||
|
||||
# Cross Promotion
|
||||
box = layout.box()
|
||||
text = "Explore my other Blender Addons designed for more efficient game asset workflows!"
|
||||
label_multiline(
|
||||
context=context,
|
||||
text=text,
|
||||
parent=box
|
||||
)
|
||||
|
||||
col = box.column(align=True)
|
||||
row = col.row()
|
||||
row.operator("wm.url_open", text="Simple Collider",
|
||||
icon="URL").url = "https://superhivemarket.com/products/simple-collider"
|
||||
row = col.row()
|
||||
row.operator("wm.url_open", text="Simple Camera Manager",
|
||||
icon="URL").url = "https://superhivemarket.com/products/simple-camera-manager"
|
||||
row = col.row()
|
||||
row.label(text='Support & Feedback')
|
||||
row = col.row()
|
||||
row.operator("wm.url_open", text="Join Discord", icon="URL").url = "https://discord.gg/VRzdcFpczm"
|
||||
|
||||
@@ -15,13 +15,6 @@ types_of_selected = (
|
||||
|
||||
|
||||
def draw_renaming_panel(layout, context):
|
||||
from ..operators.version_check import update_available, latest_version_str
|
||||
|
||||
if update_available:
|
||||
row = layout.row(align=True)
|
||||
row.alert = True
|
||||
row.label(text=f"Update available: v{latest_version_str}", icon='ERROR')
|
||||
|
||||
scene = context.scene
|
||||
|
||||
row = layout.row(align=True)
|
||||
@@ -47,7 +40,7 @@ def draw_renaming_panel(layout, context):
|
||||
elif str(scene.renaming_object_types) in types_selected:
|
||||
layout.prop(scene, "renaming_only_selection", text="Only Selected")
|
||||
elif str(scene.renaming_object_types) == 'COLLECTION':
|
||||
if bpy.context.space_data.type == 'OUTLINER':
|
||||
if bpy.context.space_data and bpy.context.space_data.type == 'OUTLINER':
|
||||
row = layout.row(align=True)
|
||||
row.prop(scene, "renaming_only_selection", text="Only Selected")
|
||||
else:
|
||||
@@ -191,13 +184,12 @@ class VIEW3D_PT_tools_renaming_panel(bpy.types.Panel):
|
||||
def draw_header(self, context):
|
||||
layout = self.layout
|
||||
row = layout.row(align=True)
|
||||
row.operator("wm.url_open", text="", icon='HELP').url = "https://weisl.github.io/renaming_overview/"
|
||||
row.operator("wm.url_open", text="", icon='HELP').url = "https://weisl.github.io/simple_renaming/renaming_overview/"
|
||||
addon_name = get_addon_name()
|
||||
|
||||
op = row.operator("preferences.rename_addon_search", text="", icon='PREFERENCES')
|
||||
op.addon_name = addon_name
|
||||
op.prefs_tabs = 'UI'
|
||||
row.operator("renaming.reload_addon", text="", icon='FILE_REFRESH')
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
@@ -21,18 +21,4 @@ class PREFERENCES_OT_open_addon(bpy.types.Operator):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
prefs.prefs_tabs = self.prefs_tabs
|
||||
|
||||
import addon_utils
|
||||
mod = addon_utils.addons_fake_modules.get('simple_renaming')
|
||||
|
||||
# mod is None the first time the operation is called :/
|
||||
if mod:
|
||||
mod.bl_info['show_expanded'] = True
|
||||
|
||||
# Find User Preferences area and redraw it
|
||||
for window in bpy.context.window_manager.windows:
|
||||
for area in window.screen.areas:
|
||||
if area.type == 'USER_PREFERENCES':
|
||||
area.tag_redraw()
|
||||
|
||||
bpy.ops.preferences.addon_expand(module=self.addon_name)
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import importlib
|
||||
|
||||
import bpy
|
||||
from bpy.utils import register_class, unregister_class
|
||||
import importlib
|
||||
|
||||
_VPM_AGENT_IMMEDIATE_REGISTER_DONE = locals().get("_VPM_AGENT_IMMEDIATE_REGISTER_DONE", False)
|
||||
|
||||
@@ -15,16 +16,19 @@ module_names = (
|
||||
"prefs",
|
||||
"sidebar",
|
||||
"tweak_builtin_pies",
|
||||
|
||||
"pie_animation",
|
||||
"pie_playback",
|
||||
"pie_keyframes",
|
||||
"pie_copy_attributes",
|
||||
"pie_apply_transform",
|
||||
"pie_camera",
|
||||
"pie_curve_delete",
|
||||
"pie_preferences",
|
||||
"pie_editor_split_merge",
|
||||
"pie_editor_switch",
|
||||
"pie_file",
|
||||
"pie_manipulator",
|
||||
"pie_mesh_delete",
|
||||
"pie_mesh_edge",
|
||||
"pie_mesh_flatten",
|
||||
"pie_mesh_merge",
|
||||
"pie_object_add",
|
||||
@@ -33,16 +37,15 @@ module_names = (
|
||||
"pie_proportional_editing",
|
||||
"pie_relationship_delete",
|
||||
"pie_sculpt_brush_select",
|
||||
"pie_select_similar",
|
||||
"pie_selection",
|
||||
"pie_set_origin",
|
||||
"pie_view_3d",
|
||||
"pie_window",
|
||||
)
|
||||
|
||||
modules = [
|
||||
__import__(__package__ + "." + submod, {}, {}, submod)
|
||||
for submod in module_names
|
||||
]
|
||||
modules = [__import__(__package__ + "." + submod, {}, {}, submod) for submod in module_names]
|
||||
|
||||
|
||||
def register_unregister_modules(modules: list, register: bool):
|
||||
"""Recursively register or unregister modules by looking for either
|
||||
@@ -71,12 +74,14 @@ def register_unregister_modules(modules: list, register: bool):
|
||||
elif hasattr(m, 'unregister'):
|
||||
m.unregister()
|
||||
|
||||
|
||||
def delayed_register(_scene=None):
|
||||
# Register whole add-on with a slight delay,
|
||||
# to make sure Keymap data we need already exists on Blender launch.
|
||||
# Otherwise, keyconfigs.user.keymaps is an empty list, we can't find fallback ops.
|
||||
register_unregister_modules(modules, True)
|
||||
|
||||
|
||||
def register():
|
||||
"""
|
||||
We prefer an *immediate* register during startup, because other add-ons may touch
|
||||
@@ -96,9 +101,10 @@ def register():
|
||||
# Keep behavior unchanged (fallback to timer), but avoid raising during registration.
|
||||
pass
|
||||
|
||||
# NOTE: persistent=True must be set, otherwise this doesn't work when opening
|
||||
# NOTE: persistent=True must be set, otherwise this doesn't work when opening
|
||||
# a .blend file directly from a file browser.
|
||||
bpy.app.timers.register(delayed_register, first_interval=0.0, persistent=True)
|
||||
|
||||
|
||||
def unregister():
|
||||
register_unregister_modules(reversed(modules), False)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
schema_version = "1.0.0"
|
||||
id = "viewport_pie_menus"
|
||||
name = "3D Viewport Pie Menus"
|
||||
version = "1.7.3"
|
||||
version = "1.7.4"
|
||||
tagline = "Various pie menus to speed up your workflow"
|
||||
maintainer = "Community"
|
||||
type = "add-on"
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
# This file requires (and is made possible by) Blender 5.0 due to the find_match() API call.
|
||||
|
||||
import hashlib
|
||||
from typing import Callable, Any
|
||||
import json
|
||||
from typing import Any, Callable
|
||||
|
||||
import bpy
|
||||
import json
|
||||
from bpy.app.translations import pgettext_n as n_
|
||||
from bpy.app.translations import pgettext_rpt as rpt_
|
||||
from bpy.types import KeyMap, KeyMapItem, UILayout
|
||||
|
||||
# Preserve across Reload Scripts (module reload) when possible, but also keep
|
||||
@@ -27,11 +29,14 @@ KEYMAP_ICONS = {
|
||||
'Armature': 'ARMATURE_DATA',
|
||||
'Pose': 'POSE_HLT',
|
||||
'Weight Paint': 'WPAINT_HLT',
|
||||
'Graph Editor': 'GRAPH',
|
||||
'Dopesheet': 'ACTION',
|
||||
'Curve': 'CURVE_DATA',
|
||||
}
|
||||
|
||||
KEYMAP_UI_NAMES = {
|
||||
'Armature': "Armature Edit",
|
||||
'Object Non-modal': "Object Mode",
|
||||
'Armature': n_("Armature Edit"),
|
||||
'Object Non-modal': n_("Object Mode"),
|
||||
}
|
||||
|
||||
KMI_DEFAULTS = {
|
||||
@@ -75,9 +80,9 @@ def register_hotkey(
|
||||
existing_kmi = find_kmi_in_km_by_data(addon_km, hotkey_kwargs, bl_idname, op_kwargs)
|
||||
if existing_kmi:
|
||||
# NOTE: It is extremely important to not register duplicate add-on keymaps, AND
|
||||
# to NOT remove them on add-on unregister, because once an add-on keymap is registered,
|
||||
# to NOT remove them on add-on unregister, because once an add-on keymap is registered,
|
||||
# it is SUPPOSED TO stick around for ever.
|
||||
# This allows Blender to store the associated user keymap, meaning the user's modifications
|
||||
# This allows Blender to store the associated user keymap, meaning the user's modifications
|
||||
# will be stored and restored as expected, whenever the add-on is enabled again.
|
||||
if (addon_km, existing_kmi) not in ADDON_KEYMAPS:
|
||||
ADDON_KEYMAPS.append((addon_km, existing_kmi))
|
||||
@@ -213,7 +218,7 @@ def get_kmi_ui_info(km, kmi) -> tuple[str, str, str]:
|
||||
return km_icon, km_name, kmi_name
|
||||
|
||||
def find_kmi_in_km_by_data(km: KeyMap, hotkey_kwargs: dict, op_idname: str, op_kwargs: dict) -> KeyMapItem | None:
|
||||
"""Loop over KeyMapItems of the provided KeyMap, and return the first entry,
|
||||
"""Loop over KeyMapItems of the provided KeyMap, and return the first entry,
|
||||
if any, which matches the passed key combo, operator, and operator properties.
|
||||
"""
|
||||
|
||||
@@ -369,7 +374,7 @@ class WINDOW_OT_restore_deleted_hotkeys(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
num_restored = restore_deleted_keymap_items_global(context)
|
||||
self.report({'INFO'}, f"Restored {num_restored} deleted keymaps.")
|
||||
self.report({'INFO'}, rpt_("Restored {num_restored} deleted keymaps.").format(num_restored=num_restored))
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
|
||||
@@ -175,7 +175,7 @@ def x_mirror_value(value):
|
||||
if isinstance(value, str):
|
||||
return flip_name(value)
|
||||
elif isinstance(value, Object):
|
||||
get_opposite_obj(value)
|
||||
return get_opposite_obj(value)
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
@@ -1,39 +1,35 @@
|
||||
# SPDX-FileCopyrightText: 2016-2024 Blender Foundation
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
import os
|
||||
from bpy.types import Operator
|
||||
from bpy.props import StringProperty, BoolProperty
|
||||
import json
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import bpy
|
||||
from bpy.props import BoolProperty, StringProperty
|
||||
from bpy.types import Context, Event, Operator
|
||||
|
||||
from .bs_utils.hotkeys import register_hotkey
|
||||
|
||||
|
||||
def idname_to_op_class(idname: str):
|
||||
parts = idname.split(".")
|
||||
op = bpy.ops
|
||||
for part in parts:
|
||||
op = getattr(op, part)
|
||||
return op
|
||||
|
||||
|
||||
class WM_OT_call_menu_pie_drag_only(Operator):
|
||||
"""Summon a pie menu only on mouse drag, otherwise initiate an operator"""
|
||||
# This class is for cases where we want to overwrite a built-in shortcut which triggers on Press,
|
||||
# with a pie menu that only appears on mouse drag.
|
||||
|
||||
# This class is for cases where we want to overwrite a built-in shortcut which triggers on Press,
|
||||
# with a pie menu that only appears on mouse drag.
|
||||
# If the user does not mouse drag, invoke the provided fallback operator.
|
||||
# In register_drag_hotkey(), the fallback operator is automagically extracted from the keymap,
|
||||
# at the moment the hotkey is registered.
|
||||
|
||||
bl_idname = "wm.call_menu_pie_drag_only"
|
||||
bl_label = "Pie Menu on Drag"
|
||||
bl_options = {'REGISTER', 'INTERNAL'}
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def update_kmi(self, context):
|
||||
def update_kmi(self, context: Context) -> None:
|
||||
if not hasattr(context, 'keymapitem'):
|
||||
return
|
||||
kmi = context.keymapitem # Set via UILayout.context_pointer_set().
|
||||
kmi = context.keymapitem # Set via UILayout.context_pointer_set().
|
||||
kmi.type = kmi.type
|
||||
|
||||
name: StringProperty(options={'SKIP_SAVE'})
|
||||
@@ -47,7 +43,7 @@ class WM_OT_call_menu_pie_drag_only(Operator):
|
||||
fallback_operator: StringProperty(options={'SKIP_SAVE'})
|
||||
fallback_op_kwargs: StringProperty(default="{}", options={'SKIP_SAVE'})
|
||||
|
||||
def invoke(self, context, event):
|
||||
def invoke(self, context: Context, event: Event) -> set[str]:
|
||||
if not self.on_drag:
|
||||
return self.execute(context)
|
||||
self.init_mouse_x = event.mouse_x
|
||||
@@ -55,33 +51,30 @@ class WM_OT_call_menu_pie_drag_only(Operator):
|
||||
context.window_manager.modal_handler_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def invoke_fallback_operator(self):
|
||||
def invoke_fallback_operator(self) -> None:
|
||||
op_cls = idname_to_op_class(self.fallback_operator)
|
||||
if not op_cls:
|
||||
return
|
||||
|
||||
fallback_op_kwargs = json.loads(self.fallback_op_kwargs)
|
||||
if type(fallback_op_kwargs) == str:
|
||||
# Not sure why json.loads seems to sometimes return a string, but it does
|
||||
# when eg. setting Shift+C to drag, and using it when first launching Blender.
|
||||
# After Reload Scripts, it works fine without this workaround. Weird af.
|
||||
if isinstance(fallback_op_kwargs, str):
|
||||
# Not sure why json.loads sometimes returns a string, but it does
|
||||
# when eg. setting Shift+C to drag and using it on first launch.
|
||||
# After Reload Scripts it works fine without this. Weird.
|
||||
fallback_op_kwargs = json.loads(fallback_op_kwargs)
|
||||
|
||||
if op_cls.poll():
|
||||
try:
|
||||
# 1. Execute the original operator and capture the result
|
||||
result = op_cls('INVOKE_DEFAULT', **fallback_op_kwargs)
|
||||
|
||||
# 2. [Added] Check if it's a Save operation and report the message manually
|
||||
if 'FINISHED' in result and self.fallback_operator == 'wm.save_mainfile':
|
||||
# Get current filename, or default to untitled
|
||||
filename = os.path.basename(bpy.data.filepath) if bpy.data.filepath else "untitled.blend"
|
||||
# HACK: Fix save notification not appearing when tapping Ctrl+S.
|
||||
filename = os.path.basename(bpy.data.filepath)
|
||||
self.report({'INFO'}, f'Saved "{filename}"')
|
||||
except TypeError:
|
||||
# This can apparently happen sometimes, see issue #86.
|
||||
print(f"Pie Menu Fallback Operator failed: {self.fallback_operator}, {self.fallback_op_kwargs}")
|
||||
|
||||
def modal(self, context, event):
|
||||
def modal(self, context: Context, event: Event) -> set[str]:
|
||||
if event.value == 'RELEASE':
|
||||
if self.fallback_operator:
|
||||
self.invoke_fallback_operator()
|
||||
@@ -91,10 +84,9 @@ class WM_OT_call_menu_pie_drag_only(Operator):
|
||||
delta_y = abs(event.mouse_y - self.init_mouse_y)
|
||||
if delta_x > threshold or delta_y > threshold:
|
||||
return self.execute(context)
|
||||
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, _context: Context) -> set[str]:
|
||||
bpy.ops.wm.call_menu_pie(name=self.name)
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -104,16 +96,16 @@ class WM_OT_call_menu_pie_drag_only(Operator):
|
||||
*,
|
||||
keymap_name: str,
|
||||
pie_name: str,
|
||||
hotkey_kwargs=None,
|
||||
default_fallback_op="",
|
||||
default_fallback_kwargs=None,
|
||||
on_drag=True,
|
||||
):
|
||||
hotkey_kwargs: dict[str, Any] | None = None,
|
||||
default_fallback_op: str = "",
|
||||
default_fallback_kwargs: dict[str, Any] | None = None,
|
||||
on_drag: bool = True,
|
||||
) -> None:
|
||||
if hotkey_kwargs is None:
|
||||
hotkey_kwargs = {'type': "SPACE", 'value': "PRESS"}
|
||||
context = bpy.context
|
||||
fallback_operator = default_fallback_op
|
||||
fallback_op_kwargs = default_fallback_kwargs if default_fallback_kwargs is not None else {}
|
||||
fallback_op_kwargs: dict[str, Any] = default_fallback_kwargs if default_fallback_kwargs is not None else {}
|
||||
|
||||
# IMPORTANT:
|
||||
# Do NOT derive fallback operator/kwargs from the USER keyconfig.
|
||||
@@ -137,21 +129,24 @@ class WM_OT_call_menu_pie_drag_only(Operator):
|
||||
kmi.oskey == hotkey_kwargs.get('oskey', False),
|
||||
kmi.any == hotkey_kwargs.get('any', False),
|
||||
kmi.key_modifier == hotkey_kwargs.get('key_modifier', 'NONE'),
|
||||
kmi.active
|
||||
kmi.active,
|
||||
]:
|
||||
if not condition:
|
||||
break
|
||||
else:
|
||||
fallback_operator = kmi.idname
|
||||
if kmi.properties:
|
||||
fallback_op_kwargs = {k:getattr(kmi.properties, k) if hasattr(kmi.properties, k) else v for k, v in kmi.properties.items()}
|
||||
fallback_op_kwargs = {
|
||||
k: getattr(kmi.properties, k) if hasattr(kmi.properties, k) else v
|
||||
for k, v in kmi.properties.items()
|
||||
}
|
||||
break
|
||||
|
||||
register_hotkey(
|
||||
cls.bl_idname,
|
||||
op_kwargs={
|
||||
'name': pie_name,
|
||||
'fallback_operator': fallback_operator,
|
||||
'name': pie_name,
|
||||
'fallback_operator': fallback_operator,
|
||||
# Deterministic JSON (sort_keys=True) helps keep KMI identity stable across sessions.
|
||||
'fallback_op_kwargs': json.dumps(fallback_op_kwargs, sort_keys=True),
|
||||
'on_drag': on_drag,
|
||||
@@ -160,10 +155,20 @@ class WM_OT_call_menu_pie_drag_only(Operator):
|
||||
keymap_name=keymap_name,
|
||||
)
|
||||
|
||||
def register():
|
||||
|
||||
def idname_to_op_class(idname: str) -> Any:
|
||||
parts = idname.split(".")
|
||||
op = bpy.ops
|
||||
for part in parts:
|
||||
op = getattr(op, part)
|
||||
return op
|
||||
|
||||
|
||||
def register() -> None:
|
||||
bpy.utils.register_class(WM_OT_call_menu_pie_drag_only)
|
||||
|
||||
def unregister():
|
||||
|
||||
def unregister() -> None:
|
||||
# HACK: As a workaround to https://projects.blender.org/blender/blender/issues/150229, we do not unregister
|
||||
# this operator when the add-on is uninstalled, which is pretty bad.
|
||||
# bpy.utils.unregister_class(WM_OT_call_menu_pie_drag_only)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# SPDX-FileCopyrightText: 2016-2024 Blender Foundation
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import Menu, Operator
|
||||
from bpy.types import Context, Event, Menu, Operator
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
|
||||
|
||||
@@ -11,34 +12,26 @@ class PIE_MT_apply_transforms(Menu):
|
||||
bl_idname = "PIE_MT_apply_transforms"
|
||||
bl_label = "Apply Transforms"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
# 4 - LEFT
|
||||
props = pie.operator("object.transform_apply", text="Rot/Scale", icon='CON_SIZELIKE')
|
||||
props.location, props.rotation, props.scale = (False, True, True)
|
||||
# 6 - RIGHT
|
||||
props = pie.operator(
|
||||
"object.transform_apply", text="Loc/Rot/Scale", icon='ORIENTATION_LOCAL'
|
||||
)
|
||||
props = pie.operator("object.transform_apply", text="Loc/Rot/Scale", icon='ORIENTATION_LOCAL')
|
||||
props.location, props.rotation, props.scale = (True, True, True)
|
||||
# 2 - BOTTOM
|
||||
pie.operator("object.apply_transforms_of_constraints", text="Soft-Apply Constraints", icon='CONSTRAINT')
|
||||
|
||||
# 8 - TOP
|
||||
props = pie.operator(
|
||||
"object.transform_apply", text="Rotation", icon='CON_ROTLIKE'
|
||||
)
|
||||
props = pie.operator("object.transform_apply", text="Rotation", icon='CON_ROTLIKE')
|
||||
props.location, props.rotation, props.scale = (False, True, False)
|
||||
# 7 - TOP - LEFT
|
||||
props = pie.operator(
|
||||
"object.transform_apply", text="Location", icon='CON_LOCLIKE'
|
||||
)
|
||||
props = pie.operator("object.transform_apply", text="Location", icon='CON_LOCLIKE')
|
||||
props.location, props.rotation, props.scale = (True, False, False)
|
||||
# 9 - TOP - RIGHT
|
||||
props = pie.operator(
|
||||
"object.transform_apply", text="Scale", icon='CON_SIZELIKE'
|
||||
)
|
||||
props = pie.operator("object.transform_apply", text="Scale", icon='CON_SIZELIKE')
|
||||
props.location, props.rotation, props.scale = (False, False, True)
|
||||
# 1 - BOTTOM - LEFT
|
||||
if (
|
||||
@@ -49,11 +42,10 @@ class PIE_MT_apply_transforms(Menu):
|
||||
):
|
||||
pie.operator('object.instancer_empty_to_collection', icon='LINKED')
|
||||
else:
|
||||
pie.operator(
|
||||
"object.make_single_user", text="Make Single-User", icon='DUPLICATE'
|
||||
).obdata = True
|
||||
pie.operator("object.apply_modifiers_shapekeys", icon='MODIFIER_DATA')
|
||||
|
||||
# 3 - BOTTOM - RIGHT
|
||||
pie.menu("PIE_MT_clear_menu", text="Clear Transforms", icon='THREE_DOTS')
|
||||
pie.menu("PIE_MT_apply_transforms_extras", text="More...", icon='THREE_DOTS')
|
||||
|
||||
|
||||
class OBJECT_OT_apply_transforms_of_constraints(Operator):
|
||||
@@ -64,18 +56,13 @@ class OBJECT_OT_apply_transforms_of_constraints(Operator):
|
||||
bl_options = {'REGISTER'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not any(
|
||||
[
|
||||
any([c.enabled and c.influence > 0 for c in obj.constraints])
|
||||
for obj in context.selected_objects
|
||||
]
|
||||
):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
if not any(any(c.enabled and c.influence > 0 for c in obj.constraints) for obj in context.selected_objects):
|
||||
cls.poll_message_set("No selected objects with enabled constraints.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, _context: Context):
|
||||
# This is just a wrapper operator, to add a better tooltip and poll message.
|
||||
return bpy.ops.object.visual_transform_apply()
|
||||
|
||||
@@ -88,15 +75,13 @@ class OBJECT_OT_make_meshes_single_user(Operator):
|
||||
bl_options = {'REGISTER'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not any(
|
||||
[obj.data and obj.data.users > 1 for obj in context.selected_objects]
|
||||
):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
if not any(obj.data and obj.data.users > 1 for obj in context.selected_objects):
|
||||
cls.poll_message_set("No selected objects with multi-user meshes.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, _context: Context):
|
||||
# This is just a wrapper operator, to add a better tooltip and poll message.
|
||||
return bpy.ops.object.duplicates_make_real()
|
||||
|
||||
@@ -107,25 +92,63 @@ class OBJECT_OT_clear_all_transforms(Operator):
|
||||
bl_description = "Clear All Transforms"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, _context: Context):
|
||||
bpy.ops.object.location_clear()
|
||||
bpy.ops.object.rotation_clear()
|
||||
bpy.ops.object.scale_clear()
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class OBJECT_OT_apply_modifiers_shapekeys(Operator):
|
||||
"""Apply the object's modifier stack and shape keys. Same as calling "Convert To -> Mesh".
|
||||
|
||||
Shift: Keep the original object as a duplicate."""
|
||||
|
||||
bl_idname = "object.apply_modifiers_shapekeys"
|
||||
bl_label = "Apply Modifiers & Shape Keys"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return bool(context.active_object)
|
||||
|
||||
def invoke(self, _context: Context, event: Event):
|
||||
bpy.ops.object.convert(target='MESH', keep_original=event.shift)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class PIE_MT_clear_transforms(Menu):
|
||||
bl_idname = "PIE_MT_clear_menu"
|
||||
bl_label = "Clear Transforms"
|
||||
bl_idname = "PIE_MT_apply_transforms_extras"
|
||||
bl_label = "Apply Transforms Extras"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
layout = self.layout
|
||||
layout.operator("clear.all", text="Clear All", icon='NONE')
|
||||
layout.operator("object.location_clear", text="Clear Location", icon='NONE')
|
||||
layout.operator("object.rotation_clear", text="Clear Rotation", icon='NONE')
|
||||
layout.operator("object.scale_clear", text="Clear Scale", icon='NONE')
|
||||
layout.operator("object.origin_clear", text="Clear Origin", icon='NONE')
|
||||
layout.label(text="Clear Transforms")
|
||||
layout.operator("clear.all", text="Clear All", icon='X')
|
||||
layout.operator("object.location_clear", text="Clear Location", icon='CON_LOCLIKE')
|
||||
layout.operator("object.rotation_clear", text="Clear Rotation", icon='CON_ROTLIKE')
|
||||
layout.operator("object.scale_clear", text="Clear Scale", icon='CON_SIZELIKE')
|
||||
layout.operator("object.origin_clear", text="Clear Origin", icon='PIVOT_CURSOR')
|
||||
|
||||
layout.separator()
|
||||
layout.label(text="To Deltas")
|
||||
props = layout.operator("object.transforms_to_deltas", text="Location to Deltas", icon='CON_LOCLIKE')
|
||||
props.mode = 'LOC'
|
||||
props = layout.operator("object.transforms_to_deltas", text="Rotation to Deltas", icon='CON_ROTLIKE')
|
||||
props.mode = 'ROT'
|
||||
props = layout.operator("object.transforms_to_deltas", text="Scale to Deltas", icon='CON_SIZELIKE')
|
||||
props.mode = 'SCALE'
|
||||
props = layout.operator(
|
||||
"object.transforms_to_deltas", text="All Transforms to Deltas", icon='ORIENTATION_LOCAL'
|
||||
)
|
||||
props.mode = 'ALL'
|
||||
layout.operator("object.anim_transforms_to_deltas", text="Animated Transforms to Deltas", icon='KEYINGSET')
|
||||
|
||||
layout.separator()
|
||||
layout.label(text="Mesh")
|
||||
make_single = layout.operator("object.make_single_user", text="Make Single-User", icon='DUPLICATE')
|
||||
make_single.obdata = True
|
||||
layout.operator("object.duplicates_make_real", text="Make Instances Real", icon='DUPLICATE')
|
||||
|
||||
|
||||
registry = (
|
||||
@@ -133,6 +156,7 @@ registry = (
|
||||
OBJECT_OT_apply_transforms_of_constraints,
|
||||
OBJECT_OT_make_meshes_single_user,
|
||||
OBJECT_OT_clear_all_transforms,
|
||||
OBJECT_OT_apply_modifiers_shapekeys,
|
||||
PIE_MT_clear_transforms,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# SPDX-FileCopyrightText: 2016-2024 Blender Foundation
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import Menu, Operator
|
||||
from bpy.types import Context, Event, Menu, Object, Operator
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
|
||||
# Some magic numbers that are either hard-coded into Blender,
|
||||
@@ -16,7 +17,7 @@ class PIE_MT_camera(Menu):
|
||||
bl_idname = "PIE_MT_camera"
|
||||
bl_label = "Camera"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context):
|
||||
pie = self.layout.menu_pie()
|
||||
scene = context.scene
|
||||
space = context.area.spaces.active
|
||||
@@ -35,9 +36,7 @@ class PIE_MT_camera(Menu):
|
||||
text = "View Active Camera"
|
||||
if context.space_data.region_3d.view_perspective == 'CAMERA':
|
||||
text = "Return to Viewport"
|
||||
pie.operator(
|
||||
'view3d.view_camera_with_poll', text=text, icon='VIEW_CAMERA_UNSELECTED'
|
||||
)
|
||||
pie.operator('view3d.view_camera_with_poll', text=text, icon='VIEW_CAMERA_UNSELECTED')
|
||||
# 2 - BOTTOM
|
||||
box = pie.box().column(align=True)
|
||||
row = box.row()
|
||||
@@ -48,9 +47,20 @@ class PIE_MT_camera(Menu):
|
||||
if camera:
|
||||
box.prop(camera.data, 'lens')
|
||||
box.prop(camera.data, 'sensor_width')
|
||||
|
||||
row = box.row(align=True)
|
||||
row.prop(camera.data.dof, 'use_dof', text="")
|
||||
fstop_row = row.row()
|
||||
fstop_row.enabled = camera.data.dof.use_dof
|
||||
fstop_row.prop(camera.data.dof, 'aperture_fstop', text="Depth of Field F-Stop")
|
||||
|
||||
box.separator()
|
||||
row = box.row(align=True)
|
||||
row.prop(camera.data, 'show_passepartout', text="")
|
||||
row.prop(camera.data, 'passepartout_alpha')
|
||||
row_alpha = row.row()
|
||||
row_alpha.enabled = camera.data.show_passepartout
|
||||
row_alpha.prop(camera.data, 'passepartout_alpha')
|
||||
|
||||
row = box.row(align=True)
|
||||
row.prop(camera.data, 'show_composition_thirds')
|
||||
row.prop(camera.data, 'show_composition_center')
|
||||
@@ -88,13 +98,13 @@ class VIEW3D_OT_lock_active_camera_transforms(Operator):
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
if not get_current_camera(context):
|
||||
cls.poll_message_set("No active camera in this viewport.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context):
|
||||
camera = get_current_camera(context)
|
||||
|
||||
lock = False
|
||||
@@ -119,16 +129,14 @@ class VIEW3D_OT_camera_fit_view(Operator):
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
if not context.space_data or not hasattr(context.space_data, 'region_3d'):
|
||||
return False
|
||||
if context.space_data.region_3d.view_perspective == 'CAMERA':
|
||||
cls.poll_message_set("Already in a camera view.")
|
||||
return False
|
||||
camera = get_current_camera(context)
|
||||
if not camera and not (
|
||||
context.active_object and context.active_object.type == 'CAMERA'
|
||||
):
|
||||
if not camera and not (context.active_object and context.active_object.type == 'CAMERA'):
|
||||
cls.poll_message_set("No active camera.")
|
||||
return False
|
||||
if camera and (any(camera.lock_location) or any(camera.lock_rotation)):
|
||||
@@ -136,14 +144,12 @@ class VIEW3D_OT_camera_fit_view(Operator):
|
||||
return False
|
||||
return True
|
||||
|
||||
def invoke(self, context, _event):
|
||||
if not context.scene.camera and not (
|
||||
context.active_object and context.active_object.type == 'CAMERA'
|
||||
):
|
||||
def invoke(self, context: Context, _event: Event):
|
||||
if not context.scene.camera and not (context.active_object and context.active_object.type == 'CAMERA'):
|
||||
self.create_camera = True
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context):
|
||||
cam = get_current_camera(context)
|
||||
|
||||
space = context.space_data
|
||||
@@ -173,7 +179,7 @@ class VIEW3D_OT_camera_from_view(Operator):
|
||||
bl_label = "New Camera from View"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context):
|
||||
if context.mode != 'OBJECT':
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
bpy.ops.object.camera_add()
|
||||
@@ -193,7 +199,7 @@ class VIEW3D_OT_set_active_camera(Operator):
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
if not context.active_object:
|
||||
cls.poll_message_set("No active object.")
|
||||
return False
|
||||
@@ -205,7 +211,7 @@ class VIEW3D_OT_set_active_camera(Operator):
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context):
|
||||
context.scene.camera = context.active_object
|
||||
self.report({'INFO'}, f"Set active camera: {context.active_object.name}")
|
||||
return {'FINISHED'}
|
||||
@@ -219,17 +225,17 @@ class VIEW3D_OT_view_camera_with_poll(Operator):
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
if not get_current_camera(context):
|
||||
cls.poll_message_set("No active camera.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, _context: Context):
|
||||
return bpy.ops.view3d.view_camera()
|
||||
|
||||
|
||||
def get_current_camera(context):
|
||||
def get_current_camera(context: Context) -> Object | None:
|
||||
space = context.area.spaces.active
|
||||
if space.type == 'VIEW_3D' and space.use_local_camera:
|
||||
return space.camera
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,150 @@
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import Context, Menu, Object, Operator, Spline
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
|
||||
|
||||
class PIE_MT_curve_delete(Menu):
|
||||
bl_idname = "PIE_MT_curve_delete"
|
||||
bl_label = "Curve Delete"
|
||||
|
||||
def draw(self, _context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
# 4 - LEFT
|
||||
pie.operator(CURVE_OT_dissolve_verts.bl_idname, text="Dissolve Points")
|
||||
# 6 - RIGHT
|
||||
pie.operator(CURVE_OT_delete_verts.bl_idname, text="Delete Points")
|
||||
# 2 - BOTTOM
|
||||
pie.operator(CURVE_OT_delete_segment.bl_idname, text="Delete Segments")
|
||||
# 8 - TOP
|
||||
# 7 - TOP - LEFT
|
||||
# 9 - TOP - RIGHT
|
||||
# 1 - BOTTOM - LEFT
|
||||
# 3 - BOTTOM - RIGHT
|
||||
|
||||
|
||||
class CURVE_OT_delete_verts(Operator):
|
||||
"""Delete selected curve points"""
|
||||
|
||||
bl_idname = "curve.delete_verts"
|
||||
bl_label = "Delete Points"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
curve_objects = _curve_objects_in_edit_mode(context)
|
||||
if not curve_objects:
|
||||
cls.poll_message_set("Must be editing a curve.")
|
||||
return False
|
||||
if any(spline_has_any_selected(spline) for obj in curve_objects for spline in obj.data.splines):
|
||||
return True
|
||||
cls.poll_message_set("No points are selected.")
|
||||
return False
|
||||
|
||||
def execute(self, _context: Context):
|
||||
return bpy.ops.curve.delete(type='VERT')
|
||||
|
||||
|
||||
class CURVE_OT_dissolve_verts(Operator):
|
||||
"""Dissolve selected Bezier points, preserving the curve shape"""
|
||||
|
||||
bl_idname = "curve.dissolve_verts_py"
|
||||
bl_label = "Dissolve Points"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
curve_objects = _curve_objects_in_edit_mode(context)
|
||||
if not curve_objects:
|
||||
cls.poll_message_set("Must be editing a curve.")
|
||||
return False
|
||||
if any(
|
||||
spline_has_dissolvable_bezier_point(spline) #
|
||||
for obj in curve_objects
|
||||
for spline in obj.data.splines
|
||||
):
|
||||
return True
|
||||
cls.poll_message_set("No selected Bezier points with neighbours on both sides.")
|
||||
return False
|
||||
|
||||
def execute(self, _context: Context):
|
||||
return bpy.ops.curve.dissolve_verts()
|
||||
|
||||
|
||||
class CURVE_OT_delete_segment(Operator):
|
||||
"""Delete segments between selected consecutive curve points"""
|
||||
|
||||
bl_idname = "curve.delete_segment"
|
||||
bl_label = "Delete Segments"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
curve_objects = _curve_objects_in_edit_mode(context)
|
||||
if not curve_objects:
|
||||
cls.poll_message_set("Must be editing a curve.")
|
||||
return False
|
||||
if any(
|
||||
spline_has_consecutive_selected(spline) #
|
||||
for obj in curve_objects
|
||||
for spline in obj.data.splines
|
||||
):
|
||||
return True
|
||||
cls.poll_message_set("No two consecutive points are selected.")
|
||||
return False
|
||||
|
||||
def execute(self, _context: Context):
|
||||
return bpy.ops.curve.delete(type='SEGMENT')
|
||||
|
||||
|
||||
def _curve_objects_in_edit_mode(context: Context) -> list[Object]:
|
||||
return [obj for obj in (context.objects_in_mode or []) if obj.type == 'CURVE' and obj.data]
|
||||
|
||||
|
||||
def spline_has_dissolvable_bezier_point(spline: Spline) -> bool:
|
||||
if spline.type != 'BEZIER':
|
||||
return False
|
||||
points = spline.bezier_points
|
||||
point_count = len(points)
|
||||
if spline.use_cyclic_u:
|
||||
return point_count > 2 and spline_has_any_selected(spline)
|
||||
return point_count >= 3 and any(point.select_control_point for point in points[1 : point_count - 1])
|
||||
|
||||
|
||||
def spline_has_any_selected(spline: Spline) -> bool:
|
||||
if spline.type == 'BEZIER':
|
||||
return any(point.select_control_point for point in spline.bezier_points)
|
||||
return any(point.select for point in spline.points)
|
||||
|
||||
|
||||
def spline_has_consecutive_selected(spline: Spline) -> bool:
|
||||
if spline.type == 'BEZIER':
|
||||
sel = [point.select_control_point for point in spline.bezier_points]
|
||||
else:
|
||||
sel = [point.select for point in spline.points]
|
||||
for prev, curr in zip(sel, sel[1:]):
|
||||
if prev and curr:
|
||||
return True
|
||||
return spline.use_cyclic_u and len(sel) >= 2 and sel[-1] and sel[0]
|
||||
|
||||
|
||||
registry = [
|
||||
CURVE_OT_delete_verts,
|
||||
CURVE_OT_dissolve_verts,
|
||||
CURVE_OT_delete_segment,
|
||||
PIE_MT_curve_delete,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
WM_OT_call_menu_pie_drag_only.register_drag_hotkey(
|
||||
keymap_name="Curve",
|
||||
pie_name=PIE_MT_curve_delete.bl_idname,
|
||||
hotkey_kwargs={'type': "X", 'value': "PRESS"},
|
||||
on_drag=True,
|
||||
)
|
||||
@@ -1,109 +1,79 @@
|
||||
# SPDX-FileCopyrightText: 2016-2024 Blender Foundation
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy, sys, json
|
||||
from bpy.types import Menu, Operator
|
||||
import json
|
||||
|
||||
import bpy
|
||||
from bpy.props import StringProperty
|
||||
from bpy.types import Context, Menu, Operator
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
|
||||
|
||||
AVAILABLE_PROPERTIES_EDITORS = []
|
||||
|
||||
|
||||
class PIE_MT_editor_switch(Menu):
|
||||
bl_idname = "PIE_MT_editor_switch"
|
||||
bl_label = "Editor Switch"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
# 4 - LEFT
|
||||
pie.operator(
|
||||
'wm.call_menu_pie', text="Video Editing...", icon="SEQUENCE"
|
||||
).name = "PIE_MT_editor_switch_video"
|
||||
pie.operator('wm.call_menu_pie', text="Video Editing...", icon="SEQUENCE").name = "PIE_MT_editor_switch_video"
|
||||
# 6 - RIGHT
|
||||
pie.operator('wm.call_menu_pie', text="Nodes...", icon="NODETREE").name = (
|
||||
"PIE_MT_editor_switch_nodes"
|
||||
)
|
||||
pie.operator('wm.call_menu_pie', text="Nodes...", icon="NODETREE").name = "PIE_MT_editor_switch_nodes"
|
||||
# 2 - BOTTOM
|
||||
pie.operator('wm.call_menu_pie', text="Data...", icon="PRESET").name = (
|
||||
"PIE_MT_editor_switch_data"
|
||||
)
|
||||
pie.operator('wm.call_menu_pie', text="Data...", icon="PRESET").name = "PIE_MT_editor_switch_data"
|
||||
# 8 - TOP
|
||||
pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="3D View", icon="VIEW3D"
|
||||
).ui_type = "VIEW_3D"
|
||||
pie.operator(WM_OT_set_area_type.bl_idname, text="3D View", icon="VIEW3D").ui_type = "VIEW_3D"
|
||||
# 7 - TOP - LEFT
|
||||
pie.operator(
|
||||
'wm.call_menu_pie', text="2D Editors...", icon="FILE_IMAGE"
|
||||
).name = "PIE_MT_editor_switch_image"
|
||||
pie.operator('wm.call_menu_pie', text="2D Editors...", icon="FILE_IMAGE").name = "PIE_MT_editor_switch_image"
|
||||
# 9 - TOP - RIGHT
|
||||
pie.operator('wm.call_menu_pie', text="Script...", icon="SCRIPT").name = (
|
||||
"PIE_MT_editor_switch_script"
|
||||
)
|
||||
pie.operator('wm.call_menu_pie', text="Script...", icon="SCRIPT").name = "PIE_MT_editor_switch_script"
|
||||
# 1 - BOTTOM - LEFT
|
||||
pie.separator()
|
||||
# 3 - BOTTOM - RIGHT
|
||||
pie.operator(
|
||||
'wm.call_menu_pie', text="Animation...", icon="ARMATURE_DATA"
|
||||
).name = "PIE_MT_editor_switch_anim"
|
||||
pie.operator('wm.call_menu_pie', text="Animation...", icon="ARMATURE_DATA").name = "PIE_MT_editor_switch_anim"
|
||||
|
||||
|
||||
class PIE_MT_editor_switch_video(Menu):
|
||||
bl_idname = "PIE_MT_editor_switch_video"
|
||||
bl_label = "Editor Switch: Video"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
|
||||
# 4 - LEFT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname,
|
||||
text="Sequencer & Preview",
|
||||
icon="SEQ_SPLITVIEW",
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Sequencer & Preview", icon="SEQ_SPLITVIEW")
|
||||
op.ui_type = "SEQUENCE_EDITOR"
|
||||
op.view_type = 'SEQUENCER_PREVIEW'
|
||||
# 6 - RIGHT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Movie Clip Editor", icon="SEQUENCE"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Movie Clip Editor", icon="SEQUENCE")
|
||||
op.ui_type = "CLIP_EDITOR"
|
||||
op.mode = 'TRACKING'
|
||||
op.view = 'CLIP'
|
||||
# 2 - BOTTOM
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Clip Mask Editor", icon="MOD_MASK"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Clip Mask Editor", icon="MOD_MASK")
|
||||
op.ui_type = "CLIP_EDITOR"
|
||||
op.mode = 'MASK'
|
||||
# 8 - TOP
|
||||
pie.separator()
|
||||
# 7 - TOP - LEFT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Preview", icon="SEQ_PREVIEW"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Preview", icon="SEQ_PREVIEW")
|
||||
op.ui_type = "SEQUENCE_EDITOR"
|
||||
op.view_type = 'PREVIEW'
|
||||
# 9 - TOP - RIGHT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Tracking: Dopesheet", icon="ACTION"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Tracking: Dopesheet", icon="ACTION")
|
||||
op.ui_type = "CLIP_EDITOR"
|
||||
op.mode = 'TRACKING'
|
||||
op.view = 'DOPESHEET'
|
||||
# 1 - BOTTOM - LEFT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Sequencer", icon="SEQ_SEQUENCER"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Sequencer", icon="SEQ_SEQUENCER")
|
||||
op.ui_type = "SEQUENCE_EDITOR"
|
||||
op.view_type = 'SEQUENCER'
|
||||
# 3 - BOTTOM - RIGHT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Tracking: Graph", icon="GRAPH"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Tracking: Graph", icon="GRAPH")
|
||||
op.ui_type = "CLIP_EDITOR"
|
||||
op.mode = 'TRACKING'
|
||||
op.view = 'GRAPH'
|
||||
@@ -113,42 +83,30 @@ class PIE_MT_editor_switch_nodes(Menu):
|
||||
bl_idname = "PIE_MT_editor_switch_nodes"
|
||||
bl_label = "Editor Switch: Nodes"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
|
||||
# 4 - LEFT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Compositing", icon="NODE_COMPOSITING"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Compositing", icon="NODE_COMPOSITING")
|
||||
op.ui_type = 'CompositorNodeTree'
|
||||
# 6 - RIGHT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Shader", icon="NODE_MATERIAL"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Shader", icon="NODE_MATERIAL")
|
||||
op.ui_type = 'ShaderNodeTree'
|
||||
op.shader_type = 'OBJECT'
|
||||
# 2 - BOTTOM
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Geometry", icon="GEOMETRY_NODES"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Geometry", icon="GEOMETRY_NODES")
|
||||
op.ui_type = 'GeometryNodeTree'
|
||||
# 8 - TOP
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Texture", icon="TEXTURE")
|
||||
op.ui_type = 'TextureNodeTree'
|
||||
# 7 - TOP - LEFT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Line Style", icon="LINE_DATA"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Line Style", icon="LINE_DATA")
|
||||
op.ui_type = 'ShaderNodeTree'
|
||||
op.shader_type = 'LINESTYLE'
|
||||
# 9 - TOP - RIGHT
|
||||
if get_custom_nodetree_types(context, layout):
|
||||
pie.menu(
|
||||
'PIE_MT_editor_switch_nodes_custom',
|
||||
text="Custom Nodes",
|
||||
icon='NODETREE',
|
||||
)
|
||||
if get_custom_nodetree_types(context):
|
||||
pie.menu('PIE_MT_editor_switch_nodes_custom', text="Custom Nodes", icon='NODETREE')
|
||||
else:
|
||||
pie.separator()
|
||||
# 1 - BOTTOM - LEFT
|
||||
@@ -163,253 +121,117 @@ class PIE_MT_editor_switch_nodes_custom(Menu):
|
||||
bl_idname = "PIE_MT_editor_switch_nodes_custom"
|
||||
bl_label = "Editor Switch: Custom Nodes"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
|
||||
for ui_type, label, icon in get_custom_nodetree_types(context, layout):
|
||||
layout.operator(
|
||||
WM_OT_set_area_type.bl_idname, text=label, icon=icon
|
||||
).ui_type = ui_type
|
||||
|
||||
|
||||
def get_custom_nodetree_types(context, layout):
|
||||
"""There are no rules In love, war, and Python scripting.
|
||||
The only way to get custom node tree types is this:
|
||||
- Try to set the UI type to some nonsense
|
||||
- The resulting error tells you the possible UI types
|
||||
- Catch and crunch the error string into a set
|
||||
- Subtract from this the set of built-in UI types
|
||||
- The result is a set of the custom node tree type names
|
||||
- We can get their class with bl_rna_get_subclass_py
|
||||
|
||||
"""
|
||||
builtin_types = set(
|
||||
[
|
||||
'VIEW_3D',
|
||||
'IMAGE_EDITOR',
|
||||
'UV',
|
||||
'CompositorNodeTree',
|
||||
'TextureNodeTree',
|
||||
'GeometryNodeTree',
|
||||
'ShaderNodeTree',
|
||||
'SEQUENCE_EDITOR',
|
||||
'CLIP_EDITOR',
|
||||
'DOPESHEET',
|
||||
'TIMELINE',
|
||||
'FCURVES',
|
||||
'DRIVERS',
|
||||
'NLA_EDITOR',
|
||||
'TEXT_EDITOR',
|
||||
'CONSOLE',
|
||||
'INFO',
|
||||
'OUTLINER',
|
||||
'PROPERTIES',
|
||||
'FILES',
|
||||
'ASSETS',
|
||||
'SPREADSHEET',
|
||||
'PREFERENCES',
|
||||
]
|
||||
)
|
||||
ret = []
|
||||
try:
|
||||
context.area.ui_type = "Nonsense."
|
||||
except Exception as exc:
|
||||
exc_type, exc_value, tb = sys.exc_info()
|
||||
if not exc_value:
|
||||
return []
|
||||
type_list_str = (
|
||||
str(exc_value)
|
||||
.split("not found in ")[-1]
|
||||
.replace("(", "[")
|
||||
.replace(")", "]")
|
||||
.replace("'", '"')
|
||||
)
|
||||
type_list = set(json.loads(type_list_str))
|
||||
custom_types = type_list - builtin_types
|
||||
|
||||
for type_name in custom_types:
|
||||
type_class = bpy.types.NodeTree.bl_rna_get_subclass_py(type_name)
|
||||
if not type_class:
|
||||
continue
|
||||
ret.append((type_name, type_class.bl_label, type_class.bl_icon))
|
||||
|
||||
return ret
|
||||
for ui_type, label, icon in get_custom_nodetree_types(context):
|
||||
layout.operator(WM_OT_set_area_type.bl_idname, text=label, icon=icon).ui_type = ui_type
|
||||
|
||||
|
||||
class PIE_MT_editor_switch_data(Menu):
|
||||
bl_idname = "PIE_MT_editor_switch_data"
|
||||
bl_label = "Editor Switch: Data"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
pie = self.layout.menu_pie()
|
||||
|
||||
# 4 - LEFT
|
||||
pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="File Browser", icon="FILEBROWSER"
|
||||
).ui_type = "FILES"
|
||||
pie.operator(WM_OT_set_area_type.bl_idname, text="File Browser", icon="FILEBROWSER").ui_type = "FILES"
|
||||
# 6 - RIGHT
|
||||
pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Preferences", icon="PREFERENCES"
|
||||
).ui_type = "PREFERENCES"
|
||||
pie.operator(WM_OT_set_area_type.bl_idname, text="Preferences", icon="PREFERENCES").ui_type = "PREFERENCES"
|
||||
# 2 - BOTTOM
|
||||
pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Outliner", icon="OUTLINER"
|
||||
).ui_type = "OUTLINER"
|
||||
pie.operator(WM_OT_set_area_type.bl_idname, text="Outliner", icon="OUTLINER").ui_type = "OUTLINER"
|
||||
# 8 - TOP
|
||||
pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Properties", icon="PROPERTIES"
|
||||
).ui_type = "PROPERTIES"
|
||||
pie.operator(WM_OT_set_area_type.bl_idname, text="Properties", icon="PROPERTIES").ui_type = "PROPERTIES"
|
||||
# 7 - TOP - LEFT
|
||||
pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Asset Browser", icon="ASSET_MANAGER"
|
||||
).ui_type = "ASSETS"
|
||||
pie.operator(WM_OT_set_area_type.bl_idname, text="Asset Browser", icon="ASSET_MANAGER").ui_type = "ASSETS"
|
||||
# 9 - TOP - RIGHT
|
||||
pie.menu(
|
||||
"PIE_MT_editor_switch_properties",
|
||||
text="Properties Types...",
|
||||
icon="PROPERTIES",
|
||||
)
|
||||
pie.menu("PIE_MT_editor_switch_properties", text="Properties Types...", icon="PROPERTIES")
|
||||
# 1 - BOTTOM - LEFT
|
||||
pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Spreadsheet", icon="SPREADSHEET"
|
||||
).ui_type = "SPREADSHEET"
|
||||
pie.operator(WM_OT_set_area_type.bl_idname, text="Spreadsheet", icon="SPREADSHEET").ui_type = "SPREADSHEET"
|
||||
# 3 - BOTTOM - RIGHT
|
||||
pie.menu(
|
||||
"PIE_MT_editor_switch_outliner", text="Outliner Types...", icon="OUTLINER"
|
||||
)
|
||||
pie.menu("PIE_MT_editor_switch_outliner", text="Outliner Types...", icon="OUTLINER")
|
||||
|
||||
|
||||
class PIE_MT_editor_switch_outliner(Menu):
|
||||
bl_idname = "PIE_MT_editor_switch_outliner"
|
||||
bl_label = "Editor Switch: Outliner"
|
||||
|
||||
def draw(self, context):
|
||||
pie = self.layout#.menu_pie()
|
||||
def draw(self, _context: Context):
|
||||
layout = self.layout
|
||||
|
||||
# 4 - LEFT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Scenes", icon="SCENE_DATA"
|
||||
)
|
||||
op = layout.operator(WM_OT_set_area_type.bl_idname, text="Scenes", icon="SCENE_DATA")
|
||||
op.ui_type = "OUTLINER"
|
||||
op.display_mode = 'SCENES'
|
||||
# 6 - RIGHT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="View Layer", icon="RENDERLAYERS"
|
||||
)
|
||||
op = layout.operator(WM_OT_set_area_type.bl_idname, text="View Layer", icon="RENDERLAYERS")
|
||||
op.ui_type = "OUTLINER"
|
||||
op.display_mode = 'VIEW_LAYER'
|
||||
# 2 - BOTTOM
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Video Sequencer", icon="SEQUENCE"
|
||||
)
|
||||
op = layout.operator(WM_OT_set_area_type.bl_idname, text="Video Sequencer", icon="SEQUENCE")
|
||||
op.ui_type = "OUTLINER"
|
||||
op.display_mode = 'SEQUENCE'
|
||||
# 8 - TOP
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Blender File", icon="FILE_BLEND"
|
||||
)
|
||||
op = layout.operator(WM_OT_set_area_type.bl_idname, text="Blender File", icon="FILE_BLEND")
|
||||
op.ui_type = "OUTLINER"
|
||||
op.display_mode = 'LIBRARIES'
|
||||
# 7 - TOP - LEFT
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Data API", icon="RNA")
|
||||
op = layout.operator(WM_OT_set_area_type.bl_idname, text="Data API", icon="RNA")
|
||||
op.ui_type = "OUTLINER"
|
||||
op.display_mode = 'DATA_API'
|
||||
# 9 - TOP - RIGHT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname,
|
||||
text="Library Overrides",
|
||||
icon="LIBRARY_DATA_OVERRIDE",
|
||||
)
|
||||
op = layout.operator(WM_OT_set_area_type.bl_idname, text="Library Overrides", icon="LIBRARY_DATA_OVERRIDE")
|
||||
op.ui_type = "OUTLINER"
|
||||
op.display_mode = 'LIBRARY_OVERRIDES'
|
||||
# 1 - BOTTOM - LEFT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Orphan Data", icon="ORPHAN_DATA"
|
||||
)
|
||||
op = layout.operator(WM_OT_set_area_type.bl_idname, text="Orphan Data", icon="ORPHAN_DATA")
|
||||
op.ui_type = "OUTLINER"
|
||||
op.display_mode = 'ORPHAN_DATA'
|
||||
# 3 - BOTTOM - RIGHT
|
||||
|
||||
|
||||
def get_available_properties_editors(context):
|
||||
"""Similar hack as get_custom_nodetree_types.
|
||||
Ideally we need an existing Properties editor, otherwise
|
||||
the list of available types is not up to date.
|
||||
Although even in that case, we still don't get an error,
|
||||
so this is fine.
|
||||
"""
|
||||
org_ui_type = context.area.ui_type
|
||||
try:
|
||||
space = None
|
||||
for area in context.screen.areas:
|
||||
if area.ui_type == 'PROPERTIES':
|
||||
space = area.spaces.active
|
||||
break
|
||||
if not space:
|
||||
context.area.ui_type = 'PROPERTIES'
|
||||
space = context.space_data
|
||||
space.context = "Nonsense."
|
||||
except Exception as exc:
|
||||
exc_type, exc_value, tb = sys.exc_info()
|
||||
if not exc_value:
|
||||
return []
|
||||
type_list_str = (
|
||||
str(exc_value)
|
||||
.split("not found in ")[-1]
|
||||
.replace("(", "[")
|
||||
.replace(")", "]")
|
||||
.replace("'", '"')
|
||||
)
|
||||
available_types = set(json.loads(type_list_str))
|
||||
context.area.ui_type = org_ui_type
|
||||
return available_types
|
||||
|
||||
|
||||
class PIE_MT_editor_switch_properties(Menu):
|
||||
bl_idname = "PIE_MT_editor_switch_properties"
|
||||
bl_label = "Editor Switch: Properties"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
|
||||
available_editors = get_available_properties_editors(context)
|
||||
|
||||
def safe_draw(pie, text, icon, context_name):
|
||||
def draw_entry(text, icon, context_name):
|
||||
if context_name in available_editors:
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text=text, icon=icon)
|
||||
op = layout.operator(WM_OT_set_area_type.bl_idname, text=text, icon=icon)
|
||||
op.ui_type = "PROPERTIES"
|
||||
op.context_name = context_name
|
||||
else:
|
||||
pie.separator()
|
||||
layout.separator()
|
||||
|
||||
safe_draw(layout, "Render", 'SCENE', 'RENDER')
|
||||
safe_draw(layout, "Output", 'OUTPUT', 'OUTPUT')
|
||||
safe_draw(layout, "View Layer", 'RENDERLAYERS', 'VIEW_LAYER')
|
||||
safe_draw(layout, "Scene", 'SCENE_DATA', 'SCENE')
|
||||
safe_draw(layout, "World", 'WORLD', 'WORLD')
|
||||
draw_entry("Render", 'SCENE', 'RENDER')
|
||||
draw_entry("Output", 'OUTPUT', 'OUTPUT')
|
||||
draw_entry("View Layer", 'RENDERLAYERS', 'VIEW_LAYER')
|
||||
draw_entry("Scene", 'SCENE_DATA', 'SCENE')
|
||||
draw_entry("World", 'WORLD', 'WORLD')
|
||||
layout.separator()
|
||||
safe_draw(layout, "Object", 'OBJECT_DATAMODE', 'OBJECT')
|
||||
safe_draw(layout, "Modifiers", 'MODIFIER', 'MODIFIER')
|
||||
safe_draw(layout, "Particles", 'PARTICLES', 'PARTICLES')
|
||||
safe_draw(layout, "Physics", 'PHYSICS', 'PHYSICS')
|
||||
safe_draw(layout, "Constraints", 'CONSTRAINT', 'CONSTRAINT')
|
||||
safe_draw(layout, "Object Data", 'OUTLINER_DATA_MESH', 'DATA')
|
||||
safe_draw(layout, "Material", 'MATERIAL', 'MATERIAL')
|
||||
draw_entry("Object", 'OBJECT_DATAMODE', 'OBJECT')
|
||||
draw_entry("Modifiers", 'MODIFIER', 'MODIFIER')
|
||||
draw_entry("Particles", 'PARTICLES', 'PARTICLES')
|
||||
draw_entry("Physics", 'PHYSICS', 'PHYSICS')
|
||||
draw_entry("Constraints", 'CONSTRAINT', 'CONSTRAINT')
|
||||
draw_entry("Object Data", 'OUTLINER_DATA_MESH', 'DATA')
|
||||
draw_entry("Material", 'MATERIAL', 'MATERIAL')
|
||||
layout.separator()
|
||||
safe_draw(layout, "Texture", 'TEXTURE', 'TEXTURE')
|
||||
draw_entry("Texture", 'TEXTURE', 'TEXTURE')
|
||||
|
||||
|
||||
class PIE_MT_editor_switch_image(Menu):
|
||||
bl_idname = "PIE_MT_editor_switch_image"
|
||||
bl_label = "Editor Switch: 2D Editors"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
pie = self.layout.menu_pie()
|
||||
|
||||
# 4 - LEFT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Image Viewer", icon="SEQ_PREVIEW"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Image Viewer", icon="SEQ_PREVIEW")
|
||||
op.ui_type = "IMAGE_EDITOR"
|
||||
op.ui_mode = "VIEW"
|
||||
# 6 - RIGHT
|
||||
@@ -420,17 +242,13 @@ class PIE_MT_editor_switch_image(Menu):
|
||||
# 8 - TOP
|
||||
pie.separator()
|
||||
# 7 - TOP - LEFT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Image Paint", icon="TPAINT_HLT"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Image Paint", icon="TPAINT_HLT")
|
||||
op.ui_type = "IMAGE_EDITOR"
|
||||
op.ui_mode = "PAINT"
|
||||
# 9 - TOP - RIGHT
|
||||
pie.separator()
|
||||
# 1 - BOTTOM - LEFT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Image Masking", icon="MOD_MASK"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Image Masking", icon="MOD_MASK")
|
||||
op.ui_type = "IMAGE_EDITOR"
|
||||
op.ui_mode = "PAINT"
|
||||
# 3 - BOTTOM - RIGHT
|
||||
@@ -440,23 +258,17 @@ class PIE_MT_editor_switch_script(Menu):
|
||||
bl_idname = "PIE_MT_editor_switch_script"
|
||||
bl_label = "Editor Switch: Script"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
pie = self.layout.menu_pie()
|
||||
|
||||
# 4 - LEFT
|
||||
pie.separator()
|
||||
# 6 - RIGHT
|
||||
pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Text Editor", icon="TEXT"
|
||||
).ui_type = "TEXT_EDITOR"
|
||||
pie.operator(WM_OT_set_area_type.bl_idname, text="Text Editor", icon="TEXT").ui_type = "TEXT_EDITOR"
|
||||
# 2 - BOTTOM
|
||||
pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Python Console", icon="CONSOLE"
|
||||
).ui_type = "CONSOLE"
|
||||
pie.operator(WM_OT_set_area_type.bl_idname, text="Python Console", icon="CONSOLE").ui_type = "CONSOLE"
|
||||
# 8 - TOP
|
||||
pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Info", icon="INFO"
|
||||
).ui_type = "INFO"
|
||||
pie.operator(WM_OT_set_area_type.bl_idname, text="Info", icon="INFO").ui_type = "INFO"
|
||||
# 7 - TOP - LEFT
|
||||
# 9 - TOP - RIGHT
|
||||
# 1 - BOTTOM - LEFT
|
||||
@@ -467,26 +279,20 @@ class PIE_MT_editor_switch_anim(Menu):
|
||||
bl_idname = "PIE_MT_editor_switch_anim"
|
||||
bl_label = "Editor Switch: Animation"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
|
||||
# 4 - LEFT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Grease Pencil", icon="GREASEPENCIL"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Grease Pencil", icon="GREASEPENCIL")
|
||||
op.ui_type = 'DOPESHEET'
|
||||
op.ui_mode = 'GPENCIL'
|
||||
# 6 - RIGHT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Dopesheet", icon="ACTION"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Dopesheet", icon="ACTION")
|
||||
op.ui_type = 'DOPESHEET'
|
||||
op.ui_mode = 'DOPESHEET'
|
||||
# 2 - BOTTOM
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Driver Editor", icon="DRIVER"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Driver Editor", icon="DRIVER")
|
||||
op.ui_type = 'DRIVERS'
|
||||
# 8 - TOP
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Timeline", icon="TIME")
|
||||
@@ -494,17 +300,13 @@ class PIE_MT_editor_switch_anim(Menu):
|
||||
# 7 - TOP - LEFT
|
||||
pie.separator()
|
||||
# 9 - TOP - RIGHT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Graph Editor", icon="GRAPH"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Graph Editor", icon="GRAPH")
|
||||
op.ui_type = 'FCURVES'
|
||||
# 1 - BOTTOM - LEFT
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="NLA Editor", icon="NLA")
|
||||
op.ui_type = 'NLA_EDITOR'
|
||||
# 3 - BOTTOM - RIGHT
|
||||
op = pie.operator(
|
||||
WM_OT_set_area_type.bl_idname, text="Action Editor", icon="OBJECT_DATAMODE"
|
||||
)
|
||||
op = pie.operator(WM_OT_set_area_type.bl_idname, text="Action Editor", icon="OBJECT_DATAMODE")
|
||||
op.ui_type = 'DOPESHEET'
|
||||
op.ui_mode = 'ACTION'
|
||||
|
||||
@@ -525,7 +327,7 @@ class WM_OT_set_area_type(Operator):
|
||||
mode: StringProperty(default="", options={'SKIP_SAVE'})
|
||||
view: StringProperty(default="", options={'SKIP_SAVE'})
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context):
|
||||
context.area.ui_type = self.ui_type
|
||||
if self.view_type:
|
||||
context.space_data.view_type = self.view_type
|
||||
@@ -541,10 +343,66 @@ class WM_OT_set_area_type(Operator):
|
||||
context.space_data.mode = self.mode
|
||||
if self.view:
|
||||
context.space_data.view = self.view
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def get_custom_nodetree_types(context: Context) -> list[tuple[str, str, str]]:
|
||||
"""There are no rules in love, war, and Python scripting.
|
||||
The only way to get custom node tree types is this:
|
||||
- Try to set the UI type to some nonsense
|
||||
- The resulting error tells you the possible UI types
|
||||
- Catch and crunch the error string into a set
|
||||
- Subtract from this the set of built-in UI types
|
||||
- The result is a set of the custom node tree type names
|
||||
- We can get their class with bl_rna_get_subclass_py
|
||||
"""
|
||||
builtin_types = {
|
||||
'VIEW_3D', 'IMAGE_EDITOR', 'UV',
|
||||
'CompositorNodeTree', 'TextureNodeTree', 'GeometryNodeTree', 'ShaderNodeTree',
|
||||
'SEQUENCE_EDITOR', 'CLIP_EDITOR',
|
||||
'DOPESHEET', 'TIMELINE', 'FCURVES', 'DRIVERS', 'NLA_EDITOR',
|
||||
'TEXT_EDITOR', 'CONSOLE', 'INFO',
|
||||
'OUTLINER', 'PROPERTIES', 'FILES', 'ASSETS', 'SPREADSHEET', 'PREFERENCES',
|
||||
}
|
||||
ret = []
|
||||
try:
|
||||
context.area.ui_type = "Nonsense."
|
||||
except Exception as exc:
|
||||
type_list_str = str(exc).split("not found in ")[-1].replace("(", "[").replace(")", "]").replace("'", '"')
|
||||
custom_types = set(json.loads(type_list_str)) - builtin_types
|
||||
for type_name in custom_types:
|
||||
type_class = bpy.types.NodeTree.bl_rna_get_subclass_py(type_name)
|
||||
if not type_class:
|
||||
continue
|
||||
ret.append((type_name, type_class.bl_label, type_class.bl_icon))
|
||||
return ret
|
||||
|
||||
|
||||
def get_available_properties_editors(context: Context) -> set[str]:
|
||||
"""Similar hack as get_custom_nodetree_types.
|
||||
Ideally we need an existing Properties editor, otherwise
|
||||
the list of available types is not up to date.
|
||||
Although even in that case, we still don't get an error,
|
||||
so this is fine.
|
||||
"""
|
||||
org_ui_type = context.area.ui_type
|
||||
try:
|
||||
space = None
|
||||
for area in context.screen.areas:
|
||||
if area.ui_type == 'PROPERTIES':
|
||||
space = area.spaces.active
|
||||
break
|
||||
if not space:
|
||||
context.area.ui_type = 'PROPERTIES'
|
||||
space = context.space_data
|
||||
space.context = "Nonsense."
|
||||
except Exception as exc:
|
||||
type_list_str = str(exc).split("not found in ")[-1].replace("(", "[").replace(")", "]").replace("'", '"')
|
||||
context.area.ui_type = org_ui_type
|
||||
return set(json.loads(type_list_str))
|
||||
return set()
|
||||
|
||||
|
||||
registry = [
|
||||
PIE_MT_editor_switch,
|
||||
PIE_MT_editor_switch_video,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# SPDX-FileCopyrightText: 2016-2024 Blender Foundation
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
|
||||
import bpy
|
||||
from bpy.types import Menu, Operator
|
||||
from bpy.props import BoolProperty
|
||||
from bpy.types import Context, Menu, Operator
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ class PIE_MT_file(Menu):
|
||||
bl_idname = "PIE_MT_file"
|
||||
bl_label = "File"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
# 4 - LEFT
|
||||
@@ -25,13 +25,9 @@ class PIE_MT_file(Menu):
|
||||
# 8 - TOP
|
||||
pie.menu("TOPBAR_MT_file_import", text="Import...", icon='IMPORT')
|
||||
# 7 - TOP - LEFT
|
||||
pie.operator('wm.call_menu_pie', text="Library...", icon='LINK_BLEND').name = (
|
||||
"PIE_MT_library"
|
||||
)
|
||||
pie.operator('wm.call_menu_pie', text="Library...", icon='LINK_BLEND').name = "PIE_MT_library"
|
||||
# 9 - TOP - RIGHT
|
||||
pie.operator(
|
||||
"wm.save_mainfile", text="Save Incremental", icon='FILE_TICK'
|
||||
).incremental = True
|
||||
pie.operator("wm.save_mainfile", text="Save Incremental", icon='FILE_TICK').incremental = True
|
||||
# 1 - BOTTOM - LEFT
|
||||
pie.menu("TOPBAR_MT_file_open_recent", icon='FILE_FOLDER')
|
||||
# 3 - BOTTOM - RIGHT
|
||||
@@ -42,7 +38,7 @@ class PIE_MT_library(Menu):
|
||||
bl_idname = "PIE_MT_library"
|
||||
bl_label = "Library"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
pie = self.layout.menu_pie()
|
||||
|
||||
# 4 - LEFT
|
||||
@@ -50,29 +46,17 @@ class PIE_MT_library(Menu):
|
||||
# 6 - RIGHT
|
||||
pie.operator("wm.append", text="Append", icon='APPEND_BLEND')
|
||||
# 2 - BOTTOM
|
||||
pie.operator(
|
||||
"object.make_local", text="Make All Local", icon='FILE_BLEND'
|
||||
).type = 'ALL'
|
||||
pie.operator("object.make_local", text="Make All Local", icon='FILE_BLEND').type = 'ALL'
|
||||
# 8 - TOP
|
||||
pie.prop(bpy.data, 'use_autopack', text="Auto-pack")
|
||||
# 7 - TOP - LEFT
|
||||
pie.operator(
|
||||
"file.pack_res_and_lib", text="Pack All Local", icon='PACKAGE'
|
||||
).pack = True
|
||||
pie.operator("file.pack_res_and_lib", text="Pack All Local", icon='PACKAGE').pack = True
|
||||
# 9 - TOP - RIGHT
|
||||
pie.operator(
|
||||
"file.pack_res_and_lib", text="Unpack All Local", icon='UGLYPACKAGE'
|
||||
).pack = False
|
||||
pie.operator("file.pack_res_and_lib", text="Unpack All Local", icon='UGLYPACKAGE').pack = False
|
||||
# 1 - BOTTOM - LEFT
|
||||
pie.operator(
|
||||
"file.make_paths_relative", icon='FILE', text="Make Paths Relative"
|
||||
)
|
||||
pie.operator("file.make_paths_relative", icon='FILE', text="Make Paths Relative")
|
||||
# 3 - BOTTOM - RIGHT
|
||||
pie.operator(
|
||||
"file.make_paths_absolute",
|
||||
icon='LIBRARY_DATA_BROKEN',
|
||||
text="Make Paths Absolute",
|
||||
)
|
||||
pie.operator("file.make_paths_absolute", icon='LIBRARY_DATA_BROKEN', text="Make Paths Absolute")
|
||||
|
||||
|
||||
class WM_OT_pack_res_and_lib(Operator):
|
||||
@@ -84,17 +68,21 @@ class WM_OT_pack_res_and_lib(Operator):
|
||||
|
||||
pack: BoolProperty()
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, _context: Context):
|
||||
if self.pack:
|
||||
bpy.ops.file.pack_libraries()
|
||||
bpy.ops.file.pack_all()
|
||||
else:
|
||||
bpy.ops.file.unpack_libraries()
|
||||
bpy.ops.file.unpack_all()
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def draw_revert(self, _context: Context):
|
||||
self.layout.operator('wm.revert_mainfile', icon='LOOP_BACK')
|
||||
self.layout.separator()
|
||||
|
||||
|
||||
registry = [
|
||||
PIE_MT_file,
|
||||
PIE_MT_library,
|
||||
@@ -102,11 +90,6 @@ registry = [
|
||||
]
|
||||
|
||||
|
||||
def draw_revert(self, context):
|
||||
self.layout.operator('wm.revert_mainfile', icon='LOOP_BACK')
|
||||
self.layout.separator()
|
||||
|
||||
|
||||
def register():
|
||||
WM_OT_call_menu_pie_drag_only.register_drag_hotkey(
|
||||
keymap_name="Window",
|
||||
@@ -115,7 +98,6 @@ def register():
|
||||
default_fallback_op='wm.save_mainfile',
|
||||
on_drag=True,
|
||||
)
|
||||
|
||||
bpy.types.TOPBAR_MT_file_recover.prepend(draw_revert)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from bpy.types import Context, Menu
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
|
||||
|
||||
class PIE_MT_keyframe_type(Menu):
|
||||
bl_idname = "PIE_MT_keyframe_type"
|
||||
bl_label = "Keyframe Type"
|
||||
|
||||
def draw(self, context: Context):
|
||||
pie = self.layout.menu_pie()
|
||||
|
||||
# 4 - LEFT
|
||||
pie.operator('action.keyframe_type', text="Keyframe", icon='KEYTYPE_KEYFRAME_VEC').type = 'KEYFRAME'
|
||||
# 6 - RIGHT
|
||||
pie.operator('action.keyframe_type', text="Extreme", icon='KEYTYPE_EXTREME_VEC').type = 'EXTREME'
|
||||
# 2 - BOTTOM
|
||||
pie.operator('action.keyframe_type', text="Breakdown", icon='KEYTYPE_BREAKDOWN_VEC').type = 'BREAKDOWN'
|
||||
# 8 - TOP
|
||||
pie.operator('action.keyframe_type', text="Moving Hold", icon='KEYTYPE_MOVING_HOLD_VEC').type = 'MOVING_HOLD'
|
||||
# 7 - TOP - LEFT
|
||||
pie.separator()
|
||||
# 9 - TOP - RIGHT
|
||||
pie.separator()
|
||||
# 1 - BOTTOM - LEFT
|
||||
pie.operator('action.keyframe_type', text="Generated", icon='KEYTYPE_GENERATED_VEC').type = 'GENERATED'
|
||||
# 3 - BOTTOM - RIGHT
|
||||
pie.operator('action.keyframe_type', text="Jitter", icon='KEYTYPE_JITTER_VEC').type = 'JITTER'
|
||||
|
||||
|
||||
class PIE_MT_handle_type(Menu):
|
||||
bl_idname = "PIE_MT_handle_type"
|
||||
bl_label = "Handle Type"
|
||||
|
||||
def draw(self, context: Context):
|
||||
pie = self.layout.menu_pie()
|
||||
|
||||
base = 'graph' if context.area.type == 'GRAPH_EDITOR' else 'action'
|
||||
|
||||
# 4 - LEFT
|
||||
pie.operator(f'{base}.handle_type', text="Free", icon='HANDLE_FREE').type = 'FREE'
|
||||
# 6 - RIGHT
|
||||
pie.operator(f'{base}.handle_type', text="Aligned", icon='HANDLE_ALIGNED').type = 'ALIGNED'
|
||||
# 2 - BOTTOM
|
||||
pie.operator(f'{base}.handle_type', text="Vector", icon='HANDLE_VECTOR').type = 'VECTOR'
|
||||
# 8 - TOP
|
||||
pie.operator(f'{base}.handle_type', text="Automatic", icon='HANDLE_AUTO').type = 'AUTO'
|
||||
# 7 - TOP - LEFT
|
||||
pie.operator(f'{base}.handle_type', text="Auto Clamped", icon='HANDLE_AUTOCLAMPED').type = 'AUTO_CLAMPED'
|
||||
|
||||
|
||||
class PIE_MT_interpolation_mode(Menu):
|
||||
bl_idname = "PIE_MT_interpolation_mode"
|
||||
bl_label = "Interpolation Mode"
|
||||
|
||||
def draw(self, context: Context):
|
||||
pie = self.layout.menu_pie()
|
||||
|
||||
base = 'graph' if context.area.type == 'GRAPH_EDITOR' else 'action'
|
||||
|
||||
# 4 - LEFT
|
||||
pie.operator(f'{base}.interpolation_type', text="Constant", icon='IPO_CONSTANT').type = 'CONSTANT'
|
||||
# 6 - RIGHT
|
||||
pie.operator(f'{base}.interpolation_type', text="Bezier", icon='IPO_BEZIER').type = 'BEZIER'
|
||||
# 2 - BOTTOM
|
||||
pie.separator()
|
||||
# 8 - TOP
|
||||
pie.operator(f'{base}.interpolation_type', text="Linear", icon='IPO_LINEAR').type = 'LINEAR'
|
||||
# 7 - TOP - LEFT
|
||||
pie.separator()
|
||||
# 9 - TOP - RIGHT
|
||||
pie.menu("PIE_MT_interpolation_dynamic", text="Dynamic...", icon='IPO_BACK')
|
||||
# 1 - BOTTOM - LEFT
|
||||
pie.separator()
|
||||
# 3 - BOTTOM - RIGHT
|
||||
pie.menu("PIE_MT_interpolation_easing", text="Easing...", icon='IPO_SINE')
|
||||
|
||||
|
||||
class PIE_MT_interpolation_easing(Menu):
|
||||
bl_idname = "PIE_MT_interpolation_easing"
|
||||
bl_label = "Easing Interpolation"
|
||||
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
base = 'graph' if context.area.type == 'GRAPH_EDITOR' else 'action'
|
||||
layout.operator(f'{base}.interpolation_type', text="Sinusoidal", icon='IPO_SINE').type = 'SINE'
|
||||
layout.operator(f'{base}.interpolation_type', text="Quadratic", icon='IPO_QUAD').type = 'QUAD'
|
||||
layout.operator(f'{base}.interpolation_type', text="Cubic", icon='IPO_CUBIC').type = 'CUBIC'
|
||||
layout.operator(f'{base}.interpolation_type', text="Quartic", icon='IPO_QUART').type = 'QUART'
|
||||
layout.operator(f'{base}.interpolation_type', text="Quintic", icon='IPO_QUINT').type = 'QUINT'
|
||||
layout.operator(f'{base}.interpolation_type', text="Exponential", icon='IPO_EXPO').type = 'EXPO'
|
||||
layout.operator(f'{base}.interpolation_type', text="Circular", icon='IPO_CIRC').type = 'CIRC'
|
||||
|
||||
|
||||
class PIE_MT_interpolation_dynamic(Menu):
|
||||
bl_idname = "PIE_MT_interpolation_dynamic"
|
||||
bl_label = "Dynamic Effects"
|
||||
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
|
||||
base = 'graph' if context.area.type == 'GRAPH_EDITOR' else 'action'
|
||||
|
||||
layout.operator(f'{base}.interpolation_type', text="Back", icon='IPO_BACK').type = 'BACK'
|
||||
layout.operator(f'{base}.interpolation_type', text="Bounce", icon='IPO_BOUNCE').type = 'BOUNCE'
|
||||
layout.operator(f'{base}.interpolation_type', text="Elastic", icon='IPO_ELASTIC').type = 'ELASTIC'
|
||||
|
||||
|
||||
class PIE_MT_easing_mode(Menu):
|
||||
bl_idname = "PIE_MT_easing_mode"
|
||||
bl_label = "Easing Mode"
|
||||
|
||||
def draw(self, context: Context):
|
||||
pie = self.layout.menu_pie()
|
||||
|
||||
base = 'graph' if context.area.type == 'GRAPH_EDITOR' else 'action'
|
||||
|
||||
# 4 - LEFT
|
||||
pie.operator(f'{base}.easing_type', text="Ease In", icon='IPO_EASE_IN').type = 'EASE_IN'
|
||||
# 6 - RIGHT
|
||||
pie.operator(f'{base}.easing_type', text="Ease Out", icon='IPO_EASE_OUT').type = 'EASE_OUT'
|
||||
# 2 - BOTTOM
|
||||
pie.operator(f'{base}.easing_type', text="Ease In and Out", icon='IPO_EASE_IN_OUT').type = 'EASE_IN_OUT'
|
||||
# 8 - TOP
|
||||
pie.operator(f'{base}.easing_type', text="Automatic", icon='AUTO').type = 'AUTO'
|
||||
|
||||
|
||||
class PIE_MT_extrapolation_mode(Menu):
|
||||
bl_idname = "PIE_MT_extrapolation_mode"
|
||||
bl_label = "Extrapolation Mode"
|
||||
|
||||
def draw(self, _context: Context):
|
||||
pie = self.layout.menu_pie()
|
||||
|
||||
# 4 - LEFT
|
||||
pie.operator('graph.extrapolation_type', text="Constant", icon='IPO_CONSTANT').type = 'CONSTANT'
|
||||
# 6 - RIGHT
|
||||
pie.operator('graph.extrapolation_type', text="Linear", icon='IPO_LINEAR').type = 'LINEAR'
|
||||
# 2 - BOTTOM
|
||||
pie.operator('graph.extrapolation_type', text="Clear Cyclic", icon='X').type = 'CLEAR_CYCLIC'
|
||||
# 8 - TOP
|
||||
pie.operator('graph.extrapolation_type', text="Make Cyclic", icon='FILE_REFRESH').type = 'MAKE_CYCLIC'
|
||||
|
||||
|
||||
registry = [
|
||||
PIE_MT_keyframe_type,
|
||||
PIE_MT_handle_type,
|
||||
PIE_MT_interpolation_mode,
|
||||
PIE_MT_interpolation_easing,
|
||||
PIE_MT_interpolation_dynamic,
|
||||
PIE_MT_easing_mode,
|
||||
PIE_MT_extrapolation_mode,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
WM_OT_call_menu_pie_drag_only.register_drag_hotkey(
|
||||
keymap_name='Dopesheet',
|
||||
pie_name=PIE_MT_keyframe_type.bl_idname,
|
||||
hotkey_kwargs={'type': "R", 'value': "PRESS"},
|
||||
on_drag=False,
|
||||
)
|
||||
for keymap in ('Dopesheet', 'Graph Editor'):
|
||||
WM_OT_call_menu_pie_drag_only.register_drag_hotkey(
|
||||
keymap_name=keymap,
|
||||
pie_name=PIE_MT_handle_type.bl_idname,
|
||||
hotkey_kwargs={'type': "V", 'value': "PRESS"},
|
||||
on_drag=False,
|
||||
)
|
||||
WM_OT_call_menu_pie_drag_only.register_drag_hotkey(
|
||||
keymap_name=keymap,
|
||||
pie_name=PIE_MT_interpolation_mode.bl_idname,
|
||||
hotkey_kwargs={'type': "T", 'value': "PRESS"},
|
||||
on_drag=False,
|
||||
)
|
||||
WM_OT_call_menu_pie_drag_only.register_drag_hotkey(
|
||||
keymap_name=keymap,
|
||||
pie_name=PIE_MT_easing_mode.bl_idname,
|
||||
hotkey_kwargs={'type': "E", 'value': "PRESS", 'ctrl': True},
|
||||
on_drag=False,
|
||||
)
|
||||
WM_OT_call_menu_pie_drag_only.register_drag_hotkey(
|
||||
keymap_name=keymap,
|
||||
pie_name=PIE_MT_extrapolation_mode.bl_idname,
|
||||
hotkey_kwargs={'type': "E", 'value': "PRESS", 'shift': True},
|
||||
on_drag=False,
|
||||
)
|
||||
@@ -1,9 +1,9 @@
|
||||
# SPDX-FileCopyrightText: 2016-2024 Blender Foundation
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from bpy.types import Menu, Operator
|
||||
from bpy.props import EnumProperty, BoolProperty
|
||||
from bpy.props import BoolProperty, EnumProperty
|
||||
from bpy.types import Context, Event, Menu, Operator
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
|
||||
@@ -12,10 +12,9 @@ class PIE_MT_manipulator(Menu):
|
||||
bl_idname = "PIE_MT_manipulator"
|
||||
bl_label = "Manipulator"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
|
||||
space = context.space_data
|
||||
|
||||
# 4 - LEFT
|
||||
@@ -27,25 +26,31 @@ class PIE_MT_manipulator(Menu):
|
||||
'view3d.set_manipulator', text="Scale", icon='CON_SIZELIKE', depress=space.show_gizmo_object_scale
|
||||
).manipulator = 'SCALE'
|
||||
# 2 - BOTTOM
|
||||
pie.operator(
|
||||
'view3d.set_manipulator', text="None", icon='X'
|
||||
).manipulator='NONE'
|
||||
pie.operator('view3d.set_manipulator', text="None", icon='X').manipulator = 'NONE'
|
||||
# 8 - TOP
|
||||
pie.operator(
|
||||
'view3d.set_manipulator', text="Location", icon='CON_LOCLIKE', depress=space.show_gizmo_object_translate
|
||||
).manipulator = 'LOC'
|
||||
|
||||
# 7 - TOP-LEFT
|
||||
# 7 - TOP - LEFT
|
||||
pie.operator(
|
||||
'view3d.set_manipulator', text="Loc/Rot", icon='CON_LOCLIKE', depress=space.show_gizmo_object_translate and space.show_gizmo_object_rotate
|
||||
).manipulator='LOCROT'
|
||||
# 9 - TOP-RIGHT
|
||||
'view3d.set_manipulator',
|
||||
text="Loc/Rot",
|
||||
icon='CON_LOCLIKE',
|
||||
depress=space.show_gizmo_object_translate and space.show_gizmo_object_rotate,
|
||||
).manipulator = 'LOCROT'
|
||||
# 9 - TOP - RIGHT
|
||||
pie.operator(
|
||||
'view3d.set_manipulator', text="Loc/Scale", icon='CON_SIZELIKE', depress=space.show_gizmo_object_translate and space.show_gizmo_object_scale
|
||||
).manipulator='LOCSCALE'
|
||||
# 1 - BOT-LEFT
|
||||
'view3d.set_manipulator',
|
||||
text="Loc/Scale",
|
||||
icon='CON_SIZELIKE',
|
||||
depress=space.show_gizmo_object_translate and space.show_gizmo_object_scale,
|
||||
).manipulator = 'LOCSCALE'
|
||||
# 1 - BOTTOM - LEFT
|
||||
pie.operator(
|
||||
'view3d.set_manipulator', text="Loc/Rot/Scale", icon='CON_LOCLIKE', depress=space.show_gizmo_object_translate and space.show_gizmo_object_rotate and space.show_gizmo_object_scale
|
||||
'view3d.set_manipulator',
|
||||
text="Loc/Rot/Scale",
|
||||
icon='CON_LOCLIKE',
|
||||
depress=space.show_gizmo_object_translate and space.show_gizmo_object_rotate and space.show_gizmo_object_scale,
|
||||
).manipulator = 'LOCROTSCALE'
|
||||
|
||||
|
||||
@@ -75,19 +80,19 @@ class VIEW3D_OT_set_manipulator(Operator):
|
||||
name="Toggle",
|
||||
description="Hold Shift to toggle the selected manipulator rather than setting it as the only active one",
|
||||
default=False,
|
||||
options={'SKIP_SAVE'}
|
||||
options={'SKIP_SAVE'},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
space = context.space_data
|
||||
return space and space.type == 'VIEW_3D'
|
||||
|
||||
def invoke(self, context, event):
|
||||
def invoke(self, context: Context, event: Event):
|
||||
self.toggle = event.shift
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context):
|
||||
space = context.space_data
|
||||
|
||||
if self.manipulator != 'NONE':
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# SPDX-FileCopyrightText: 2016-2024 Blender Foundation
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from bpy.types import Menu
|
||||
from bpy.types import Context, Menu
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
|
||||
@@ -11,39 +11,29 @@ class PIE_MT_mesh_delete(Menu):
|
||||
bl_idname = "PIE_MT_mesh_delete"
|
||||
bl_label = "Mesh Delete"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
# 4 - LEFT
|
||||
box = pie.split().column()
|
||||
box.operator(
|
||||
"mesh.dissolve_limited", text="Limited Dissolve", icon='STICKY_UVS_LOC'
|
||||
)
|
||||
box.operator("mesh.dissolve_limited", text="Limited Dissolve", icon='STICKY_UVS_LOC')
|
||||
box.operator("mesh.delete_edgeloop", text="Delete Edge Loops", icon='NONE')
|
||||
box.operator("mesh.edge_collapse", text="Edge Collapse", icon='UV_EDGESEL')
|
||||
# 6 - RIGHT
|
||||
box = pie.split().column()
|
||||
box.operator("mesh.remove_doubles", text="Merge By Distance", icon='NONE')
|
||||
box.operator("mesh.delete", text="Only Edge & Faces", icon='NONE').type = (
|
||||
'EDGE_FACE'
|
||||
)
|
||||
box.operator("mesh.delete", text="Only Faces", icon='UV_FACESEL').type = (
|
||||
'ONLY_FACE'
|
||||
)
|
||||
box.operator("mesh.delete", text="Only Edge & Faces", icon='NONE').type = 'EDGE_FACE'
|
||||
box.operator("mesh.delete", text="Only Faces", icon='UV_FACESEL').type = 'ONLY_FACE'
|
||||
# 2 - BOTTOM
|
||||
pie.operator("mesh.dissolve_edges", text="Dissolve Edges", icon='SNAP_EDGE')
|
||||
# 8 - TOP
|
||||
pie.operator("mesh.delete", text="Delete Edges", icon='EDGESEL').type = 'EDGE'
|
||||
# 7 - TOP - LEFT
|
||||
pie.operator("mesh.delete", text="Delete Vertices", icon='VERTEXSEL').type = (
|
||||
'VERT'
|
||||
)
|
||||
pie.operator("mesh.delete", text="Delete Vertices", icon='VERTEXSEL').type = 'VERT'
|
||||
# 9 - TOP - RIGHT
|
||||
pie.operator("mesh.delete", text="Delete Faces", icon='FACESEL').type = 'FACE'
|
||||
# 1 - BOTTOM - LEFT
|
||||
pie.operator(
|
||||
"mesh.dissolve_verts", text="Dissolve Vertices", icon='SNAP_VERTEX'
|
||||
)
|
||||
pie.operator("mesh.dissolve_verts", text="Dissolve Vertices", icon='SNAP_VERTEX')
|
||||
# 3 - BOTTOM - RIGHT
|
||||
pie.operator("mesh.dissolve_faces", text="Dissolve Faces", icon='SNAP_FACE')
|
||||
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bmesh
|
||||
import bpy
|
||||
from bmesh.types import BMEdge
|
||||
from bpy.types import Context, Menu, Object, Operator
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
|
||||
# Above this edge count the state check is skipped to avoid poll lag.
|
||||
_EDGE_POLL_LIMIT = 100_000
|
||||
|
||||
|
||||
class PIE_MT_mesh_edge(Menu):
|
||||
bl_idname = "PIE_MT_mesh_edge"
|
||||
bl_label = "Mesh Edge"
|
||||
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
# 4 - LEFT
|
||||
pie.operator(MESH_OT_pie_clear_seam.bl_idname, text="Clear Seam", icon='EDGE_SEAM')
|
||||
# 6 - RIGHT
|
||||
pie.operator(MESH_OT_pie_mark_seam.bl_idname, text="Mark Seam", icon='EDGE_SEAM')
|
||||
# 2 - BOTTOM
|
||||
pie.operator(MESH_OT_pie_clear_sharp.bl_idname, text="Clear Sharp", icon='EDGE_SHARP')
|
||||
# 8 - TOP
|
||||
pie.operator(MESH_OT_pie_mark_sharp.bl_idname, text="Mark Sharp", icon='EDGE_SHARP')
|
||||
# 7 - TOP - LEFT
|
||||
pie.operator(MESH_OT_pie_clear_freestyle_edge.bl_idname, text="Clear Freestyle Edge", icon='EDGESEL')
|
||||
# 9 - TOP - RIGHT
|
||||
if context.tool_settings.mesh_select_mode[0]:
|
||||
pie.operator("transform.vert_crease", text="Vertex Crease", icon='VERTEX_CREASE')
|
||||
else:
|
||||
pie.operator("transform.edge_crease", text="Edge Crease", icon='EDGE_CREASE')
|
||||
# 1 - BOTTOM - LEFT
|
||||
pie.operator(MESH_OT_pie_mark_freestyle_edge.bl_idname, text="Mark Freestyle Edge", icon='EDGESEL')
|
||||
# 3 - BOTTOM - RIGHT
|
||||
pie.operator("transform.edge_bevelweight", text="Edge Bevel Weight", icon='EDGE_BEVEL')
|
||||
|
||||
|
||||
class _EdgeMarkBase(Operator):
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
edge_data_type: str
|
||||
do_clear: bool
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
obj = context.active_object
|
||||
if not (obj and obj.type == 'MESH' and obj.mode == 'EDIT'):
|
||||
cls.poll_message_set("Must be in Mesh Edit mode")
|
||||
return False
|
||||
if len(obj.data.edges) > _EDGE_POLL_LIMIT:
|
||||
return True
|
||||
any_have, all_have = selected_edge_data_state(obj, cls.edge_data_type)
|
||||
if not any_have and all_have:
|
||||
cls.poll_message_set("No edges selected")
|
||||
return False
|
||||
if cls.do_clear:
|
||||
if not any_have:
|
||||
cls.poll_message_set("No selected edges are marked")
|
||||
return False
|
||||
else:
|
||||
if all_have:
|
||||
cls.poll_message_set("All selected edges are already marked")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, _context: Context):
|
||||
if self.edge_data_type == 'SEAM':
|
||||
bpy.ops.mesh.mark_seam(clear=self.do_clear)
|
||||
elif self.edge_data_type == 'SHARP':
|
||||
bpy.ops.mesh.mark_sharp(clear=self.do_clear)
|
||||
elif self.edge_data_type == 'FREESTYLE':
|
||||
bpy.ops.mesh.mark_freestyle_edge(clear=self.do_clear)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class MESH_OT_pie_mark_seam(_EdgeMarkBase):
|
||||
bl_idname = "mesh.pie_mark_seam"
|
||||
bl_label = "Mark Seam"
|
||||
bl_description = "Mark selected edges as seams for UV unwrapping"
|
||||
edge_data_type = 'SEAM'
|
||||
do_clear = False
|
||||
|
||||
|
||||
class MESH_OT_pie_clear_seam(_EdgeMarkBase):
|
||||
bl_idname = "mesh.pie_clear_seam"
|
||||
bl_label = "Clear Seam"
|
||||
bl_description = "Remove seam marks from selected edges"
|
||||
edge_data_type = 'SEAM'
|
||||
do_clear = True
|
||||
|
||||
|
||||
class MESH_OT_pie_mark_sharp(_EdgeMarkBase):
|
||||
bl_idname = "mesh.pie_mark_sharp"
|
||||
bl_label = "Mark Sharp"
|
||||
bl_description = "Mark selected edges as sharp"
|
||||
edge_data_type = 'SHARP'
|
||||
do_clear = False
|
||||
|
||||
|
||||
class MESH_OT_pie_clear_sharp(_EdgeMarkBase):
|
||||
bl_idname = "mesh.pie_clear_sharp"
|
||||
bl_label = "Clear Sharp"
|
||||
bl_description = "Remove sharp marks from selected edges"
|
||||
edge_data_type = 'SHARP'
|
||||
do_clear = True
|
||||
|
||||
|
||||
class MESH_OT_pie_mark_freestyle_edge(_EdgeMarkBase):
|
||||
bl_idname = "mesh.pie_mark_freestyle_edge"
|
||||
bl_label = "Mark Freestyle Edge"
|
||||
bl_description = "Mark selected edges as Freestyle edges"
|
||||
edge_data_type = 'FREESTYLE'
|
||||
do_clear = False
|
||||
|
||||
|
||||
class MESH_OT_pie_clear_freestyle_edge(_EdgeMarkBase):
|
||||
bl_idname = "mesh.pie_clear_freestyle_edge"
|
||||
bl_label = "Clear Freestyle Edge"
|
||||
bl_description = "Remove Freestyle marks from selected edges"
|
||||
edge_data_type = 'FREESTYLE'
|
||||
do_clear = True
|
||||
|
||||
|
||||
def _edge_has_data(edge: BMEdge, edge_data_type: str, freestyle_layer) -> bool:
|
||||
if edge_data_type == 'SEAM':
|
||||
return edge.seam
|
||||
if edge_data_type == 'SHARP':
|
||||
return not edge.smooth
|
||||
if edge_data_type == 'FREESTYLE':
|
||||
return edge[freestyle_layer] if freestyle_layer else False
|
||||
return False
|
||||
|
||||
|
||||
def selected_edge_data_state(obj: Object, edge_data_type: str) -> tuple[bool, bool]:
|
||||
"""Return (any_have, all_have) for selected edges of the given data type."""
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
freestyle_layer = bm.edges.layers.bool.get("freestyle_edge") if edge_data_type == 'FREESTYLE' else None
|
||||
|
||||
any_have = False
|
||||
all_have = True
|
||||
found = False
|
||||
for edge in bm.edges:
|
||||
if not edge.select:
|
||||
continue
|
||||
found = True
|
||||
has = _edge_has_data(edge, edge_data_type, freestyle_layer)
|
||||
any_have |= has
|
||||
all_have &= has
|
||||
if any_have and not all_have:
|
||||
break # Both states seen, result can't change.
|
||||
|
||||
if not found:
|
||||
return False, False
|
||||
return any_have, all_have
|
||||
|
||||
|
||||
registry = [
|
||||
PIE_MT_mesh_edge,
|
||||
MESH_OT_pie_mark_seam,
|
||||
MESH_OT_pie_clear_seam,
|
||||
MESH_OT_pie_mark_sharp,
|
||||
MESH_OT_pie_clear_sharp,
|
||||
MESH_OT_pie_mark_freestyle_edge,
|
||||
MESH_OT_pie_clear_freestyle_edge,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
WM_OT_call_menu_pie_drag_only.register_drag_hotkey(
|
||||
keymap_name="Mesh",
|
||||
pie_name=PIE_MT_mesh_edge.bl_idname,
|
||||
hotkey_kwargs={'type': "E", 'value': "PRESS", 'shift': True},
|
||||
on_drag=True,
|
||||
)
|
||||
@@ -1,10 +1,10 @@
|
||||
# SPDX-FileCopyrightText: 2016-2024 Blender Foundation
|
||||
# SPDX-FileCopyrightText: 2016-2026 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import Menu, Operator
|
||||
from bpy.props import EnumProperty
|
||||
from bpy.types import Context, Menu, Operator
|
||||
|
||||
from .op_pie_wrappers import WM_OT_call_menu_pie_drag_only
|
||||
|
||||
@@ -13,7 +13,7 @@ class PIE_MT_mesh_flatten(Menu):
|
||||
bl_idname = "PIE_MT_mesh_flatten"
|
||||
bl_label = "Mesh Flatten"
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, _context: Context):
|
||||
layout = self.layout
|
||||
pie = layout.menu_pie()
|
||||
# 4 - LEFT
|
||||
@@ -65,11 +65,11 @@ class TRANSFORM_OT_flatten_to_center(Operator):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context):
|
||||
obj = context.active_object
|
||||
return obj and obj.type == "MESH"
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, _context: Context):
|
||||
values = {
|
||||
'X': [(0, 1, 1), (True, False, False)],
|
||||
'Y': [(1, 0, 1), (False, True, False)],
|
||||
@@ -106,11 +106,11 @@ class TRANSFORM_OT_flatten_to_object_origin(Operator):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context):
|
||||
obj = context.active_object
|
||||
return obj and obj.type == "MESH"
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, _context: Context):
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
index = "XYZ".find(self.axis)
|
||||
for vert in bpy.context.object.data.vertices:
|
||||
@@ -149,11 +149,11 @@ class TRANSFORM_OT_flatten_to_selection_bounding_box(Operator):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context):
|
||||
obj = context.active_object
|
||||
return obj and obj.type == "MESH"
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, _context: Context):
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
count = 0
|
||||
axis_idx = "XYZ".find(self.axis)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user