Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 51 additions & 2 deletions data/tool-guides/object-assembly-guide.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
---
title: "Multi-Part Object Assembly Guide"
category: "object-assembly"
tags: ["assembly", "compound", "multi-part", "parent", "attach", "connect", "hierarchy", "empty", "root-control", "locator", "pole", "arm", "joint", "furniture", "vehicle", "building", "add_empty_object", "set_empty_properties", "parent_set", "set_object_transform"]
triggered_by: ["add_empty_object", "set_empty_properties", "parent_set", "set_object_transform"]
tags: ["assembly", "compound", "multi-part", "parent", "attach", "connect", "hierarchy", "empty", "root-control", "locator", "pole", "arm", "joint", "furniture", "vehicle", "building", "array", "pattern", "grid", "radial", "add_empty_object", "set_empty_properties", "parent_set", "set_object_transform", "arrange_objects", "duplicate_object_pattern"]
triggered_by: ["add_empty_object", "set_empty_properties", "parent_set", "set_object_transform", "arrange_objects", "duplicate_object_pattern"]
description: "Domain knowledge for building compound objects from primitives. Covers connection point calculation, parent-child assembly, flush attachment, and common archetypes like furniture, street infrastructure, and architectural elements."
blender_version: "4.0+"
---
Expand All @@ -28,6 +28,55 @@ When building any compound object from primitives:
4. **Create child parts at calculated positions** — Each child's position derives from its parent geometry
5. **Optionally parent all parts** — Group under an Empty for unified control

## DIRECT PATTERN TOOLS

Use `arrange_objects` and `duplicate_object_pattern` before `execute_code` for repeated objects, scene dressing, and symmetrical layouts. These tools cover the common Python-loop cases:

- bar stools in front of a counter
- columns, fence posts, books, lamps, table legs, wheels, bolts, and tiles
- product objects evenly spaced on a pedestal
- plants/rocks/props distributed in a row or grid
- circular/radial displays around a centerpiece

### Arrange Existing Objects

Use this when all objects already exist and only need clean spacing:

```text
arrange_objects(
names=["Ball_Red", "Ball_Green", "Ball_Blue"],
layout="CIRCLE",
center=[0, 0, 0.25],
radius=0.9,
plane="XY",
align_bottom_to_z=0.1
)
```

`align_bottom_to_z` grounds every arranged object by its bounding-box bottom. Use it for objects sitting on a shared plane such as a pedestal top, floor, shelf, or table.

### Duplicate Into A Pattern

Use this when one source object should become many repeated parts:

```text
duplicate_object_pattern(
name="Stool",
count=3,
layout="LINE",
name_prefix="Counter_Stool",
spacing=0.75,
axis="X",
center=[0, 1.2, 0],
include_source=true,
align_bottom_to_z=0
)
```

Prefer `linked=true` for lightweight repeated mesh instances when the duplicates should share geometry. Use `linked=false` when later edits may make the copies unique.

Do not write generated Python loops for simple rows, grids, or radial duplication. Use focused `execute_code` only when the pattern must follow a custom curve, irregular terrain, or semantic per-item variation that these direct tools cannot express.

### Connection Point Formulas

**Top of a vertical piece (pole, leg, column):**
Expand Down
256 changes: 256 additions & 0 deletions desktop/assets/vipermesh-addon.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ def _execute_command_internal(self, command):
"align_object_attachment_points": self.align_object_attachment_points,
"rename_object": self.rename_object,
"duplicate_object": self.duplicate_object,
"arrange_objects": self.arrange_objects,
"duplicate_object_pattern": self.duplicate_object_pattern,
"join_objects": self.join_objects,
"add_empty_object": self.add_empty_object,
"set_empty_properties": self.set_empty_properties,
Expand Down Expand Up @@ -1600,6 +1602,260 @@ def duplicate_object(self, name, new_name=None, linked=False):
except Exception as e:
return {"error": f"Failed to duplicate object: {str(e)}"}

def _pattern_positions(
self,
count,
layout="LINE",
center=None,
spacing=None,
axis="X",
radius=1.0,
plane="XY",
columns=None,
row_spacing=None,
start_angle_degrees=0.0,
):
if count < 1:
raise ValueError("count must be at least 1")

def vector3(value, fallback, label):
raw = fallback if value is None else value
if not isinstance(raw, (list, tuple)) or len(raw) != 3:
raise ValueError(f"{label} must be [x,y,z]")
return Vector((float(raw[0]), float(raw[1]), float(raw[2])))

