diff --git a/__init__.py b/__init__.py index c5eeffc..609bafd 100644 --- a/__init__.py +++ b/__init__.py @@ -2,6 +2,8 @@ from __future__ import annotations import bpy 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 .ui import panels +from .property_groups.bpy_property_groups import SlicerObjectPropertyGroup bl_info = { "name": "NodeSlicer", @@ -13,16 +15,33 @@ bl_info = { "category": "Node", } +_registered_nodeslicer = False + def register(): + global _registered_nodeslicer registry.blender_register_classes() bpy.types.NODE_MT_add.append(tree.draw_add_menu) 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') 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) 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') if __name__ == "__main__": diff --git a/core/nodeslicer/nodes.py b/core/nodeslicer/nodes.py index 6e12c97..4e81f93 100644 --- a/core/nodeslicer/nodes.py +++ b/core/nodeslicer/nodes.py @@ -376,35 +376,18 @@ class SLICENODE_IntMath(MathNode): class SLICENODE_GetMesh(SliceNode): 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): self.add_input("SLICESOCK_Object") - self.add_input("SLICESOCK_Overrides") - self.add_input("SLICESOCK_Int", "Extruder").value = 1 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: if socket_name != "Mesh": return None obj: Any = self.resolve("Object") - modifiers: dict[str, str] = self.resolve("Overrides") - extruder = self.resolve("Extruder") - part_type = self.part_type + + modifiers = {v.field: v.value_str for k, v in dict(obj.nodeslicer.modifiers).items()} + extruder = obj.nodeslicer.extruder + part_type = obj.nodeslicer.object_type so: SlicingObject = SlicingObject(obj, obj.name) so.assign_modifiers(modifiers) diff --git a/core/nodeslicer/sockets.py b/core/nodeslicer/sockets.py index 36fceac..1b1ab46 100644 --- a/core/nodeslicer/sockets.py +++ b/core/nodeslicer/sockets.py @@ -23,7 +23,10 @@ class InputNodeSocket(SliceNodeSocket): if not self.is_output and self.is_linked: layout.label(text=text or self.name) 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): layout.label(text=text or self.name) @@ -131,6 +134,8 @@ class SLICESOCK_PrusaslicerConf(LabelNodeSocket): default = None bl_color = (0.95, 0., 0., 1.0) # #F26C21 + + @register_class class OverrideItem(bpy.types.PropertyGroup): field: NfStringProperty(name="Field", default="") # pyright: ignore[reportInvalidTypeForm] @@ -140,43 +145,19 @@ class OverrideItem(bpy.types.PropertyGroup): def is_active(self): return bool(self.field) or bool(self.value_str) -class CollectionInputNodeSocket(InputNodeSocket): - items: Any - - def __init__(self, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) - if len(self.items) == 0: - self.items.add() - - 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): + def draw_item(self, layout): + row = layout.row(align=True) + row.prop(self, "field", text="") + row.prop(self, "value_str", text="") + +from ...infra.mixins import CollectionMixin +class SLICESOCK_Overrides_Common(CollectionMixin, InputNodeSocket): bl_label = "Overrides" items: CollectionProperty(type=OverrideItem) # pyright: ignore[reportInvalidTypeForm] 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 def value(self) -> dict[str, str]: overrides = {} diff --git a/core/nodeslicer/tree.py b/core/nodeslicer/tree.py index 60aeac5..5c7e0e7 100644 --- a/core/nodeslicer/tree.py +++ b/core/nodeslicer/tree.py @@ -4,6 +4,7 @@ import bpy from bpy.types import NodeTree, Operator, Menu from bpy.app.handlers import persistent +from ...infra.mixins import update_collection from ...registry import register_class if TYPE_CHECKING: @@ -91,14 +92,25 @@ def tree_update_handler(scene, depsgraph): finally: _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 def depsgraph_update_handler(scene, depsgraph): global _is_updating, _tree_changed if _is_updating: return tree_update_handler(scene, depsgraph) + object_init_handler(scene, depsgraph) - @register_class class SLICETREE_NodeTree(NodeTree): bl_idname = "SLICETREE_NodeTree" diff --git a/infra/mixins.py b/infra/mixins.py new file mode 100644 index 0000000..71051d1 --- /dev/null +++ b/infra/mixins.py @@ -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): ... \ No newline at end of file diff --git a/property_groups/bpy_property_groups.py b/property_groups/bpy_property_groups.py new file mode 100644 index 0000000..178816c --- /dev/null +++ b/property_groups/bpy_property_groups.py @@ -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] \ No newline at end of file diff --git a/property_groups/property_groups.py b/property_groups/property_groups.py new file mode 100644 index 0000000..69b012a --- /dev/null +++ b/property_groups/property_groups.py @@ -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] \ No newline at end of file diff --git a/ui/panels.py b/ui/panels.py new file mode 100644 index 0000000..0ce6713 --- /dev/null +++ b/ui/panels.py @@ -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)