object properties

This commit is contained in:
Nicolas Predella 2026-05-17 20:19:14 +03:00
parent 5050a5fa5f
commit 8ec3ae38fa
8 changed files with 229 additions and 55 deletions

View file

@ -2,6 +2,8 @@ from __future__ import annotations
import bpy import bpy
from . import registry from . import registry
from .core.nodeslicer import sockets, nodes, tree # sockets and nodes need also to be imported in order for them to be autoregistered from .core.nodeslicer import sockets, nodes, tree # sockets and nodes need also to be imported in order for them to be autoregistered
from .ui import panels
from .property_groups.bpy_property_groups import SlicerObjectPropertyGroup
bl_info = { bl_info = {
"name": "NodeSlicer", "name": "NodeSlicer",
@ -13,16 +15,33 @@ bl_info = {
"category": "Node", "category": "Node",
} }
_registered_nodeslicer = False
def register(): def register():
global _registered_nodeslicer
registry.blender_register_classes() registry.blender_register_classes()
bpy.types.NODE_MT_add.append(tree.draw_add_menu) bpy.types.NODE_MT_add.append(tree.draw_add_menu)
bpy.app.handlers.depsgraph_update_post.append(tree.depsgraph_update_handler) bpy.app.handlers.depsgraph_update_post.append(tree.depsgraph_update_handler)
if not hasattr(bpy.types.Object, "nodeslicer"):
bpy.types.Object.nodeslicer = bpy.props.PointerProperty(type=SlicerObjectPropertyGroup, name="nodeslicer") #type: ignore
_registered_nodeslicer = True
print('Nodeslicer Loaded') print('Nodeslicer Loaded')
def unregister(): def unregister():
bpy.app.handlers.depsgraph_update_post.remove(tree.depsgraph_update_handler) global _registered_nodeslicer
if tree.depsgraph_update_handler in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.remove(tree.depsgraph_update_handler)
bpy.types.NODE_MT_add.remove(tree.draw_add_menu) bpy.types.NODE_MT_add.remove(tree.draw_add_menu)
registry.blender_unregister_classes() registry.blender_unregister_classes()
if _registered_nodeslicer:
if hasattr(bpy.types.Object, "nodeslicer"):
del bpy.types.Object.nodeslicer #type: ignore
_registered_nodeslicer = False
print('Nodeslicer Unloaded') print('Nodeslicer Unloaded')
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -376,35 +376,18 @@ class SLICENODE_IntMath(MathNode):
class SLICENODE_GetMesh(SliceNode): class SLICENODE_GetMesh(SliceNode):
bl_label = "Get Mesh" bl_label = "Get Mesh"
part_type: NfEnumProperty( # pyright: ignore[reportInvalidTypeForm]
name='',
items=[
('ModelPart', 'Model Part', ''),
('NegativeVolume', 'Negative Volume', ''),
('ParameterModifier', 'Parameter Modifier', ''),
('SupportBlocker', 'Support Blocker', ''),
('SupportEnforcer', 'Support Enforcer', '')
],
default='ModelPart'
)
def init(self, context): def init(self, context):
self.add_input("SLICESOCK_Object") self.add_input("SLICESOCK_Object")
self.add_input("SLICESOCK_Overrides")
self.add_input("SLICESOCK_Int", "Extruder").value = 1
self.add_output("SLICESOCK_Mesh") self.add_output("SLICESOCK_Mesh")
def draw_buttons(self, context, layout):
assert layout
layout.prop(self, "part_type")
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingCollection | None: def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingCollection | None:
if socket_name != "Mesh": return None if socket_name != "Mesh": return None
obj: Any = self.resolve("Object") obj: Any = self.resolve("Object")
modifiers: dict[str, str] = self.resolve("Overrides")
extruder = self.resolve("Extruder") modifiers = {v.field: v.value_str for k, v in dict(obj.nodeslicer.modifiers).items()}
part_type = self.part_type extruder = obj.nodeslicer.extruder
part_type = obj.nodeslicer.object_type
so: SlicingObject = SlicingObject(obj, obj.name) so: SlicingObject = SlicingObject(obj, obj.name)
so.assign_modifiers(modifiers) so.assign_modifiers(modifiers)

View file

@ -23,7 +23,10 @@ class InputNodeSocket(SliceNodeSocket):
if not self.is_output and self.is_linked: if not self.is_output and self.is_linked:
layout.label(text=text or self.name) layout.label(text=text or self.name)
else: else:
layout.prop(self, "value", text=text or self.name) self._draw_prop(text, layout)
def _draw_prop(self, text, layout):
layout.prop(self, "value", text=text or self.name)
def _draw_as_ro(self, context: Any, layout: Any, node: Any, text: str | None): def _draw_as_ro(self, context: Any, layout: Any, node: Any, text: str | None):
layout.label(text=text or self.name) layout.label(text=text or self.name)
@ -131,6 +134,8 @@ class SLICESOCK_PrusaslicerConf(LabelNodeSocket):
default = None default = None
bl_color = (0.95, 0., 0., 1.0) # #F26C21 bl_color = (0.95, 0., 0., 1.0) # #F26C21
@register_class @register_class
class OverrideItem(bpy.types.PropertyGroup): class OverrideItem(bpy.types.PropertyGroup):
field: NfStringProperty(name="Field", default="") # pyright: ignore[reportInvalidTypeForm] field: NfStringProperty(name="Field", default="") # pyright: ignore[reportInvalidTypeForm]
@ -140,43 +145,19 @@ class OverrideItem(bpy.types.PropertyGroup):
def is_active(self): def is_active(self):
return bool(self.field) or bool(self.value_str) return bool(self.field) or bool(self.value_str)
class CollectionInputNodeSocket(InputNodeSocket): def draw_item(self, layout):
items: Any row = layout.row(align=True)
row.prop(self, "field", text="")
def __init__(self, *args, **kwargs) -> None: row.prop(self, "value_str", text="")
super().__init__(*args, **kwargs)
if len(self.items) == 0: from ...infra.mixins import CollectionMixin
self.items.add() class SLICESOCK_Overrides_Common(CollectionMixin, InputNodeSocket):
def update(self):
if self.items[-1].is_active:
self.items.add()
while len(self.items) >= 2:
if not self.items[-1].is_active and not self.items[-2].is_active:
self.items.remove(len(self.items) - 1)
else:
break
class SLICESOCK_Overrides_Common(CollectionInputNodeSocket):
bl_label = "Overrides" bl_label = "Overrides"
items: CollectionProperty(type=OverrideItem) # pyright: ignore[reportInvalidTypeForm] items: CollectionProperty(type=OverrideItem) # pyright: ignore[reportInvalidTypeForm]
bl_color = (1.0, 1.0, 0.0, 1.0) # Yellow bl_color = (1.0, 1.0, 0.0, 1.0) # Yellow
def draw(self, context, layout, node, text):
if not self.is_output and self.is_linked:
layout.label(text=text or self.name)
return
col = layout.column(align=True)
for item in self.items:
row = col.row(align=True)
row.prop(item, "field", text="")
row.prop(item, "value_str", text="")
@property @property
def value(self) -> dict[str, str]: def value(self) -> dict[str, str]:
overrides = {} overrides = {}

View file

@ -4,6 +4,7 @@ import bpy
from bpy.types import NodeTree, Operator, Menu from bpy.types import NodeTree, Operator, Menu
from bpy.app.handlers import persistent from bpy.app.handlers import persistent
from ...infra.mixins import update_collection
from ...registry import register_class from ...registry import register_class
if TYPE_CHECKING: if TYPE_CHECKING:
@ -91,14 +92,25 @@ def tree_update_handler(scene, depsgraph):
finally: finally:
_is_updating = False _is_updating = False
def object_init_handler(scene, depsgraph):
obj = bpy.context.active_object
if not obj or not hasattr(obj, "nodeslicer"):
return
pg = getattr(obj, 'nodeslicer', None)
if not pg:
return
update_collection(pg.modifiers)
@persistent @persistent
def depsgraph_update_handler(scene, depsgraph): def depsgraph_update_handler(scene, depsgraph):
global _is_updating, _tree_changed global _is_updating, _tree_changed
if _is_updating: return if _is_updating: return
tree_update_handler(scene, depsgraph) tree_update_handler(scene, depsgraph)
object_init_handler(scene, depsgraph)
@register_class @register_class
class SLICETREE_NodeTree(NodeTree): class SLICETREE_NodeTree(NodeTree):
bl_idname = "SLICETREE_NodeTree" bl_idname = "SLICETREE_NodeTree"

37
infra/mixins.py Normal file
View file

@ -0,0 +1,37 @@
import bpy
class DrawCollectionMixin():
items: bpy.props.CollectionProperty # pyright: ignore[reportGeneralTypeIssues]
def draw_collection(self, items, layout):
col = layout.column(align=True)
for item in items:
item.draw_item(col)
def _draw_prop(self, text, layout):
self.draw_collection(self.items, layout)
def update_collection(items: bpy.props.CollectionProperty) -> None: # pyright: ignore[reportGeneralTypeIssues]
if len(items) == 0:
items.add()
if items[-1].is_active:
items.add()
while len(items) >= 2:
if not items[-1].is_active and not items[-2].is_active:
items.remove(len(items) - 1)
else:
break
class UpdateCollectionMixin():
items: bpy.props.CollectionProperty # pyright: ignore[reportGeneralTypeIssues]
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.update()
def update(self):
update_collection(self.items)
class CollectionMixin(DrawCollectionMixin, UpdateCollectionMixin): ...

View file

@ -0,0 +1,32 @@
import bpy
from ..core.nodeslicer.sockets import OverrideItem
from ..registry import register_class
extruder_options: list[tuple[str, str, str]] = [
("0", "Default Extruder", "Default Extruder"),
("1", "Extruder 1", "Extruder 1"),
("2", "Extruder 2", "Extruder 2"),
("3", "Extruder 3", "Extruder 3"),
("4", "Extruder 4", "Extruder 4"),
("5", "Extruder 5", "Extruder 5"),
]
object_type_options: list[tuple[str, str, str]] = [
("ModelPart", "Part", "Model Part"),
("NegativeVolume", "Negative Volume", "Negative Volume"),
("ParameterModifier", "Modifier", "Modifier"),
("SupportBlocker", "Support Blocker", "Support Blocker"),
("SupportEnforcer", "Support Enforcer", "Support Enforcer"),
("WipeTower", "Wipe Tower", "Wipe Tower"),
("Ignore", "Ignore", "Ignore"),
]
from ..core.nodeslicer.bpy_ns_types import NfEnumProperty, NfStringProperty
@register_class
class SlicerObjectPropertyGroup(bpy.types.PropertyGroup):
object_type: NfEnumProperty(name="Part type", default="ModelPart", items=object_type_options) # pyright: ignore[reportInvalidTypeForm]
extruder: NfEnumProperty(name="Extruder", default="0", items=extruder_options) # pyright: ignore[reportInvalidTypeForm]
search_term: NfStringProperty(name="Search") # pyright: ignore[reportInvalidTypeForm]
modifiers: bpy.props.CollectionProperty(type=OverrideItem) # pyright: ignore[reportInvalidTypeForm]

View file

@ -0,0 +1,79 @@
import bpy
from bpy.types import bpy_struct
import math
from typing import cast
def _pv(inst: bpy_struct) -> 'PrusaSlicerTypes':
return cast(PrusaSlicerTypes, inst)
def get_prop_bool(inst: bpy_struct) -> bool:
return _pv(inst).param_value == '1'
def set_prop_bool(inst: bpy_struct, value: bool) -> None:
_pv(inst).param_value = '1' if value else '0'
def get_prop_float(inst: bpy_struct) -> float:
return float(_pv(inst).param_value) if _pv(inst).param_value else 0
def set_prop_float(inst: bpy_struct, value: float) -> None:
_pv(inst).param_value = str(round(value, 5))
def get_prop_int(inst: bpy_struct) -> int:
return int(_pv(inst).param_value) if _pv(inst).param_value else 0
def set_prop_int(inst: bpy_struct, value: int) -> None:
_pv(inst).param_value = str(value)
def get_prop_perc(inst: bpy_struct) -> float:
if _pv(inst).param_value:
return float(_pv(inst).param_value.rstrip('%')) if _pv(inst).param_value else 0
return 0
def set_prop_perc(inst: bpy_struct, value: float) -> None:
_pv(inst).param_value = str(value)+'%'
def get_prop_angle(inst: bpy_struct) -> float:
return (float(_pv(inst).param_value) * math.pi) / 180
def set_prop_angle(inst: bpy_struct, value: float) -> None:
_pv(inst).param_value = str(round((value * 180) / math.pi, 5))
class PrusaSlicerTypes():
param_value: bpy.props.StringProperty(name='') # pyright: ignore[reportInvalidTypeForm]
param_bool: bpy.props.BoolProperty(name='',
get=get_prop_bool,
set=set_prop_bool,
default=False,
) # pyright: ignore[reportInvalidTypeForm]
param_float: bpy.props.FloatProperty(name='',
get=get_prop_float,
set=set_prop_float,
soft_min=0,
step = 5,
) # pyright: ignore[reportInvalidTypeForm]
param_int: bpy.props.IntProperty(name='',
get=get_prop_int,
set=set_prop_int,
) # pyright: ignore[reportInvalidTypeForm]
param_perc: bpy.props.FloatProperty(name='',
get=get_prop_perc,
set=set_prop_perc,
subtype='PERCENTAGE',
soft_min=0,
soft_max=100,
step = 5,
) # pyright: ignore[reportInvalidTypeForm]
param_angle: bpy.props.FloatProperty(name='',
get=get_prop_angle,
set=set_prop_angle,
subtype='ANGLE',
soft_min=0,
soft_max=360,
step = 1,
) # pyright: ignore[reportInvalidTypeForm]

31
ui/panels.py Normal file
View file

@ -0,0 +1,31 @@
from bpy.types import Panel
from ..infra.mixins import DrawCollectionMixin
from ..registry import register_class
@register_class
class SlicerObjectPanel(Panel, DrawCollectionMixin):
bl_label = "NodeSlicer Settings"
bl_idname = f"OBJECT_PT_nodeslicer"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "object"
def draw(self, context):
layout = self.layout
if not layout: return
obj = context.object
if not obj: return
pg = getattr(obj, 'nodeslicer', None)
if not pg:
layout.label(text="Property not registered")
return
layout.prop(pg, "object_type", text="Object type")
layout.prop(pg, "extruder", text="Extruder")
if pg.object_type in ['ParameterModifier', 'ModelPart']:
self.draw_collection(pg.modifiers, layout)