You've already forked stocker_helper
Migrated from Stocker project
This commit is contained in:
@@ -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.
|
||||||
+166
@@ -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()
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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.")
|
||||||
@@ -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.")
|
||||||
@@ -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.")
|
||||||
Reference in New Issue
Block a user