From 7872d294a5e3ae4bcccc809b7cd717dc87fe7906 Mon Sep 17 00:00:00 2001 From: Gary Ritchie Date: Fri, 10 Apr 2026 12:22:21 -0400 Subject: [PATCH] Migrated from Stocker project --- README.md | 44 +++++ __init__.py | 166 ++++++++++++++++++ __pycache__/__init__.cpython-311.pyc | Bin 0 -> 8611 bytes .../__pycache__/phase1_logic.cpython-310.pyc | Bin 0 -> 1008 bytes .../phase2_geometry.cpython-310.pyc | Bin 0 -> 1678 bytes .../phase2_geometry.cpython-311.pyc | Bin 0 -> 3275 bytes scripts/phase1_logic.py | 44 +++++ scripts/phase2_geometry.py | 71 ++++++++ scripts/phase3_operator.py | 59 +++++++ 9 files changed, 384 insertions(+) create mode 100644 README.md create mode 100644 __init__.py create mode 100644 __pycache__/__init__.cpython-311.pyc create mode 100644 scripts/__pycache__/phase1_logic.cpython-310.pyc create mode 100644 scripts/__pycache__/phase2_geometry.cpython-310.pyc create mode 100644 scripts/__pycache__/phase2_geometry.cpython-311.pyc create mode 100644 scripts/phase1_logic.py create mode 100644 scripts/phase2_geometry.py create mode 100644 scripts/phase3_operator.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..5bfd7e7 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# Stocker Helper + +A Blender add-on for [Stocker Geometry Nodes](httpshttps://garylritchie.gumroad.com/l/stocker). + +## Overview + +The **Stocker Helper** creates a boundary cube that encompasses selected objects. It calculates the global bounding box, generates a mesh, aligns the origin to the bottom-front-left corner, and applies Geometry Node templates. + +## Features + +* **Bounding Box Calculation:** Calculates the global spatial extent of selected mesh objects. +* **Origin Alignment:** Sets the boundary object's origin to the **Back-Left** extreme (+Y, -X). +* **Geometry Nodes Injection:** Applies a `NODES` modifier using templates named `grid_pack` or `circle_pack`. +* **Asset Fetching:** Scans registered Asset Libraries for required node groups. +* **Height Buffer Control:** Adds a percentage-based buffer to the Z-height via a slider. +* **UI:** Access via the **Stocker** tab in the 3D Viewport N-Panel. + +## Requirements + +* **Blender Version:** 4.5 +* **Node Group Templates:** The `.blend` file or the Stocker Asset Library **must** contain Geometry Node groups named `grid_pack` and `circle_pack`. + +## Installation + +1. Download `stocker.zip`. +2. Drag the `.zip` file into Blender. + +## Usage + +1. **Select Objects:** Select the objects for the packing area. +2. **Open Sidebar:** Press `N` in the 3D Viewport. +3. **Locate Tab:** Click the **Stocker** tab. +4. **Adjust Height Buffer:** Set the height buffer percentage. +5. **Run Setup:** Click **Grid Boundary** or **Circle Boundary**. +6. **Result:** An object named `Stocker_Boundary` is created with the configured Geometry Nodes modifier and origin. + +## Recent Improvements + +* **Asset Library Scan:** Implemented priority for "stocker" libraries and automatic appending of missing node groups. +* **Code Robustness:** Updated imports and classmethod poll attributes for Blender Extension compliance. + +## License + +Distributed as an open-source utility. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..bfabef1 --- /dev/null +++ b/__init__.py @@ -0,0 +1,166 @@ +import bpy + +# Import the logic from previous phases +from .scripts import phase2_geometry as logic + +bl_info = { + "name": "Stocker Helper", + "author": "Gary Ritchie", + "version": (0, 1, 0), + "blender": (4, 0, 0), + "location": "View3D > Sidebar > Stocker", + "description": "Automates boundary creation for Geometry Nodes packing", + "category": "Object", +} + +def find_and_append_node_group(name): + """Search asset libraries for a node group and append it if found.""" + import os + + if name in bpy.data.node_groups: + return bpy.data.node_groups[name] + + # Get libraries, prioritizing those starting with 'stocker' (lowercase) + libraries = list(bpy.context.preferences.filepaths.asset_libraries) + libraries.sort(key=lambda l: not os.path.basename(l.path).lower().startswith("stocker")) + + for lib in libraries: + if not lib.path or not os.path.exists(lib.path): + continue + + print(f"Stocker: Searching library {lib.path} for {name}...") + + for root, dirs, files in os.walk(lib.path): + for file in files: + if file.endswith(".blend"): + path = os.path.join(root, file) + try: + with bpy.data.libraries.load(path) as (data_from, data_to): + if name in data_from.node_groups: + data_to.node_groups = [name] + if name in bpy.data.node_groups: + print(f"Stocker: Successfully appended {name} from {path}") + return bpy.data.node_groups[name] + except: + continue + return None + +class STOCKER_PT_panel(bpy.types.Panel): + bl_label = "Stocker Helper" + bl_idname = "STOCKER_PT_panel" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = 'Stocker' + + def draw(self, context): + layout = self.layout + col = layout.column(align=True) + + # New Height Buffer slider + col.prop(context.scene, "stocker_height_buffer", text="Height Buffer (%)") + col.separator() + + op_grid = col.operator("object.stocker_setup", text="Grid Boundary") + op_grid.mode = 'GRID' + op_grid.height_buffer = context.scene.stocker_height_buffer + + op_circle = col.operator("object.stocker_setup", text="Circle Boundary") + op_circle.mode = 'CIRCLE' + op_circle.height_buffer = context.scene.stocker_height_buffer + +class STOCKER_OT_setup(bpy.types.Operator): + bl_idname = "object.stocker_setup" + bl_label = "Stocker Helper" + bl_description = "Generate a boundary cube for the selected objects" + bl_options = {'REGISTER', 'UNDO'} + + mode: bpy.props.EnumProperty( + name="Packing Mode", + description="Select the geometry nodes template to apply", + items=[ + ('GRID', "Grid Pack", "Use the Grid Pack template"), + ('CIRCLE', "Circle Pack", "Use the Circle Pack template"), + ], + default='GRID' + ) + + height_buffer: bpy.props.FloatProperty( + name="Height Buffer", + description="Percentage to add to the Z height (0.1 = 10%)", + default=0.1, + min=0.0, + max=2.0 + ) + + def execute(self, context): + # 1. Get selected objects + selected_objs = context.selected_objects + + # 2. Run logic from previous phases + bounds = logic.calculate_global_bounds(selected_objs) + if not bounds: + self.report({'ERROR'}, "No valid mesh objects selected.") + return {'CANCELLED'} + + # Apply the Z height buffer + if self.height_buffer > 0: + height_add = bounds['size'].z * self.height_buffer + bounds['size'].z += height_add + bounds['max'].z += height_add + + # 3. Create the cube + new_obj = logic.create_aligned_boundary_cube(bounds) + if not new_obj: + self.report({'ERROR'}, "Failed to create boundary cube.") + return {'CANCELLED'} + + # 4. Modifier Injection + group_name = "grid_pack" if self.mode == 'GRID' else "circle_pack" + mod_name = "grid_pack" if self.mode == 'GRID' else "circle_pack" + + # Try to get or fetch the node group with visual feedback + context.window.cursor_modal_set('WAIT') + self.report({'INFO'}, f"Searching asset libraries for {group_name}...") + + try: + node_group = find_and_append_node_group(group_name) + finally: + context.window.cursor_modal_restore() + + if not node_group: + self.report({'WARNING'}, f"Node Group '{group_name}' not found in file or Stocker Asset Libraries.") + return {'FINISHED'} + + mod = new_obj.modifiers.new(name=mod_name, type='NODES') + mod.node_group = node_group + + self.report({'INFO'}, f"Stocker setup complete using {self.mode} mode.") + return {'FINISHED'} + + @classmethod + def poll(cls, context): + return context.selected_objects + +classes = ( + STOCKER_PT_panel, + STOCKER_OT_setup, +) + +def register(): + bpy.types.Scene.stocker_height_buffer = bpy.props.FloatProperty( + name="Stocker Height Buffer", + description="Default height buffer for new boundaries", + default=0.1, + min=0.0, + max=2.0 + ) + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + del bpy.types.Scene.stocker_height_buffer + +if __name__ == "__main__": + register() diff --git a/__pycache__/__init__.cpython-311.pyc b/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cae8d056366f5afb15501bfefb70714a5f7a40b1 GIT binary patch literal 8611 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 PLVhJ0F}tCd%+mh?$2A45 literal 0 HcmV?d00001 diff --git a/scripts/__pycache__/phase1_logic.cpython-310.pyc b/scripts/__pycache__/phase1_logic.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..46cfe5898eee92fe89bea4b68894475f14a56546 GIT binary patch literal 1008 zcmYjQO>5LZ7@m(Ln@zK&hoU`c5Ja}nYV{^X1r?X=mdAi~BIuKBES>Pa&pU6P_iH9mydp1Vuz3b4A3$X! zqd7TVBR!@W%lJof%mmMzGp26{Cvz_;1#8eq-}{Lua+J~<+O+}nAkQJ?Cf-z}^bPn; zBpDEcyq9!ETVySAAcx;+(QVPI$P4oR>8ew8P8s^(diVSeZ0?YiUv**!whPAIs$2QS z``oFzU+5ND1;(#v6`Xc28EW|zgB`$TusQ4v*xZDb({dcxZp(3Ddo9O1e}sB%w>X6Y zv#d`6v!DX2_=qEmTX_xA9Riy{dllayjj!Qk-X~{3`kkZY_K}Lz@KmIxj7pIMMYeCb zEJjOb5866!UJhO@`=DJUxy+)8C{L~;Mru}Mku2I!sfzu+WBEiDP|4f|nVg8Cguk>s zljyN9vC?uZi+<0#Q!NW)-BGR*V;!@Y3L8!mqvbsQpmd(ut#+5>dF$y8>x%JE&21Mi zPi98urS;ySZ>6<^d$x3L>B7>b#y3X#8+PmeSaCb2b#e)6UaE8kFHnlYwC7W>2RX5SuUQ`|6CbD`2;;P|6WL)zyARAd=Xgy literal 0 HcmV?d00001 diff --git a/scripts/__pycache__/phase2_geometry.cpython-310.pyc b/scripts/__pycache__/phase2_geometry.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9cb906b232a012a307599d3705f5b0f72ada4b01 GIT binary patch literal 1678 zcmZWpNpIvt6m~6MdPysc5KK^jFa(GZGb2TFK!ApQxiBCFafpO0x2wA2j@#~(tC{Y| zK82sbAZ7A@4498>No`7 ze1KKH4I&&tGUvGEPz4hr_$gCw zo$U)3_FzKm4k&~FZr!aqnqRtAcSW|Hx~IL0RK1CRL2Ck;LJlBP$P97^GSl6PZ(=xP zcN616_BJuz*@LtF$}h*Z3sj^P8!=?LmDg}`+ks3$QTMh3+a^YMKk_3d^dA|P=A(0d z7xXj3zx?#*i*G`1*fh;S;)UsobSm>QE%MSlGc2?&rqM_hxsEb9(qYfIqpXOv;d(if zraz6fN*B@NLS@2qhQ&M=(Xd#U4jdcIVd2hBf&nIz~%(Ga_Xq*+p zI74^nv9!&U4?sBKWS8y|P6N^<6x;(JkUs5`_8k!Vgm*a!SU{d70rCD)FC+0Or1~)Q zuo|TP0pVYyPRN8_GC@`Z$Z?=?t5sxW(c?_qnpJG#!Rl9ZWha8_K(HN@6HEXdR~k6q zfo%lqO}WwB9bn}u^8c1L9<+(O%D>XbLCwdG@O~n-Tan971+8*di~ymR-4zB;_b&UD zBm5l)U3kDIjkYQZJO+=<_R;1pKC&M+R?CnnJH-DXYinEEe?{npymh`}!|qr+vv$6= zx3ad#THCQ-+pqdm>}xQn{nmP&tX{WP>q4xpre3w=4NI&G*2d5r$|g51v!!D-mO3g_ zI!^O&VAv3N;Ud;CKnOg_(y&}UHvSc|{RCqq7us}_BD1K+kQtt(`6<%UX=!>Gv}rV3 zwl(H6aH;H1Ol^UfAS;sCCWXH7S{NE`cTzjn-NXLCdAo)Cy;OFXMsb33bT?<66=gF3 zEqTdi?IucEbjMjb&M}5fUED&H%!kspU%m-qQ9hvC!~?QvZ$NG}*uF)cBin>O3AzI} zfZqf9)w{N85Jl4%a~z!?er<?y&o^VWckos7$@V_9TjFP6HS>5XqsX{3mFD_JCw8b+(@yZ-v=H-5KD4)04nP7YAOQ{T1P6V>|NaN) C@uAHC literal 0 HcmV?d00001 diff --git a/scripts/__pycache__/phase2_geometry.cpython-311.pyc b/scripts/__pycache__/phase2_geometry.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..beaf96812991b82c2f44ca8a3746d33fb4304652 GIT binary patch literal 3275 zcmcIlU1-}@6uz>q#@W)e9%;-%Gs-9}{c+o^P1=z*4}~HWOLm>gR*;k| zG4k+6U}pHi4}qqThir6RN?T~5+d9}_d)uQO3^9U0VHB1(L)lB&!_JlB->qRA?ASV2 z_xxU+d+&F?_Ih0i==0w;6Mxtd`jt#-MDM)3I1ewkkbneALf5R3x<(1MC3MXu(7Iu!okcTD?M-*Jt~QOq->rFTdW3XJchA6HT67n(+;3!}S}bi~ zNosFWo29MAk_OBAdfF^$t*aITvg2K{;}%P1m+VZ7CA&*@HoubpPPgxznEDzKYN{Ef z+jKfcw^r^I^~E})v;WwZO2hpD51To%I!(rQZ-HH6rGcJP2}Q|Jk3i`52%W&^h0jA5J=VG;$>0A-CIQz6R7xJ%W2Ph`q!A-!>hv& znRW4==Yi)N@oD6-^h7Eg$c<$qTcBodA6`BDuxow(Uf+YhADO4I@4Y{GE%mdmp6o=$ z*Iz{)OxR>Oy5jE0A1%ypxciFkzKSRCth+DQQSk@L{(&4@dA)l%Sw#*fSOd$Qt@I9j z^?rHq$fK#!;IZ=Hu~P4`+{NdCy(`R$zWQ+~Fj5YT@NHI*Sgl3^|@kjtmJ#Q?0a|1_r~7{!Ll>Bc3}O$7l$8pJ?(nj_Y?CA^Ru@&Fj4Bf zRPMY~wPO!kMHXY&7qAJa1>;GdF)@Cr1><$-3f;umqznafThrX8)l@Jx(6oV_Yjxag zGDJe#UC+?3h(ivGNw12d(`{&nTO!qQd4^m)lV%z*>nL2)kx|F3I0U^B>$V0C3(!4d zbh8BB>$v7dL@NN|{8eU3&3;buw8q#?*BT3EO)=UtwQA#z3=>BJ`z4hD>|1k_yzQie zh0flEeY#)YD++-%0gAuSU69szS6v+GZOIu_0`Vwn=B-CKlV|t3r62Z2LAj58LD@Y@6U~*(z^= zKsiX*5HN^f3I>VbP)!9=Q*eL?LNyggO@X--;%uqR;2j1%4Pb}`UgH6i0iH$Gpi|;U zhP?*#c7R(=T+j?xRF+5+NXRJzONrD?#Ycj566ObHxMPZ(hT@e_BT-KnP*R7EI*!&RNVf;Q2u!K%yR;}`4gM&{TuH6#r}6n?$NS) zG<#;t<;@=|yLvWV`!`%f@#sj&b+qg{nmxUR?MoNRxTlQY+{6br@WFMugon#`xQK_V zEOPFtIKBDT3;x1M0C4klaCxY(cV(dH*jI77a#!+GxpRdNb3QUTnbY#~H9LRd%<|cy zqqo^>aCNBY7-;xiS)PJ8Rln2AW+T-A?FFE$F%Y;A_t*cX`!Gb#}iTDydVlAAy#=C zf+*y+m>t#J9}+XgCq<4km{}!}((WNMm$|<$9mkTgFeiz}l@s7a?uHrzR;4M5svuX^ z{8dnAvG%XxehSJ^XQ!xs%17oq>=XsPB$wWxyNh&p1v|5mqU(4GpD5!K#oE70BisXO Ha~*#IVtT}4 literal 0 HcmV?d00001 diff --git a/scripts/phase1_logic.py b/scripts/phase1_logic.py new file mode 100644 index 0000000..ea2917f --- /dev/null +++ b/scripts/phase1_logic.py @@ -0,0 +1,44 @@ +import bpy +import mathutils + +def calculate_global_bounds(selected_objects): + if not selected_objects: + print("No objects selected.") + return None + + min_x = float('inf') + min_y = float('inf') + min_z = float('inf') + max_x = float('-inf') + max_y = float('-inf') + max_z = float('-inf') + + for obj in selected_objects: + if obj.type != 'MESH': + continue + + # Transform local bounding box corners to world space + matrix = obj.matrix_all_world = obj.matrix_world + for corner in obj.bound_box: + world_corner = matrix @ mathutils.Vector(corner) + + min_x = min(min_x, world_corner.x) + min_y = min(min_y, world_corner.y) + min_z = min(min_z, world_corner.z) + max_x = max(max_x, world_corner.x) + max_y = max(max_y, world_corner.y) + max_z = max(max_z, world_corner.z) + + if min_x == float('inf'): + print("No valid mesh objects found in selection.") + return None + + return { + 'min': (min_x, min_y, min_z), + 'max': (max_x, max_y, max_z), + 'dimensions': (max_x - min_x, max_y - min_y, max_z - min_z), + 'target_origin': (max_x, min_y, min_z) + } + +if __name__ == "__main__": + print("Phase 1: Logic verification script loaded.") diff --git a/scripts/phase2_geometry.py b/scripts/phase2_geometry.py new file mode 100644 index 0000000..f1c6842 --- /dev/null +++ b/scripts/phase2_geometry.py @@ -0,0 +1,71 @@ +import bpy +import mathutils + +def calculate_global_bounds(selected_objects): + if not selected_objects: + return None + + min_x, min_y, min_z = float('inf'), float('inf'), float('inf') + max_x, max_y, max_z = float('-inf'), float('-inf'), float('-inf') + + found_any_mesh = False + for obj in selected_objects: + if obj.type != 'MESH': + continue + + found_any_mesh = True + matrix = obj.matrix_world + for corner in obj.bound_box: + world_corner = matrix @ mathutils.Vector(corner) + + min_x = min(min_x, world_corner.x) + min_y = min(min_y, world_corner.y) + min_z = min(min_z, world_corner.z) + max_x = max(max_x, world_corner.x) + max_y = max(max_y, world_corner.y) + max_z = max(max_z, world_corner.z) + + if not found_any_mesh: + return None + + return { + 'min': mathutils.Vector((min_x, min_y, min_z)), + 'max': mathutils.Vector((max_x, max_y, max_z)), + 'size': mathutils.Vector((max_x - min_x, max_y - min_y, max_z - min_z)) + } + +def create_aligned_boundary_cube(bounds): + if not bounds: + return None + + min_v = bounds['min'] + size = bounds['size'] + + mesh = bpy.data.meshes.new("Stocker_Boundary") + obj = bpy.data.objects.new("Stocker_Boundary", mesh) + bpy.context.collection.objects.link(obj) + + verts = [ + (0, 0, 0), + (size.x, 0, 0), + (size.x, -size.y, 0), + (0, -size.y, 0), + (0, 0, size.z), + (size.x, 0, size.z), + (size.x, -size.y, size.z), + (0, -size.y, size.z) + ] + faces = [ + (0, 1, 2, 3), (4, 5, 6, 7), (0, 1, 5, 4), + (1, 2, 6, 5), (2, 3, 7, 6), (3, 0, 4, 7) + ] + + mesh.from_pydata(verts, [], faces) + mesh.update() + + # Place origin at Back-Left (Min X, Max Y, Min Z) + obj.location = (bounds['min'].x, bounds['max'].y, bounds['min'].z) + return obj + +if __name__ == "__main__": + print("Phase 2: Geometry (Origin Alignment) logic loaded.") \ No newline at end of file diff --git a/scripts/phase3_operator.py b/scripts/phase3_operator.py new file mode 100644 index 0000000..4717688 --- /dev/null +++ b/scripts/phase3_operator.py @@ -0,0 +1,59 @@ +import bpy + +def setup_stocker_operator(logic_module): + """ + Wraps the phase 1 & 2 logic into a Blender Operator. + """ + class STOCKER_OT_setup(bpy.types.Operator): + bl_idname = "object.stocker_setup" + bl_label = "Stocker Helper" + bl_description = "Generate a boundary cube for the selected objects" + bl_options = {'REGISTER', 'UNDO'} + + mode: bpy.props.EnumProperty( + name="Packing Mode", + description="Select the geometry nodes template to apply", + items=[ + ('GRID', "Grid Pack", "Use the Grid Pack template"), + ('CIRCLE', "Circle Pack", "Use the Circle Pack template"), + # Placeholder for future modes + ], + default='GRID' + ) + + def execute(self, context): + # 1. Get selected objects + selected_objs = context.selected_objects + + # 2. Run logic from previous phases + bounds = logic_module.calculate_global_bounds(selected_objs) + if not bounds: + self.report({'ERROR'}, "No valid mesh objects selected.") + return {'CANCELLED'} + + # 3. Create the cube + new_obj = logic_module.create_aligned_boundary_cube(bounds) + if not new_obj: + self.report({'ERROR'}, "Failed to create boundary cube.") + return {'CANCELLED'} + + # 4. Modifier Injection + # Search for the node group + group_name = "grid_pack" if self.mode == 'GRID' else "Stocker_Circle" + node_group = bpy.data.node_groups.get(group_name) + + if not node_group: + self.report({'WARNING'}, f"Node Group '{group_name}' not found. Boundary created without modifier.") + return {'FINISHED'} + + # Add Geometry Nodes modifier + mod = new_obj.modifiers.new(name="circle_pack", type='NODES') + mod.node_group = node_group + + self.report({'INFO'}, f"Stocker setup complete using {self.mode} mode.") + return {'FINISHED'} + + return STOCKER_OT_setup + +if __name__ == "__main__": + print("Phase 3: Integration (The Operator) logic loaded.")