resolved_center = vector3(center, (0.0, 0.0, 0.0), "center")
resolved_layout = str(layout or "LINE").upper()
resolved_axis = str(axis or "X").upper()
resolved_plane = str(plane or "XY").upper()

axis_vectors = {
"X": Vector((1.0, 0.0, 0.0)),
"Y": Vector((0.0, 1.0, 0.0)),
"Z": Vector((0.0, 0.0, 1.0)),
}
plane_axes = {
"XY": (Vector((1.0, 0.0, 0.0)), Vector((0.0, 1.0, 0.0))),
"XZ": (Vector((1.0, 0.0, 0.0)), Vector((0.0, 0.0, 1.0))),
"YZ": (Vector((0.0, 1.0, 0.0)), Vector((0.0, 0.0, 1.0))),
}

if resolved_layout == "LINE":
if isinstance(spacing, (list, tuple)):
step = vector3(spacing, (1.0, 0.0, 0.0), "spacing")
else:
if resolved_axis not in axis_vectors:
raise ValueError("axis must be X, Y, or Z")
step = axis_vectors[resolved_axis] * float(1.0 if spacing is None else spacing)
midpoint = (count - 1) / 2.0
return [resolved_center + step * (index - midpoint) for index in range(count)]

if resolved_layout == "CIRCLE":
if resolved_plane not in plane_axes:
raise ValueError("plane must be XY, XZ, or YZ")
basis_a, basis_b = plane_axes[resolved_plane]
resolved_radius = max(float(radius), 0.0)
start_angle = math.radians(float(start_angle_degrees or 0.0))
angle_step = (math.tau / count) if count > 0 else 0.0
return [
resolved_center
+ basis_a * (math.cos(start_angle + angle_step * index) * resolved_radius)
+ basis_b * (math.sin(start_angle + angle_step * index) * resolved_radius)
for index in range(count)
]

if resolved_layout == "GRID":
if resolved_plane not in plane_axes:
raise ValueError("plane must be XY, XZ, or YZ")
basis_a, basis_b = plane_axes[resolved_plane]
resolved_columns = int(columns or math.ceil(math.sqrt(count)))
if resolved_columns < 1:
raise ValueError("columns must be at least 1")
if isinstance(spacing, (list, tuple)):
step_a = float(spacing[0])
step_b = float(spacing[1] if len(spacing) > 1 else spacing[0])
else:
step_a = float(1.0 if spacing is None else spacing)
step_b = float(row_spacing if row_spacing is not None else step_a)
rows = math.ceil(count / resolved_columns)
x_midpoint = (resolved_columns - 1) / 2.0
y_midpoint = (rows - 1) / 2.0
positions = []
for index in range(count):
column = index % resolved_columns
row = index // resolved_columns
positions.append(
resolved_center
+ basis_a * ((column - x_midpoint) * step_a)
+ basis_b * ((row - y_midpoint) * step_b)
)
return positions

raise ValueError("layout must be LINE, CIRCLE, or GRID")

def _align_object_bottom_to_z(self, obj, target_z):
if target_z is None:
return
if not hasattr(obj, "bound_box") or not obj.bound_box:
obj.location.z = float(target_z)
return
bottom_z = min((obj.matrix_world @ Vector(corner)).z for corner in obj.bound_box)
obj.location.z += float(target_z) - bottom_z

def _move_object_to_pattern_position(self, obj, position, align_bottom_to_z=None):
matrix = obj.matrix_world.copy()
matrix.translation = Vector(position)
obj.matrix_world = matrix
self._align_object_bottom_to_z(obj, align_bottom_to_z)

def arrange_objects(
self,
names,
layout="LINE",
center=None,
spacing=None,
axis="X",
radius=1.0,
plane="XY",
columns=None,
row_spacing=None,
start_angle_degrees=0.0,
align_bottom_to_z=None,
):
"""Arrange existing objects into a line, circle, or grid without Python loops."""
try:
if not isinstance(names, (list, tuple)) or not names:
return {"error": "names must be a non-empty list"}
objects = []
for name in names:
obj = bpy.data.objects.get(str(name))
if not obj:
return {"error": f"Object not found: {name}"}
objects.append(obj)

positions = self._pattern_positions(
len(objects),
layout=layout,
center=center,
spacing=spacing,
axis=axis,
radius=radius,
plane=plane,
columns=columns,
row_spacing=row_spacing,
start_angle_degrees=start_angle_degrees,
)

arranged = []
for obj, position in zip(objects, positions):
self._move_object_to_pattern_position(obj, position, align_bottom_to_z)
arranged.append({
"name": obj.name,
"location": [round(value, 6) for value in obj.location],
})

return {
"success": True,
"layout": str(layout or "LINE").upper(),
"arranged_count": len(arranged),
"objects": arranged,
"next_safe_action": "validate_object_clearance or inspect with get_all_object_info after arranging objects",
}
except Exception as e:
return {"error": f"Failed to arrange objects: {str(e)}"}

def duplicate_object_pattern(
self,
name,
count=1,
layout="LINE",
name_prefix=None,
linked=False,
include_source=False,
center=None,
spacing=None,
axis="X",
radius=1.0,
plane="XY",
columns=None,
row_spacing=None,
start_angle_degrees=0.0,
align_bottom_to_z=None,
):
"""Duplicate one object into a repeated line, circle, or grid pattern."""
try:
source = bpy.data.objects.get(name)
if not source:
return {"error": f"Object not found: {name}"}
resolved_count = int(count or 1)
if resolved_count < 1 or resolved_count > 100:
return {"error": "count must be between 1 and 100"}

def coerce_bool(value, default=False):
if value is None:
return default
if isinstance(value, bool):
return value
if isinstance(value, str):
return value.strip().lower() in {"1", "true", "yes", "on"}
return bool(value)

resolved_linked = coerce_bool(linked, False)
resolved_include_source = coerce_bool(include_source, False)
objects_to_place = [source] if resolved_include_source else []
copies_to_create = resolved_count - len(objects_to_place)
if copies_to_create < 0:
copies_to_create = 0

collection = source.users_collection[0] if source.users_collection else bpy.context.collection
prefix = str(name_prefix or source.name)
created = []
for index in range(copies_to_create):
new_obj = source.copy()
if source.data and not resolved_linked:
new_obj.data = source.data.copy()
new_obj.name = f"{prefix}_{index + 1:02d}" if not resolved_include_source else f"{prefix}_{index + 2:02d}"
if new_obj.data and not resolved_linked:
new_obj.data.name = new_obj.name
new_obj.animation_data_clear()
collection.objects.link(new_obj)
objects_to_place.append(new_obj)
created.append(new_obj.name)

positions = self._pattern_positions(
len(objects_to_place),
layout=layout,
center=center,
spacing=spacing,
axis=axis,
radius=radius,
plane=plane,
columns=columns,
row_spacing=row_spacing,
start_angle_degrees=start_angle_degrees,
)

placed = []
for obj, position in zip(objects_to_place, positions):
self._move_object_to_pattern_position(obj, position, align_bottom_to_z)
placed.append({
"name": obj.name,
"location": [round(value, 6) for value in obj.location],
})

return {
"success": True,
"source": source.name,
"layout": str(layout or "LINE").upper(),
"linked": resolved_linked,
"include_source": resolved_include_source,
"created": created,
"placed": placed,
"next_safe_action": "validate_object_clearance or inspect with get_all_object_info after duplicating the pattern",
}
except Exception as e:
return {"error": f"Failed to duplicate object pattern: {str(e)}"}

def join_objects(self, names):
"""Join multiple objects into one. First name becomes the active (target) object."""
try:
Expand Down
4 changes: 3 additions & 1 deletion docs/blender-mcp-capability-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
## Summary

- Official Blender MCP tools found: 26
- ViperMesh addon commands found: 86
- ViperMesh addon commands found: 88
- Decision: keep ViperMesh addon as the product connector, use official MCP as a benchmark/reference, and build deterministic ViperMesh tools where both sides currently rely on Python.

## Capability Matrix
Expand Down Expand Up @@ -70,6 +70,7 @@
- `aim_camera_at`
- `apply_modifier`
- `apply_transforms`
- `arrange_objects`
- `assign_material`
- `configure_constraint`
- `configure_modifier`
Expand All @@ -81,6 +82,7 @@
- `download_polyhaven_asset`
- `download_sketchfab_model`
- `duplicate_object`
- `duplicate_object_pattern`
- `execute_code`
- `export_object`
- `focus_viewport_on_objects`
Expand Down
Loading
Loading