diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..901878c --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,32 @@ +name: Publish connector overview + +on: + push: + branches: [main] + paths: ["site/**", ".github/workflows/pages.yml"] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: github-pages + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/checkout@v6 + - uses: actions/configure-pages@v5 + - uses: actions/upload-pages-artifact@v4 + with: + path: site + - uses: actions/deploy-pages@v4 + id: deployment diff --git a/CHANGELOG.md b/CHANGELOG.md index 186e002..df3c220 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,37 @@ All notable changes to ViperMesh for Blender are documented here. ## [Unreleased] +## [1.3.0] - 2026-10-07 + +### Added + +- Repository-connected GitHub Pages overview, setup and honest capability limits. +- Inline bounded PNG/JPEG output for image-aware MCP clients. +- Geometry/deformation diagnostics and guarded addon operations already present + in the current Blender tool surface, synchronized into the public distribution. + +### Changed + +- Finalize can preserve existing camera/lighting and render without saving a blend + file. Named spatial failures block final output and retain repair details. +- Removed fixed preview counts; require task-based visual review and disclose + unresolved defects instead of treating file health as a quality verdict. +- Blend saves are explicit and refuse existing destinations in staged finalize. +- Updated the public dependency lock and SDK floor; clean install audit is clear. +- Package validation checks every shipped skill reference. +- Corrected retarget rollback after backup or export failure, partial-import + cleanup, cancelled operator handling, enum-flag argument conversion and + implementation-aware addon fingerprints. Added Blender runtime regressions. + +### Changed + +- Replaced the exported private tool-guide corpus with a concise, installable + public agent skill and adaptable reference documents. +- Added explicit native stdio client setup and clarified the optional, + not-yet-supported Docker MCP Toolkit route. +- Added export checks that prevent private `data/tool-guides` content from + entering public releases. + ## [1.2.0] - 2026-07-25 ### Added @@ -21,5 +52,6 @@ All notable changes to ViperMesh for Blender are documented here. - Explicit `execute_code` fallback for genuinely custom Blender work. - Local-only loopback transport and public release validation. -[Unreleased]: https://github.com/Ker102/vipermesh-blender/compare/v1.2.0...HEAD +[Unreleased]: https://github.com/Ker102/vipermesh-blender/compare/v1.3.0...HEAD +[1.3.0]: https://github.com/Ker102/vipermesh-blender/compare/v1.2.0...v1.3.0 [1.2.0]: https://github.com/Ker102/vipermesh-blender/releases/tag/v1.2.0 diff --git a/README.md b/README.md index b45930d..292da6a 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,9 @@ **A persistent Model Context Protocol server and deterministic Blender addon for AI agents.** +[Project overview and setup](https://ker102.github.io/vipermesh-blender/) +| [ViperMesh Studio waitlist](https://vipermesh-studio.vercel.app/waitlist) + ViperMesh for Blender gives MCP-compatible agents structured scene inspection, reusable Blender operations, bounded batches, local 3D workflow guidance, and an explicit Python fallback. It is designed to reduce repeated Blender API @@ -25,7 +28,7 @@ broad deterministic tool surface for common operations. | Deterministic tools | Structured operations for assembly, materials, cameras, lighting, rendering, rigging, UVs, retopology, export, and inspection | | Scene validation | Grounding, support, clearance, spatial-relation, file-health, and render-artifact checks | | Compact workflows | Bounded batch calls plus separate build, inspect/repair, and finalize stages | -| Agent onboarding | Session bootstrap, MCP resources, concise operating rules, and local task guides | +| Agent onboarding | Session bootstrap, MCP resources, concise operating rules, and portable tool skills | | Custom fallback | `execute_code` remains available when a deterministic operation is not the right tool | ## Architecture @@ -59,7 +62,7 @@ loopback by default and reports **Stopped**, **Ready**, **Agent connected**, or ### 1. Install the Blender addon -Download `vipermesh-addon.py` from the +Download the versioned addon `.py` or addon `.zip` from the [latest release](https://github.com/Ker102/vipermesh-blender/releases/latest). In Blender: @@ -84,22 +87,25 @@ npm run build ### 3. Configure your MCP client -Use the absolute repository path: +Use the built entry point from an absolute path: ```json { "mcpServers": { "vipermesh-blender": { - "command": "npm", - "args": ["run", "mcp"], - "cwd": "C:/absolute/path/to/vipermesh-blender" + "command": "node", + "args": [ + "C:/absolute/path/to/vipermesh-blender/dist/public-portable-blender-mcp.js" + ] } } } ``` Start this command once through the MCP client. Do not invoke `npm`, `npx`, or -`tsx` again for each Blender operation. +`tsx` again for each Blender operation. See [MCP client setup](docs/client-setup.md) +for Codex, JSON-configured clients, compatibility, and the optional Docker MCP +Toolkit route. ## First Agent Calls @@ -122,7 +128,9 @@ The public MCP surface includes: - `get_3d_guidance_document` Read the [complete connector manual](docs/portable-blender-mcp.md) for request -shapes, batching rules, staged workflows, local guidance, and troubleshooting. +shapes, batching rules, staged workflows, local tool skills, and +troubleshooting. The repository also ships an installable +[`using-vipermesh-blender` agent skill](skills/using-vipermesh-blender/SKILL.md). ## Security @@ -136,7 +144,7 @@ reporting process. ## Public Connector Scope This repository contains the open-source Blender addon, portable MCP server, -local operating guidance, and connector tests. It does not include the +portable public tool skills, and connector tests. It does not include the ViperMesh application, authentication, billing, private prompts, private RAG data, cloud model routing, private assets, or benchmark evidence. @@ -157,6 +165,13 @@ unusual node graphs, and uncovered workflows. The process retains a serialized connection to Blender. Relaunching it for every call adds avoidable startup, transport, and agent-tool overhead. +### Does it require Docker? + +No. Local stdio-capable MCP clients launch the Node server directly. Docker MCP +Toolkit is an optional packaging and gateway route, and is not yet a supported +ViperMesh installation path because host-to-Blender loopback connectivity still +requires cross-platform validation. + ### Does the public connector require ViperMesh cloud authentication? No. The public addon and local MCP server work without ViperMesh authentication. diff --git a/addon/vipermesh-addon.py b/addon/vipermesh-addon.py index 3faf017..d9db359 100644 --- a/addon/vipermesh-addon.py +++ b/addon/vipermesh-addon.py @@ -16,19 +16,28 @@ import shutil import zipfile import itertools +import hashlib +import inspect +import marshal +import uuid from array import array from mathutils import Vector +from mathutils.bvhtree import BVHTree +from mathutils.kdtree import KDTree from bpy.props import StringProperty, IntProperty, BoolProperty, EnumProperty import io from contextlib import redirect_stdout, suppress -ADDON_VERSION = (1, 2, 0) +ADDON_VERSION = (1, 3, 0) ADDON_VERSION_LABEL = ".".join(str(part) for part in ADDON_VERSION) +MAX_POSE_DEFORMATION_WORK = 2_000_000 +MAX_MESH_SURFACE_COMPARISON_FACES = 500_000 +MAX_MESH_SURFACE_COMPARISON_VERTICES = 1_000_000 bl_info = { "name": "ViperMesh for Blender", "author": "ViperMesh Team", - "version": (1, 2, 0), + "version": (1, 3, 0), "blender": (3, 0, 0), "location": "View3D > Sidebar > ViperMesh", "description": "Persistent local Blender bridge for ViperMesh and MCP-compatible agents", @@ -257,6 +266,7 @@ def _execute_command_internal(self, command): "set_active_collection": self.set_active_collection, "execute_code": self.execute_code, "save_blend_file": self.save_blend_file, + "restore_blend_checkpoint": self.restore_blend_checkpoint, "list_materials": self.list_materials, "delete_object": self.delete_object, "delete_objects": self.delete_objects, @@ -264,6 +274,7 @@ def _execute_command_internal(self, command): "align_object_to_surface": self.align_object_to_surface, "validate_object_clearance": self.validate_object_clearance, "inspect_spatial_relations": self.inspect_spatial_relations, + "inspect_mesh_intersections": self.inspect_mesh_intersections, "inspect_scene_grounding": self.inspect_scene_grounding, "align_object_attachment_points": self.align_object_attachment_points, "rename_object": self.rename_object, @@ -286,6 +297,7 @@ def _execute_command_internal(self, command): "validate_mesh_geometry": self.validate_mesh_geometry, "repair_mesh_geometry": self.repair_mesh_geometry, "inspect_retopology_readiness": self.inspect_retopology_readiness, + "compare_mesh_surfaces": self.compare_mesh_surfaces, "decimate_mesh": self.decimate_mesh, "voxel_remesh_mesh": self.voxel_remesh_mesh, "quadriflow_remesh_mesh": self.quadriflow_remesh_mesh, @@ -294,6 +306,7 @@ def _execute_command_internal(self, command): "inspect_edit_bone_alignment": self.inspect_edit_bone_alignment, "set_edit_bone_alignment": self.set_edit_bone_alignment, "inspect_weight_paint_readiness": self.inspect_weight_paint_readiness, + "inspect_pose_deformation": self.inspect_pose_deformation, "normalize_vertex_group_weights": self.normalize_vertex_group_weights, "create_rigify_metarig": self.create_rigify_metarig, "generate_rigify_rig": self.generate_rigify_rig, @@ -301,6 +314,7 @@ def _execute_command_internal(self, command): "transfer_vertex_group_weights": self.transfer_vertex_group_weights, "project_vertex_group_weights": self.project_vertex_group_weights, "inspect_animation_data": self.inspect_animation_data, + "retarget_animation_clip": self.retarget_animation_clip, "inspect_shape_keys": self.inspect_shape_keys, "extract_shape_key_to_object": self.extract_shape_key_to_object, "create_shape_key_from_object": self.create_shape_key_from_object, @@ -334,8 +348,12 @@ def _execute_command_internal(self, command): "validate_export_readiness": self.validate_export_readiness, "inspect_export_texture_dependencies": self.inspect_export_texture_dependencies, "export_asset_package": self.export_asset_package, + "validate_gltf_roundtrip": self.validate_gltf_roundtrip, + "get_vipermesh_runtime_identity": self.get_vipermesh_runtime_identity, "export_object": self.export_object, "list_installed_addons": self.list_installed_addons, + "inspect_addon_capabilities": self.inspect_addon_capabilities, + "invoke_addon_capability": self.invoke_addon_capability, "create_material": self.create_material, "assign_material": self.assign_material, "create_material_preset": self.create_material_preset, @@ -743,6 +761,17 @@ def record_external_file(kind, name, raw_path, packed=False, library=None): return { "status": health_status, + "scene_evidence": { + key: bpy.context.scene.get(key) + for key in ( + "vipermesh_fixture_id", + "vipermesh_fixture_source", + "vipermesh_fixture_license", + "vipermesh_asset_glb_sha256", + "vipermesh_candidate_glb_sha256", + ) + if bpy.context.scene.get(key) is not None + }, "file": { "filepath": filepath, "directory": os.path.dirname(filepath) if filepath else "", @@ -1362,7 +1391,7 @@ def execute_code(self, code): except Exception as e: raise Exception(f"Code execution error: {str(e)}") - def save_blend_file(self, filepath, make_dirs=True, check_existing=False): + def save_blend_file(self, filepath, make_dirs=True, check_existing=False, copy=False): """Save the current .blend file to an explicit path.""" try: if not filepath or not str(filepath).strip(): @@ -1382,11 +1411,12 @@ def save_blend_file(self, filepath, make_dirs=True, check_existing=False): if check_existing and os.path.exists(resolved_path): return {"error": f"File already exists: {resolved_path}. Set check_existing=false to overwrite."} - bpy.ops.wm.save_as_mainfile(filepath=resolved_path, check_existing=bool(check_existing)) + bpy.ops.wm.save_as_mainfile(filepath=resolved_path, check_existing=bool(check_existing), copy=bool(copy)) return { "success": True, "filepath": resolved_path, + "copy": bool(copy), "exists": os.path.exists(resolved_path), "size": os.path.getsize(resolved_path) if os.path.exists(resolved_path) else None, "next_safe_action": "attach this .blend path as benchmark or export evidence", @@ -1394,6 +1424,44 @@ def save_blend_file(self, filepath, make_dirs=True, check_existing=False): except Exception as e: return {"error": f"Failed to save blend file: {str(e)}"} + def restore_blend_checkpoint(self, checkpoint_filepath, original_filepath): + """Restore a saved checkpoint and return Blender to the original .blend path.""" + try: + checkpoint_path = os.path.abspath( + os.path.expanduser(str(checkpoint_filepath)) + ) + original_path = os.path.abspath( + os.path.expanduser(str(original_filepath)) + ) + if not checkpoint_path.lower().endswith(".blend"): + return {"error": "checkpoint_filepath must end with .blend"} + if not original_path.lower().endswith(".blend"): + return {"error": "original_filepath must end with .blend"} + if not os.path.isfile(checkpoint_path): + return { + "error": f"Checkpoint file does not exist: {checkpoint_path}" + } + + bpy.ops.wm.open_mainfile(filepath=checkpoint_path) + bpy.ops.wm.save_as_mainfile( + filepath=original_path, + check_existing=False, + ) + return { + "success": True, + "restored": True, + "checkpoint_filepath": checkpoint_path, + "filepath": bpy.data.filepath, + "original_filepath": original_path, + "dirty": bool(getattr(bpy.data, "is_dirty", False)), + "next_safe_action": "re-run inspect_blend_file_health before retrying the failed operation", + } + except Exception as e: + return { + "error": f"Failed to restore blend checkpoint: {str(e)}", + "restored": False, + } + def list_materials(self): """List all materials in the .blend file with their node counts and linked objects""" try: @@ -1983,6 +2051,135 @@ def relation_passes(relation_type, subject, reference, other, relation): except Exception as e: return {"error": f"Failed to inspect spatial relations: {str(e)}"} + def inspect_mesh_intersections(self, pairs=None, max_intersections=10000, epsilon=0.000001): + """Inspect evaluated mesh-surface intersections for explicit object pairs.""" + try: + if pairs is None: + pairs = [] + if isinstance(pairs, str): + pairs = json.loads(pairs) + if not isinstance(pairs, list) or not pairs: + return {"error": "pairs must be a non-empty list of {subject, reference} objects"} + if len(pairs) > 100: + return {"error": "pairs may contain at most 100 checks"} + + resolved_limit = max(1, min(int(max_intersections), 100000)) + resolved_epsilon = max(0.0, min(float(epsilon), 1.0)) + depsgraph = bpy.context.evaluated_depsgraph_get() + cache = {} + + def object_tree(obj): + if obj.name in cache: + return cache[obj.name] + evaluated = obj.evaluated_get(depsgraph) + mesh = evaluated.to_mesh() + try: + mesh.calc_loop_triangles() + vertices = [ + evaluated.matrix_world @ vertex.co + for vertex in mesh.vertices + ] + polygons = [ + tuple(int(index) for index in triangle.vertices) + for triangle in mesh.loop_triangles + ] + tree = BVHTree.FromPolygons( + vertices, + polygons, + all_triangles=True, + epsilon=resolved_epsilon, + ) + finally: + evaluated.to_mesh_clear() + cache[obj.name] = tree + return tree + + checked = [] + missing_objects = [] + for index, pair in enumerate(pairs): + if not isinstance(pair, dict): + checked.append({ + "index": index, + "status": "fail", + "error": "pair must be an object", + }) + continue + subject_name = str( + pair.get("subject") or pair.get("name") or "" + ).strip() + reference_name = str( + pair.get("reference") or pair.get("other_name") or "" + ).strip() + subject = bpy.data.objects.get(subject_name) + reference = bpy.data.objects.get(reference_name) + missing = [ + name + for name, obj in ( + (subject_name or "subject", subject), + (reference_name or "reference", reference), + ) + if obj is None + ] + if missing: + missing_objects.extend(missing) + checked.append({ + "index": index, + "subject": subject_name, + "reference": reference_name, + "status": "fail", + "missing": missing, + }) + continue + if subject.type != "MESH" or reference.type != "MESH": + checked.append({ + "index": index, + "subject": subject_name, + "reference": reference_name, + "status": "fail", + "error": "both objects must be meshes", + }) + continue + if subject == reference: + checked.append({ + "index": index, + "subject": subject_name, + "reference": reference_name, + "status": "fail", + "error": "subject and reference must be different objects", + }) + continue + + overlaps = object_tree(subject).overlap(object_tree(reference)) + intersection_count = len(overlaps) + checked.append({ + "index": index, + "subject": subject_name, + "reference": reference_name, + "status": "pass" if intersection_count == 0 else "fail", + "intersects": intersection_count > 0, + "intersection_count": intersection_count, + "sample_polygon_pairs": [ + [int(subject_polygon), int(reference_polygon)] + for subject_polygon, reference_polygon + in overlaps[:resolved_limit] + ], + "intersections_truncated": intersection_count > resolved_limit, + }) + + failed = [item for item in checked if item.get("status") != "pass"] + return { + "success": len(missing_objects) == 0, + "ready": len(failed) == 0, + "checked_count": len(checked), + "pass_count": len(checked) - len(failed), + "fail_count": len(failed), + "missing_objects": sorted(set(missing_objects)), + "pairs": checked, + "next_safe_action": "fix intersecting mesh pairs, then run inspect_mesh_intersections again", + } + except Exception as e: + return {"error": f"Failed to inspect mesh intersections: {str(e)}"} + def inspect_scene_grounding(self, names=None, ground_z=0.0, tolerance=0.02, max_objects=100, include_supports=True): """Report mesh objects whose lower bounds float above ground or nearby support surfaces.""" try: @@ -4369,7 +4566,13 @@ def _mesh_geometry_report(self, obj, cleanup=False): "boundary_edges": boundary_edges, "non_manifold_edges": non_manifold_edges, "loose_edges": loose_edges, - "valid": not validate_changed and zero_area_faces == 0 and non_manifold_edges == 0, + "valid": ( + not validate_changed + and zero_area_faces == 0 + and boundary_edges == 0 + and non_manifold_edges == 0 + and loose_edges == 0 + ), "cleanup_applied": bool(cleanup), } finally: @@ -4875,10 +5078,12 @@ def inspect_retopology_readiness(self, names=None, max_objects=120, high_density triangle_faces = 0 quad_faces = 0 ngon_faces = 0 + evaluated_triangles = 0 material_indices = set() smooth_faces = 0 for poly in mesh.polygons: sides = len(poly.vertices) + evaluated_triangles += max(0, sides - 2) if sides == 3: triangle_faces += 1 elif sides == 4: @@ -4922,6 +5127,7 @@ def inspect_retopology_readiness(self, names=None, max_objects=120, high_density "vertices": len(mesh.vertices), "edges": len(mesh.edges), "faces": face_count, + "evaluated_triangles": evaluated_triangles, "triangle_faces": triangle_faces, "quad_faces": quad_faces, "ngon_faces": ngon_faces, @@ -4954,6 +5160,286 @@ def inspect_retopology_readiness(self, names=None, max_objects=120, high_density except Exception as e: return {"error": f"Failed to inspect retopology readiness: {str(e)}"} + def compare_mesh_surfaces( + self, + source_name, + candidate_name, + distance_tolerance=0.01, + seam_tolerance=None, + max_surface_samples=10000, + max_attribute_samples=10000, + ): + """Compare two evaluated meshes without changing either object.""" + try: + source = bpy.data.objects.get(str(source_name)) + candidate = bpy.data.objects.get(str(candidate_name)) + missing = [ + name for name, obj in ( + (str(source_name), source), + (str(candidate_name), candidate), + ) + if obj is None + ] + if missing: + return {"success": False, "error": "Objects not found", "missing_names": missing} + invalid = [obj.name for obj in (source, candidate) if obj.type != "MESH"] + if invalid: + return { + "success": False, + "error": "Mesh surface comparison only supports mesh objects", + "invalid_names": invalid, + } + if source == candidate: + return { + "success": False, + "error": "source_name and candidate_name must identify different objects", + } + + distance_tolerance = float(distance_tolerance) + if not math.isfinite(distance_tolerance) or distance_tolerance < 0: + return {"success": False, "error": "distance_tolerance must be a finite non-negative number"} + seam_tolerance = distance_tolerance if seam_tolerance is None else float(seam_tolerance) + if not math.isfinite(seam_tolerance) or seam_tolerance < 0: + return {"success": False, "error": "seam_tolerance must be a finite non-negative number"} + max_surface_samples = max(1, min(int(max_surface_samples), 20000)) + max_attribute_samples = max(1, min(int(max_attribute_samples), 20000)) + + depsgraph = bpy.context.evaluated_depsgraph_get() + + def snapshot(obj): + evaluated = obj.evaluated_get(depsgraph) + mesh = evaluated.to_mesh() + try: + if len(mesh.polygons) > MAX_MESH_SURFACE_COMPARISON_FACES: + raise ValueError( + f"{obj.name} exceeds the {MAX_MESH_SURFACE_COMPARISON_FACES} evaluated-face comparison limit" + ) + if len(mesh.vertices) > MAX_MESH_SURFACE_COMPARISON_VERTICES: + raise ValueError( + f"{obj.name} exceeds the {MAX_MESH_SURFACE_COMPARISON_VERTICES} evaluated-vertex comparison limit" + ) + matrix = evaluated.matrix_world + vertices = [tuple(matrix @ vertex.co) for vertex in mesh.vertices] + polygons = [tuple(polygon.vertices) for polygon in mesh.polygons] + polygon_centers = [] + material_names = [] + for polygon in mesh.polygons: + polygon_centers.append(tuple(matrix @ polygon.center)) + material = ( + evaluated.material_slots[polygon.material_index].material + if polygon.material_index < len(evaluated.material_slots) + else None + ) + material_names.append(material.name if material else None) + seam_segments = [] + for edge in mesh.edges: + if not bool(getattr(edge, "use_seam", False)): + continue + start, end = edge.vertices + start_point = matrix @ mesh.vertices[start].co + end_point = matrix @ mesh.vertices[end].co + seam_segments.append({ + "start": tuple(start_point), + "end": tuple(end_point), + "midpoint": tuple((start_point + end_point) * 0.5), + }) + return { + "vertices": vertices, + "polygons": polygons, + "polygon_centers": polygon_centers, + "material_names": material_names, + "seam_segments": seam_segments, + } + finally: + evaluated.to_mesh_clear() + + def sample_indices(count, maximum): + if count <= maximum: + return list(range(count)) + return sorted({ + min(count - 1, int(index * count / maximum)) + for index in range(maximum) + }) + + def build_bvh(snapshot): + if not snapshot["vertices"] or not snapshot["polygons"]: + raise ValueError("Both objects need evaluated polygon surfaces") + return BVHTree.FromPolygons( + snapshot["vertices"], + snapshot["polygons"], + all_triangles=False, + ) + + def nearest_distances(points, tree, maximum): + distances = [] + for index in sample_indices(len(points), maximum): + nearest = tree.find_nearest(Vector(points[index])) + if nearest is None or nearest[3] is None: + raise ValueError("Nearest-surface comparison failed") + distances.append(float(nearest[3])) + return distances + + def polygon_center(snapshot, polygon_index): + return Vector(snapshot["polygon_centers"][polygon_index]) + + def material_mismatches(origin, target, target_tree): + mismatches = 0 + compared = 0 + for polygon_index in sample_indices( + len(origin["polygons"]), + max_attribute_samples, + ): + nearest = target_tree.find_nearest( + polygon_center(origin, polygon_index), + ) + target_index = nearest[2] if nearest else None + if target_index is None: + mismatches += 1 + elif ( + origin["material_names"][polygon_index] + != target["material_names"][target_index] + ): + mismatches += 1 + compared += 1 + return mismatches, compared + + def seam_mismatches(origin_segments, target_segments): + sampled = sample_indices(len(origin_segments), max_attribute_samples) + if not sampled: + return 0, 0 + if not target_segments: + return len(sampled), len(sampled) + tree = KDTree(len(target_segments)) + for index, segment in enumerate(target_segments): + tree.insert(segment["midpoint"], index) + tree.balance() + mismatches = 0 + matched_target_indices = set() + for index in sampled: + origin = origin_segments[index] + matches = sorted( + tree.find_range( + origin["midpoint"], + max(seam_tolerance, 1e-12), + ), + key=lambda match: float(match[2]), + ) + matched = False + for _co, target_index, _distance in matches: + if target_index in matched_target_indices: + continue + target = target_segments[target_index] + direct = max( + (Vector(origin["start"]) - Vector(target["start"])).length, + (Vector(origin["end"]) - Vector(target["end"])).length, + ) + reversed_match = max( + (Vector(origin["start"]) - Vector(target["end"])).length, + (Vector(origin["end"]) - Vector(target["start"])).length, + ) + if min(direct, reversed_match) <= seam_tolerance: + matched_target_indices.add(target_index) + matched = True + break + if not matched: + mismatches += 1 + return mismatches, len(sampled) + + source_snapshot = snapshot(source) + candidate_snapshot = snapshot(candidate) + source_tree = build_bvh(source_snapshot) + candidate_tree = build_bvh(candidate_snapshot) + + source_surface_points = ( + source_snapshot["vertices"] + source_snapshot["polygon_centers"] + ) + candidate_surface_points = ( + candidate_snapshot["vertices"] + candidate_snapshot["polygon_centers"] + ) + source_distances = nearest_distances( + source_surface_points, + candidate_tree, + max_surface_samples, + ) + candidate_distances = nearest_distances( + candidate_surface_points, + source_tree, + max_surface_samples, + ) + all_distances = source_distances + candidate_distances + + source_material_mismatches, source_material_compared = material_mismatches( + source_snapshot, + candidate_snapshot, + candidate_tree, + ) + candidate_material_mismatches, candidate_material_compared = material_mismatches( + candidate_snapshot, + source_snapshot, + source_tree, + ) + source_seam_mismatches, source_seams_compared = seam_mismatches( + source_snapshot["seam_segments"], + candidate_snapshot["seam_segments"], + ) + candidate_seam_mismatches, candidate_seams_compared = seam_mismatches( + candidate_snapshot["seam_segments"], + source_snapshot["seam_segments"], + ) + + max_surface_deviation = max(all_distances) if all_distances else 0.0 + return { + "success": True, + "policy_version": "mesh-surface-comparison-v1", + "source_object": source.name, + "candidate_object": candidate.name, + "distance_tolerance": distance_tolerance, + "seam_tolerance": seam_tolerance, + "surface_sampling_mode": "vertices-and-polygon-centers", + "source_vertex_count": len(source_snapshot["vertices"]), + "candidate_vertex_count": len(candidate_snapshot["vertices"]), + "source_polygon_count": len(source_snapshot["polygons"]), + "candidate_polygon_count": len(candidate_snapshot["polygons"]), + "max_surface_deviation": max_surface_deviation, + "mean_surface_deviation": ( + sum(all_distances) / len(all_distances) if all_distances else 0.0 + ), + "surface_deviation_exceeds_tolerance": ( + max_surface_deviation > distance_tolerance + ), + "surface_sample_count": len(all_distances), + "surface_sampling_complete": ( + len(source_surface_points) <= max_surface_samples + and len(candidate_surface_points) <= max_surface_samples + ), + "material_sample_count": ( + source_material_compared + candidate_material_compared + ), + "material_sampling_complete": ( + len(source_snapshot["polygons"]) <= max_attribute_samples + and len(candidate_snapshot["polygons"]) <= max_attribute_samples + ), + "material_boundary_mismatch_count": ( + source_material_mismatches + candidate_material_mismatches + ), + "source_uv_seam_count": len(source_snapshot["seam_segments"]), + "candidate_uv_seam_count": len(candidate_snapshot["seam_segments"]), + "uv_seam_sample_count": source_seams_compared + candidate_seams_compared, + "uv_seam_sampling_complete": ( + len(source_snapshot["seam_segments"]) <= max_attribute_samples + and len(candidate_snapshot["seam_segments"]) <= max_attribute_samples + ), + "uv_seam_mismatch_count": ( + source_seam_mismatches + candidate_seam_mismatches + ), + "note": ( + "This is bounded geometric evidence. The artist decides whether " + "the candidate preserves the intended visual result." + ), + } + except Exception as e: + return {"success": False, "error": f"Failed to compare mesh surfaces: {str(e)}"} + def decimate_mesh( self, name, @@ -7542,6 +8028,285 @@ def restore_existing_groups(): except Exception as e: return {"error": f"Failed to project vertex group weights: {str(e)}"} + def inspect_pose_deformation( + self, + names=None, + baseline_frame=None, + stress_frames=None, + max_objects=20, + max_vertices_sample=10000, + max_edges_sample=20000, + edge_ratio_threshold=2.0, + displacement_epsilon=0.0001, + ): + """Inspect evaluated rigged meshes across frames without changing persistent scene data.""" + scene = bpy.context.scene + original_frame = scene.frame_current + original_subframe = scene.frame_subframe + reports = [] + try: + def parse_names(raw_names): + if raw_names is None: + return [] + if isinstance(raw_names, str): + return [part.strip() for part in raw_names.split(",") if part.strip()] + if isinstance(raw_names, (list, tuple)): + return [str(name).strip() for name in raw_names if str(name).strip()] + raise ValueError("names must be an array of mesh object names, a comma-separated string, or omitted") + + def parse_frames(raw_frames): + if raw_frames is None: + return [int(scene.frame_current)] + if isinstance(raw_frames, (int, float)): + return [int(raw_frames)] + if isinstance(raw_frames, str): + raw_frames = [part.strip() for part in raw_frames.split(",") if part.strip()] + if not isinstance(raw_frames, (list, tuple)): + raise ValueError("stress_frames must be an array of frame numbers, a comma-separated string, or omitted") + frames = [] + for value in raw_frames: + frame = int(value) + if frame not in frames: + frames.append(frame) + if not frames: + raise ValueError("stress_frames must contain at least one frame") + return frames[:24] + + requested_names = parse_names(names) + baseline_frame = int(scene.frame_start if baseline_frame is None else baseline_frame) + stress_frames = parse_frames(stress_frames) + if baseline_frame in stress_frames: + return { + "success": False, + "error": "baseline_frame must differ from every stress frame", + "baseline_frame": baseline_frame, + "stress_frames": stress_frames, + } + max_objects = max(1, min(int(max_objects), 20)) + max_vertices_sample = max(1, min(int(max_vertices_sample), 50000)) + max_edges_sample = max(1, min(int(max_edges_sample), 100000)) + edge_ratio_threshold = max(1.0, min(float(edge_ratio_threshold), 100.0)) + displacement_epsilon = max(0.0, float(displacement_epsilon)) + + if requested_names: + if len(requested_names) != len(set(requested_names)): + return {"success": False, "error": "names must not contain duplicates"} + if len(requested_names) > max_objects: + return { + "success": False, + "error": f"names exceeds max_objects ({max_objects})", + } + missing = [name for name in requested_names if bpy.data.objects.get(name) is None] + if missing: + return { + "error": "Objects not found", + "missing_names": missing, + "original_frame": original_frame, + } + objects = [bpy.data.objects[name] for name in requested_names] + invalid = [obj.name for obj in objects if obj.type != "MESH"] + if invalid: + return { + "error": "Pose deformation inspection only supports mesh objects", + "invalid_names": invalid, + "original_frame": original_frame, + } + else: + objects = [ + obj for obj in scene.objects + if obj.type == "MESH" and any(mod.type == "ARMATURE" for mod in obj.modifiers) + ][:max_objects] + + if not objects: + return { + "success": False, + "error": "No rigged mesh objects found", + "baseline_frame": baseline_frame, + "stress_frames": stress_frames, + "original_frame": original_frame, + } + + def valid_armature_modifiers(obj): + return [ + modifier for modifier in obj.modifiers + if ( + modifier.type == "ARMATURE" + and modifier.show_viewport + and modifier.object is not None + and modifier.object.type == "ARMATURE" + ) + ] + + invalid_rigged_meshes = [ + obj.name for obj in objects if not valid_armature_modifiers(obj) + ] + if invalid_rigged_meshes: + return { + "success": False, + "error": "Every inspected mesh needs an enabled Armature modifier with a valid armature target", + "invalid_rigged_meshes": invalid_rigged_meshes, + } + + estimated_work = ( + len(objects) + * len(stress_frames) + * (max_vertices_sample + max_edges_sample) + ) + if estimated_work > MAX_POSE_DEFORMATION_WORK: + return { + "success": False, + "error": "Requested pose deformation inspection exceeds the bounded work budget", + "estimated_work": estimated_work, + "max_work": MAX_POSE_DEFORMATION_WORK, + } + + depsgraph = bpy.context.evaluated_depsgraph_get() + + def evaluated_snapshot(obj): + evaluated = obj.evaluated_get(depsgraph) + mesh = evaluated.to_mesh() + try: + vertices = [ + tuple(vertex.co) + for vertex in itertools.islice(mesh.vertices, max_vertices_sample) + ] + edges = [ + tuple(edge.vertices) + for edge in itertools.islice(mesh.edges, max_edges_sample) + ] + return { + "vertex_count": len(mesh.vertices), + "edge_count": len(mesh.edges), + "vertices": vertices, + "edges": edges, + } + finally: + evaluated.to_mesh_clear() + + scene.frame_set(baseline_frame) + bpy.context.view_layer.update() + baselines = {obj.name: evaluated_snapshot(obj) for obj in objects} + + threshold_failure_count = 0 + reports_by_name = { + obj.name: { + "name": obj.name, + "baseline_vertex_count": baselines[obj.name]["vertex_count"], + "baseline_edge_count": baselines[obj.name]["edge_count"], + "sampled_vertices_truncated": ( + baselines[obj.name]["vertex_count"] > len(baselines[obj.name]["vertices"]) + ), + "sampled_edges_truncated": ( + baselines[obj.name]["edge_count"] > len(baselines[obj.name]["edges"]) + ), + "frames": [], + } + for obj in objects + } + for frame in stress_frames: + scene.frame_set(frame) + bpy.context.view_layer.update() + for obj in objects: + baseline = baselines[obj.name] + current = evaluated_snapshot(obj) + topology_changed = ( + current["vertex_count"] != baseline["vertex_count"] + or current["edge_count"] != baseline["edge_count"] + ) + invalid_coordinate_count = 0 + deformed_vertex_count = 0 + displacement_total = 0.0 + max_displacement = 0.0 + compared_vertices = min(len(baseline["vertices"]), len(current["vertices"])) + for index in range(compared_vertices): + base_co = baseline["vertices"][index] + current_co = current["vertices"][index] + if not all(math.isfinite(value) for value in current_co): + invalid_coordinate_count += 1 + continue + displacement = math.sqrt(sum( + (current_co[axis] - base_co[axis]) ** 2 for axis in range(3) + )) + displacement_total += displacement + max_displacement = max(max_displacement, displacement) + if displacement > displacement_epsilon: + deformed_vertex_count += 1 + + outlier_edge_ratio_count = 0 + max_symmetric_edge_ratio = 1.0 + compared_edges = 0 + if not topology_changed: + for edge_index, (start, end) in enumerate(baseline["edges"]): + if edge_index >= len(current["edges"]) or current["edges"][edge_index] != (start, end): + topology_changed = True + break + if start >= compared_vertices or end >= compared_vertices: + continue + base_length = math.dist(baseline["vertices"][start], baseline["vertices"][end]) + current_length = math.dist(current["vertices"][start], current["vertices"][end]) + if base_length <= 1e-12: + continue + compared_edges += 1 + if current_length <= 1e-12: + ratio = edge_ratio_threshold + 1.0 + else: + ratio = max( + current_length / base_length, + base_length / current_length, + ) + max_symmetric_edge_ratio = max(max_symmetric_edge_ratio, ratio) + if ratio > edge_ratio_threshold: + outlier_edge_ratio_count += 1 + + threshold_failed = ( + topology_changed + or invalid_coordinate_count > 0 + or outlier_edge_ratio_count > 0 + ) + if threshold_failed: + threshold_failure_count += 1 + reports_by_name[obj.name]["frames"].append({ + "frame": frame, + "topology_changed": topology_changed, + "sampled_vertex_count": compared_vertices, + "invalid_coordinate_count": invalid_coordinate_count, + "deformed_vertex_count": deformed_vertex_count, + "mean_displacement": ( + displacement_total / compared_vertices if compared_vertices else 0.0 + ), + "max_displacement": max_displacement, + "sampled_edge_count": compared_edges, + "outlier_edge_ratio_count": outlier_edge_ratio_count, + "max_symmetric_edge_ratio": max_symmetric_edge_ratio, + "threshold_failed": threshold_failed, + }) + reports = [reports_by_name[obj.name] for obj in objects] + + return { + "success": True, + "policy_version": "pose-deformation-v1", + "baseline_frame": baseline_frame, + "stress_frames": stress_frames, + "stress_frame_count": len(stress_frames), + "object_count": len(reports), + "thresholds": { + "edge_ratio_threshold": edge_ratio_threshold, + "edge_ratio_mode": "symmetric", + "displacement_epsilon": displacement_epsilon, + }, + "threshold_failure_count": threshold_failure_count, + "reports": reports, + "note": ( + "Threshold results identify mechanical anomalies only. " + "The artist still decides whether deformation quality is acceptable." + ), + } + except Exception as e: + return {"error": f"Failed to inspect pose deformation: {str(e)}"} + finally: + scene.frame_set(original_frame, subframe=original_subframe) + bpy.context.view_layer.update() + def inspect_animation_data( self, names=None, @@ -8995,32 +9760,573 @@ def ensure_single_user_action(): except Exception as e: return {"error": f"Failed to set shape key value: {str(e)}"} - def set_keyframe_animation( + def retarget_animation_clip( self, - name, - data_path="location", - keyframes=None, - interpolation="BEZIER", - easing=None, - rotation_unit="radians", - clear_existing=False, - set_scene_range=True, + filepath, + target_armature, + bone_map, + action_name, + export_names, + output_filepath, + source_armature=None, + root_motion="in_place", + root_source_bone=None, + frame_start=None, + frame_end=None, + bake_step=1, + cleanup_imported=True, + overwrite=False, ): - """Create bounded transform keyframes for location, rotation_euler, or scale without arbitrary Python.""" - try: - import math - - obj = bpy.data.objects.get(str(name)) - if obj is None: - return {"error": f"Object '{name}' not found"} + """Import BVH/FBX motion, retarget it through explicit bone constraints, bake, and export a GLB candidate.""" + imported_objects = [] + created_data = {} + source_armature_name = None + added_constraints = [] + baked_action = None + previous_action = None + previous_selection = list(bpy.context.selected_objects) + previous_active = bpy.context.view_layer.objects.active + previous_mode = ( + previous_active.mode + if previous_active is not None + else "OBJECT" + ) + scene = bpy.context.scene + previous_frame_start = int(scene.frame_start) + previous_frame_end = int(scene.frame_end) + previous_frame = int(scene.frame_current) + output_path = None + output_backup_path = None + exported = False + output_existed = False + backup_complete = False + output_touched = False + tracked_data = {} + before_names = {} + previous_pose = {} + target_had_animation_data = False + recovery_failed = False - allowed_paths = {"location", "rotation_euler", "scale"} - data_path = str(data_path or "location").strip() - if data_path not in allowed_paths: - return {"error": f"Unsupported data_path '{data_path}'. Use one of: {', '.join(sorted(allowed_paths))}"} + def coerce_bool(value, default=False): + if value is None: + return default + if isinstance(value, str): + return value.strip().lower() in { + "true", + "1", + "yes", + "y", + "on", + } + return bool(value) - if not isinstance(keyframes, (list, tuple)) or len(keyframes) < 2: - return {"error": "keyframes must contain at least two entries with frame and value fields"} + def cleanup_created_imports(): + nonlocal created_data, imported_objects + if before_names: + created_data = { + key: [block for block in collection if block.name not in before_names[key]] + for key, collection in tracked_data.items() + } + imported_objects = list(created_data.get("objects", [])) + for obj in list(imported_objects): + if obj.name in bpy.data.objects: + bpy.data.objects.remove(obj, do_unlink=True) + for data_name, blocks in created_data.items(): + if data_name == "objects": + continue + collection = getattr(bpy.data, data_name) + for block in list(blocks): + block_name = block.name + if ( + block is baked_action + or collection.get(block_name) is None + or block.users > 0 + ): + continue + collection.remove(block) + + def remove_added_constraints(): + for pose_bone, constraint in reversed( + added_constraints + ): + if constraint in pose_bone.constraints.values(): + pose_bone.constraints.remove(constraint) + added_constraints.clear() + + def rollback(): + nonlocal baked_action + with suppress(Exception): + if bpy.context.object and bpy.context.object.mode != "OBJECT": + bpy.ops.object.mode_set(mode="OBJECT") + remove_added_constraints() + target = bpy.data.objects.get(str(target_armature)) + if target is not None: + if target_had_animation_data: + target.animation_data_create() + target.animation_data.action = previous_action + elif previous_pose: + target.animation_data_clear() + if baked_action is not None and baked_action.users == 0: + with suppress(Exception): + bpy.data.actions.remove(baked_action) + baked_action = None + cleanup_created_imports() + if backup_complete and output_backup_path and os.path.isfile(output_backup_path): + os.replace(output_backup_path, output_path) + elif output_touched and not output_existed and output_path and os.path.isfile(output_path): + os.remove(output_path) + elif output_backup_path and not backup_complete and os.path.isfile(output_backup_path): + os.remove(output_backup_path) + + try: + input_path = bpy.path.abspath(str(filepath)) + input_extension = os.path.splitext(input_path)[1].lower() + if input_extension not in {".bvh", ".fbx"}: + return { + "error": "filepath must point to a .bvh or .fbx motion file" + } + if not os.path.isfile(input_path): + return { + "error": f"Motion file not found: {input_path}" + } + + target = bpy.data.objects.get(str(target_armature)) + if target is None or target.type != "ARMATURE": + return { + "error": f"Target armature '{target_armature}' not found" + } + previous_action = ( + target.animation_data.action + if target.animation_data + else None + ) + target_had_animation_data = target.animation_data is not None + previous_pose = {bone.name: (bone.matrix_basis.copy(), bone.rotation_mode) for bone in target.pose.bones} + if not isinstance(bone_map, dict) or not bone_map: + return { + "error": "bone_map must be a non-empty object mapping source bone names to target bone names" + } + normalized_map = {} + for source_name, target_name in bone_map.items(): + source_key = str(source_name).strip() + target_key = str(target_name).strip() + if not source_key or not target_key: + return { + "error": "bone_map names must be non-empty strings" + } + normalized_map[source_key] = target_key + + resolved_action_name = str(action_name).strip() + if not resolved_action_name: + return {"error": "action_name is required"} + if bpy.data.actions.get(resolved_action_name) is not None: + return { + "error": f"Action already exists: {resolved_action_name}" + } + + if not isinstance(export_names, (list, tuple)): + return { + "error": "export_names must be a non-empty array of explicit object names" + } + normalized_export_names = list( + dict.fromkeys( + str(name).strip() + for name in export_names + if str(name).strip() + ) + ) + if not normalized_export_names: + return { + "error": "export_names must be a non-empty array of explicit object names" + } + if target.name not in normalized_export_names: + return { + "error": "export_names must include the target armature" + } + missing_export_names = [ + name + for name in normalized_export_names + if bpy.data.objects.get(name) is None + ] + if missing_export_names: + return { + "error": "Export objects not found: " + + ", ".join(missing_export_names) + } + + raw_output_path = str(output_filepath).strip() + if not os.path.isabs(raw_output_path): + return {"error": "output_filepath must be absolute"} + output_path = os.path.abspath(raw_output_path) + if os.path.splitext(output_path)[1].lower() != ".glb": + return { + "error": "output_filepath must end with .glb" + } + output_directory = os.path.dirname(output_path) + if not os.path.isdir(output_directory): + return { + "error": f"Output directory does not exist: {output_directory}" + } + if os.path.exists(output_path) and not coerce_bool( + overwrite, + False, + ): + return { + "error": f"Output file already exists: {output_path}" + } + if os.path.exists(output_path): + output_existed = True + output_backup_path = ( + f"{output_path}.vipermesh-backup-{uuid.uuid4().hex}" + ) + shutil.copy2(output_path, output_backup_path) + backup_complete = True + + root_motion_mode = str(root_motion or "in_place").lower() + if root_motion_mode not in {"in_place", "preserve"}: + return { + "error": "root_motion must be IN_PLACE or PRESERVE" + } + try: + step = int(bake_step) + except (TypeError, ValueError): + return {"error": "bake_step must be an integer"} + if step < 1 or step > 16: + return { + "error": "bake_step must be between 1 and 16" + } + + tracked_data = { + "objects": bpy.data.objects, + "armatures": bpy.data.armatures, + "meshes": bpy.data.meshes, + "actions": bpy.data.actions, + "collections": bpy.data.collections, + "materials": bpy.data.materials, + "images": bpy.data.images, + } + before_names = { + key: {block.name for block in collection} + for key, collection in tracked_data.items() + } + + if bpy.context.object and bpy.context.object.mode != "OBJECT": + bpy.ops.object.mode_set(mode="OBJECT") + bpy.ops.object.select_all(action="DESELECT") + if input_extension == ".bvh": + import_result = bpy.ops.import_anim.bvh( + filepath=input_path, + target="ARMATURE", + rotate_mode="NATIVE", + update_scene_duration=True, + ) + else: + import_result = bpy.ops.import_scene.fbx( + filepath=input_path, + use_anim=True, + ignore_leaf_bones=False, + ) + if "FINISHED" not in import_result: + raise RuntimeError( + f"Motion import operator returned {sorted(import_result)}" + ) + + created_data = { + key: [ + block + for block in collection + if block.name not in before_names[key] + ] + for key, collection in tracked_data.items() + } + imported_objects = list(created_data["objects"]) + imported_armatures = [ + obj for obj in imported_objects if obj.type == "ARMATURE" + ] + if source_armature: + source = bpy.data.objects.get(str(source_armature)) + if source not in imported_armatures: + raise ValueError( + "source_armature must name an armature imported from filepath" + ) + elif len(imported_armatures) == 1: + source = imported_armatures[0] + else: + raise ValueError( + "Motion import must create exactly one armature unless source_armature is explicit" + ) + + source_action = ( + source.animation_data.action + if source.animation_data + else None + ) + if source_action is None: + raise ValueError( + "Imported source armature has no active animation action" + ) + source_armature_name = source.name + + missing_source_bones = [ + name + for name in normalized_map + if source.pose.bones.get(name) is None + ] + missing_target_bones = [ + name + for name in normalized_map.values() + if target.pose.bones.get(name) is None + ] + if missing_source_bones or missing_target_bones: + raise ValueError( + "Bone map could not be resolved; missing source bones: " + + ", ".join(missing_source_bones or ["none"]) + + "; missing target bones: " + + ", ".join(missing_target_bones or ["none"]) + ) + + resolved_root_source = ( + str(root_source_bone).strip() + if root_source_bone + else None + ) + if root_motion_mode == "preserve": + if resolved_root_source is None: + root_candidates = [ + source_name + for source_name in normalized_map + if source.pose.bones[source_name].parent is None + ] + if len(root_candidates) != 1: + raise ValueError( + "PRESERVE root motion requires root_source_bone when the mapped root is ambiguous" + ) + resolved_root_source = root_candidates[0] + if resolved_root_source not in normalized_map: + raise ValueError( + "root_source_bone must be present in bone_map" + ) + + target.animation_data_create() + baked_action = bpy.data.actions.new( + name=resolved_action_name + ) + target.animation_data.action = baked_action + + for source_name, target_name in normalized_map.items(): + target_pose_bone = target.pose.bones[target_name] + rotation_constraint = ( + target_pose_bone.constraints.new( + type="COPY_ROTATION" + ) + ) + rotation_constraint.name = ( + f"ViperMesh_Retarget_Rotation_{source_name}" + ) + rotation_constraint.target = source + rotation_constraint.subtarget = source_name + rotation_constraint.target_space = "POSE" + rotation_constraint.owner_space = "POSE" + rotation_constraint.mix_mode = "REPLACE" + added_constraints.append( + (target_pose_bone, rotation_constraint) + ) + + if ( + root_motion_mode == "preserve" + and resolved_root_source is not None + ): + root_target_name = normalized_map[ + resolved_root_source + ] + root_target_pose = target.pose.bones[ + root_target_name + ] + location_constraint = ( + root_target_pose.constraints.new( + type="COPY_LOCATION" + ) + ) + location_constraint.name = ( + "ViperMesh_Retarget_Root_Location" + ) + location_constraint.target = source + location_constraint.subtarget = ( + resolved_root_source + ) + location_constraint.target_space = "POSE" + location_constraint.owner_space = "POSE" + added_constraints.append( + (root_target_pose, location_constraint) + ) + + source_start = int(math.floor(source_action.frame_range[0])) + source_end = int(math.ceil(source_action.frame_range[1])) + resolved_start = ( + source_start + if frame_start is None + else int(frame_start) + ) + resolved_end = ( + source_end if frame_end is None else int(frame_end) + ) + if resolved_start < 0 or resolved_end < resolved_start: + raise ValueError( + "frame_start and frame_end must define a non-negative ordered range" + ) + scene.frame_start = resolved_start + scene.frame_end = resolved_end + scene.frame_set(resolved_start) + + bpy.ops.object.select_all(action="DESELECT") + target.select_set(True) + bpy.context.view_layer.objects.active = target + bpy.ops.object.mode_set(mode="POSE") + bpy.ops.pose.select_all(action="DESELECT") + for target_name in normalized_map.values(): + target.pose.bones[target_name].select = True + bake_result = bpy.ops.nla.bake( + frame_start=resolved_start, + frame_end=resolved_end, + step=step, + only_selected=True, + visual_keying=True, + clear_constraints=False, + clear_parents=False, + use_current_action=True, + clean_curves=False, + bake_types={"POSE"}, + channel_types={ + "LOCATION", + "ROTATION", + "SCALE", + "BBONE", + "PROPS", + }, + ) + if "FINISHED" not in bake_result: + raise RuntimeError( + f"NLA bake operator returned {sorted(bake_result)}" + ) + bpy.ops.object.mode_set(mode="OBJECT") + remove_added_constraints() + bpy.context.view_layer.update() + + output_touched = True + export_result = self.export_object( + normalized_export_names, + output_path, + "GLB", + ) + if export_result.get("error"): + raise RuntimeError(export_result["error"]) + if coerce_bool(cleanup_imported, True): + cleanup_created_imports() + + animation_report = self.inspect_animation_data( + names=[target.name], + include_actions=True, + max_actions=20, + ) + if output_backup_path and os.path.isfile(output_backup_path): + os.remove(output_backup_path) + output_backup_path = None + exported = True + return { + "success": True, + "source_filepath": input_path, + "source_armature": source_armature_name, + "target_armature": target.name, + "bone_map": normalized_map, + "mapped_bone_count": len(normalized_map), + "root_motion": root_motion_mode, + "root_source_bone": resolved_root_source, + "action_name": baked_action.name, + "frame_start": resolved_start, + "frame_end": resolved_end, + "bake_step": step, + "cleanup_imported": coerce_bool( + cleanup_imported, + True, + ), + "export_names": normalized_export_names, + "output_filepath": output_path, + "file_size_bytes": export_result.get( + "file_size_bytes", + 0, + ), + "animation_report": animation_report, + "next_safe_action": "run inspect_animation_data and inspect_pose_deformation, then review the exported candidate before acceptance", + } + except Exception as error: + try: + rollback() + except Exception as recovery_error: + recovery_failed = True + return {"error": f"Failed to retarget animation clip: {error}; recovery failed: {recovery_error}", "rolled_back": False, "recovery_backup": output_backup_path} + return { + "error": f"Failed to retarget animation clip: {str(error)}", + "rolled_back": True, + } + finally: + with suppress(Exception): + if bpy.context.object and bpy.context.object.mode != "OBJECT": + bpy.ops.object.mode_set(mode="OBJECT") + bpy.ops.object.select_all(action="DESELECT") + for obj in previous_selection: + if obj.name in bpy.data.objects: + obj.select_set(True) + if ( + previous_active is not None + and previous_active.name in bpy.data.objects + ): + bpy.context.view_layer.objects.active = ( + previous_active + ) + if previous_mode != "OBJECT": + bpy.ops.object.mode_set(mode=previous_mode) + scene.frame_start = previous_frame_start + scene.frame_end = previous_frame_end + scene.frame_set(previous_frame) + if not exported and previous_pose: + target = bpy.data.objects.get(str(target_armature)) + if target is not None: + for name, (matrix, rotation_mode) in previous_pose.items(): + bone = target.pose.bones.get(name) + if bone is not None: + bone.rotation_mode = rotation_mode + bone.matrix_basis = matrix + bpy.context.view_layer.update() + if output_backup_path and backup_complete and not output_touched and not recovery_failed: + with suppress(Exception): + os.remove(output_backup_path) + + def set_keyframe_animation( + self, + name, + data_path="location", + keyframes=None, + interpolation="BEZIER", + easing=None, + rotation_unit="radians", + clear_existing=False, + set_scene_range=True, + ): + """Create bounded transform keyframes for location, rotation_euler, or scale without arbitrary Python.""" + try: + import math + + obj = bpy.data.objects.get(str(name)) + if obj is None: + return {"error": f"Object '{name}' not found"} + + allowed_paths = {"location", "rotation_euler", "scale"} + data_path = str(data_path or "location").strip() + if data_path not in allowed_paths: + return {"error": f"Unsupported data_path '{data_path}'. Use one of: {', '.join(sorted(allowed_paths))}"} + + if not isinstance(keyframes, (list, tuple)) or len(keyframes) < 2: + return {"error": "keyframes must contain at least two entries with frame and value fields"} def coerce_bool(value, default=False): if value is None: @@ -11405,6 +12711,237 @@ def export_object(self, names, filepath, file_format='GLB'): except Exception as e: return {"error": f"Failed to export: {str(e)}"} + def get_vipermesh_runtime_identity(self): + """Return a fingerprint that capability sync can bind to this exact add-on runtime.""" + config_directory = bpy.utils.user_resource( + "CONFIG", + path="vipermesh", + create=True, + ) + device_id_path = os.path.join( + config_directory, + "runtime-device-id", + ) + runtime_device_key = None + if os.path.isfile(device_id_path): + with open(device_id_path, "r", encoding="utf-8") as device_file: + candidate = device_file.read().strip() + try: + runtime_device_key = str(uuid.UUID(candidate)) + except (ValueError, AttributeError): + runtime_device_key = None + if runtime_device_key is None: + runtime_device_key = str(uuid.uuid4()) + temporary_path = f"{device_id_path}.tmp" + with open(temporary_path, "w", encoding="utf-8") as device_file: + device_file.write(runtime_device_key) + os.replace(temporary_path, device_id_path) + addon_path = os.path.abspath(__file__) + with open(addon_path, "rb") as addon_file: + source_fingerprint = hashlib.sha256( + addon_file.read() + ).hexdigest() + return { + "success": True, + "device_key": runtime_device_key, + "source_fingerprint": f"sha256:{source_fingerprint}", + "blender_version": bpy.app.version_string, + "addon_version": ".".join( + str(value) for value in bl_info.get("version", ()) + ), + } + + def validate_gltf_roundtrip(self, filepath, cleanup=True): + """Temporarily import a glTF/GLB export, report its contents, then remove imported data.""" + imported_objects = [] + created_datablocks = {} + tracked_collections = {} + before_names = {} + previous_selection = list(bpy.context.selected_objects) + previous_active = bpy.context.view_layer.objects.active + try: + absolute_filepath = bpy.path.abspath(str(filepath)) + extension = os.path.splitext(absolute_filepath)[1].lower() + if extension not in {".glb", ".gltf"}: + return {"error": "filepath must point to a .glb or .gltf file"} + if not os.path.isfile(absolute_filepath): + return {"error": f"glTF file not found: {absolute_filepath}"} + + tracked_collections = { + "objects": bpy.data.objects, + "meshes": bpy.data.meshes, + "materials": bpy.data.materials, + "images": bpy.data.images, + "armatures": bpy.data.armatures, + "actions": bpy.data.actions, + "collections": bpy.data.collections, + } + before_names = { + key: {block.name for block in collection} + for key, collection in tracked_collections.items() + } + + if bpy.context.object and bpy.context.object.mode != "OBJECT": + bpy.ops.object.mode_set(mode="OBJECT") + bpy.ops.object.select_all(action="DESELECT") + import_result = bpy.ops.import_scene.gltf(filepath=absolute_filepath) + if "FINISHED" not in import_result: + raise RuntimeError(f"glTF import operator returned {sorted(import_result)}") + + created_datablocks = { + key: [ + block + for block in collection + if block.name not in before_names[key] + ] + for key, collection in tracked_collections.items() + } + imported_objects = list(created_datablocks["objects"]) + reportable_objects = [ + obj + for obj in imported_objects + if not any( + collection.name == "glTF_not_exported" + for collection in obj.users_collection + ) + ] + mesh_objects = [ + obj for obj in reportable_objects if obj.type == "MESH" + ] + armature_objects = [ + obj for obj in reportable_objects if obj.type == "ARMATURE" + ] + def bound_armatures(obj): + names = set() + for modifier in obj.modifiers: + if ( + modifier.type != "ARMATURE" + or modifier.object is None + ): + continue + armature = modifier.object + bone_names = { + bone.name for bone in armature.data.bones + } + vertex_group_indices = { + group.index + for group in obj.vertex_groups + if group.name in bone_names + } + has_weighted_vertices = any( + assignment.group in vertex_group_indices + and assignment.weight > 0 + for vertex in obj.data.vertices + for assignment in vertex.groups + ) + if has_weighted_vertices: + names.add(armature.name) + return sorted(names) + + skinned_mesh_objects = [ + obj + for obj in mesh_objects + if bound_armatures(obj) + ] + evaluated_triangles = sum( + max(0, len(poly.vertices) - 2) + for obj in mesh_objects + for poly in obj.data.polygons + ) + material_names = sorted({ + slot.material.name + for obj in mesh_objects + for slot in obj.material_slots + if slot.material + }) + missing_images = sorted({ + image.name + for image in created_datablocks["images"] + if image.source == "FILE" + and not image.packed_file + and image.filepath + and not os.path.isfile(bpy.path.abspath(image.filepath, library=image.library)) + }) + issues = [] + if not reportable_objects: + issues.append("no_objects_imported") + if not mesh_objects: + issues.append("no_meshes_imported") + if missing_images: + issues.append("missing_images") + with open(absolute_filepath, "rb") as checkpoint_file: + checkpoint_sha256 = hashlib.sha256( + checkpoint_file.read() + ).hexdigest() + + return { + "success": True, + "roundtrip_valid": len(issues) == 0, + "filepath": absolute_filepath, + "sha256": checkpoint_sha256, + "file_size_bytes": os.path.getsize(absolute_filepath), + "object_count": len(reportable_objects), + "mesh_count": len(mesh_objects), + "armature_count": len(armature_objects), + "skinned_mesh_count": len(skinned_mesh_objects), + "material_count": len(material_names), + "image_count": len(created_datablocks["images"]), + "action_count": len(created_datablocks["actions"]), + "evaluated_triangles": evaluated_triangles, + "objects": [ + {"name": obj.name, "type": obj.type} + for obj in reportable_objects + ], + "skinned_meshes": [ + { + "name": obj.name, + "armatures": bound_armatures(obj), + } + for obj in skinned_mesh_objects + ], + "armatures": [ + { + "name": obj.name, + "bones": [ + bone.name for bone in obj.data.bones + ], + } + for obj in armature_objects + ], + "materials": material_names, + "missing_images": missing_images, + "issues": issues, + "cleanup_requested": bool(cleanup), + "next_safe_action": "use the imported evidence to approve the export" if not issues else "fix round-trip issues before approving the export", + } + except Exception as e: + return {"error": f"Failed to validate glTF round trip: {str(e)}"} + finally: + if cleanup: + with suppress(Exception): + if before_names: + created_datablocks = { + key: [block for block in collection if block.name not in before_names[key]] + for key, collection in tracked_collections.items() + } + imported_objects = list(created_datablocks.get("objects", [])) + if bpy.context.object and bpy.context.object.mode != "OBJECT": + bpy.ops.object.mode_set(mode="OBJECT") + for obj in imported_objects: + if obj.name in bpy.data.objects: + bpy.data.objects.remove(obj, do_unlink=True) + for key in ("collections", "actions", "armatures", "meshes", "materials", "images"): + collection = getattr(bpy.data, key) + for block in created_datablocks.get(key, []): + if block.name in collection and block.users == 0: + collection.remove(block) + bpy.ops.object.select_all(action="DESELECT") + for obj in previous_selection: + if obj.name in bpy.data.objects: + obj.select_set(True) + if previous_active and previous_active.name in bpy.data.objects: + bpy.context.view_layer.objects.active = previous_active + # ---------- Phase 3: Dynamic Addon Detection ---------- def list_installed_addons(self): @@ -11437,6 +12974,377 @@ def list_installed_addons(self): except Exception as e: return {"error": f"Failed to list addons: {str(e)}"} + def _json_safe_rna_value(self, value): + if value is None or isinstance(value, (str, int, float, bool)): + return value + if isinstance(value, (list, tuple, set)): + return [self._json_safe_rna_value(item) for item in value] + try: + return [self._json_safe_rna_value(item) for item in value] + except (TypeError, AttributeError): + return str(value) + + def _find_operator_class(self, operator_id): + queue = list(bpy.types.Operator.__subclasses__()) + seen = set() + while queue: + operator_class = queue.pop() + if operator_class in seen: + continue + seen.add(operator_class) + if getattr(operator_class, "bl_idname", "") == operator_id: + return operator_class + queue.extend(operator_class.__subclasses__()) + return None + + def _operator_callable(self, operator_id): + if not isinstance(operator_id, str) or operator_id.count(".") != 1: + raise ValueError("operator_id must use Blender's category.operation form") + category, operation = operator_id.split(".", 1) + operator_group = getattr(bpy.ops, category, None) + operator_callable = getattr(operator_group, operation, None) if operator_group else None + if operator_callable is None: + raise ValueError(f"Blender operator is not registered: {operator_id}") + return operator_callable + + def _rna_property_schema(self, prop): + scalar_types = { + "BOOLEAN": "boolean", + "INT": "integer", + "FLOAT": "number", + "STRING": "string", + "ENUM": "string", + } + value_schema = {"type": scalar_types.get(prop.type, "unsupported")} + supported = prop.type in scalar_types + if prop.type == "ENUM": + value_schema["enum"] = [ + item.identifier + for item in prop.enum_items + if getattr(item, "identifier", "") + ] + if getattr(prop, "is_enum_flag", False): + value_schema = { + "type": "array", + "items": value_schema, + "uniqueItems": True, + } + if prop.type in {"INT", "FLOAT"}: + value_schema["minimum"] = prop.hard_min + value_schema["maximum"] = prop.hard_max + if getattr(prop, "array_length", 0) > 0: + value_schema = { + "type": "array", + "items": value_schema, + "minItems": prop.array_length, + "maxItems": prop.array_length, + } + default_source = ( + getattr(prop, "default_flag", None) + if prop.type == "ENUM" and getattr(prop, "is_enum_flag", False) + else getattr(prop, "default_array", None) + if getattr(prop, "array_length", 0) > 0 + else getattr(prop, "default", None) + ) + default = self._json_safe_rna_value(default_source) + if default is not None: + value_schema["default"] = default + return { + "identifier": prop.identifier, + "name": prop.name or prop.identifier, + "description": prop.description or "", + "schema": value_schema, + "supported": supported and not prop.is_readonly, + "read_only": bool(prop.is_readonly), + "hidden": bool(getattr(prop, "is_hidden", False)), + "subtype": str(getattr(prop, "subtype", "NONE")), + "enum_flag": prop.type == "ENUM" and bool(getattr(prop, "is_enum_flag", False)), + } + + def _inspect_addon_operator(self, addon_module, operator_id, addon_version=""): + operator_class = self._find_operator_class(operator_id) + if operator_class is None: + raise ValueError(f"Unable to resolve operator class: {operator_id}") + class_module = getattr(operator_class, "__module__", "") + if class_module != addon_module and not class_module.startswith(f"{addon_module}."): + raise ValueError( + f"Operator {operator_id} is owned by {class_module or 'an unknown module'}, " + f"not {addon_module}" + ) + + operator_callable = self._operator_callable(operator_id) + properties = [] + for prop in operator_callable.get_rna_type().properties: + if prop.identifier == "rna_type": + continue + properties.append(self._rna_property_schema(prop)) + input_schema = { + "type": "object", + "properties": { + prop["identifier"]: prop["schema"] + for prop in properties + if prop["supported"] and not prop["hidden"] + }, + "additionalProperties": False, + } + try: + poll_available = bool(operator_callable.poll()) + poll_error = None + except Exception as exc: + poll_available = False + poll_error = str(exc) + + fingerprint_payload = { + "addon_module": addon_module, + "addon_version": addon_version, + "blender_version": ".".join(str(v) for v in bpy.app.version), + "operator_id": operator_id, + "properties": properties, + "implementation": self._operator_implementation_fingerprint(operator_class), + } + fingerprint = hashlib.sha256( + json.dumps( + fingerprint_payload, + sort_keys=True, + separators=(",", ":"), + ).encode("utf-8") + ).hexdigest() + return { + "operator_id": operator_id, + "label": getattr(operator_class, "bl_label", "") or operator_id, + "description": getattr(operator_class, "bl_description", "") or "", + "class_module": class_module, + "input_schema": input_schema, + "properties": properties, + "poll_available": poll_available, + "poll_error": poll_error, + "source_fingerprint": f"sha256:{fingerprint}", + } + + def _operator_implementation_fingerprint(self, operator_class): + provenance = {} + try: + source_path = inspect.getsourcefile(operator_class) + except (TypeError, OSError): + source_path = None + if source_path and os.path.isfile(source_path): + digest = hashlib.sha256() + with open(source_path, "rb") as source: + for chunk in iter(lambda: source.read(65536), b""): + digest.update(chunk) + provenance["module_sha256"] = digest.hexdigest() + for name in ("execute", "invoke", "poll", "modal", "draw", "__init__"): + function = getattr(operator_class, name, None) + function = getattr(function, "__func__", function) + code = getattr(function, "__code__", None) + if code is not None: + payload = marshal.dumps(code) + repr(getattr(function, "__defaults__", None)).encode("utf-8") + provenance[name] = hashlib.sha256(payload).hexdigest() + if not provenance: + raise ValueError("Operator implementation provenance is unavailable; review a custom invocation instead") + return provenance + + def _enabled_addon_metadata(self, addon_module): + import addon_utils + + if addon_module not in bpy.context.preferences.addons: + raise ValueError(f"Addon is not enabled: {addon_module}") + for module in addon_utils.modules(): + if module.__name__ != addon_module: + continue + bl_info = getattr(module, "bl_info", {}) + version = bl_info.get("version", ()) + version_label = ".".join(str(part) for part in version) if version else "" + return { + "module": addon_module, + "name": bl_info.get("name", addon_module), + "version": version_label, + "description": bl_info.get("description", ""), + } + return { + "module": addon_module, + "name": addon_module, + "version": "", + "description": "", + } + + def inspect_addon_capabilities( + self, + addon_module, + operator_ids=None, + max_operators=100, + ): + """Inspect enabled add-on operators and RNA inputs without executing them.""" + try: + addon = self._enabled_addon_metadata(addon_module) + max_operators = max(1, min(int(max_operators), 250)) + if operator_ids is None: + operator_ids = sorted({ + getattr(operator_class, "bl_idname", "") + for operator_class in self._all_operator_classes() + if ( + getattr(operator_class, "__module__", "") == addon_module + or getattr(operator_class, "__module__", "").startswith( + f"{addon_module}." + ) + ) + and getattr(operator_class, "bl_idname", "") + }) + if not isinstance(operator_ids, (list, tuple)): + return {"error": "operator_ids must be an array when provided"} + + unique_operator_ids = list(dict.fromkeys(operator_ids)) + capabilities = [] + errors = [] + for operator_id in unique_operator_ids[:max_operators]: + try: + capabilities.append( + self._inspect_addon_operator( + addon_module, + operator_id, + addon["version"], + ) + ) + except Exception as exc: + errors.append({"operator_id": operator_id, "error": str(exc)}) + inventory_payload = { + "addon": addon, + "blender_version": ".".join(str(v) for v in bpy.app.version), + "capability_fingerprints": [ + capability["source_fingerprint"] for capability in capabilities + ], + } + inventory_fingerprint = hashlib.sha256( + json.dumps( + inventory_payload, + sort_keys=True, + separators=(",", ":"), + ).encode("utf-8") + ).hexdigest() + return { + "addon": addon, + "blender_version": inventory_payload["blender_version"], + "capabilities": capabilities, + "count": len(capabilities), + "total_operator_count": len(unique_operator_ids), + "truncated": len(unique_operator_ids) > max_operators, + "errors": errors, + "inventory_fingerprint": f"sha256:{inventory_fingerprint}", + "executed_operators": 0, + } + except Exception as e: + return {"error": f"Failed to inspect addon capabilities: {str(e)}"} + + def _all_operator_classes(self): + queue = list(bpy.types.Operator.__subclasses__()) + seen = set() + while queue: + operator_class = queue.pop() + if operator_class in seen: + continue + seen.add(operator_class) + yield operator_class + queue.extend(operator_class.__subclasses__()) + + def _addon_invocation_scene_state(self): + active = bpy.context.view_layer.objects.active + return { + "object_count": len(bpy.context.scene.objects), + "objects": sorted(obj.name for obj in bpy.context.scene.objects), + "selected": sorted(obj.name for obj in bpy.context.selected_objects), + "active": active.name if active else None, + "mode": bpy.context.mode, + } + + def invoke_addon_capability( + self, + addon_module, + operator_id, + arguments=None, + expected_source_fingerprint=None, + dry_run=True, + approved=False, + checkpoint_filepath=None, + ): + """Dry-run or explicitly invoke one fingerprint-matched enabled add-on operator.""" + try: + addon = self._enabled_addon_metadata(addon_module) + capability = self._inspect_addon_operator( + addon_module, + operator_id, + addon["version"], + ) + if expected_source_fingerprint != capability["source_fingerprint"]: + return { + "error": "Capability fingerprint is missing or stale; inspect again", + "actual_source_fingerprint": capability["source_fingerprint"], + } + arguments = arguments or {} + if not isinstance(arguments, dict): + return {"error": "arguments must be an object"} + supported_properties = capability["input_schema"]["properties"] + unknown_arguments = sorted(set(arguments) - set(supported_properties)) + if unknown_arguments: + return { + "error": "Invocation contains unknown or unsupported arguments", + "unknown_arguments": unknown_arguments, + } + if not capability["poll_available"]: + return { + "error": "Operator is unavailable in the current Blender context", + "poll_error": capability["poll_error"], + } + if dry_run: + return { + "success": True, + "dry_run": True, + "would_invoke": operator_id, + "arguments": arguments, + "capability": capability, + "executed_operators": 0, + } + if approved is not True: + return {"error": "Explicit approved=true is required for invocation"} + if ( + not isinstance(checkpoint_filepath, str) + or not os.path.isabs(checkpoint_filepath) + or not checkpoint_filepath.lower().endswith(".blend") + ): + return { + "error": "An absolute .blend checkpoint_filepath is required" + } + + os.makedirs(os.path.dirname(checkpoint_filepath), exist_ok=True) + if os.path.exists(checkpoint_filepath): + return {"error": "Checkpoint already exists; choose a fresh checkpoint path"} + saved = bpy.ops.wm.save_as_mainfile(filepath=checkpoint_filepath, copy=True) + if "FINISHED" not in saved or not os.path.isfile(checkpoint_filepath): + return {"error": "Checkpoint save did not finish; operator was not invoked", "executed_operators": 0} + enum_flags = {prop["identifier"] for prop in capability["properties"] if prop.get("enum_flag")} + for key in enum_flags & set(arguments): + if not isinstance(arguments[key], list) or any(not isinstance(value, str) for value in arguments[key]): + return {"error": f"Enum flag argument {key} must be an array of strings", "executed_operators": 0} + runtime_arguments = {key: set(value) if key in enum_flags else value for key, value in arguments.items()} + before = self._addon_invocation_scene_state() + result = self._operator_callable(operator_id)(**runtime_arguments) + after = self._addon_invocation_scene_state() + if "FINISHED" not in result: + return {"success": False, "error": f"Operator did not finish: {sorted(result)}", "checkpoint_filepath": checkpoint_filepath, "executed_operators": 1, "before": before, "after": after} + return { + "success": True, + "dry_run": False, + "operator_id": operator_id, + "operator_result": sorted(str(item) for item in result), + "arguments": arguments, + "checkpoint_filepath": checkpoint_filepath, + "source_fingerprint": capability["source_fingerprint"], + "before": before, + "after": after, + "executed_operators": 1, + } + except Exception as e: + return {"error": f"Failed to invoke addon capability: {str(e)}"} + # ---------- Phase 5: Material / Lighting / Camera / Render Tools ---------- def create_material(self, name, color=None, metallic=None, roughness=None, use_nodes=True): diff --git a/data/tool-guides/aesthetic-quality-guide.md b/data/tool-guides/aesthetic-quality-guide.md deleted file mode 100644 index 1c754d5..0000000 --- a/data/tool-guides/aesthetic-quality-guide.md +++ /dev/null @@ -1,133 +0,0 @@ ---- -title: "Aesthetic Quality & Stylistic Coherence Guide" -category: "scene-design" -tags: ["aesthetic", "quality", "style", "decorative", "detail", "execute_code", "procedural", "torch", "lantern", "ornament", "medieval", "modern", "furniture"] -description: "Principles for creating visually convincing, aesthetically rich 3D scenes. Prevents minimalistic primitive-based shortcuts and teaches the agent to match object detail to scene context while preferring direct tools before execute_code fallbacks." -blender_version: "5.0+" ---- - -# Aesthetic Quality & Stylistic Coherence Guide - -## CORE PRINCIPLE: VISUAL CONVICTION OVER SPEED - -When a user describes a scene, they expect objects that **look like the real thing**, not geometric approximations. A torch is not a cylinder with a sphere on top. A chair is not a cube with four sticks. Every object must be recognizable at first glance without needing the user to imagine what it's supposed to be. - -**Rule:** If an object would not be recognizable in a photograph, it is not finished. Add more geometry until it reads correctly. - -## ANTI-MINIMALISM RULES - -### Never Substitute Primitives for Complex Objects -The following patterns are UNACCEPTABLE: - -| Object | ❌ Wrong (Primitive) | ✅ Correct (Detailed) | -|---|---|---| -| Torch | Cylinder + sphere | Bracket + handle + irregular flame shape | -| Chair | Cube + 4 thin cubes | Seat + backrest + legs with slight taper | -| Tree | Cylinder + sphere/cone | Trunk with taper + branch structure + leaf volume | -| Lamp/Lantern | Cylinder + point light | Housing + glass panels + chain/mount + bulb | -| Sword | Thin cube | Blade (tapered) + crossguard + grip + pommel | -| Bookshelf | Cube with lines | Frame + individual shelves + books (varied sizes) | - -### Minimum Geometry Budget Per Object -Every named object in the scene should have AT LEAST: -- **Decorative/props:** 3+ distinct mesh operations (not just a single primitive) -- **Furniture:** 4+ sub-components (e.g., legs, seat, back, armrests) -- **Architectural features:** Follow the Architectural Completeness Guide -- **Organic shapes:** Use subdivision + proportional editing, never raw primitives - -### When to Use Direct Tools First - -Use direct tools first for repeatable operations: transforms, materials, lighting, cameras, render settings, collection organization, parenting, modifiers, constraints, mesh validation, and viewport verification. - -Use direct tools for the object parts they already cover: - -1. `create_primitive_assembly` for grouped local-coordinate furniture, fixtures, props, panels, baskets, crates, brackets, handles, and repeated hard-surface parts. -2. `soften_mesh_edges` for bevels, rounded corners, weighted normals, and less primitive-looking hard-surface parts. -3. `create_draped_surface_mesh` for bounded sagging or wavy surfaces such as tarps, flags, canopies, awnings, cloth panels, terrain patches, and irregular panels. -4. `create_mesh_from_data` for known vertex/face surfaces and custom silhouette pieces not covered by the draped-surface helper. -5. `add_curve_object` for cables, vines, rails, straps, ropes, arcs, and tube-like strokes. - -Use `execute_code` only when direct tools cannot express the required custom geometry, such as an object that: -1. Has **irregular or organic shapes** (flames, plants, terrain, fabric) -2. Requires **procedural deformation or generation** beyond direct assembly tools -3. Needs **vertex-level manipulation** (tapering, bending, sculpting) -4. Needs mesh operations not covered by `create_draped_surface_mesh`, `create_mesh_from_data`, `soften_mesh_edges`, or modifier tools -5. Involves custom shader nodes, procedural effects, simulation, or animation internals - -**Rule:** If you catch yourself making a single `create_cube` or `create_cylinder` call for a decorative object, STOP and decompose it into a multi-part assembly. Use direct tools for the parts they cover, and use a focused `execute_code` fallback only for the custom geometry that remains. - -## STYLISTIC COHERENCE - -### Match Object Style to Scene Context -Every object in a scene must visually belong to the same world. The prompt's keywords define the style vocabulary: - -| Prompt Keywords | Style Vocabulary | Material Palette | -|---|---|---| -| "medieval", "dungeon", "castle" | Rough stone, iron brackets, wood planks, rivets | Dark metals, weathered wood, rough stone | -| "modern", "apartment", "office" | Clean lines, smooth surfaces, chrome/glass | Polished metal, white/gray matte, glass | -| "sci-fi", "spaceship", "futuristic" | Panel lines, glowing accents, hexagonal patterns | Emissive strips, brushed aluminum, dark composites | -| "rustic", "cabin", "farmhouse" | Rough-hewn wood, hand-forged iron, woven fabric | Warm browns, muted greens, worn textures | -| "fantasy", "magical", "enchanted" | Organic curves, carved details, crystal/gem shapes | Glowing emissive, deep jewel tones, gold accents | - -**Rule:** Before building ANY sub-object, ask: "Does this shape and material belong in the world described by the prompt?" A smooth chrome cylinder does NOT belong in a medieval dungeon. - -### Contextual Material Selection -Materials must match the era and setting. Derive roughness and color from the **scene context**, not from defaults: -- **Aged/rustic settings:** Higher roughness (0.6–0.8), muted warm tones, metallic only on iron/steel -- **Modern/clean settings:** Lower roughness (0.05–0.3), neutral or cool tones, polished metals -- **Fantasy/magical:** Emissive accents, jewel tones, gold/copper metallics - -**Rule:** Never use default gray or smooth chrome for objects in non-modern settings. Always set roughness and base color intentionally to match the scene's era. - -## MULTI-COMPONENT ASSEMBLY PATTERN - -### Generic Direct-Tool Pattern - -When building ANY complex object, follow this pattern with direct tools first: - -```text -1. Create context-appropriate materials first. -2. Use create_primitive_assembly for the structural body and named sub-parts. -3. Use create_draped_surface_mesh for bounded cloth, tarp, canopy, flag, terrain-patch, or irregular-panel surfaces. -4. Use create_mesh_from_data only for the parts that need custom vertices/faces beyond the draped-surface helper. -5. Use add_curve_object for ropes, straps, cables, vines, rails, or handle arcs. -6. Use soften_mesh_edges on hard-surface families so primitives do not look raw. -7. Run inspect_scene_grounding and inspect_spatial_relations before final render. -8. Use execute_code only for the remaining geometry or shader step that no direct tool covers. -``` - -### Key Techniques for Realistic Shapes - -- **Tapered forms:** Use cone primitive parts where available, or a focused mesh-data/custom fallback when tapering is the only missing step. -- **Organic shapes:** Prefer assets for high-detail plants/foliage and use focused custom geometry only when an asset does not fit. -- **Repeated elements:** Use multiple named assembly parts for legs, slats, rungs, chain links, bricks, and panel details. -- **Edge softening:** Use `soften_mesh_edges` (small width, 2 segments, weighted normals) to avoid sharp CG edges. -- **Surface detail:** Use `create_draped_surface_mesh` for bounded sagging/wavy surfaces and `create_mesh_from_data` for other bounded custom surfaces; use `execute_code` only for subdivision/displacement workflows not covered by direct tools. - -### Few-Shot: What "Multi-Part" Means in Practice -These are hints — adapt the decomposition to whatever object the prompt requires: -- **Light fixture** → mount/bracket + housing body + shade/glass + bulb/flame + point light -- **Seating** → legs (4, tapered) + seat surface (beveled) + back support + optional armrests -- **Weapon/tool** → blade/head (tapered) + guard/collar + grip (wrapped) + pommel/end cap -- **Container** → body (hollow or thick-walled) + rim/lip + handles + lid (if closed) - -## QUALITY CHECKLIST - -Before finishing any scene, verify: - -1. **Silhouette test:** Would each object be identifiable from its silhouette alone? -2. **Style consistency:** Do all objects share the same era/aesthetic? -3. **Material variety:** Are there at least 3 distinct materials in the scene? -4. **No naked primitives:** Is every cube/cylinder/sphere part of a larger assembly? -5. **Light motivation:** Does every light source have a visible physical object (lamp, torch, window)? -6. **Scale plausibility:** Are objects proportioned correctly relative to each other? - -## COMMON AESTHETIC FAILURES - -1. ❌ Single cylinder as a "torch" — use multi-part assembly with bracket, handle, flame -2. ❌ Smooth metallic surface for medieval iron — use higher roughness (0.6-0.8) -3. ❌ Perfect geometric shapes for organic/rustic objects — add imperfection via displacement -4. ❌ Using only one material for everything — minimum 3 distinct materials per scene -5. ❌ Point lights with no visible source — every light needs a physical emitter object -6. ❌ Chrome/polished finishes in medieval/rustic scenes — match material to era -7. ❌ All furniture same height/size — vary proportions for visual interest diff --git a/data/tool-guides/animation-guide.md b/data/tool-guides/animation-guide.md deleted file mode 100644 index 1bd3c54..0000000 --- a/data/tool-guides/animation-guide.md +++ /dev/null @@ -1,246 +0,0 @@ ---- -title: "Animation & Keyframing Guide" -category: "animation" -tags: ["animation", "keyframe", "F-curve", "interpolation", "easing", "timeline", "shape key", "shape_key", "morph target", "blend shape", "viseme", "facial expression", "same-topology", "target mesh", "slider range", "mute", "rename", "delete", "duplicate", "mix", "bake", "squash", "stretch", "bounce", "timing", "turntable", "product spin", "keyframe_insert", "fcurves", "NLA", "inspect_shape_keys", "extract_shape_key_to_object", "create_shape_key_from_object", "set_shape_key_properties", "rename_shape_key", "delete_shape_key", "duplicate_shape_key", "create_shape_key_from_mix", "set_shape_key_value", "set_timeline_settings", "set_keyframe_animation", "create_turntable_animation"] -description: "Research-backed animation guide covering Blender 5.0 breaking API changes, keyframe performance hierarchy (RNA vs bpy.ops), F-curve access/modifier patterns, and the 12 Disney animation principles with volume-preserving code. Based on NotebookLM research with 8 cited sources." -blender_version: "5.0+" -triggered_by: ["inspect_animation_data", "inspect_shape_keys", "extract_shape_key_to_object", "create_shape_key_from_object", "set_shape_key_properties", "rename_shape_key", "delete_shape_key", "duplicate_shape_key", "create_shape_key_from_mix", "set_shape_key_value", "set_timeline_settings", "set_keyframe_animation", "create_turntable_animation"] ---- - -# Animation Guide — Blender Agent Best Practices - -> **Domain:** Keyframing, F-Curves, Interpolation, Animation Principles -> **Version:** Blender 5.x (verified via NotebookLM research) -> **Purpose:** Research-backed guide for the agent. Use `inspect_animation_data` before changing existing animation, `inspect_shape_keys` before morph/viseme/expression edits, `extract_shape_key_to_object` to materialize an existing morph as an editable target, `create_shape_key_from_object` when a same-topology target mesh already contains the desired morph geometry, `set_shape_key_properties` for slider ranges or mute state, `rename_shape_key` and `delete_shape_key` for bounded shape-key control cleanup, `duplicate_shape_key` for shape-key variants, `create_shape_key_from_mix` for baked composite morph controls, `set_timeline_settings` for timeline range/FPS/current-frame setup, `create_turntable_animation` for product/model showcase spins, then `set_keyframe_animation` or `set_shape_key_value` for bounded keys. Reserve `execute_code` only for unsupported animation systems such as custom drivers, rig controls, arbitrary shape-key sculpting without a target mesh, particles, or procedural simulations. - ---- - -## CRITICAL: Blender 5.0 Breaking Changes - -> **Source:** Blender 5.0 Python API review (ojambo.com), BlenderArtists forums - -- **Dict-like property access removed:** `bpy.context.scene['cycles']` no longer works -- **New `get_transform` / `set_transform` methods** replace older getter/setter workflows -- **Bundled modules now private:** `bl_console_utils`, `bl_rna_utils` cannot be used by scripts -- **`property_unset`** replaces dict-like access for old property patterns - ---- - -## 0. Inspect Before Editing Existing Animation - -Call `inspect_animation_data` before creating or editing keyframes on an existing scene, rig, material, driver, or NLA setup. Use its timeline, action, F-curve, driver, and NLA report to avoid overwriting the wrong action slot, duplicating curves, or changing hidden animation state. - -Use `set_timeline_settings` for frame range, FPS, current frame, preview range, and playback sync before animation or render workflows. Use `create_turntable_animation` for model showcase/product spin loops because it owns target validation, rotation axis, linear interpolation, timeline expansion, and frame restoration. Use `set_keyframe_animation` for object transform animation on `location`, `rotation_euler`, or `scale` when the motion is not a simple turntable. - -Use `inspect_shape_keys` before changing morph targets, visemes, blend shapes, or facial expression sliders. Use `extract_shape_key_to_object` to turn an existing shape key into a separate editable same-topology target mesh. Use `create_shape_key_from_object` when the requested shape-key geometry can be copied from a same-topology duplicate or edited target mesh. Use `set_shape_key_properties` when only the slider range or mute state needs to change. Use `rename_shape_key` or `delete_shape_key` when the requested work only cleans up existing shape-key controls. Use `duplicate_shape_key` when a new morph should start from an existing shape key. Use `create_shape_key_from_mix` when existing shape-key values should be baked into a new composite control. Use `set_shape_key_value` when the requested work only needs existing shape-key values or value keyframes. Keep `execute_code` for arbitrary custom shape-key sculpting when there is no same-topology target object to copy from. - -Keep `execute_code` for unsupported animation systems only, and verify all generated F-curves with `inspect_animation_data`. - ---- - -## 1. Performance: NEVER Use bpy.ops for Keyframes in Loops - -> **Source:** StackExchange "Improving Python Performance with Blender Operators" - -`bpy.ops` operators cause **implicit scene updates** on every call. In a loop adding N objects, -this causes O(n²) total checks — exponential slowdown. - -### API Hierarchy (fastest to slowest): -| Method | Speed | Use When | -|--------|-------|----------| -| Low-level F-Curve API (manual `keyframe_points`) | ⚡ Fastest | Bulk animation, many objects | -| `Object.keyframe_insert()` (RNA method) | ✅ Fast | Standard use — **PREFERRED** | -| `bpy.ops.anim.keyframe_insert()` | ❌ Slow | Never use in scripts | -| `bpy.ops.anim.keyframe_insert_menu()` | ❌ UI only | Never use in scripts | - -### Correct Pattern: RNA Method -```python -import bpy - -obj = bpy.data.objects['Cube'] -obj.location = (0, 0, 0) -obj.keyframe_insert(data_path='location', frame=1) - -obj.location = (0, 0, 5) -obj.keyframe_insert(data_path='location', frame=30) -``` - -### Fastest Pattern: Low-Level F-Curve API -```python -import bpy - -obj = bpy.data.objects['Cube'] - -# Ensure animation data exists -if not obj.animation_data: - obj.animation_data_create() -if not obj.animation_data.action: - obj.animation_data.action = bpy.data.actions.new(name=obj.name + 'Action') - -action = obj.animation_data.action - -# Add F-curve for location Z (index=2) -fc = action.fcurves.new(data_path='location', index=2) -# Add keyframe points directly -fc.keyframe_points.add(count=2) -fc.keyframe_points[0].co = (1.0, 0.0) # frame 1, z=0 -fc.keyframe_points[1].co = (30.0, 5.0) # frame 30, z=5 -fc.keyframe_points.update() - -# Manual scene update required with low-level API -bpy.context.view_layer.update() -``` - ---- - -## 2. F-Curve Access and Interpolation - -> **Source:** StackExchange "Set Keyframe interpolation CONSTANT" - -### Finding F-Curves (Blender 5.0+ Channelbag API) - -> **CRITICAL:** In Blender 5.0+, `action.fcurves` was REMOVED. -> Use the channelbag API instead: - -```python -from bpy_extras import anim_utils - -action = obj.animation_data.action -slot = obj.animation_data.action_slot -channelbag = anim_utils.action_get_channelbag_for_slot(action, slot) - -# Find a specific F-curve -fcu = channelbag.fcurves.find('rotation_euler', index=1) # Y rotation - -# Or iterate all F-curves -for fcu in channelbag.fcurves: - print(fcu.data_path, fcu.array_index) -``` - -### Setting Interpolation Per-Keyframe -```python -# All keyframes on a curve — both interpolation AND easing are valid properties -for pt in fcu.keyframe_points: - pt.interpolation = 'BEZIER' # or 'LINEAR', 'CONSTANT', 'BOUNCE' - pt.easing = 'EASE_IN_OUT' # or 'EASE_IN', 'EASE_OUT', 'AUTO' - -# Alternative: use handle types for finer control -for pt in fcu.keyframe_points: - pt.interpolation = 'BEZIER' - pt.handle_left_type = 'AUTO_CLAMPED' - pt.handle_right_type = 'AUTO_CLAMPED' - -# Specific frame only -frame_num = 24 -target = [pt for pt in fcu.keyframe_points if pt.co[0] == frame_num] -if target: - target[0].interpolation = 'CONSTANT' -``` - -### Material Animation F-Curves -```python -# Material animations live in node_tree, NOT in object.animation_data -from bpy_extras import anim_utils - -mat = bpy.data.materials['Material'] -mat_action = mat.node_tree.animation_data.action -mat_slot = mat.node_tree.animation_data.action_slot -mat_cb = anim_utils.action_get_channelbag_for_slot(mat_action, mat_slot) -for fcu in mat_cb.fcurves: # access material fcurves here - print(fcu.data_path) -``` - ---- - -## 3. F-Curve Modifiers via Python - -> **Source:** StackExchange "modifying fcurve modifiers in python" - -```python -# Add a Noise modifier to an F-curve (use channelbag API) -from bpy_extras import anim_utils - -action = obj.animation_data.action -slot = obj.animation_data.action_slot -channelbag = anim_utils.action_get_channelbag_for_slot(action, slot) - -fcu = channelbag.fcurves.find('location', index=2) -modifier = fcu.modifiers.new('NOISE') -modifier.scale = 10 # adjust noise scale -modifier.strength = 0.5 # adjust noise strength - -# Cycles modifier (for repeating animation) -cycle_mod = fcu.modifiers.new('CYCLES') -# Properties come with defaults — only set what you need to change -``` - -Available modifier types: `'NOISE'`, `'CYCLES'`, `'LIMITS'`, `'GENERATOR'`, -`'ENVELOPE'`, `'FNGENERATOR'`, `'STEPPED'` - ---- - -## 4. The 12 Animation Principles (Disney) - -> **Source:** Wikipedia "Twelve basic principles of animation" (Johnston & Thomas, 1981) - -These principles guide HOW the agent should create animations, not just the API calls: - -### Volume-Preserving Squash & Stretch -When an object squashes, its width must increase. When it stretches, width decreases. -**Rule: X × Y × Z scale product must stay constant.** - -```python -# Impact frame: squash (wider, shorter) -obj.scale = (1.3, 1.3, 0.6) # 1.3 * 1.3 * 0.6 ≈ 1.01 (volume preserved) -obj.keyframe_insert(data_path='scale', frame=15) - -# Stretch frame: taller, thinner -obj.scale = (0.8, 0.8, 1.56) # 0.8 * 0.8 * 1.56 ≈ 1.0 -obj.keyframe_insert(data_path='scale', frame=20) -``` - -### Slow In / Slow Out -More frames near start and end of action = realistic acceleration/deceleration. -Use `BEZIER` interpolation with `EASE_IN_OUT` — this is the Blender equivalent. - -### Anticipation -Before a big action, add a small opposite movement (wind-up before punch, crouch before jump). - -### Follow Through -After main action stops, appendages/loose parts continue moving briefly, then settle. -Implement with offset keyframes on child objects or secondary bones. - -### Arcs -Natural movement follows curved paths, not straight lines. Exception: mechanical/robotic motion -uses `LINEAR` interpolation deliberately. - -### Timing -Fewer frames = faster/lighter. More frames = slower/heavier. -- Light object bounce: 8-12 frames per bounce -- Heavy object: 20-30 frames per bounce - ---- - -## 5. Timeline Setup - -> **Source:** BlenderArtists "Set first and end frame by script" - -```python -scene = bpy.context.scene -scene.frame_start = 1 -scene.frame_end = 120 # 5 sec at 24fps -scene.render.fps = 24 # Film standard -scene.frame_set(1) # Go to frame 1 -``` - ---- - -## 6. Common Pitfalls - -| Problem | Cause | Fix (Research-backed) | -|---------|-------|----------------------| -| Keyframes not appearing | Wrong `data_path` string | Check exact path in Blender tooltip | -| Animation data is None | No keyframes inserted yet | Call `obj.animation_data_create()` first | -| Material anim not found | Looking in object instead of node_tree | Use `material.node_tree.animation_data` | -| Slow with many objects | Using `bpy.ops` in loops | Switch to RNA `keyframe_insert()` or low-level API | -| Stiff/robotic motion | Linear interpolation | Use `BEZIER` + `EASE_IN_OUT`, apply 12 principles | diff --git a/data/tool-guides/architectural-completeness-guide.md b/data/tool-guides/architectural-completeness-guide.md deleted file mode 100644 index 7cb2533..0000000 --- a/data/tool-guides/architectural-completeness-guide.md +++ /dev/null @@ -1,116 +0,0 @@ ---- -title: "Architectural Completeness & Scene Construction Guide" -category: "scene-design" -tags: ["architecture", "room", "walls", "floor", "ceiling", "window", "door", "boolean", "interior", "exterior", "glass", "opening", "create_room_shell", "create_wall_opening"] -triggered_by: ["create_room_shell", "create_wall_opening"] -description: "General 3D engineering principles for constructing architecturally complete scenes. Covers room construction, wall openings (windows/doors), component completeness, and structural integrity." -blender_version: "5.0+" ---- - -# Architectural Completeness & Scene Construction Guide - -## CORE PRINCIPLE: COMPONENT COMPLETENESS - -Every real-world architectural element is composed of multiple sub-components. When creating any architectural feature, always model ALL sub-components that make it physically complete. - -| Element | Required Sub-Components | Often Forgotten | -|---|---|---| -| Window | Boolean opening + frame + **glass pane** + sill | Glass pane, sill | -| Door | Boolean opening + frame + door panel + handle | Handle, threshold | -| Wall | Geometry with **real thickness** (not single plane) | Thickness for EEVEE | -| Staircase | Treads + risers + stringers + railing | Risers, railing | -| Shelf/Counter | Top surface + front face + supporting structure | Side panels | - -**Rule:** After creating an opening (window/door), ALWAYS add the fill component (glass pane, door panel). A hole in a wall is not a window — it needs glass. - -## ROOM CONSTRUCTION RULES - -### Wall Construction -Use `create_room_shell` for interior room blockouts, room anchors, or background architecture before adding furniture, windows, doors, lighting, or camera setup. It creates floor, walls, and optional ceiling with real thickness so the agent does not need generated Python for the common room-shell pattern. - -Use `create_wall_opening` for bounded window/door openings after the wall exists. It owns the Boolean cut, cutter cleanup, optional glass/door fill, and simple frame pieces. Choose the existing wall object deliberately, provide the opening center in world coordinates, and use `wall_axis: "auto"` unless the wall normal is known. Afterward, validate fill/frame placement with `validate_object_clearance` before adding materials or furniture around it. - -1. **Continuous walls:** Build walls as a single continuous object by extruding the next wall from the corner of the previous one. This prevents texture clipping at corners. -2. **Real thickness:** ALL walls, floors, and ceilings MUST have thickness (use 0.15–0.3m). Single-plane geometry causes severe **light bleed** in EEVEE where outdoor light leaks through edges. -3. **Apply transforms:** Always `bpy.ops.object.transform_apply(scale=True)` after setting scale. - -### Wall Thickness Pattern (Blender 5.x) -```python -# CORRECT: Wall with real thickness -wall = create_cube("Wall", 1, (0, 2.5, 1.4), (4, 0.2, 2.8)) -# 0.2m thick — prevents EEVEE light bleed - -# WRONG: Paper-thin wall (causes light bleed) -wall = create_cube("Wall", 1, (0, 2.5, 1.4), (4, 0.01, 2.8)) -``` - -### Floor as Distinct Surface -The floor MUST have a different material from furniture. In real interiors, floors are: -- Harder, more worn (higher roughness) -- Different color/texture from furniture -- Often darker (absorbs dirt/wear) - -## WINDOW CREATION PATTERN - -### Step 1: Cut Opening with Boolean -```python -# Create cutter object -cutter = create_cube("Window_Cutter", 1, (-2.0, 0, 1.4), (0.5, 2.0, 1.2)) -bool_mod = wall.modifiers.new(name="WindowCut", type='BOOLEAN') -bool_mod.operation = 'DIFFERENCE' -bool_mod.object = cutter -bool_mod.solver = 'EXACT' # REQUIRED in Blender 5.x -bpy.context.view_layer.objects.active = wall -bpy.ops.object.modifier_apply(modifier="WindowCut") -bpy.data.objects.remove(cutter) -``` - -### Step 2: Add Glass Pane (CRITICAL — don't skip!) -```python -# Glass pane fills the window opening -glass = create_cube("Window_Glass", 1, (-2.0, 0, 1.4), (0.02, 1.9, 1.1)) - -# Then call create_material_preset with preset "glass" and object_name "Window_Glass". -# Follow with inspect_material_node_graph to verify Transmission Weight, IOR, and assignment. -``` - -**EEVEE Note:** For interior scenes, using a Transparent shader instead of Glass may prevent light bleed artifacts if exact refraction isn't needed. - -### Step 3: Add Window Frame (Optional but recommended) -```python -# Simple frame around the opening -frame = create_cube("Window_Frame", 1, (-2.0, 0, 1.4), (0.08, 2.1, 1.3)) -# Cut out center to make frame shape -``` - -## BOOLEAN OPERATION BEST PRACTICES - -1. **Use EXACT solver** — the only reliable solver in Blender 5.x (`FAST` is removed) -2. **Watertight meshes only** — cutter and target must be closed manifold meshes -3. **No flipped normals** — check with `bpy.ops.mesh.normals_make_consistent(inside=False)` -4. **Apply transforms first** — `bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)` -5. **Cutter must overlap** — the cutter object must extend slightly beyond both sides of the wall -6. **Clean up cutters** — remove cutter objects after applying the modifier - -## SURFACE CATEGORY CHECKLIST - -When building any interior scene, ensure each of these surface categories has a DISTINCT material: - -| Category | Examples | Material Character | -|---|---|---| -| **Structural surfaces** | Walls, ceiling | High roughness (0.8+), muted colors, no metallic | -| **Walking surfaces** | Floor, stairs | Distinguished from walls — different color AND roughness | -| **Furniture** | Tables, chairs, counters | Wood/upholstery materials, medium roughness | -| **Fixtures/hardware** | Faucets, handles, machines | Metallic = 1.0, low roughness, distinct from all else | -| **Transparent surfaces** | Windows, glass doors | Transmission Weight > 0, IOR set correctly | - -**Rule:** No two categories should share the exact same material. If the floor and furniture are both "wood," use different wood tones (light oak floor vs. dark walnut furniture). - -## COMMON MISTAKES TO AVOID - -1. ❌ Cutting a window hole without adding glass — always add a glass pane after boolean -2. ❌ Paper-thin walls — use 0.15–0.3m thickness to prevent EEVEE light bleed -3. ❌ Same material on floor and furniture — always differentiate functional surfaces -4. ❌ Using `FAST` boolean solver — removed in Blender 5.x, use `EXACT` -5. ❌ Not applying transforms before booleans — causes geometry artifacts -6. ❌ Leaving boolean cutter objects visible — remove them after applying diff --git a/data/tool-guides/blend-file-health-guide.md b/data/tool-guides/blend-file-health-guide.md deleted file mode 100644 index 42d7e0a..0000000 --- a/data/tool-guides/blend-file-health-guide.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: "Blend File Health Inspection Guide" -category: "inspection" -tags: ["blend file", "health", "missing files", "linked libraries", "packed assets", "orphan data", "datablocks", "usage guess", "path info", "inspect_blend_file_health", "get_blendfile_summary_path_info", "get_blendfile_summary_datablocks", "get_blendfile_summary_missing_files", "get_blendfile_summary_of_linked_libraries", "get_blendfile_summary_usage_guess"] -triggered_by: ["inspect_blend_file_health", "get_blendfile_summary_path_info", "get_blendfile_summary_datablocks", "get_blendfile_summary_missing_files", "get_blendfile_summary_of_linked_libraries", "get_blendfile_summary_usage_guess"] -description: "Guidance for using the non-mutating .blend file health summary before diagnosing missing assets, broken links, package/export issues, or large-scene problems." -blender_version: "latest stable (verified 5.1.2)" ---- - -# Blend File Health Inspection Guide - -Use `inspect_blend_file_health` before writing Python when the user reports: - -- missing textures, white materials, broken image maps, or missing linked assets, -- imported files that behave differently after reload/export, -- large scenes that are slow or hard for the agent to reason about, -- packaging/export problems where external files may not be available. - -Use focused summaries when the question is narrow: - -- `get_blendfile_summary_path_info` for saved path, directory, and dirty-state questions. -- `get_blendfile_summary_datablocks` for object/material/image/action/orphan counts. -- `get_blendfile_summary_missing_files` for missing texture, font, sound, movie, or library files. -- `get_blendfile_summary_of_linked_libraries` for linked `.blend` library path/existence checks. -- `get_blendfile_summary_usage_guess` when deciding whether a file looks like a rig, animation, render scene, textured asset, or large scene before choosing the next tool. - -## Reading the Result - -- `status: "healthy"` means no high-signal file health issue was found. -- `status: "warning"` usually means the file is unsaved, very large, or has orphaned datablocks. -- `status: "error"` usually means at least one external file or linked library is missing. - -Treat missing external files as a file/package problem first. Do not try to repaint or rebuild materials until the missing path is understood. - -## Recommended Flow - -1. Call the narrowest summary that answers the question, or `inspect_blend_file_health` for a full report. -2. If `missing_external_files` is non-empty, report the missing paths and ask for a relink/reupload/package fix. -3. If linked libraries are missing, do not attempt destructive cleanup. Explain which library path is unavailable. -4. If orphan counts are high, mention cleanup as optional. Do not purge orphan data unless the user explicitly asks. -5. If the scene is large, use `get_all_object_info` with pagination or inspect specific collections/objects rather than loading everything at once. - -## What Not To Do - -- Do not use `execute_code` just to count missing files or libraries. -- Do not hard-delete orphaned datablocks automatically. -- Do not assume packed images are missing. Packed assets are embedded in the `.blend`. diff --git a/data/tool-guides/blender-docs-lookup-guide.md b/data/tool-guides/blender-docs-lookup-guide.md deleted file mode 100644 index b6b2b45..0000000 --- a/data/tool-guides/blender-docs-lookup-guide.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: "Blender Docs Lookup Guide" -category: "documentation" -tags: ["blender api", "manual", "version verification", "execute_code", "toolification"] -triggered_by: ["search_blender_api_docs", "get_blender_api_doc", "search_blender_manual_docs"] -description: "Rules for using official Blender documentation lookup before version-sensitive Blender Python or tool guidance." -blender_version: "latest stable (verified 5.1.2)" ---- - -# Blender Docs Lookup Guide - -Use official Blender docs lookup when exact API behavior matters. The lookup tools are guardrails before `execute_code`, not scene-editing tools themselves. - -## Use API Lookup Before Python For - -- `bpy.ops` operators and their context requirements -- node tree construction and socket names -- Principled BSDF, texture, normal map, and color-space setup -- UV unwrap, original UV preservation, and export/import operators -- mesh creation and validation APIs -- render engine IDs, EEVEE/Cycles settings, camera, and lighting properties -- removed, renamed, or changed Blender 5.x properties - -## Tool Order - -1. `search_blender_api_docs(query)` for symbols, operators, properties, and classes. -2. `get_blender_api_doc(path_or_url)` when an exact entry needs confirmation. -3. `search_blender_manual_docs(query)` for artist-facing workflows or UI concepts. -4. Only then use a deterministic tool or a scoped `execute_code` fallback. - -## Citation Rule - -When creating or updating reusable tool guidance, record the exact docs URL that informed the decision. Do not promote guidance based only on model memory. - -## Failure Handling - -If lookup fails, do not guess version-sensitive API names. Use a short introspection `execute_code` call only when the scene can safely tolerate it, such as checking `dir(obj.data)`, `hasattr(...)`, or available node sockets. diff --git a/data/tool-guides/camera-guide.md b/data/tool-guides/camera-guide.md deleted file mode 100644 index 2abd70c..0000000 --- a/data/tool-guides/camera-guide.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -title: "Camera Placement & Cinematography Guide" -category: "camera" -tags: ["camera", "focal length", "depth of field", "DOF", "framing", "composition", "product shot", "portrait", "camera orbit", "aim", "look at", "setup_studio_scene", "validate_studio_scene", "add_camera", "set_camera_properties", "aim_camera_at", "create_camera_orbit_animation"] -triggered_by: ["setup_studio_scene", "validate_studio_scene", "add_camera", "set_camera_properties", "aim_camera_at", "create_camera_orbit_animation"] -description: "Domain knowledge for camera placement, focal length selection, depth of field configuration, and shot composition in Blender. Essential for producing correctly framed renders." -blender_version: "4.0+" ---- - -# Camera Placement & Cinematography Guide - -## CRITICAL RULES - -1. **DOF focus_distance ≠ camera distance.** Focus distance is WHERE the focus plane sits (the sharp zone). Camera distance is WHERE the camera is physically placed. With telephoto lenses, the camera should be 1.5–2× further than the focus distance. - -2. **Always verify with get_viewport_screenshot** after placing a camera. If the subject fills more than 90% of the frame, the camera is too close. The subject should occupy 30–70% of the frame for a well-composed shot. - -3. **Use `setup_studio_scene` for asset/model previews.** It computes target bounds, frames a camera, sets focus distance, and creates a studio light rig. Follow it with `validate_studio_scene` before rendering. - -4. **Use `create_camera_orbit_animation` for showcase spins where the model should stay still.** It computes target bounds, places/reuses a camera, keys camera location and aimed rotation, updates focus distance, and preserves the current timeline frame. Follow with `validate_studio_scene` or a viewport/render preview before final export. - -## FOCAL LENGTH → MINIMUM CAMERA DISTANCE - -For a subject approximately 2 meters wide (e.g., default cube, small object): - -| Focal Length | Lens Type | Min Distance | Recommended Distance | Use Case | -|---|---|---|---|---| -| 24mm | Wide angle | 2–3m | 3–5m | Interiors, landscapes, dramatic perspective | -| 35mm | Normal | 3–5m | 4–7m | Documentary, natural scenes | -| 50mm | Standard | 4–6m | 5–8m | Human eye equivalent, general purpose | -| 85mm | Telephoto | 6–9m | 8–12m | Portraits, product shots, compressed look | -| 135mm | Long telephoto | 10–15m | 12–20m | Product close-ups, extreme compression | - -**Rule of thumb:** For an 85mm lens, place the camera at **minimum 8 meters** from the subject center. For a 50mm lens, 5–8 meters is correct. - -## CAMERA POSITIONING PATTERNS - -### Product Shot (45° angle, looking down) -``` -Location: (6, -6, 5) — about 10m from origin -Rotation: use `aim_camera_at` to aim at the target object or (0, 0, 0) -Lens: 85mm -DOF: focus_distance = distance to subject, f/2.8 -``` - -### Eye-Level Portrait -``` -Location: (0, -8, 1.7) — 8m back, eye height -Rotation: aim at (0, 0, 1.5) -Lens: 85mm or 50mm -DOF: focus_distance = 8.0, f/2.0 -``` - -### Dramatic Low Angle -``` -Location: (4, -5, 0.5) — low, ~6.4m away -Rotation: aim at (0, 0, 2) — looking up -Lens: 35mm -``` - -### Wide Interior -``` -Location: (3, -2, 1.7) — close, eye height -Rotation: aim at (0, 5, 1.5) -Lens: 24mm -``` - -### Overhead / Top-Down -``` -Location: (0, 0, 10) — directly above -Rotation: (0, 0, 0) — pointing straight down -Lens: 50mm -``` - -## DEPTH OF FIELD SETUP - -| Parameter | Effect | Typical Values | -|---|---|---| -| `dof_use` | Enable/disable DoF | `true` for cinematic | -| `dof_focus_distance` | Distance to sharp focus plane (meters) | Match subject distance | -| `dof_aperture_fstop` | Aperture size — lower = more blur | 1.4 extreme, 2.8 moderate, 5.6 slight, 16 sharp | - -**Aperture guidelines:** -- f/1.4 – f/2.0: Extreme bokeh, only subject sharp -- f/2.8 – f/4.0: Product photography, moderate background blur -- f/5.6 – f/8.0: Landscape, most things in focus -- f/11 – f/16: Everything sharp, architectural - -## SENSOR SIZE (affects field of view) - -- Default Blender sensor: 36mm (full-frame equivalent) -- Larger sensor = wider field of view at same focal length -- For matching real cameras, set `sensor_width` accordingly - -## COMMON MISTAKES TO AVOID - -1. ❌ Placing camera at the DOF focus distance — results in subject filling entire frame with telephoto lenses -2. ❌ Using manual rotation_euler for camera aiming — fragile, use `aim_camera_at` instead -3. ❌ Forgetting to set camera as active (`set_active: true`) — render will use the wrong camera -4. ❌ Not verifying with viewport screenshot — always check framing visually diff --git a/data/tool-guides/execute-code-guide.md b/data/tool-guides/execute-code-guide.md deleted file mode 100644 index 47b30c0..0000000 --- a/data/tool-guides/execute-code-guide.md +++ /dev/null @@ -1,137 +0,0 @@ ---- -title: "Execute Code Scene-Building Guide" -category: "execute-code" -tags: ["execute_code", "scene building", "reference image", "blockout", "refinement", "silhouette", "assembly", "blender python", "staging"] -triggered_by: ["execute_code"] -description: "General-purpose guidance for Blender Python scene construction in Blender 5.x+. Teaches staged reference reconstruction, focused refinement passes, and expert-level prop decomposition without collapsing an entire scene into one coarse script." -blender_version: "5.0+" ---- - -# Execute Code Scene-Building Guide - -## CORE PRINCIPLE: STAGE THE SCENE, THEN REFINE WHAT THE CAMERA WILL REVEAL - -`execute_code` is the strongest geometry tool, but it should NOT be used as one giant "do everything at once" script for a whole reference image. - -An expert workflow is: -1. Build the **scene anchors** and overall layout -2. Verify proportions and placement visually -3. Refine the **camera-visible props** that still read incorrectly -4. Leave camera, lighting, render settings, and simple materials to direct tools when possible - -**Rule:** Use `execute_code` in focused passes. Each pass should create or improve one logical cluster, not the entire final scene in one shot. - -## WHEN `execute_code` IS THE RIGHT TOOL - -Use `execute_code` for: -- Complex geometry creation -- Multi-part assemblies -- Organic or irregular forms -- Rebuilds that need vertex-level or modifier-level control -- Focused refinement of visible props after screenshot review - -Do NOT use `execute_code` for: -- Simple light setup that direct light tools can handle -- Camera creation or basic lens changes -- Straightforward render settings -- Simple Principled material creation when `create_material` + `assign_material` is enough - -## REFERENCE-DRIVEN RECONSTRUCTION WORKFLOW - -### Pass 1: Structural Blockout -Create only the large spatial anchors: -- Room envelope or backdrop surfaces -- Ground plane / floor -- Main furniture or architectural masses -- Large hero objects that define composition - -At this stage: -- Prioritize correct scale, spacing, alignment, and silhouette -- Use simple but intentional materials -- Do NOT spend time on delicate secondary props yet - -### Pass 2: Visual Checkpoint -After the blockout, inspect a viewport screenshot and ask: -- Is the composition correct? -- Are the main masses aligned to the reference? -- Are the relative sizes plausible? -- Which objects still fail silhouette recognition? - -If the answer is "mostly correct but some visible props read poorly", do NOT rebuild the whole scene. Run a focused refinement pass. - -### Pass 3: Focused Refinement -Refine high-value props in separate `execute_code` calls. - -A prop deserves its own refinement pass when it is: -- Near the camera -- High contrast against the background -- Positioned near the visual center -- Semantically distinctive enough that a crude proxy will be obvious - -**Rule:** Small does not mean unimportant. If the viewer can clearly read the object, it needs a recognizable silhouette. - -### Pass 4: Finalization -Once geometry is stable: -- Use direct tools for camera, lighting, render settings, and simple materials -- Use another focused `execute_code` pass only when a direct tool cannot produce the required result - -## PROMINENCE-BASED DETAIL ALLOCATION - -Not every object needs the same detail level. Allocate effort by **screen prominence**, not by object count. - -### High-Prominence Objects -These need strong silhouette fidelity and secondary shape breakup: -- Large objects near frame center -- Foreground props -- Objects crossing strong light/shadow boundaries -- Objects with a very recognizable profile - -### Medium-Prominence Objects -These need correct proportions and 2-4 meaningful sub-parts, but not micro-detail. - -### Low-Prominence Objects -These may stay simplified, but must still avoid obviously nonsensical primitive stacks. - -## MINIMUM-PART DECOMPOSITION RULE - -For any camera-visible prop, preserve the silhouette with a minimum-part decomposition rather than a single primitive or disconnected rough stack. - -General patterns: -- **Structural hard-surface prop:** primary mass + support/attachment element + edge treatment -- **Organic sprig/foliage prop:** stem or branch structure + leaf-bearing surfaces or clustered leaf volume -- **Contained or hollow prop:** body volume + opening/rim + support or handle logic -- **Wearable or contact-based prop:** upper body mass + contact/base mass + toe/heel/end-shape logic where applicable - -**Rule:** If the object is represented only by thin lines, raw primitives, or unrelated blocks, it is still in blockout, not finished. - -## ASSEMBLY AND POSITIONING RULES INSIDE `execute_code` - -When constructing multi-part geometry: -- Build from the base outward -- Derive attachment points from dimensions, radii, and thicknesses -- Compute contact surfaces instead of eyeballing offsets -- Use `mathutils.Vector` and `matrix_world @ Vector(...)` when rotation affects placement - -For close-contact placement: -- Determine the supporting surface of the existing object -- Determine the actual half-extent of the incoming object -- Place by surface-to-surface contact plus a small margin - -Do not hardcode arbitrary offsets when you can calculate attachment or support positions from geometry. - -## VISUAL QUALITY RULES - -- Add bevels or softened edges to avoid overly sharp CG primitives -- Prefer tapered and non-uniform forms over perfect cylinders and cubes when realism requires it -- Use repeated sub-elements or clustered forms instead of bare placeholders for organic detail -- Keep naming explicit so follow-up refinement passes can target the right object cleanly - -## COMMON FAILURES TO AVOID - -1. ❌ Building the entire reference scene in one monolithic `execute_code` pass -2. ❌ Treating screenshot review as optional instead of as the trigger for focused refinement -3. ❌ Leaving prominent props in raw blockout state because the overall layout is "good enough" -4. ❌ Using line-like proxies for foliage or other volume-bearing organic forms -5. ❌ Using disconnected primitive stacks for camera-visible props that need a clear unified silhouette -6. ❌ Rebuilding stable scene anchors when only one prop actually needs refinement -7. ❌ Using `execute_code` for lights, camera, or render settings when direct tools already cover the task diff --git a/data/tool-guides/export-guide.md b/data/tool-guides/export-guide.md deleted file mode 100644 index b071027..0000000 --- a/data/tool-guides/export-guide.md +++ /dev/null @@ -1,112 +0,0 @@ ---- -title: "Export & File Format Guide" -category: "export" -tags: ["export", "GLB", "GLTF", "FBX", "OBJ", "STL", "file format", "3D printing", "game engine", "web", "UV", "cleanup", "texture dependencies", "missing image paths", "prepare_uv_layout", "validate_export_readiness", "inspect_export_cleanup_candidates", "inspect_export_texture_dependencies", "export_asset_package", "export_object"] -triggered_by: ["prepare_uv_layout", "validate_export_readiness", "inspect_export_cleanup_candidates", "inspect_export_texture_dependencies", "export_asset_package", "export_object"] -description: "Domain knowledge for 3D model export, file format selection, pre-export preparation, and format-specific considerations." -blender_version: "4.0+" ---- - -# Export & File Format Guide - -## FORMAT SELECTION - -| Format | Extension | Best For | Materials | Animation | Notes | -|---|---|---|---|---|---| -| **GLB** | `.glb` | Web, AR/VR, Three.js | ✅ PBR | ✅ | **Default choice** — single binary file, widely supported | -| GLTF | `.gltf` | Web (separate files) | ✅ PBR | ✅ | Same as GLB but textures as separate files | -| FBX | `.fbx` | Game engines (Unity, Unreal) | ✅ | ✅ | Industry standard for game dev | -| OBJ | `.obj` | Legacy compatibility | ⚠️ Basic | ❌ | Oldest format, universally supported | -| STL | `.stl` | 3D printing | ❌ No | ❌ | Mesh-only, no colors or materials | - -### Quick Decision Guide -- **Web/online viewer?** → GLB -- **Unity/Unreal?** → FBX -- **3D printing?** → STL -- **Simple mesh exchange?** → OBJ -- **Not sure?** → GLB (most versatile) - -## PRE-EXPORT CHECKLIST - -### Always Do Before Export -1. **Apply transforms** (`apply_transforms`) — Resets scale/rotation to identity, prevents distorted models -2. **Apply all modifiers** (if you want clean geometry) — SubSurf, mirrors, booleans become permanent -3. **Set correct origin** — Usually center of geometry or bottom center for grounding -4. **Inspect cleanup candidates** (`inspect_export_cleanup_candidates`) — Find helper empties, reference objects, hidden construction geometry, and protected modifier/constraint targets before cleanup -5. **Validate export readiness** (`validate_export_readiness`) — Catches missing UVs, invalid mesh data, missing textures, unapplied transforms, and format/path issues before export - -### UV Policy -- Imported assets: call `prepare_uv_layout` with `mode: "preserve_original"` first. Do not overwrite original UVs unless the user asks or validation reports no usable UVs. -- Generated or procedural meshes without UVs: call `prepare_uv_layout` with `mode: "smart_project"` before applying image textures or exporting GLB/FBX/OBJ. -- Existing UVs that only need cleaner packing: use `mode: "pack_existing"`. -- Bake/lightmap workflows: use `mode: "lightmap_pack"` with a named UV map. -- Treat partial UVs, degenerate UV islands, and UV bounds far outside 0-1 as export-readiness problems. Run `validate_export_readiness` after any UV prep before exporting. - -### Texture Portability Policy -- For portable GLB/FBX/OBJ delivery, prefer PNG or JPEG image textures. -- Use `inspect_export_texture_dependencies` before repair work when a model has missing textures, white materials, empty image paths, packed images, or risky image formats. It gives the exact material/node/image dependency list without mutating the scene. -- If `validate_export_readiness` or `inspect_export_texture_dependencies` reports WebP, AVIF, HEIC, PSD, TIFF, missing image paths, empty image paths, or unpacked sidecar risks, fix those before export when the asset needs to work in browsers, game engines, or external viewers. -- Packed images are acceptable for Blender-internal work, but exported interchange assets should still be validated in the target viewer. - -### Recommended Tool Sequence -Use `export_asset_package` for normal user-requested exports. It runs UV prep, optional rotation/scale application, readiness validation, export, and expected-file reporting in one deterministic flow: - -```text -export_asset_package( - names=["HeroMesh"], - filepath="C:/exports/hero.glb", - file_format="GLB", - uv_mode="preserve_original", - apply_transforms=true -) -``` - -Use the lower-level sequence only when a validation report needs manual repair: - -1. `prepare_uv_layout(names, mode: "preserve_original")` -2. If missing UVs on a generated mesh, `prepare_uv_layout(names, mode: "smart_project")` -3. `inspect_export_cleanup_candidates(names, action: "report")` before deleting, hiding, or moving helpers -4. `inspect_export_texture_dependencies(names, file_format)` when texture portability is uncertain -5. `validate_export_readiness(names, filepath, file_format)` -6. Fix reported errors -7. `export_object(names, filepath, file_format)` - -### Format-Specific Prep -- **GLB/GLTF:** Ensure materials use Principled BSDF (auto-converts to PBR) -- **FBX:** Apply scale — FBX uses centimeters by default, Blender uses meters -- **STL:** Make sure mesh is watertight (no holes) for 3D printing -- **OBJ:** Materials export as separate .mtl file — keep files together - -## SCALE CONSIDERATIONS - -| Scenario | Scale Factor | Notes | -|---|---|---| -| Blender → Web (Three.js) | 1.0 (no change) | GLB preserves Blender units | -| Blender → Unity | 1.0 | Unity reads FBX scale correctly | -| Blender → Unreal | 100× | Unreal uses centimeters | -| Blender → 3D Print (mm) | 1000× | Blender meters → millimeters | - -## PATH HANDLING - -- Use **absolute paths**: `/tmp/model.glb`, `C:/tmp/export.fbx` -- Include the file extension in the path — it determines the format -- The tool creates parent directories if needed -- Existing files at the same path are overwritten - -## MATERIAL COMPATIBILITY - -| Material Feature | GLB | FBX | OBJ | STL | -|---|---|---|---|---| -| Base Color | ✅ | ✅ | ✅ | ❌ | -| Metallic/Roughness | ✅ | ✅ | ❌ | ❌ | -| Normal Maps | ✅ | ✅ | ⚠️ | ❌ | -| Emission | ✅ | ✅ | ❌ | ❌ | -| Transparency | ✅ | ✅ | ⚠️ | ❌ | - -## COMMON MISTAKES TO AVOID - -1. ❌ Exporting without applying transforms — causes scale/rotation issues in target application -2. ❌ Using STL when materials are needed — STL is mesh-only -3. ❌ Forgetting file extension in path — the tool uses file_format param or extension to determine format -4. ❌ Exporting unapplied modifiers — geometry won't match what you see in viewport -5. ❌ Using OBJ for PBR materials — OBJ only supports basic diffuse/specular, use GLB instead diff --git a/data/tool-guides/lighting-guide.md b/data/tool-guides/lighting-guide.md deleted file mode 100644 index 72d4b10..0000000 --- a/data/tool-guides/lighting-guide.md +++ /dev/null @@ -1,143 +0,0 @@ ---- -title: "Lighting Setup & Configuration Guide" -category: "lighting" -tags: ["lighting", "light", "energy", "color temperature", "three-point", "studio", "sun", "spot", "area", "point", "HDRI", "environment", "world", "background", "set_world_environment", "setup_studio_scene", "validate_studio_scene", "frame_camera_to_targets", "add_light", "set_light_properties", "lamp", "candle", "lantern", "torch", "sconce", "chandelier", "emission", "shade", "lamp shade"] -triggered_by: ["set_world_environment", "setup_studio_scene", "validate_studio_scene", "frame_camera_to_targets", "add_light", "set_light_properties"] -description: "Domain knowledge for light type selection, energy scaling, color temperature, shadow configuration, and professional lighting setups in Blender." -blender_version: "4.0+" ---- - -# Lighting Setup & Configuration Guide - -## LIGHT TYPE SELECTION - -## STUDIO PREVIEW TOOLING - -For asset previews, product shots, or "make this model look good" requests, prefer `setup_studio_scene` before manually placing lights. It builds a reusable key/fill/rim area-light setup, frames the camera, and sets preview render defaults. - -Recommended sequence: -1. `setup_studio_scene(target_names, preset: "studio")` -2. `validate_studio_scene(target_names)` -3. If framing is weak, `frame_camera_to_targets(target_names, desired_frame_fill: 0.62)` and validate again. -4. `render_image(output_path)` - -`validate_studio_scene` now checks all eight world-space target bounding-box corners in camera space and reports `camera.frame_bounds`. Treat frame fill warnings as real composition problems: -- "too little" means the model is distant or visually weak; call `frame_camera_to_targets` with a higher `desired_frame_fill` before changing the whole lighting rig. -- "too much" or outside-frame warnings mean the model is cropped; call `frame_camera_to_targets` with a lower `desired_frame_fill` or larger `frame_margin`, then validate again. -- For product previews, a fill around 0.35-0.75 usually leaves enough professional breathing room. - -Use `add_light` and `set_light_properties` for custom scene lighting after the studio preset is in place or when the user asks for a specific lamp/sun/spot setup. - -Use `set_world_environment` for world/background changes after the scene rig is in place: -- `mode: "solid"` for clean studio backgrounds and controlled lighting. -- `mode: "hdri"` when the user provides or downloads an HDRI path and needs image-based environment lighting or rotation. -- `mode: "sky"` for outdoor daylight without downloading an HDRI. - -Do not write `execute_code` just to adjust background color, HDRI strength, or environment rotation. - -| Type | Behavior | Best For | Key Properties | -|---|---|---|---| -| **POINT** | Emits light in all directions from a point | Lamps, candles, fill lights | `energy`, `shadow_soft_size` | -| **SUN** | Parallel rays, position doesn't matter (only rotation) | Outdoor scenes, directional key light | `energy` (1–5 for EEVEE), `shadow_soft_size` | -| **SPOT** | Cone of light from a point | Stage lighting, focused highlights, dramatic | `energy`, `spot_size` (degrees), `spot_blend` | -| **AREA** | Emits from a rectangular/square surface | Soft studio lighting, product photography | `energy`, `size` (larger = softer shadows) | - -## ENERGY SCALE BY ENGINE - -Energy values differ dramatically between EEVEE and Cycles: - -| Light Type | EEVEE Energy | Cycles Energy | Note | -|---|---|---|---| -| POINT | 100–1000 W | 100–1000 W | Similar scale | -| SUN | 1–5 | 1–5 | Low values, very powerful | -| SPOT | 200–1500 W | 200–1500 W | Similar scale | -| AREA | 200–800 W | 200–800 W | Size affects perceived brightness | - -**Rule:** Start with these ranges and adjust. If the scene is too dark, multiply energy by 2–3×. If too bright, halve it. - -## COLOR TEMPERATURE REFERENCE - -Use these RGB values (0–1 range) to simulate real-world color temperatures: - -| Temperature | Name | RGB Value | Use Case | -|---|---|---|---| -| 1800K | Candle | (1.0, 0.58, 0.16) | Warm intimate scenes | -| 2700K | Warm tungsten | (1.0, 0.76, 0.46) | Indoor residential lighting | -| 3500K | Warm white | (1.0, 0.84, 0.66) | Office/commercial lighting | -| 4500K | Neutral | (1.0, 0.92, 0.82) | Neutral fill light | -| 5500K | Daylight | (1.0, 0.96, 0.92) | Outdoor daylight, key light | -| 6500K | Overcast | (0.87, 0.91, 1.0) | Cloudy sky, cool fill | -| 8000K | Blue sky | (0.75, 0.83, 1.0) | Shade/sky fill, cool rim | - -## PROFESSIONAL LIGHTING SETUPS - -### Three-Point Lighting (Most Common) -- **Key light** (AREA, right side, 45° above): Energy 600–800, warm daylight color, size 2.0 -- **Fill light** (AREA, left side, lower): Energy 200–400 (30–50% of key), cool blue tint, size 4.0 -- **Rim/back light** (SPOT, behind subject): Energy 400–600, white, creates edge separation - -Positioning relative to target at origin: -- Key: (5, -5, 5) — 45° right, 45° above -- Fill: (-4, -3, 2) — opposite side, lower -- Rim: (1, 6, 4) — behind, slightly off-center - -### Studio Product Lighting -- 4-light setup: Key (top-right), Fill (top-left), Ground bounce (below), Background (behind) -- All AREA lights with large sizes (5–8m) for extremely soft shadows -- Key energy 600, Fill 250, Bounce 100, Background 200 - -### Outdoor Sunlight -- SUN light: Energy 3–5, warm color (1.0, 0.95, 0.85), small angle (0.5°) for sharp shadows -- Sky fill: Large AREA light from above, energy 80–100, cool blue (0.3, 0.55, 0.9) - -### Dramatic/Cinematic -- Single strong SPOT as key (one side), energy 1000–1200 -- Colored rim SPOT from opposite side, energy 600–800, blue tint (0.3, 0.5, 1.0) -- Minimal or no fill light for high contrast - -## SHADOW GUIDELINES - -| Parameter | Effect | Recommendation | -|---|---|---| -| `shadow_soft_size` | Larger = softer/blurrier shadows | 0.5–1.0 for hard, 2.0–4.0 for soft | -| AREA `size` | Larger area = naturally softer shadows | 2.0 sharp, 5.0+ soft studio | -| SUN `angle` | Controls sun shadow softness | 0.5° sharp, 2–5° soft | - -## LIGHT-EMITTING OBJECT GEOMETRY — CRITICAL - -When creating geometry for objects that emit light (floor lamps, table lamps, candles, lanterns, sconces, chandeliers, torches), you MUST ensure light can actually escape the geometry: - -### Open Geometry Rule -- **Lamp shades** created from cones or cylinders MUST have an **open bottom** so the point light inside illuminates downward. A closed cone traps light completely — nothing reaches the scene. -- Use `bpy.ops.mesh.primitive_cone_add()` and then **delete the bottom face** (the n-gon cap), or create the shade manually from vertices without a bottom face. -- Alternatively, for simple shades, use a torus or ring shape that is inherently open. - -### Emission Material Rule -- The shade/housing material should use **Emission Color + Emission Strength** so the shade itself appears to glow, matching the point light inside: - ``` - bsdf.inputs['Emission Color'].default_value = (warm_r, warm_g, warm_b, 1.0) - bsdf.inputs['Emission Strength'].default_value = 2.0–5.0 - ``` -- This is especially important for translucent materials (fabric shades, paper lanterns, frosted glass) where real light would shine through. - -### Point Light Placement -- Place the POINT light **at the center of the open space**, not inside solid geometry. -- For a floor lamp with a cone shade, place the light at the **bottom opening of the cone**, slightly inside. -- For candles, place the light just above the flame geometry, not inside the wax body. - -### Checklist for Light-Emitting Objects -1. ✅ Shade geometry has at least one opening (bottom, top, or sides) -2. ✅ Shade material has Emission Color set to a warm tone -3. ✅ POINT light is placed at the opening, not trapped inside solid mesh -4. ✅ Light energy is high enough to illuminate surrounding objects (500W+ for lamps) -5. ✅ If the shade is translucent (paper, fabric), lower Roughness and add Emission Strength 2–5 - -## COMMON MISTAKES TO AVOID - -1. ❌ Using POINT lights for all lighting — AREA is better for soft, realistic shadows -2. ❌ Setting SUN energy to 100+ — SUN uses much lower values (1–5 for EEVEE) -3. ❌ Forgetting light color — default white (1,1,1) feels flat; warm key + cool fill adds depth -4. ❌ Only using one light — even simple scenes benefit from key + fill at minimum -5. ❌ Not aiming lights at the target — use Track To constraint or calculate rotation toward subject -6. ❌ Creating closed lamp shade geometry (cone/cylinder) around a point light — the light gets trapped and nothing illuminates the scene -7. ❌ Not adding emission material to lamp shades — the shade looks dark even when a light is inside, making it appear non-functional diff --git a/data/tool-guides/local-asset-guide.md b/data/tool-guides/local-asset-guide.md deleted file mode 100644 index b57435b..0000000 --- a/data/tool-guides/local-asset-guide.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -title: "Local Asset Library Guide" -category: "assets" -tags: ["asset library", "catalog", "local assets", "curated assets", "import", "commodity props", "search_local_assets", "import_local_asset", "polyhaven", "sketchfab", "external assets"] -triggered_by: ["get_local_asset_library_status", "search_local_assets", "import_local_asset", "get_polyhaven_status", "get_polyhaven_categories", "search_polyhaven_assets", "download_polyhaven_asset", "get_sketchfab_status", "search_sketchfab_models", "download_sketchfab_model"] -description: "Guidance for using the curated local ViperMesh asset library before procedural modeling or external downloads." -blender_version: "5.0+" ---- - -# Local Asset Library Guide - -## WHEN TO USE LOCAL ASSETS FIRST - -Prefer the local curated asset library when the request includes: -- commodity props with familiar silhouettes -- repeated production assets used across many scenes -- realism-oriented decor, furniture, foliage, accessories, or footwear -- user-provided private assets that should stay available across sessions - -Examples of good local-asset candidates: -- shoes, bags, lamps, books, vases, baskets -- small furniture and decor pieces -- plant arrangements and common foliage props -- branded or user-owned assets that should not be regenerated - -## DECISION RULE - -Choose the local asset library before `execute_code` when: -1. the prop is recognizable and reusable across scenes -2. the task benefits from realistic silhouette more than from bespoke procedural control -3. the asset does not need scene-specific topology generation - -Choose `execute_code` instead when: -- the object is unique to the scene -- the shape is simple and faster to model than to search -- the user explicitly asked for procedural generation -- no acceptable local asset match exists - -Choose PolyHaven when: -- you need CC0-safe external textures, HDRIs, or models -- the local library does not contain a suitable match - -Keep the workflow hybrid: -- use direct tools for room structure, spatial layout, cameras, lights, transforms, and render setup -- reserve `execute_code` for unsupported custom hero forms or procedural geometry that direct tools cannot express -- use local curated assets for commodity props that benefit from a prepared silhouette -- after import, always treat scale and orientation as suspect until checked in scene context - -## SEARCH STRATEGY - -When searching the local catalog: -- start with the object's plain-language noun phrase -- add category or style filters only after the first search -- prefer the highest-quality match that already fits the intended realism/style -- do not import multiple near-duplicate assets unless comparison is necessary - -## IMPORT STRATEGY - -After importing a local asset: -- inspect what objects or collections were added -- place and scale it deliberately in scene context -- if the asset is complex, adjust only the root-level transforms first -- confirm silhouette and proportion with `get_viewport_screenshot` -- do not assume two different local assets share consistent real-world scale; correct them relative to nearby objects - -## QUALITY RULE - -For camera-visible detail props, a strong local asset match is better than a crude procedural proxy. - -Do not spend multiple refinement passes procedurally rebuilding a common prop if the local curated library already contains a better reusable asset. diff --git a/data/tool-guides/material-realism-guide.md b/data/tool-guides/material-realism-guide.md deleted file mode 100644 index 7cb4315..0000000 --- a/data/tool-guides/material-realism-guide.md +++ /dev/null @@ -1,123 +0,0 @@ ---- -title: "Material Realism & Surface Differentiation Guide" -category: "materials" -tags: ["material", "realism", "PBR", "glass", "transparent", "IOR", "surface", "differentiation", "floor", "wall", "furniture", "category"] -triggered_by: ["create_material"] -description: "Higher-level material engineering principles for creating visually distinct and physically plausible materials across different functional surfaces. Covers material differentiation strategy, transparency setup, and surface category guidelines." -blender_version: "5.0+" ---- - -# Material Realism & Surface Differentiation Guide - -## CORE PRINCIPLE: FUNCTIONAL SURFACE DIFFERENTIATION - -In any scene, objects serve different functional roles. Each functional category MUST have a visually distinct material. Viewers subconsciously expect: -- **Floors** to look different from **walls** -- **Furniture** to look different from **structural surfaces** -- **Metal fixtures** to stand out from **wood furniture** -- **Transparent surfaces** (glass) to be clearly distinguishable - -**Rule:** Before assigning materials, categorize every object into a functional group. Then ensure NO two groups share identical material parameters. - -## MATERIAL DIFFERENTIATION STRATEGY - -### Category-Based Material Planning -When building a scene with many objects, plan materials by category FIRST: - -``` -Scene: Coffee Shop -├── STRUCTURAL (walls, ceiling) → Wall_Mat: cream, roughness 0.85 -├── WALKING SURFACE (floor) → Floor_Mat: dark wood, roughness 0.65 -├── FURNITURE (tables, chairs, counter) → Furniture_Mat: medium wood, roughness 0.55 -├── FIXTURES (coffee machine, taps) → Metal_Mat: metallic 1.0, roughness 0.25 -├── TRANSPARENT (windows) → Glass_Mat: transmission 0.95, IOR 1.5 -└── DECORATIVE (cushions, plants) → Varied materials per item -``` - -### Same-Material-Type Differentiation -When multiple surfaces use the same base type (e.g., "wood"), differentiate with: - -| Differentiator | Floor Wood | Furniture Wood | -|---|---|---| -| Color tone | Darker/grayer | Warmer/richer | -| Roughness | 0.6–0.7 (worn) | 0.45–0.55 (polished) | -| Example RGB | [0.35, 0.22, 0.12] | [0.55, 0.35, 0.18] | - -## TRANSPARENT MATERIAL SETUP (Blender 5.x) - -### EEVEE Glass — Correct Pattern -Glass in EEVEE requires a full Principled BSDF setup with Blender 5.x socket names and transparency settings. -Prefer `create_material_preset` with preset `glass` for this work. The preset tool owns: - -- `Transmission Weight` instead of the old `Transmission` socket, -- IOR and roughness defaults, -- transparent material settings when needed, -- optional object assignment through `object_name`. - -After creating glass, call `inspect_material_node_graph` to verify that Blender accepted the expected sockets -and that the material is assigned to the intended object. Use `execute_code` only for custom shader networks -that the preset tool cannot express. - -### Common IOR Values -| Material | IOR | Notes | -|---|---|---| -| Air | 1.000 | Reference baseline | -| Water | 1.333 | Clear liquid | -| Ice | 1.309 | Frozen water | -| Glass (window) | 1.500 | Standard architectural glass | -| Glass (Pyrex) | 1.474 | Heat-resistant | -| Crystal | 2.000 | Decorative items | -| Plastic (clear) | 1.460 | Containers, packaging | -| Diamond | 2.418 | Gemstones | - -### EEVEE Transparency Alternatives -For interior windows where refraction isn't critical: -- **Transparent shader** is often better than Glass shader in EEVEE — prevents light bleed artifacts -- If reflections aren't needed, simply removing glass geometry is acceptable -- For scenes prioritizing visual quality, consider using Cycles for final render - -## PBR REALISM RULES - -### Metal vs. Non-Metal (Strict Binary) -| Property | Metal | Non-Metal | -|---|---|---| -| Metallic | **1.0** | **0.0** | -| Specular | Has no effect (driven by Base Color) | Leave at default 0.5 (= 4% reflectance) | -| Base Color | Bright values (HSV value ≥ 0.7) | Avoid pure black or pure white | - -**Rule:** Metallic is ALWAYS 0 or 1. Intermediate values (0.3, 0.5) create materials that don't exist in reality. - -### Surface Category Material Properties -| Surface | Roughness | Metallic | Character | -|---|---|---|---| -| Walls (plaster/paint) | 0.8–0.95 | 0.0 | Matte, light-absorbing backdrop | -| Floor (hardwood) | 0.5–0.7 | 0.0 | Semi-glossy, shows reflections | -| Floor (tile/stone) | 0.6–0.8 | 0.0 | Harder surface, slight sheen | -| Furniture (wood) | 0.4–0.6 | 0.0 | Polished, warmer color | -| Fabric/upholstery | 0.85–0.95 | 0.0 | Very matte, use Sheen for cloth | -| Metal fixtures | 0.15–0.35 | 1.0 | Highly reflective, contrasts room | -| Ceramic | 0.1–0.2 | 0.0 | Very smooth, near-mirror finish | - -### Cloth/Fabric Simulation -For fabric surfaces (cushions, upholstery), use the Sheen input: -```python -bsdf.inputs["Sheen Weight"].default_value = 0.5 # Blender 5.x name -bsdf.inputs["Roughness"].default_value = 0.9 -``` - -## MATERIAL ORGANIZATION BEST PRACTICES - -1. **Name materials descriptively**: `Wood_Oak_Light`, `Metal_Chrome`, `Glass_Window` — not `Material.001` -2. **One material per functional purpose**: Don't reuse `Wood_Mat` for both floor AND furniture -3. **Create materials before assigning**: Plan all materials, create them, THEN assign in a single pass -4. **Avoid duplicate material creation**: Check if a material with the same name already exists before creating - -## COMMON MISTAKES TO AVOID - -1. ❌ Same material on floor and furniture — always differentiate by color AND roughness -2. ❌ Setting metallic to 0.5 — use exactly 0 (non-metal) or 1 (metal) -3. ❌ Using "Transmission" instead of "Transmission Weight" — old 3.x socket name fails in 5.x -4. ❌ Creating glass with `create_material` alone — use `create_material_preset` preset `glass` for IOR/Transmission Weight -5. ❌ Forgetting `mat.blend_method = 'BLEND'` for transparent materials in EEVEE -6. ❌ Walls with same roughness as furniture — walls should be 0.8+ (matte), furniture 0.4–0.6 -7. ❌ Pure white/black base colors for non-metals — stay within 10–95% brightness range diff --git a/data/tool-guides/materials-guide.md b/data/tool-guides/materials-guide.md deleted file mode 100644 index f26cd93..0000000 --- a/data/tool-guides/materials-guide.md +++ /dev/null @@ -1,128 +0,0 @@ ---- -title: "Materials & PBR Configuration Guide" -category: "materials" -tags: ["material", "PBR", "Principled BSDF", "metallic", "roughness", "color", "color space", "normal map", "glass", "metal", "wood", "plastic", "texture folder", "create_material", "assign_material", "create_material_preset", "create_pbr_material_from_textures", "inspect_material_node_graph", "normalize_material_texture_channels"] -triggered_by: ["list_materials", "create_material", "assign_material", "create_material_preset", "create_pbr_material_from_textures", "inspect_material_node_graph", "normalize_material_texture_channels", "set_texture"] -description: "Domain knowledge for Principled BSDF material setup, PBR parameter ranges, common surface presets, and Blender 5.x material API changes." -blender_version: "4.0+" ---- - -# Materials & PBR Configuration Guide - -## PRINCIPLED BSDF PARAMETER REFERENCE - -Prefer `create_material_preset` for material work. It creates a Principled BSDF graph, applies physically plausible preset defaults, optionally wires PBR texture maps, assigns the material to an object, and returns warnings if a socket or texture path is not usable. - -Use `create_pbr_material_from_textures` when the user gives a texture directory or file list instead of exact `texture_maps`. It infers common PBR map roles, calls `create_material_preset`, reports ignored or duplicate candidates, and should be followed by `inspect_material_node_graph`. - -Use `create_material` + `assign_material` only for very simple color/metallic/roughness surfaces. Use `execute_code` only for genuinely custom shader networks that the preset tool cannot express. - -Use `normalize_material_texture_channels` before hand-written repair scripts when imported materials look wrong because image color spaces may be misclassified. It is report-only by default. Set `apply_changes=true` only after reviewing the report or when the requested cleanup is clearly to correct PBR channel color spaces. - -The core Principled parameters are: - -| Parameter | Range | What It Controls | -|---|---|---| -| `color` (Base Color) | [R, G, B] 0–1 | Surface color | -| `metallic` | 0–1 | 0 = dielectric (plastic, wood), 1 = metal (steel, gold) | -| `roughness` | 0–1 | 0 = mirror/gloss, 1 = matte/diffuse | - -**Rule:** Metallic is almost always either 0 or 1. Values between 0.0 and 1.0 are physically unrealistic — real surfaces are either metal or non-metal. - -## COMMON SURFACE PRESETS - -### Metals (metallic = 1.0) -| Surface | Color [R,G,B] | Roughness | Notes | -|---|---|---|---| -| Polished steel | [0.8, 0.8, 0.85] | 0.15–0.25 | | -| Brushed steel | [0.8, 0.8, 0.85] | 0.35–0.5 | | -| Gold | [1.0, 0.84, 0.0] | 0.2–0.35 | | -| Copper | [0.95, 0.64, 0.54] | 0.25–0.4 | | -| Bronze | [0.8, 0.5, 0.2] | 0.3–0.5 | | -| Iron/cast | [0.5, 0.5, 0.5] | 0.6–0.8 | | -| Aluminum | [0.91, 0.92, 0.93] | 0.2–0.4 | | -| Chrome | [0.55, 0.55, 0.55] | 0.05–0.1 | Very reflective | - -### Non-Metals (metallic = 0.0) -| Surface | Color [R,G,B] | Roughness | Notes | -|---|---|---|---| -| White plastic | [0.9, 0.9, 0.9] | 0.3–0.5 | | -| Red plastic | [0.8, 0.1, 0.1] | 0.3–0.5 | | -| Rubber | [0.15, 0.15, 0.15] | 0.8–0.95 | Very matte | -| Wood (light) | [0.55, 0.35, 0.18] | 0.5–0.7 | | -| Wood (dark) | [0.25, 0.15, 0.08] | 0.5–0.7 | | -| Concrete | [0.5, 0.5, 0.48] | 0.85–0.95 | | -| Ceramic/porcelain | [0.95, 0.95, 0.92] | 0.1–0.2 | Very smooth | -| Fabric/cloth | [varies] | 0.85–0.95 | Very matte | - -### Special Materials - -| Surface | Key Properties | Notes | -|---|---|---| -| Glass | Transmission Weight = 0.9–1.0, IOR = 1.45–1.52, Roughness = 0.0–0.05 | Use `create_material_preset` preset `glass` | -| Water | Transmission Weight = 0.9, IOR = 1.33, Roughness = 0.02 | Use `create_material_preset` preset `glass` with IOR override | -| Skin | Subsurface Weight = 0.3, Subsurface Radius = [0.1, 0.05, 0.02] | | -| Emissive | Emission Color + Emission Strength | Use `create_material_preset` preset `emissive` | - -## BLENDER 5.x SOCKET NAME CHANGES - -> **CRITICAL:** These socket names changed in Blender 4.0+ and are the ONLY valid names in 5.x: - -| Old Name (3.x) | New Name (4.0+ / 5.x) | -|---|---| -| `Clearcoat` | `Coat Weight` | -| `Clearcoat Roughness` | `Coat Roughness` | -| `Sheen` | `Sheen Weight` | -| `Transmission` | `Transmission Weight` | -| `Subsurface` | `Subsurface Weight` | - -Always use the new names. The old names will cause errors in Blender 5.x. - -## MATERIAL ASSIGNMENT RULES - -1. **create_material_preset** can create and assign in one call when `object_name` is provided -2. **create_material** creates a simple material, **assign_material** applies it to an object -3. Default slot_index = 0 replaces the first material slot (most common) -4. Use slot_index = -1 to append as a new material slot (for multi-material objects) -5. An object can have multiple material slots, each applied to different faces - -## PBR TEXTURE MAP RULES - -When using `create_material_preset.texture_maps`, use these roles: - -| Role aliases | Connected to | Color space | -|---|---|---| -| `base_color`, `albedo`, `diffuse`, `color` | Principled Base Color | sRGB | -| `roughness`, `rough` | Principled Roughness | Non-Color | -| `metallic`, `metalness`, `metal` | Principled Metallic | Non-Color | -| `normal`, `normal_map` | Image Texture -> Normal Map -> Principled Normal | Non-Color | -| `emission`, `emissive` | Principled Emission Color | sRGB | -| `displacement`, `height` | Image Texture -> Displacement -> Material Output | Non-Color | - -After complex material setup, call `inspect_material_node_graph` to verify: -- non-color maps are actually Non-Color, -- normal maps route through a Normal Map node, -- the material is assigned to the intended object and slot, -- Blender accepted the expected 5.x socket names. - -For imported assets, run `normalize_material_texture_channels` when texture maps look too washed out, too dark, overly glossy, or normal maps appear flat. The tool infers texture roles from node labels, image names/paths, and linked sockets. It expects base color and emission maps to use sRGB, while roughness, metallic, normal, displacement, alpha, and ambient-occlusion maps use Non-Color. - -## COLOR VALUE REFERENCE - -Common colors in [R, G, B] format (0–1 range): - -| Color | RGB | | Color | RGB | -|---|---|---|---|---| -| Pure white | [1.0, 1.0, 1.0] | | Pure black | [0.0, 0.0, 0.0] | -| Red | [0.8, 0.1, 0.1] | | Blue | [0.1, 0.2, 0.8] | -| Green | [0.1, 0.6, 0.1] | | Yellow | [0.9, 0.8, 0.1] | -| Orange | [0.9, 0.4, 0.05] | | Purple | [0.5, 0.1, 0.7] | -| Teal | [0.05, 0.6, 0.55] | | Pink | [0.9, 0.3, 0.5] | -| Dark gray | [0.15, 0.15, 0.15] | | Light gray | [0.75, 0.75, 0.75] | - -## COMMON MISTAKES TO AVOID - -1. ❌ Setting metallic to 0.5 — it should be 0 or 1, not in between -2. ❌ Using old socket names (Clearcoat, Transmission) — use Coat Weight, Transmission Weight -3. ❌ Forgetting to assign the material after creating it — pass `object_name` to `create_material_preset` or call `assign_material` -4. ❌ Setting glass/water via `create_material` alone — use `create_material_preset` so Transmission Weight and IOR are deterministic diff --git a/data/tool-guides/modifiers-guide.md b/data/tool-guides/modifiers-guide.md deleted file mode 100644 index 60f74e4..0000000 --- a/data/tool-guides/modifiers-guide.md +++ /dev/null @@ -1,142 +0,0 @@ ---- -title: "Modifier Usage & Configuration Guide" -category: "modifiers" -tags: ["modifier", "subdivision", "SubSurf", "bevel", "mirror", "boolean", "array", "smooth", "shade_smooth", "auto-smooth", "constraint", "stack", "mesh", "primitive", "cube", "sphere", "cylinder", "staircase", "stairs", "steps", "vertices", "faces", "repair", "cleanup", "loose", "normals", "decimate", "decimation", "voxel", "remesh", "quadriflow", "quad", "retopology", "add_mesh_primitive", "create_parametric_staircase", "create_mesh_from_data", "validate_mesh_geometry", "repair_mesh_geometry", "decimate_mesh", "voxel_remesh_mesh", "quadriflow_remesh_mesh", "inspect_modifier_constraint_stack", "add_modifier", "apply_modifier", "apply_transforms", "shade_smooth"] -triggered_by: ["add_mesh_primitive", "create_parametric_staircase", "create_mesh_from_data", "validate_mesh_geometry", "repair_mesh_geometry", "inspect_retopology_readiness", "decimate_mesh", "voxel_remesh_mesh", "quadriflow_remesh_mesh", "inspect_modifier_constraint_stack", "add_modifier", "configure_modifier", "configure_constraint", "add_object_constraint", "remove_object_constraint", "apply_modifier", "apply_transforms", "shade_smooth"] -description: "Domain knowledge for mesh modifier selection, configuration, stack ordering, and shading setup in Blender. Covers SubSurf, bevel, boolean, mirror, array, and auto-smooth." -blender_version: "4.0+" ---- - -# Modifier Usage & Configuration Guide - -## MESH FACTORY POLICY - -Use `add_mesh_primitive` for common base forms such as cubes, spheres, cylinders, cones, planes, and tori. This keeps simple object creation deterministic and leaves `execute_code` for genuinely custom geometry. - -Use `create_parametric_staircase` for stairs, stairways, stepped platforms, and architectural/interior staircase blockouts. Keep `shade_smooth: false` for hard-surface stair edges unless the user explicitly asks for a softened stylized result, then validate before adding bevels, railings, booleans, or export steps. - -Use `create_mesh_from_data` when the agent already has explicit vertices/faces for a simple custom mesh. This keeps mesh construction schema-owned and avoids generated Python for basic `mesh.from_pydata` work. - -Recommended sequence: -1. `add_mesh_primitive(primitive_type, name, validate: true)` for common primitives, `create_parametric_staircase(name, step_count, validate: true)` for staircase forms, or `create_mesh_from_data(name, vertices, faces, validate: true)` for explicit topology -2. `validate_mesh_geometry(names, cleanup: false)` -3. Add modifiers only after the validation report is clean - -Use `inspect_modifier_constraint_stack` before adding or applying modifiers to existing objects. This prevents duplicate SubSurf/Bevel/Boolean entries, catches missing Boolean targets, and shows whether constraints are already controlling an object. - -Use `validate_mesh_geometry(require_closed: true)` before 3D-printing or watertight export requests. Leave `cleanup: false` for inspection; use `cleanup: true` only when it is acceptable for Blender to mutate invalid mesh data. - -Use `decimate_mesh` for common dense-mesh reduction after `inspect_retopology_readiness`. Leave the default `mode: "COPY"` for source-preserving retopology experiments, then inspect and validate the result before rigging, export, or further remesh operations. Use `mode: "REPLACE"` only when the user explicitly asks for destructive replacement. - -Use `voxel_remesh_mesh` for common neural/sculpted mesh unification after inspection. Keep `mode: "COPY"` while evaluating voxel size, use smaller `voxel_size` values for detail preservation, and inspect/validate the result before decimation, QuadriFlow, rigging, or export. - -Use `quadriflow_remesh_mesh` when the goal is quad-dominant topology for rigging, animation, subdivision, or cleaner deformation. Keep `mode: "COPY"` while evaluating `target_faces`, and inspect/validate the result before binding, animation, subdivision, or export. Other QuadriFlow density modes are not exposed because latest-stable Blender 5.1.2 operator validation did not show stable behavior. - -Use `repair_mesh_geometry` when validation, retopology readiness, or export inspection finds duplicate vertices, loose vertices/edges, or inconsistent normals. Keep `mode: "COPY"` first, then validate the repaired result before retopology, rigging, or export. - -## MODIFIER SELECTION REFERENCE - -| Modifier | Type Enum | Purpose | Key Properties | -|---|---|---|---| -| **SUBSURF** | `SUBSURF` | Smooth mesh, add detail | `levels` (viewport), `render_levels` | -| **BEVEL** | `BEVEL` | Rounded edges | `width`, `segments`, `limit_method` | -| **MIRROR** | `MIRROR` | Symmetry | `use_axis` [X,Y,Z], `use_clip`, `merge_threshold` | -| **BOOLEAN** | `BOOLEAN` | Cut/join meshes | `operation`, `object`, `solver` | -| **ARRAY** | `ARRAY` | Repeat pattern | `count`, `relative_offset_displace` | -| **SOLIDIFY** | `SOLIDIFY` | Add thickness | `thickness`, `offset` | -| **DECIMATE** | `DECIMATE` | Reduce polygons | `ratio`, `decimate_type` | -| **SMOOTH** | `SMOOTH` | Smooth vertices | `factor`, `iterations` | -| **WIREFRAME** | `WIREFRAME` | Wire mesh | `thickness` | - -## SUBDIVISION SURFACE GUIDELINES - -| Level | Polygon Multiplier | Use Case | -|---|---|---| -| 0 | 1× | No smoothing (flat surfaces) | -| 1 | 4× | Light smoothing, preview | -| 2 | 16× | Standard quality for renders | -| 3 | 64× | High-quality close-ups | -| 4+ | 256×+ | Extreme detail — USE SPARINGLY (very slow) | - -**Rules:** -- Keep viewport levels ≤ 2 for performance -- Set render_levels 1 higher than viewport if needed -- Always apply SubSurf before export if target needs clean geometry - -## BEVEL GUIDELINES - -| Parameter | Purpose | Typical Values | -|---|---|---| -| `width` | Bevel size | 0.01–0.05 for subtle edge rounding, 0.1–0.5 for visible bevels | -| `segments` | Smoothness | 1 = chamfer, 3 = smooth, 6+ = very round | -| `limit_method` | Which edges to bevel | `ANGLE` (most common), `WEIGHT`, `NONE` | -| `angle_limit` | Angle threshold (if ANGLE) | 30° typical for hard-surface models | - -**Scale matters:** Bevel width is in world units. A 0.02m bevel on a 2m cube is subtle; on a 0.1m object it's huge. Always consider object scale. - -## BOOLEAN OPERATIONS - -| Operation | Result | -|---|---| -| `DIFFERENCE` | Cut the target shape out of the object | -| `UNION` | Merge two shapes together | -| `INTERSECT` | Keep only the overlapping volume | - -### Blender 5.x Boolean Solver -- **Use `EXACT` solver** — most reliable, handles complex geometry -- **`FAST` solver is REMOVED** in Blender 5.x — do NOT use it -- Available solvers: `EXACT`, `FLOAT`, `MANIFOLD` -- Always use `EXACT` unless you have specific performance concerns - -## SHADE SMOOTH & AUTO-SMOOTH - -### shade_smooth Tool -- `smooth: true` → Smooth shading (interpolated normals) -- `smooth: false` → Flat shading (faceted look) -- `angle` → Auto-smooth angle in degrees - -### Auto-Smooth Angle Guidelines -| Angle | Effect | Best For | -|---|---|---| -| 15–20° | Only very gentle curves smooth | Mechanical parts, hard-surface | -| **30°** | **Standard** — sharp edges stay sharp, curves smooth | **Most objects** | -| 45° | More aggressive smoothing | Organic shapes | -| 60–80° | Almost everything smoothed | Characters, soft objects | -| 180° | Everything smooth (equivalent to full smooth) | Spheres, fluid | - -## MODIFIER STACK ORDER - -Order matters! Modifiers are applied top to bottom: - -**Recommended order:** -1. Mirror (if using symmetry) -2. Array (if repeating) -3. Boolean (if cutting) -4. Subdivision Surface (smoothing) -5. Bevel (edge detail) — usually AFTER SubSurf or on its own - -**Bad patterns:** -- ❌ SubSurf before Boolean — can cause artifacts at cut edges -- ❌ Bevel before Mirror — creates asymmetric bevels at mirror seam - -## CONSTRAINT STACK CHECKS - -Use `inspect_modifier_constraint_stack` before editing constraints. Missing targets are usually a data problem, not a reason to write custom Python. For camera aiming, rigs, and tracked props, inspect the existing constraint first so the agent does not stack duplicate `TRACK_TO`, `DAMPED_TRACK`, or `COPY_TRANSFORMS` constraints. - -## APPLY TRANSFORMS - -**When to apply transforms:** -- ✅ Before export (GLB/FBX/OBJ) — prevents scale/rotation issues -- ✅ Before Boolean operations — ensures correct intersection -- ✅ Before adding physics simulations -- ✅ When object scale is not (1, 1, 1) - -**What it does:** Resets location/rotation/scale to identity (0/0/1) while baking the current transform into the mesh data. Visual appearance stays the same. - -## COMMON MISTAKES TO AVOID - -1. ❌ Using `FAST` boolean solver — removed in Blender 5.x, use `EXACT` -2. ❌ SubSurf level 4+ in viewport — causes extreme lag, keep viewport ≤ 2 -3. ❌ Forgetting to apply transforms before export — causes scaled/rotated models -4. ❌ Bevel width too large for small objects — always consider object scale -5. ❌ Not applying modifiers before joining objects — can lose modifier effects diff --git a/data/tool-guides/object-assembly-guide.md b/data/tool-guides/object-assembly-guide.md deleted file mode 100644 index 37162fd..0000000 --- a/data/tool-guides/object-assembly-guide.md +++ /dev/null @@ -1,322 +0,0 @@ ---- -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", "array", "pattern", "grid", "radial", "local-coordinate", "local-handedness", "add_empty_object", "set_empty_properties", "parent_set", "set_object_transform", "arrange_objects", "duplicate_object_pattern", "create_primitive_assembly"] -triggered_by: ["add_empty_object", "set_empty_properties", "parent_set", "set_object_transform", "arrange_objects", "duplicate_object_pattern", "create_primitive_assembly"] -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+" ---- - -# Multi-Part Object Assembly Guide - -## CRITICAL RULES - -1. **Build bottom-up.** Start with the base/foundation piece and calculate each subsequent piece's position relative to the piece it connects to. Never guess attachment points. - -2. **Calculate connection points, don't hardcode.** Use `base_z + base_height/2` to find the top of a base, then place the next piece starting there. This works regardless of where the base was placed. - -3. **Use parenting for compound objects that move together.** Parent all parts to a root Empty so the entire assembly can be moved/rotated as one unit. - -## ASSEMBLY STRATEGY - -### Step-by-Step Assembly Process -When building any compound object from primitives: - -1. **Identify the structural hierarchy** — Which part is the base? What attaches to what? -2. **Create the base part first** — Position it at the intended world location -3. **Calculate each attachment point** — Use the base part's position + half its dimensions -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 `create_primitive_assembly`, `arrange_objects`, and `duplicate_object_pattern` before `execute_code` for repeated objects, scene dressing, local-coordinate part families, and symmetrical layouts. These tools cover the common Python-loop cases: - -- L-brackets, fixture plates, simple furniture, object families, and rotated assemblies with preserved local handedness -- 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 - -### Create A Local-Coordinate Primitive Assembly - -Use `create_primitive_assembly` when a compound object can be described as bounded cube/sphere/cylinder/cone/plane/torus parts in a shared local coordinate frame. This is the direct-tool replacement for generated Python that creates many primitive children, transforms them through a root rotation, and parents them to a root Empty. - -```text -create_primitive_assembly( - name="Bracket_Rz90", - location=[1.2, 0, 0], - rotation=[0, 0, -90], - parts=[ - { primitive_type="cube", name="Bracket_Plate", local_location=[0,0,0.7], scale=[1.0,0.2,1.4], size=1 }, - { primitive_type="cube", name="Bracket_Foot", local_location=[0.55,0,0.09], scale=[1.1,0.55,0.18], size=1 }, - { primitive_type="cylinder", name="Bracket_Bolt_A", local_location=[-0.32,-0.14,0.48], local_rotation=[90,0,0], radius=0.08, depth=0.08 } - ], - create_root_empty=true -) -``` - -Use `local_location` for each part center before the root rotation is applied. Use `local_rotation` for part orientation in the assembly frame, such as rotating cylinder depth onto a local face. After creation, run `inspect_scene_grounding` and `inspect_spatial_relations` to verify contact and handedness. - -### 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):** -``` -top_z = pole.location.z + (pole_height / 2) -``` - -**Bottom attachment (seat on legs, shelf on brackets):** -``` -seat_z = leg_top_z + (seat_thickness / 2) -``` - -**Side attachment (arm from pole, sign from post):** -``` -arm_x = pole.location.x + (pole_radius) + (arm_length / 2) -arm_z = pole_top_z # or a specific fraction of pole height -``` - -**End of a horizontal arm:** -``` -end_x = arm.location.x + (arm_length / 2) -end_z = arm.location.z -``` - -## COMMON ARCHETYPES - -### Archetype 1: Street Lamp (Pole + Arm + Head + Base) - -``` -Structure: Base ← Pole ← Arm ← Head - │ │ │ └── Light source location - │ │ └── Horizontal, attached at pole top - │ └── Vertical, standing on base - └── Wide flat disc on ground -``` - -**Position calculations:** -```python -pole_height = 3.5 -pole_radius = 0.05 -arm_length = 0.5 -arm_radius = 0.03 -base_loc = (-1.5, 0, 0) - -# Base: flat disc on ground -base_z = base_loc[2] + 0.025 # half of base depth - -# Pole: centered on base, extends upward -pole_z = base_loc[2] + pole_height / 2 - -# Arm: horizontal, attached at pole top -arm_x = base_loc[0] + arm_length / 2 # extends from pole center -arm_z = base_loc[2] + pole_height # at pole top - -# Head: at END of arm, not offset from pole -head_x = base_loc[0] + arm_length # at arm tip -head_z = arm_z # same height as arm -``` - -**Common mistake:** Placing the head offset from the *pole*, not from the *arm tip*. The head connects to the arm's end, not the pole. - -### Archetype 2: Park Bench (Seat + Backrest + Legs) - -``` -Structure: Legs ← Seat ← Backrest - │ │ └── Angled panel, FLUSH against seat back edge - │ └── Flat board, sits on top of legs - └── 4 vertical posts, bottom at Z=0 -``` - -**Position calculations:** -```python -seat_height = 0.45 # real-world bench seat height -seat_width = 1.5 -seat_depth = 0.5 -seat_thickness = 0.05 -backrest_height = 0.4 -backrest_thickness = 0.05 -backrest_angle = -15 # degrees, tilted back -leg_thickness = 0.05 - -# Legs: bottom at Z=0, top at seat bottom -leg_height = seat_height - seat_thickness / 2 -leg_z = leg_height / 2 # center origin - -# Seat: sits on top of legs -seat_z = seat_height # center of seat thickness at seat_height - -# Backrest: FLUSH AGAINST the seat's back edge -# The backrest bottom should touch the seat top -import math -theta = math.radians(abs(backrest_angle)) - -# When rotated, the bottom edge shifts. Compensate: -backrest_center_z = seat_z + (seat_thickness / 2) + (backrest_height / 2) * math.cos(theta) -backrest_y = (seat_depth / 2) - (backrest_thickness / 2) # at back edge of seat - -# The backrest's location.y needs to account for rotation shifting it -backrest_y_shift = (backrest_height / 2) * math.sin(theta) -backrest_y_final = backrest_y + backrest_y_shift -``` - -**Common mistake:** Setting backrest location without accounting for rotation. A -15° tilt on X shifts the bottom edge upward and forward. - -### Archetype 3: Table (Top + Legs) - -``` -Structure: Legs ← Table Top - └── 4 vertical posts at corners -``` - -**Position calculations:** -```python -table_height = 0.75 # standard dining table -top_thickness = 0.04 -top_width = 1.2 -top_depth = 0.75 - -# Table top -top_z = table_height # center of thickness - -# Legs: inset from corners -leg_height = table_height - top_thickness / 2 -leg_z = leg_height / 2 -inset = 0.05 # legs slightly inside table edge - -corners = [ - (-top_width/2 + inset, -top_depth/2 + inset), - ( top_width/2 - inset, -top_depth/2 + inset), - (-top_width/2 + inset, top_depth/2 - inset), - ( top_width/2 - inset, top_depth/2 - inset), -] -``` - -### Archetype 4: Bookshelf / Shelving Unit - -``` -Structure: Side Panels ← Shelves ← Back Panel (optional) -``` - -**Evenly spaced shelves:** -```python -shelf_count = 4 -unit_height = 1.8 -shelf_thickness = 0.02 -usable_height = unit_height - shelf_thickness # subtract top/bottom - -spacing = usable_height / (shelf_count - 1) -for i in range(shelf_count): - shelf_z = shelf_thickness / 2 + i * spacing -``` - -### Archetype 5: Vehicle (Body + Wheels) - -``` -Structure: Body ← Wheels (4) -``` - -**Wheel placement:** -```python -body_width = 1.8 -body_length = 4.0 -wheelbase = body_length * 0.7 # 70% of body length -track = body_width # wheels at body edges -wheel_radius = 0.3 - -# Wheels sit on ground, center at wheel_radius height -wheel_z = wheel_radius -wheel_positions = [ - (-wheelbase/2, -track/2, wheel_z), # front-left - (-wheelbase/2, track/2, wheel_z), # front-right - ( wheelbase/2, -track/2, wheel_z), # rear-left - ( wheelbase/2, track/2, wheel_z), # rear-right -] -``` - -## PARENTING FOR ASSEMBLIES - -### When to Parent -- **Always parent** when the object should move/rotate as one unit -- **Use an Empty as root** for easy manipulation of the whole assembly -- **Prefer direct tools**: use `add_empty_object` for root controls, `parent_set` for parent-child links, and `organize_collection_hierarchy` when grouping, collection movement, parenting, and visibility changes belong in one batch - -`parent_set` and `organize_collection_hierarchy` preserve world transforms so child objects do not visually jump when parented. Use `add_empty_object` for the root helper instead of generated Python. - -### Root Empty Controls - -Create a non-rendered root control before parenting compound object parts: - -```text -add_empty_object(name="Lamp_Root", location=[-1.5, 0, 0], display_type="CUBE", display_size=0.35) -parent_set(parent_name="Lamp_Root", child_name="Lamp_Base") -parent_set(parent_name="Lamp_Root", child_name="Lamp_Pole") -parent_set(parent_name="Lamp_Root", child_name="Lamp_Arm") -``` - -An Empty is a transform handle, not visible geometry. Do not use it for renderable pieces; use it so `set_object_transform(name="Lamp_Root", ...)` moves the whole assembly safely. If the handle is hard to see or too large, adjust it with `set_empty_properties(name="Lamp_Root", display_type="SPHERE", display_size=0.5, show_in_front=true)`. - -### Using parent_set MCP Tool -``` -parent_set(parent="Lamp_Root", child="Lamp_Pole", keep_transform=true) -``` - -## VERIFICATION CHECKLIST - -After assembling a compound object, verify: - -1. ☐ **No floating parts** — Every part touches or connects to its neighbor -2. ☐ **Bottom at Z=0** — The base of the assembly sits on the ground plane -3. ☐ **Proportions match reality** — Compare dimensions to reference table -4. ☐ **Rotated parts are compensated** — Angled pieces have position offsets applied -5. ☐ **Use `get_viewport_screenshot`** — Visual check catches issues formulas might miss - -## COMMON MISTAKES TO AVOID - -1. ❌ **Placing child parts relative to world origin instead of parent geometry** — Arm position must derive from pole top, not from (0,0,0) -2. ❌ **Connecting to the wrong point of a rotated piece** — A rotated arm's endpoint is NOT at `arm.location.x + arm_length/2` — use `matrix_world @ Vector(local_point)` to find the real endpoint -3. ❌ **Forgetting that cylinder depth is FULL height, not half** — A `depth=3.5` cylinder extends 1.75m above and below its origin -4. ❌ **Not accounting for object radius in side attachments** — An arm touching a pole must offset by `pole_radius + arm_radius`, not just `arm_length/2` -5. ❌ **Hardcoding heights instead of calculating from connected parts** — If you change the pole height later, all attachment points break -6. ❌ **Hand-writing parenting logic when direct tools cover it** — Use `parent_set` or `organize_collection_hierarchy` so world transforms are preserved and child objects do not visually jump -7. ❌ **Building top-down** — Always start from the ground up to prevent accumulated positioning errors diff --git a/data/tool-guides/render-guide.md b/data/tool-guides/render-guide.md deleted file mode 100644 index 7075397..0000000 --- a/data/tool-guides/render-guide.md +++ /dev/null @@ -1,162 +0,0 @@ ---- -title: "Render Settings & Output Guide" -category: "render" -tags: ["render", "EEVEE", "Cycles", "resolution", "samples", "denoising", "output", "PNG", "JPEG", "transparent", "setup_studio_scene", "validate_studio_scene", "render_thumbnail_to_path", "render_viewport_to_path", "inspect_render_artifact", "set_render_settings", "render_image"] -triggered_by: ["setup_studio_scene", "validate_studio_scene", "set_render_settings", "render_image", "render_thumbnail_to_path", "render_viewport_to_path", "inspect_render_artifact"] -description: "Domain knowledge for render engine selection, sample counts, resolution presets, output formats, and Blender 5.x EEVEE-Next changes." -blender_version: "4.0+" ---- - -# Render Settings & Output Guide - -## RENDER ENGINE COMPARISON - -| Feature | EEVEE (EEVEE-Next in 5.x) | Cycles | -|---|---|---| -| Speed | Very fast (real-time) | Slow (ray-traced) | -| Quality | Good for most use cases | Photorealistic | -| Shadows | Approximate | Physically accurate | -| Reflections | Screen-space | Full ray-traced | -| Use when | Speed matters, product previews, animations | Final quality renders, lighting-critical scenes | -| Engine enum | `BLENDER_EEVEE` | `CYCLES` | - -## SAMPLE COUNT GUIDELINES - -### EEVEE (Blender 5.x uses EEVEE-Next) -| Quality Level | Samples | Use Case | -|---|---|---| -| Preview | 16–32 | Quick checks, testing composition | -| Standard | 64 | Good quality, fast render | -| High | 128–256 | Final renders, smooth gradients | - -> **Blender 5.x sampling split:** `scene.eevee.taa_samples` controls viewport samples, while `scene.eevee.taa_render_samples` controls final render samples. - -### Cycles -| Quality Level | Samples | Use Case | -|---|---|---| -| Preview | 32–64 | Quick test renders | -| Standard | 128–256 | Good quality with denoising | -| High | 512–1024 | Noise-free, complex lighting | -| Extreme | 2048+ | Caustics, glass, very dark scenes | - -**Rule:** Always enable denoising for Cycles renders under 512 samples — it dramatically reduces noise. - -## INTERACTIVE PREVIEW RULE - -When the render is only for agent-side validation or user preview during an active build: -- Prefer **EEVEE** for fast preview renders -- If Cycles is required, keep it at **32-64 samples** with denoising -- Prefer lower preview resolution or `resolution_percentage < 100` unless the user explicitly asked for a final-quality frame -- Use higher sample counts only after the scene has already passed composition and silhouette review - -**Rule:** Do not spend interactive tool budget on near-final Cycles settings unless the user explicitly asks for final render quality. - -## CAMERA PRECONDITION - -Before `render_image`: -- ensure the scene has an active camera -- if you created a camera this run, explicitly set it active before rendering -- do not assume a newly added camera becomes active automatically in every workflow -- for model or asset previews, prefer `setup_studio_scene` and then `validate_studio_scene` before rendering - -**Rule:** If no active camera is confirmed, do not call `render_image` yet. - -## RENDER ARTIFACT INSPECTION - -After `render_thumbnail_to_path`, `render_viewport_to_path`, or `render_image` saves a file, use `inspect_render_artifact(image_path)` before treating the path as a successful visual result. - -The inspection is not a replacement for `get_viewport_screenshot` or human visual review. It catches cheap deterministic failure signals: -- dimensions are wrong or missing, -- mean brightness is near black or near white, -- alpha coverage suggests an unintentionally transparent render, -- edge pixels differ strongly from the corner background, which can indicate cropping or subject contact with the frame, -- corner background samples vary enough to question a clean studio backdrop. - -If `inspect_render_artifact.ready` is false, fix the reported issue with `validate_studio_scene`, `frame_camera_to_targets`, `set_world_environment`, or render settings before retrying the render. Do not keep rerendering the same scene without changing the cause. - -## RENDER FAILURE POLICY - -If a preview or final render fails once during the current run: -- do not keep retrying renders repeatedly in the same run -- fix obvious non-render issues only if they are cheap and certain -- otherwise stop at the failed render, explain the issue to the user, and ask whether to retry in a separate follow-up run - -**Rule:** One failed render should not consume the rest of the interactive execution budget. - -## RESOLUTION PRESETS - -| Name | Width × Height | Aspect | Use Case | -|---|---|---|---| -| 720p | 1280 × 720 | 16:9 | Quick previews | -| **1080p** | **1920 × 1080** | **16:9** | **Standard HD (most common)** | -| 1440p | 2560 × 1440 | 16:9 | QHD, high quality | -| 4K | 3840 × 2160 | 16:9 | Ultra HD, print | -| Square | 1080 × 1080 | 1:1 | Social media, Instagram | -| Portrait | 1080 × 1920 | 9:16 | Mobile/stories | - -**Use `resolution_percentage`** to scale output: 50 = half resolution (fast test), 100 = full. - -## OUTPUT FORMAT SELECTION - -| Format | Quality | File Size | Transparency | Best For | -|---|---|---|---|---| -| **PNG** | Lossless | Large | ✅ Alpha | Default, quality renders | -| JPEG | Lossy | Small | ❌ No | Quick sharing, web | -| EXR | HDR lossless | Very large | ✅ Alpha | Compositing, post-processing | -| TIFF | Lossless | Large | ✅ Alpha | Print, archival | - -**Default recommendation:** PNG for most renders. Use JPEG only when file size matters and you don't need transparency. - -## TRANSPARENT BACKGROUND (`film_transparent`) - -**Default: `film_transparent: false`** — most scenes should NOT use transparency. - -### When to USE `film_transparent: true` -- **Product/object shots** where the object will be composited onto a different background -- **Asset renders** for catalogs, UI thumbnails, or web overlays -- **Compositing workflows** where the 3D render is layered over a photo/video - -### When NOT to use `film_transparent: true` -- **Interior scenes** (rooms, kitchens, hallways) — walls and floors ARE the background -- **Exterior scenes** (landscapes, cityscapes) — the sky/environment IS the background -- **Any scene with environment geometry** (ground planes, backdrops, skyboxes) - -> **Rule:** If the scene has walls, floors, or any environmental geometry, `film_transparent` should be `false`. Enabling it will make the background geometry partially invisible or remove it entirely, producing broken-looking renders. - -The output must be saved as PNG (or EXR) to preserve the alpha channel. JPEG does NOT support transparency. - -## BLENDER 5.x EEVEE-NEXT CHANGES - -> **CRITICAL — legacy top-level EEVEE toggles changed in Blender 5.x:** - -| Legacy Property | 5.x Guidance | -|---|---| -| `scene.eevee.use_ssr`, `scene.eevee.use_ssr_refraction`, `scene.eevee.use_screen_space_reflections` | Use current EEVEE ray-tracing / probe workflow instead of legacy SSR toggles | -| `scene.eevee.use_gtao`, `scene.eevee.gtao_distance`, `scene.eevee.gtao_quality` | Use current view-layer AO controls such as `view_layer.eevee.ambient_occlusion_distance` | -| `scene.eevee.use_bloom` | Use a compositor bloom/glare workflow | -| `scene.eevee.shadow_cascade_size` | Use current shadow controls such as `scene.eevee.shadow_resolution_scale`, `shadow_ray_count`, and `shadow_step_count` | - -### Valid Sampling Properties in Blender 5.x - -| Property | Purpose | -|---|---| -| `scene.eevee.taa_samples` | Viewport sampling | -| `scene.eevee.taa_render_samples` | Final render sampling | -| `view_layer.eevee.ambient_occlusion_distance` | Ambient occlusion distance control | - -## OUTPUT PATH HANDLING - -- Use **absolute paths** for output: `/tmp/render.png`, `C:/tmp/render.png` -- If no output_path is provided, `render_image` saves to the scene's default output path -- Name files descriptively: `/tmp/product_shot.png`, `/tmp/scene_render.png` -- The render overwrites existing files at the same path without warning - -## COMMON MISTAKES TO AVOID - -1. ❌ Confusing `scene.eevee.taa_samples` (viewport) with `scene.eevee.taa_render_samples` (final render) -2. ❌ Using legacy `use_bloom`, `use_ssr`, or `use_gtao` properties in Blender 5.x -3. ❌ Using `BLENDER_EEVEE_NEXT` instead of `BLENDER_EEVEE` in Blender 5.x -4. ❌ Setting Cycles samples to 2048+ without denoising — wastes render time -5. ❌ Saving transparent renders as JPEG — alpha channel is lost, use PNG -6. ❌ Forgetting to set the engine — always explicitly set EEVEE or CYCLES -7. ❌ Using near-final Cycles settings for an iterative preview render — use EEVEE or 32–64 sample Cycles first diff --git a/data/tool-guides/rigging-guide.md b/data/tool-guides/rigging-guide.md deleted file mode 100644 index 7807239..0000000 --- a/data/tool-guides/rigging-guide.md +++ /dev/null @@ -1,238 +0,0 @@ ---- -title: "Rigging & Rigify Workflow Guide" -category: "rigging" -tags: ["rigging", "rigify", "armature", "skeleton", "metarig", "IK", "bone", "edit-bone", "bone alignment", "rig", "auto-rig", "mesh preparation", "edge loops", "joint", "weight cleanup", "create_rigify_metarig", "inspect_edit_bone_alignment", "set_edit_bone_alignment", "generate_rigify_rig", "bind_mesh_to_armature", "transfer_vertex_group_weights", "project_vertex_group_weights"] -triggered_by: ["inspect_rigging_data", "inspect_edit_bone_alignment", "set_edit_bone_alignment", "inspect_weight_paint_readiness", "normalize_vertex_group_weights", "create_rigify_metarig", "generate_rigify_rig", "bind_mesh_to_armature", "transfer_vertex_group_weights", "project_vertex_group_weights"] -description: "Research-backed guide for Rigify rigging workflows including mesh preparation, per-bone metarig alignment, IK bend requirements, rig generation, and post-rigging weight cleanup. Based on NotebookLM research with 9 cited sources." -blender_version: "4.0+ / 5.x" ---- - -# Rigging Guide — Blender Agent Best Practices - -> **Domain:** Rigging, Skeleton, Armature, Weight Painting -> **Version:** Blender 4.x / 5.x compatible -> **Purpose:** Guide the agent through correct rigging workflows. This is a RAG-injected tool guide — follow every step in order. - ---- - -## ⚠️ AGENT BEHAVIORAL RULES (MANDATORY) - -Before starting ANY rigging task, the agent MUST follow these rules: - -1. **ALWAYS delete default scene objects first.** If the scene contains a default Cube, default Light, or default Camera, delete them using `delete_object` before creating the character mesh. Check the pre-injected scene state for existing objects. - -2. **NEVER pose the rig after creation.** After rigging is complete, leave the character in **rest pose / T-pose**. Do NOT move IK handles, rotate bones, or "demonstrate" the rig works. The user will pose it themselves if needed. - -3. **NEVER add materials unless the user explicitly requests them.** Do not assign skin tones, colors, or any material to the character mesh unless the user's prompt specifically asks for materials or colors. The rigging task is about skeleton and weights — not appearance. - -4. **PREFER this guide's Rigify workflow over custom armature scripts.** If RAG context includes both this guide AND a custom armature script (like `humanoid_armature.py`), IGNORE the custom armature approach and follow the Rigify workflow described here (Section 2). Rigify produces production-quality rigs with IK controls, FK chains, and proper bone layers. - -5. **Scene cleanup comes first.** Before creating any geometry, ensure no conflicting objects exist. If the user wants a "fresh" character, the agent should clear the scene of irrelevant objects. - ---- - -## Decision: Direct Tools vs execute_code - -Rigging is still a multi-step workflow, but it should no longer default to freeform Python for -inspection and cleanup. Use direct ViperMesh tools whenever they cover the operation: - -- `inspect_rigging_data` before creating or modifying rigs, armatures, bones, vertex groups, or Armature modifiers. -- `inspect_weight_paint_readiness` before binding or cleaning weights. -- `create_rigify_metarig` for bundled human/animal templates. This creates only the blueprint and never auto-fits bones. -- `inspect_edit_bone_alignment` before and after metarig fitting to verify head, tail, roll, length, parent/child links, and Rigify type. -- `set_edit_bone_alignment` for explicit named edit-bone head/tail/roll edits. Keep each call small and model-driven. -- `generate_rigify_rig` only after every metarig bone is aligned and joint bend direction is verified. -- `bind_mesh_to_armature` to bind an existing mesh and armature with automatic, envelope, empty-group, or name-matched weights. -- `transfer_vertex_group_weights` when a clean weighted source and target have identical vertex topology. -- `project_vertex_group_weights` when a clean weighted source must project weights onto a retopologized or different-topology target. -- `normalize_vertex_group_weights` for bounded post-binding cleanup: influence limits, normalization, and tiny-weight pruning. -- `inspect_modifier_constraint_stack`, `configure_modifier`, `add_object_constraint`, and `configure_constraint` for modifier/constraint authoring that fits those schemas. - -Use `execute_code` only for custom bone placement logic that cannot be expressed as explicit named -`set_edit_bone_alignment` edits. Enabling Rigify, template creation, edit-bone inspection/alignment, -final generation, and mesh binding are now direct tools. Keep any fallback scripts focused and verify -with inspection tools before generation. - ---- - -## 1. Mesh Preparation (MANDATORY Before Rigging) - -Before adding ANY armature to a mesh, the mesh MUST be prepared: - -### 1.1 Geometry at Joints -Meshes need **sufficient vertex density at deformation zones** (knees, elbows, shoulders, neck, wrists, ankles). -Simple cylinders or low-poly primitives will deform terribly. - -```python -# Add edge loops at joint positions for clean deformation -import bpy, bmesh - -obj = bpy.data.objects['CharacterMesh'] -bpy.context.view_layer.objects.active = obj -bpy.ops.object.mode_set(mode='EDIT') - -# Subdivide the mesh to add geometry, especially at joints -bpy.ops.mesh.select_all(action='SELECT') -bpy.ops.mesh.subdivide(number_cuts=2) # 2 cuts minimum for joints - -bpy.ops.object.mode_set(mode='OBJECT') -``` - -### 1.2 Clean Topology -Before rigging, ALWAYS run these cleanup steps: - -```python -import bpy - -obj = bpy.data.objects['CharacterMesh'] -bpy.context.view_layer.objects.active = obj -bpy.ops.object.mode_set(mode='EDIT') - -# 1. Merge overlapping vertices (critical for auto-weights) -bpy.ops.mesh.select_all(action='SELECT') -bpy.ops.mesh.remove_doubles(threshold=0.0001) - -# 2. Fix normals -bpy.ops.mesh.normals_make_consistent(inside=False) - -# 3. Apply all transforms BEFORE rigging -bpy.ops.object.mode_set(mode='OBJECT') -bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) -``` - -### 1.3 Scale Check -If the mesh is small (under ~0.5 Blender units tall), **scale up 10x** before auto-weighting, -then scale back down after. Small meshes cause "Bone Heat Weighting: Failed" errors. - ---- - -## 2. Rigify Workflow (Correct Order) - -### 2.1 Template Selection -| Template | Use When | -|----------|----------| -| `basic_human` | Simple characters, no face rig needed, game-ready rigs | -| `human` | Full characters with facial animation (has jaw, eyes, lips, breast bones) | -| `basic_quadruped` | Four-legged animals | -| Other templates | `bird`, `cat`, `horse`, `shark`, `wolf` — species-specific | - -**Rule:** If unsure, use `basic_human`. The full `human` metarig has complex facial bones -that are unnecessary for most agent-generated characters and can complicate the rig. - -### 2.2 Enable Rigify - -`create_rigify_metarig` and `generate_rigify_rig` enable the bundled Rigify addon when needed. - -### 2.3 Create Metarig - -Call `create_rigify_metarig`. Prefer `basic_human` unless facial controls are explicitly needed. -The command returns bone and Rigify-type counts, then instructs the agent to stop for alignment. - -### 2.4 Align Metarig to Mesh (CRITICAL — Per-Bone, Not Uniform Scale) - -**WRONG approach** (causes rig to extend beyond mesh): -```python -# DON'T DO THIS — uniform bounding-box scale leaves bones outside limbs -scale_factor = mesh_height / meta_height -metarig.scale = (scale_factor, scale_factor, scale_factor) -``` - -**CORRECT approach** — per-bone alignment with direct tools: - -1. Call `inspect_edit_bone_alignment` on the metarig, requesting the bones you intend to fit. -2. Derive explicit target `head` and `tail` coordinates from the mesh silhouette, landmarks, or inspected bounds. -3. Call `set_edit_bone_alignment` with a small batch of named bone edits. -4. Call `inspect_edit_bone_alignment` again and verify every edited bone is centered inside the intended body volume. - -Example edit payload: - -```json -{ - "armature_name": "Character_Metarig", - "bones": [ - { "name": "spine", "head": [0, 0, 0.85], "tail": [0, 0, 1.25] }, - { "name": "upper_arm.L", "head": [-0.22, 0, 1.2], "tail": [-0.72, -0.02, 1.05] } - ] -} -``` - -### 2.5 Joint Bends (MANDATORY for IK) - -Elbows and knees MUST have a **slight bend** — never leave them perfectly straight. -The IK solver cannot determine fold direction for straight chains. - -Use `inspect_edit_bone_alignment` to read the current limb joint coordinates, then use -`set_edit_bone_alignment` to nudge elbow/knee joint heads slightly along the intended bend axis. -Keep the offset small and inspect again before generation. - -### 2.6 Generate Rig - -After alignment and inspection, call `generate_rigify_rig`. Do not set -`regenerate_existing` unless replacing the generated control rig is explicitly intended. - -### 2.7 Bind Mesh to Rig - -Call `bind_mesh_to_armature` with the target mesh and generated rig names. Use: - -- `AUTOMATIC` for initial organic-character bone heat weights. -- `ENVELOPE` only when envelope weighting is intentionally preferred. -- `EMPTY` when an artist will paint weights manually. -- `NAME` when valid bone-named vertex groups already exist. - -Leave `replace_existing` false unless inspection confirms that the previous Armature modifier, -armature parent, and matching deform groups should be replaced. - -### 2.8 Hide Metarig -```python -metarig.hide_set(True) -``` - ---- - -## 3. Post-Rigging Weight Cleanup (MANDATORY) - -After binding with auto-weights, ALWAYS run cleanup. Raw auto-weights are never good enough. - -Preferred direct-tool flow: - -1. Call `inspect_weight_paint_readiness` on the mesh and armature to confirm vertex groups, modifiers, and deform-readiness. -2. Call `normalize_vertex_group_weights` with a 4-influence limit, normalization, and tiny-weight pruning. -3. Call `inspect_rigging_data` again to verify the mesh still has the expected Armature modifier and vertex groups. - -Only use `execute_code` for cleanup when the direct tool reports that a requested operation is outside -its supported surface. - ---- - -## 4. Common Pitfalls & Troubleshooting - -### "Bone Heat Weighting: Failed to find solution" -**Cause:** Bad mesh topology. Fix with: -1. `Merge by Distance` (remove duplicate vertices) -2. Fix non-manifold geometry (edges connecting 3+ faces) -3. Separate intersecting loose parts (`P → Separate by loose parts`) -4. Scale mesh + armature up 10x, apply weights, scale back down - -### Rig Controls Extend Beyond Mesh -**Cause:** Uniform bounding-box scale on metarig, ignoring per-bone alignment. -**Fix:** Always do per-bone alignment (Section 2.4) after rough scale. - -### Mesh Warps/Pinches at Joints -**Cause:** Insufficient geometry at deformation zones. -**Fix:** Add edge loops or subdivide mesh before rigging (Section 1.1). - -### IK Limbs Bend Wrong Way -**Cause:** Perfectly straight joint chains — IK solver doesn't know which axis to fold. -**Fix:** Add slight bend to elbows (backward) and knees (forward) — Section 2.5. - ---- - -## 5. Verification Checklist - -After rigging, verify in Pose Mode: -- [ ] Select rig → enter Pose Mode (Ctrl+Tab) -- [ ] Rotate/move arm controls → mesh follows smoothly without pinching -- [ ] Rotate/move leg controls → knees bend correctly in one direction -- [ ] Move root control → entire character translates -- [ ] No mesh parts stay behind or warp unexpectedly -- [ ] Reset pose: Alt+R (rotation), Alt+G (location) diff --git a/data/tool-guides/scene-composition-guide.md b/data/tool-guides/scene-composition-guide.md deleted file mode 100644 index d04d813..0000000 --- a/data/tool-guides/scene-composition-guide.md +++ /dev/null @@ -1,212 +0,0 @@ ---- -title: "Scene Composition & Interior Design Principles" -category: "scene-design" -tags: ["composition", "interior", "camera", "framing", "layout", "furniture", "placement", "spatial", "depth", "lamp", "lighting", "pendant", "shade", "light fixture", "text", "label", "signage", "annotation", "curve", "path", "cable", "vine", "rail"] -triggered_by: ["inspect_viewport_areas", "set_viewport_shading", "focus_viewport_on_objects", "get_viewport_screenshot", "render_viewport_to_path", "add_text_object", "add_curve_object", "set_curve_properties"] -description: "General 3D engineering principles for composing visually balanced interior and exterior scenes. Covers furniture placement, camera framing, visual hierarchy, and spatial organization." -blender_version: "5.0+" ---- - -# Scene Composition & Interior Design Principles - -## VISUAL HIERARCHY IN SCENES - -Every scene should have a clear visual hierarchy — the viewer's eye should be drawn to the most important elements first. Achieve this through: - -1. **Size contrast** — Prominent objects should be appropriately sized relative to the room -2. **Material contrast** — Key objects should have distinct materials from their surroundings -3. **Lighting emphasis** — Use spotlights or directional lights to draw attention to focal points -4. **Position** — Place hero objects at optical center or along rule-of-thirds lines - -## INTERIOR FURNITURE PLACEMENT RULES - -### Against-Wall Items -Objects that go "against" a wall should be positioned with a small gap: -```python -# CORRECT: 5cm gap from wall for realism -# If back wall is at Y=2.5 and has 0.2m thickness, wall inner face is at Y=2.4 -# A bookshelf 0.3m deep: center Y = 2.4 - 0.15 = 2.25 -bookshelf_y = wall_inner_y - (shelf_depth / 2) - 0.05 # 5cm gap - -# WRONG: Object embedded in or touching wall surface exactly -bookshelf_y = wall_inner_y # Will clip into wall -``` - -### Furniture Spacing -- **Walking clearance** between furniture groups: 0.6–1.0m minimum -- **Chair push-back clearance** behind seated positions: 0.5m -- **Counter stool spacing**: 0.6m center-to-center minimum -- **Table-to-wall clearance**: 0.8m for comfortable seating - -### Grounding — Nothing Should Float -Every object must touch a surface. Common Z-position calculations: -```python -# Object sitting on the floor (Z=0) -object_z = object_height / 2 # Center of mass at half height - -# Object on a table (table top at Z=0.75) -cup_z = table_top_z + cup_height / 2 - -# Object mounted on wall (wall at X=2.0) -painting_x = wall_x - painting_depth / 2 - 0.01 # Slight offset from wall -``` - -## CAMERA FRAMING FOR INTERIORS - -### Focal Length Guide -| Focal Length | Field of View | Best For | -|---|---|---| -| 18–24mm | Ultra-wide | Full room views, small spaces | -| 28–35mm | Wide | Standard interior shots, natural perspective | -| 50mm | Normal | Detail shots, furniture groups | -| 85mm+ | Telephoto | Close-up details, material showcase | - -**Rule:** For interior scene overviews, use **24–35mm**. Ultra-wide (< 20mm) causes severe perspective distortion. - -### Camera Placement (Interior Overview) -```python -# Standard interior overview: camera near one corner, looking diagonally -# For a 4m × 5m room: -camera_location = (1.5, -2.0, 1.6) # Near front-right corner, eye height -look_at = (-0.5, 1.0, 1.0) # Center-back of room, slightly below eye level - -# Rule of thumb: camera height 1.4–1.7m simulates human eye level -# Lower angles (0.8–1.0m) create dramatic/imposing feel -# Higher angles (2.5m+) create overview/dollhouse feel -``` - -### Ensuring All Objects Are Visible -After placing the camera, verify that important objects are within the camera frustum: -- Wide enough lens to capture the full room width -- Camera positioned far enough back to see front objects -- No important objects clipped by the frame edges -- Consider rotating camera slightly to include off-center elements - -## REFERENCE RECONSTRUCTION PRIORITIES - -When matching an image reference, do not treat all objects as equally important. - -1. **Block out scene anchors first** — floor, walls, major furniture, large framing elements -2. **Check composition before chasing detail** — use a viewport screenshot to confirm layout, scale, and camera angle -3. **Refine by camera prominence** — objects near the center, foreground, or strong light boundaries should get refinement before background accents -4. **Preserve recognizable silhouettes** — a visible prop needs enough sub-parts to read correctly from the active camera, even if the scene is otherwise simple -5. **Use focused follow-up passes** — if one visible prop reads poorly, refine that prop rather than rebuilding the entire scene - -## LIGHTING BALANCE PRINCIPLES - -### Interior Lighting Hierarchy -1. **Key light** — Main illumination source, establishes mood and shadow direction -2. **Fill light** — Fills shadowed areas, prevents pure black shadows (30-50% of key brightness) -3. **Accent/rim light** — Highlights edges and creates depth separation - -### Energy Balance to Prevent Overexposure -When using multiple lights in EEVEE: -- Total combined energy should not exceed what feels natural -- If key light is AREA at 600W, fill should be 200-300W max -- Avoid stacking multiple high-energy lights pointing at the same area -- **Start low, increase gradually** — it's easier to brighten than to diagnose overexposure - -### Warm vs. Cool Light Assignment -| Light Role | Warm Scenes | Cool/Professional Scenes | -|---|---|---| -| Key light | (1.0, 0.85, 0.7) warm amber | (0.95, 0.95, 1.0) neutral | -| Fill light | (1.0, 0.9, 0.8) soft warm | (0.85, 0.9, 1.0) cool blue | -| Accent | (1.0, 0.95, 0.85) | (0.9, 0.9, 1.0) | - -**Rule:** In warm scenes (cafés, homes), ALL lights should lean warm. Don't mix pure white lights with warm-colored lights — it creates an unnatural feel. - -## LIGHT PLACEMENT IN FIXTURES — CRITICAL - -When creating lamp, pendant, chandelier, or any light-emitting object: - -### The Cardinal Rule: Light Must Be BELOW or OUTSIDE the Shade - -A light source placed INSIDE a closed shade (cylinder, sphere, cone) is **trapped** and casts no useful illumination. The Blender light object must be positioned so rays can actually reach the scene. - -```python -# CORRECT: Light below the open bottom of a pendant/lamp shade -shade_bottom_z = lamp_z # Bottom of shade geometry -light_z = shade_bottom_z - 0.05 # 5cm below opening - -# WRONG: Light at center of shade (rays blocked by shade mesh) -light_z = shade_bottom_z + shade_height / 2 # Trapped inside! -``` - -### Fixture Light Placement Rules -| Fixture Type | Light Position | Light Type | -|---|---|---| -| **Pendant lamp** | 5–10cm BELOW shade opening | SPOT (cone down) or POINT | -| **Floor lamp** | At shade opening, facing upward or downward | SPOT or POINT | -| **Table lamp** | At the OPEN END of the shade, not the closed top | POINT | -| **Chandelier** | Below the fixture body, never inside arms/structure | POINT or AREA | -| **Wall sconce** | In front of the sconce plate, facing outward | POINT or SPOT | - -### Open Geometry for Shades -If you create a cylindrical or conical shade, **delete the bottom face** so it's open: -```python -# After creating cylinder for shade: -bpy.ops.object.mode_set(mode='EDIT') -bpy.ops.mesh.select_all(action='DESELECT') -bpy.ops.object.mode_set(mode='OBJECT') -shade.data.polygons[0].select = True # Bottom face -bpy.ops.object.mode_set(mode='EDIT') -bpy.ops.mesh.delete(type='FACE') -bpy.ops.object.mode_set(mode='OBJECT') -# Then place light BELOW this opening, not inside the now-hollow cylinder -``` - -## OBJECT NAMING CONVENTIONS - -Use descriptive, hierarchical names that make the scene tree readable: -``` -Room_Floor -Room_Wall_Back -Room_Wall_Left -Room_Wall_Right -Counter_Top -Counter_Base -CoffeeMachine_Body -CoffeeMachine_Spout -Stool_01 -Stool_02 -Table_Round_Top -Table_Round_Leg -Chair_01_Seat -Chair_01_Back -Light_Key_Area -Light_Fill_Spot -Camera_Main -``` - -## TEXT, SIGNAGE, AND LABELS - -Use `add_text_object` for readable scene labels, storefront signs, title cards, control labels, and annotations. Keep text short enough to read from the active camera and place it on a supporting surface or slightly in front of a wall to avoid z-fighting. - -Recommended pattern: -1. Create or reuse a material with `create_material_preset` or `create_material` -2. Call `add_text_object(text, name, location, rotation, size, align_x, align_y, material_name)` -3. Use `convert_to_mesh: true` only when the text needs mesh-only modifiers, export validation, or boolean/geometry operations -4. Confirm visibility with `focus_viewport_on_objects` or a screenshot/render tool - -## CURVES, PATHS, AND LINEAR ELEMENTS - -Use `add_curve_object` for cables, vines, rails, piping, trims, straps, arcs, decorative strokes, simple spirals, and path-like elements that would be brittle as generated Python. Start with Bezier curves for organic arcs, polylines for routed mechanical runs, circles for rings/loops, and spirals for coils or helixes. - -Recommended pattern: -1. Create the curve with `add_curve_object(curve_type, points, location, rotation, bevel_depth, material_name)` -2. Tune thickness, smoothness, fill, and open/closed state with `set_curve_properties` -3. Assign or update material through `material_name`; use emissive materials for glowing cable/light strokes -4. Use `convert_to_mesh: true` only when mesh-only modifiers, booleans, sculpting, or export validation require mesh data - -Keep `resolution` and `bevel_resolution` as low as the visual target allows. High curve resolution and thick bevels multiply generated geometry after conversion, so convert to mesh late and only when there is a concrete reason. For closed loops, set `cyclic: true`; for hanging cables, pipes, rails, and vines that terminate at endpoints, keep the curve open and place endpoints deliberately against the objects they attach to. - -## COMMON MISTAKES TO AVOID - -1. ❌ Furniture floating above floor — always calculate Z position from surface height -2. ❌ Objects clipping into walls — leave small gaps (2-5cm) from surfaces -3. ❌ Camera too close with wide lens — causes extreme distortion -4. ❌ All lights same energy — creates flat, shadowless scene -5. ❌ Mixing warm and cool lights randomly — stick to a consistent temperature palette -6. ❌ Forgetting to include fill light — single-light scenes have harsh, unrealistic shadows -7. ❌ Camera at ceiling height — use eye-level (1.4–1.7m) for natural perspective -8. ❌ **Placing light INSIDE a shade/fixture** — light must be BELOW/OUTSIDE the shade opening, not trapped inside closed geometry. This is the #1 cause of dark, non-functional lamps in scenes. diff --git a/data/tool-guides/scene-management-guide.md b/data/tool-guides/scene-management-guide.md deleted file mode 100644 index 6d5ee38..0000000 --- a/data/tool-guides/scene-management-guide.md +++ /dev/null @@ -1,133 +0,0 @@ ---- -title: "Scene Management & Organization Guide" -category: "scene-management" -tags: ["scene", "transform", "collection", "parent", "origin", "visibility", "duplicate", "join", "rename", "delete", "hierarchy", "organization", "export cleanup", "set_object_transform", "rename_object", "duplicate_object", "join_objects", "parent_set", "parent_clear", "set_origin", "move_to_collection", "set_visibility", "delete_object", "select_scene_objects", "set_active_collection", "inspect_collection_hierarchy", "organize_collection_hierarchy", "inspect_export_cleanup_candidates"] -triggered_by: ["get_scene_info", "get_all_object_info", "select_scene_objects", "set_active_collection", "set_object_transform", "rename_object", "duplicate_object", "join_objects", "parent_set", "parent_clear", "set_origin", "move_to_collection", "set_visibility", "delete_object", "inspect_collection_hierarchy", "organize_collection_hierarchy", "inspect_export_cleanup_candidates"] -description: "Domain knowledge for object transforms, hierarchy management, collection organization, origin placement, and scene cleanup in Blender." -blender_version: "4.0+" ---- - -# Scene Management & Organization Guide - -## TRANSFORM REFERENCE - -### Location (World Space) -- Blender uses meters by default -- Origin (0, 0, 0) is the world center -- Z-axis is UP in Blender (not Y like in some game engines) -- Objects at Z=0 sit on the "ground plane" - -### Rotation (Euler XYZ, in degrees) -- The `set_object_transform` tool accepts degrees (auto-converts to radians internally) -- X rotation: tilts forward/backward (pitch) -- Y rotation: rolls left/right (roll) -- Z rotation: turns left/right (yaw) - -### Scale -- Default scale is (1, 1, 1) -- Uniform scale: use same value for all axes, e.g. (2, 2, 2) = double size -- Non-uniform scale: different per axis, e.g. (1, 1, 2) = stretched vertically -- **Apply transforms** before export to bake scale into mesh data - -## OBJECT PLACEMENT PATTERNS - -### Grounding Objects -Place objects so their bottom face sits at Z=0: -- For a 2m cube with origin at center: location Z = 1.0 (half the height) -- For a table 0.75m tall: location Z = 0.375 (if origin at center) -- Better: set origin to bottom first, then Z = 0 - -### Grid Layout -For arranging multiple objects in a row: -- Use consistent spacing (e.g., 2.5m apart for 1m objects) -- Keep objects on the same Z plane -- Example: 3 objects at X = -2.5, 0, 2.5 - -### Circular Arrangement -For objects around a center point: -- Calculate positions using angle: X = radius × sin(angle), Y = radius × cos(angle) -- Common for turntable displays, architectural columns - -## HIERARCHY (Parent-Child) - -### When to Use Parenting -- **Group movement:** Parent multiple parts to an empty, move the empty to move everything -- **Mechanical assemblies:** Parent wheels to car body, arms to torso -- **Organization:** Group related objects under an empty controller - -### Parent Types -| Type | Use Case | -|---|---| -| `OBJECT` (default) | Standard parent-child, child follows parent transforms | -| `ARMATURE` | Character rigging, deformable mesh | -| `BONE` | Attach to specific bone in armature | - -### Rules -- Child transforms are relative to parent -- `parent_clear` with `keep_transform: true` preserves world position -- Deeply nested hierarchies (>3 levels) should be avoided for performance - -## COLLECTIONS (Blender's "Folders") - -### Organization Patterns -| Collection Name | Purpose | -|---|---| -| `Geometry` | Main scene objects | -| `Lights` | All light objects | -| `Cameras` | Camera objects | -| `Helpers` | Empties, constraints, controllers | -| `Export` | Objects ready for export | -| `Reference` | Hidden reference objects | - -### Rules -- Use `create_new: true` to create collections on the fly -- An object can exist in multiple collections (but usually shouldn't) -- Use `organize_collection_hierarchy` for collection-level visibility changes. -- Use `inspect_export_cleanup_candidates` before hiding, moving, or deleting helper/reference/construction objects for export. It protects explicit export targets and objects referenced by modifiers, constraints, or parenting. - -## ORIGIN PLACEMENT - -| Origin Type | Enum | Effect | Use Case | -|---|---|---|---| -| Geometry to origin | `ORIGIN_GEOMETRY` | Moves origin to mesh center | Most common, after modeling | -| Origin to geometry | `GEOMETRY_ORIGIN` | Moves mesh so origin is at mesh center | Centering objects | -| Origin to cursor | `ORIGIN_CURSOR` | Origin snaps to 3D cursor position | Precise placement | -| Origin to volume center | `ORIGIN_CENTER_OF_VOLUME` | Origin at volumetric center | Even weight distribution | - -### Common Patterns -- **Grounding:** Set origin to bottom of mesh, then location Z=0 -- **Rotation pivot:** Set origin to the logical center of rotation -- **Export prep:** Origin at geometry center for consistent import - -## DUPLICATE vs. LINKED DUPLICATE - -| Parameter | Full Duplicate | Linked Duplicate (`linked: true`) | -|---|---|---| -| Mesh data | Independent copy | **Shared** (editing one edits all) | -| Materials | Independent | Independent | -| Memory | Uses more RAM | Very efficient | -| Use case | Independent objects | Instancing (trees, chairs, bolts) | - -**Rule:** Use linked duplicates for identical objects (like array of pillars) to save memory. Use full duplicates when each copy will be modified independently. - -## NAMING CONVENTIONS - -| Pattern | Example | Purpose | -|---|---|---| -| Descriptive | `Table_Leg_Front_Left` | Clear identification | -| Numbered | `Column_001`, `Column_002` | Repeated elements | -| Prefixed | `GEO_Wall`, `LGT_Key`, `CAM_Main` | Type identification | - -**Rules:** -- Avoid generic names like "Cube", "Cube.001" — always rename -- Use consistent naming within a scene -- `rename_object` also renames the underlying data block - -## COMMON MISTAKES TO AVOID - -1. ❌ Forgetting to apply transforms before boolean operations — causes misaligned cuts -2. ❌ Leaving objects named "Cube.001", "Cube.002" — always rename for clarity -3. ❌ Using deeply nested parent hierarchies (>3 levels) — causes transform calculation issues -4. ❌ Deleting objects that are boolean targets — breaks the boolean modifier on the source -5. ❌ Moving objects without considering origin placement — origin stays at old position -6. ❌ Joining objects with different materials without checking material slots — can lose materials diff --git a/data/tool-guides/spatial-positioning-guide.md b/data/tool-guides/spatial-positioning-guide.md deleted file mode 100644 index 81fd227..0000000 --- a/data/tool-guides/spatial-positioning-guide.md +++ /dev/null @@ -1,467 +0,0 @@ ---- -title: "Spatial Positioning & Rotation-Aware Placement Guide" -category: "spatial-positioning" -tags: ["position", "rotation", "transform", "offset", "grounding", "support surface", "alignment", "attachment point", "local point", "bounding box", "clearance", "spatial relation", "north of", "south of", "inside", "facing", "origin", "mathutils", "matrix", "execute_code", "set_object_transform", "align_object_to_surface", "align_object_attachment_points", "validate_object_clearance", "inspect_spatial_relations", "inspect_scene_grounding", "get_object_info"] -triggered_by: ["set_object_transform", "align_object_to_surface", "align_object_attachment_points", "validate_object_clearance", "inspect_spatial_relations", "inspect_scene_grounding", "get_object_info"] -description: "Domain knowledge for accurate object positioning in 3D space, including rotation-aware placement, relative offsets, grounding, alignment, and bounding-box math. Prevents floating objects, misaligned parts, and incorrect spatial relationships." -blender_version: "4.0+" ---- - -# Spatial Positioning & Rotation-Aware Placement Guide - -## CRITICAL RULES - -1. **NEVER assume an object's position — always query it first.** Before moving, stacking, or aligning any existing object, call `get_object_info` to read its current location, dimensions, and rotation. Hardcoded assumptions cause compounding errors. - -2. **Rotation changes the effective bounding box.** A 1×1×2 m box rotated 45° on X is no longer 2 m tall in world space. Always compute the world-space bounding box after rotation. - -3. **"Move up by 2" means RELATIVE to current position, not absolute Z=2.** For relative transforms, read current position first, then add the offset. - -4. **Use the `@` operator for matrix multiplication (Blender 2.8+).** The `*` operator performs element-wise multiplication and will raise a `TypeError` with Matrix/Vector types. - -## POSITIONING FUNDAMENTALS - -### Origin-Aware Height Calculation -Every Blender primitive has its origin at its geometric center by default. - -| Primitive | Origin Position | Bottom Z | Top Z | -|---|---|---|---| -| Cube (size=S) | Center | `loc.z - S/2` | `loc.z + S/2` | -| Cylinder (depth=D) | Center | `loc.z - D/2` | `loc.z + D/2` | -| Sphere (radius=R) | Center | `loc.z - R` | `loc.z + R` | -| Plane | Center (flat) | `loc.z` | `loc.z` | - -**After scaling:** Actual dimension = `size × scale_factor`. A cube with `size=1, scale=(1,1,2)` has height `1 × 2 = 2m`. - -**Formula — Ground an object (bottom at Z=0):** -``` -location.z = height / 2 -``` -For a cylinder with `depth=0.8`: `location.z = 0.4` - -**Formula — Stack object B on top of object A:** -``` -B.location.z = A.location.z + (A_height / 2) + (B_height / 2) -``` - -### Relative vs. Absolute Positioning -| User says... | Correct interpretation | -|---|---| -| "Place at Z=2" | Absolute: `obj.location.z = 2.0` | -| "Move up by 2" | Relative: `obj.location.z = current_z + 2.0` | -| "Place 3m to the right" | Relative: `obj.location.x = ref.location.x + 3.0` | -| "Place next to X" | Query X dims: `obj.location.x = X.loc.x + X_width/2 + obj_width/2 + gap` | - -**ALWAYS call `get_object_info` before applying relative transforms.** - -## ROTATION-AWARE POSITIONING - -### The Core Problem -When you rotate an object, its **local dimensions** stay the same but its **world-space bounding box** changes. A backrest panel rotated -15° on X has its bottom edge shift upward. - -### World-Space Bounding Box Calculation -`obj.bound_box` returns coordinates in **local space only**. To get the actual world-space bounding box of a rotated/scaled/moved object: - -```python -from mathutils import Vector - -def get_world_bbox(obj): - """Get all 8 bounding box corners in world space.""" - return [obj.matrix_world @ Vector(corner) for corner in obj.bound_box] - -# Usage: -world_corners = get_world_bbox(obj) -min_z = min(c.z for c in world_corners) # actual bottom -max_z = max(c.z for c in world_corners) # actual top -actual_height = max_z - min_z -``` - -### Effective Height After X-Axis Rotation -For an object with dimensions `(W, D, H)` rotated by angle `θ` around X: - -``` -effective_height = abs(H × cos(θ)) + abs(D × sin(θ)) -effective_depth = abs(H × sin(θ)) + abs(D × cos(θ)) -``` - -### Practical Example: Angled Backrest -A backrest `(3.0 × 0.1 × 0.8)` rotated -15° on X: -```python -import math -H, D = 0.8, 0.1 -theta = math.radians(15) -effective_h = abs(H * math.cos(theta)) + abs(D * math.sin(theta)) # ≈ 0.799m -bottom_shift = (H / 2) * (1 - math.cos(theta)) - (D / 2) * math.sin(theta) -# ≈ 0.0007m upward (nearly zero — the two effects almost cancel) -# For thin objects (D << H), the shift is tiny; for thick objects it matters more -``` - -**Rule: When placing a rotated object flush against a surface, account for the bottom-edge shift.** - -### Using mathutils.Matrix for Precise Placement -```python -import mathutils, math - -# Build a transformation: first rotate, then translate -rot = mathutils.Matrix.Rotation(math.radians(-15), 4, 'X') -loc = mathutils.Matrix.Translation((0, 0.25, 0.75)) -obj.matrix_world = loc @ rot - -# Order: Translation @ Rotation @ Scale -# Applied right-to-left: scale first, rotate second, translate last -``` - -### Local-to-World Attachment Points -When a piece attaches to a rotated object, compute the connection point in world space first. - -```python -from mathutils import Vector - -def local_point_to_world(obj, local_point): - return obj.matrix_world @ Vector(local_point) - -# Example pattern: -parent_tip_world = local_point_to_world(parent_obj, (0.0, 0.5, 0.0)) -child_socket_world = local_point_to_world(child_obj, (0.0, -0.25, 0.0)) -delta = parent_tip_world - child_socket_world -child_obj.matrix_world.translation += delta -``` - -**Rule:** If two rotated parts must meet cleanly, align named local attachment points in world space instead of guessing offsets from object origins. - -## ALIGNMENT PATTERNS - -### Center-to-Center (Stack Vertically) -``` -B.location.x = A.location.x -B.location.y = A.location.y -B.location.z = A.location.z + (A_height/2) + (B_height/2) -``` - -### Edge-to-Edge (Flush Surfaces) -Place B so its left face touches A's right face: -``` -B.location.x = A.location.x + (A_width/2) + (B_width/2) -``` - -### Place Object on a Surface with Gap -``` -B.location.z = surface_top_z + (B_height/2) + gap -``` - -### Support Placement via World-Space Bounding Boxes -For rotated or non-uniformly scaled objects, derive the true contact surfaces from the world-space bounding box: - -```python -from mathutils import Vector - -def world_bbox(obj): - corners = [obj.matrix_world @ Vector(corner) for corner in obj.bound_box] - min_corner = Vector((min(c.x for c in corners), min(c.y for c in corners), min(c.z for c in corners))) - max_corner = Vector((max(c.x for c in corners), max(c.y for c in corners), max(c.z for c in corners))) - return min_corner, max_corner - -obj_min, obj_max = world_bbox(obj) -support_min, support_max = world_bbox(support_obj) -delta_z = (support_max.z + 0.002) - obj_min.z -obj.matrix_world.translation += Vector((0, 0, delta_z)) -``` - -**Rule:** For "resting on" placement, move the object's actual world-space bottom to the support's actual world-space top. Do not approximate with `location.z + height/2` when rotation is involved. - -### Symmetric Placement -Place objects symmetrically around a center point: -```python -for i, offset in enumerate([-1.5, 0, 1.5]): - obj_copy.location.x = center_x + offset -``` - -## PROXIMITY & CONTACT RELATIONSHIPS - -These patterns handle natural language like "leaning against", "beside", "resting on", "inside a container". The core principle: **always compute the actual surface boundary of BOTH objects before positioning.** - -### CRITICAL: Scaled Dimensions - -```python -actual_extent_x = R × sx (NOT just R) -actual_extent_y = R × sy -actual_extent_z = R × sz -``` - - - -```python -half_x = (S / 2) × sx -half_y = (S / 2) × sy -half_z = (S / 2) × sz -``` - - -**ALWAYS multiply dimensions by scale factors before computing offsets.** - -### "Leaning Against" / "Beside" (Surface Contact) -Object B should touch but NOT penetrate object A: -```python -# Compute A's rightmost surface -A_right_edge = A.location.x + A_actual_half_width -# Compute B's leftmost surface offset from B's center -B_left_offset = B_actual_half_width -# Place B so its left surface touches A's right surface -B.location.x = A_right_edge + B_left_offset + margin # margin ≥ 0.01 -``` - -**Example — bananas leaning against a watermelon:** -```python -# Watermelon: radius=0.2, scale=(1.3, 1.0, 1.0) at X=-0.5 -wm_right_x = -0.5 + (0.2 * 1.3) # = -0.24 - -# Banana: radius=0.03 at whatever position -banana_x = wm_right_x + 0.03 + 0.01 # = -0.20 (just outside) -``` - -❌ **WRONG:** `banana_x = watermelon_x + 0.25` (ignores watermelon's actual width) -✅ **RIGHT:** `banana_x = watermelon_x + wm_radius * wm_scale_x + banana_radius + margin` - -### "Resting On" / "Sitting On" (Vertical Contact) -Object B sits on top of object A's surface: -```python -A_top_z = A.location.z + A_actual_half_height -B.location.z = A_top_z + B_actual_half_height + margin -``` - -For spheres on flat surfaces: -```python -# Sphere rests on table top -sphere.location.z = table_top_z + sphere_radius * sphere_scale_z -``` - -### "Inside a Container" (Contained Within) -Object B should be within the bounds of object A: -```python -# B center must satisfy: -# A.loc.x - (A_half_w - B_half_w) < B.loc.x < A.loc.x + (A_half_w - B_half_w) -# Same for Y and Z - -# Example: apples inside a crate -crate_interior_half_w = (crate_width / 2) - wall_thickness - apple_radius -apple.location.x = crate.location.x + random_offset # |offset| < crate_interior_half_w -apple.location.z = crate_floor_z + apple_radius # resting on crate floor -``` - -### "Clustered Together" / "Grouped" -Spread objects with minimum separation to avoid overlap: -```python -min_separation = obj_radius_A + obj_radius_B + 0.02 # 2cm gap -# For each new object, check distance to all existing objects -from mathutils import Vector -for existing in placed_objects: - dist = (Vector(new_loc) - Vector(existing.location)).length - if dist < min_separation: - # push new_loc outward -``` - -### Raycast Drop Placement for Irregular Surfaces -Axis-aligned stacking is not enough for sloped or uneven supports. Use `scene.ray_cast` in world space: - -```python -from mathutils import Vector - -depsgraph = bpy.context.evaluated_depsgraph_get() -origin = Vector((obj.location.x, obj.location.y, obj.location.z + 100.0)) -direction = Vector((0, 0, -1)) -hit, location, normal, face_index, hit_obj, matrix = bpy.context.scene.ray_cast( - depsgraph, - origin, - direction, - distance=200.0, -) -``` - -If the ray hits, translate the object so its world-space bottom sits at `location.z + margin`. - -### Direct Tool Surface Placement - -Use `align_object_to_surface` before writing Blender Python when the placement is expressible as bounding-box contact: - -```text -align_object_to_surface(name="Mug", placement="ON_TOP", reference_name="Table", margin=0.005) -validate_object_clearance(name="Mug", other_name="Table", margin=0.005) -align_object_to_surface(name="Chair", placement="GROUND", surface_value=0, margin=0) -align_object_to_surface(name="Shelf", placement="SIDE_POSITIVE", reference_name="Wall", axis="Y", margin=0.02) -validate_object_clearance(name="Shelf", other_name="Wall", margin=0.02) -``` - -This tool uses world-space bounds, so it handles scaled and rotated objects better than guessed center offsets. Use `execute_code` only for irregular/sloped support raycasts or local attachment points that the direct tool cannot represent yet. - -After any close placement, call `validate_object_clearance` when clipping would be visually obvious or functionally wrong. A positive margin means the objects must have at least that much separating gap along one axis; true overlap means their AABBs intersect on X, Y, and Z. - -### Named Relation Inspection - -Use `inspect_spatial_relations` when the task has explicit spatial constraints that a camera angle can hide: - -```text -inspect_spatial_relations(relations=[ - {"type": "north_of", "subject": "Table", "reference": "Rug"}, - {"type": "south_of", "subject": "Chair", "reference": "Table"}, - {"type": "facing", "subject": "Chair", "reference": "Table", "local_axis": "-Y"}, - {"type": "inside_xy", "subject": "Storage_Cube", "reference": "Rug"}, - {"type": "supported_by", "subject": "Laptop", "reference": "Table_Top"}, - {"type": "on_top_of", "subject": "Plant_Pot", "reference": "Shelf_Top"}, - {"type": "closer_to", "subject": "Laptop", "reference": "Chair", "other": "North_Wall"}, -]) -``` - -Use `above` and `below` only for broad vertical ordering. They do not prove physical contact. If a prompt says an object is on a table, on books, on a shelf, resting on a plinth, or supported by another object, include a `supported_by` or `on_top_of` relation for the semantic mesh that must make contact with the named support surface. This catches gaps that a render or camera angle can hide, such as a vase hovering just above a book stack while still passing an `above` check. - -If a relation fails, fix the named objects with `set_object_transform`, `align_object_to_surface`, `arrange_objects`, or curve tools, then run `inspect_spatial_relations` again. Do not use a flattering camera angle as proof that relation constraints are satisfied. - -### Direct Tool Attachment Point Alignment - -Use `align_object_attachment_points` when two rotated or offset parts must meet at precise local points: - -```text -align_object_attachment_points( - name="Sign_Arm", - obj_local_point=[-0.5, 0, 0], - reference_name="Pole", - reference_local_point=[0, 0, 1.75], - offset=[0.01, 0, 0] -) -``` - -This converts both local points with `matrix_world @ Vector(...)` and translates the moved object by the world-space difference. Use it for arms, handles, hinges, rails, decorative attachments, and assembly joints before falling back to custom matrix Python. - -### Safety Margin Reference -| Relationship | Minimum margin | -|---|---| -| Touching / leaning | 0.005–0.02 m | -| "Close but not touching" | 0.05–0.10 m | -| "Nearby" / "beside" | 0.10–0.30 m | -| "Well separated" | 0.50+ m | - -## REAL-WORLD DIMENSION REFERENCE - -Use these to validate proportions when the user doesn't provide exact sizes. - -### Furniture -| Object | Width | Depth | Height | Notes | -|---|---|---|---|---| -| Park bench (2-person) | 1.0–1.3 m | 0.45–0.50 m | Seat: 0.45 m | Backrest adds ~0.40 m | -| Park bench (3-person) | 1.35–1.80 m | 0.45–0.60 m | Seat: 0.45 m | | -| Dining chair | 0.45 m | 0.45 m | Seat: 0.45 m, Total: 0.90 m | | -| Office desk | 1.2–1.8 m | 0.60–0.75 m | 0.73–0.76 m | | -| Coffee table | 0.75–1.2 m | 0.50–0.75 m | 0.40–0.50 m | | -| Sofa (3-seat) | 2.0–2.5 m | 0.80–1.0 m | Back: 0.85 m | | -| Bed (queen) | 1.52 m | 2.03 m | Frame: 0.30–0.50 m | | - -### Street Furniture -| Object | Dimensions | Notes | -|---|---|---| -| Streetlamp (residential) | Height: 3–6 m, pole Ø: 0.08–0.12 m | Base Ø: 0.25–0.35 m | -| Streetlamp (urban) | Height: 4.5–9 m | | -| Trash can (outdoor) | Ø: 0.35–0.50 m, H: 0.75–0.95 m | | -| Fire hydrant | H: 0.60–0.75 m, Ø: ~0.20 m | | -| Bollard | H: 0.75–1.0 m, Ø: 0.10–0.15 m | | - -### Architecture -| Element | Dimensions | Notes | -|---|---|---| -| Door (standard) | 0.90 × 2.10 m | | -| Window | 1.0–1.5 m × 1.2–1.5 m | Sill at ~0.90 m | -| Ceiling height | 2.4–3.0 m | Residential | -| Wall thickness | Interior: 0.10–0.15 m | Exterior: 0.20–0.30 m | -| Stair step | Rise: 0.17 m, Run: 0.28 m | | - -### Human Reference -| Measurement | Value | -|---|---| -| Standing height | 1.70–1.80 m | -| Eye height (camera eye-level) | 1.55–1.70 m | -| Shoulder width | 0.40–0.50 m | -| Seated height | 0.80–0.90 m | - -## OBJECT FUNCTIONAL DIRECTION & CLEARANCE - -In professional 3D modeling, many objects have a **functional direction** — they are designed to be used, viewed, or accessed from a specific side. Understanding functional direction is critical for realistic scene composition. - -### Functional Direction Principle -Every object that serves a user or interacts with another object has an **intended usage axis**: - -| Object Type | Forward/Open Side | Back/Closed Side | -|---|---|---| -| Seating (chairs, sofas, benches) | Open seat side (where user sits from) | Backrest / back panel | -| Displays (monitors, TVs, paintings) | Screen / visible face | Back housing / wall mount | -| Storage (shelves, cabinets, fridges) | Door / access opening | Back panel against wall | -| Vehicles | Hood / front grille | Trunk / tailgate | -| Appliances (ovens, microwaves) | Door / control panel | Back venting | -| Speakers / audio equipment | Driver cone / front grille | Wiring / port panel | - -### Placement Rule: Face the Companion Object -When an object is described as being "at", "facing", or "pushed in" to another object, its **open/functional side faces the companion object**, and its **closed/back side faces away**. - -This means the back geometry extends **away** from the companion, never into it. - -### Algorithmic Placement for Directional Objects - -```python -# General formula: place object B facing object A -# 1. Determine A's near edge (the surface closest to where B should be) -A_near_edge = A_center - A_half_depth # or + depending on direction - -# 2. Place B so its FUNCTIONAL side (front) faces A, -# with its center offset by B's half-depth + clearance gap -B_center = A_near_edge - B_half_depth - clearance_gap - -# 3. The back of B extends AWAY from A: -B_back = B_center - B_back_extent # always farther from A, never closer - -# 4. Verify: B's closest point to A should not overlap A -B_closest_to_A = B_center + B_half_depth -assert B_closest_to_A < A_near_edge # must be outside A's bounding box -``` - -### Anti-Pattern: Reversed Functional Direction -If the back geometry is placed on the companion-object side: -- The back panel/structure **clips through** the companion -- The object appears oriented away from its companion (visually nonsensical) -- The user-facing side points at empty space instead of the interaction surface - -### AABB Clearance Validation -After placing **any** object near another, validate no intersection using axis-aligned bounding box (AABB) overlap detection: - -```python -def check_clearance(obj_a, obj_b): - """Return True if objects do NOT overlap (AABB test in world space).""" - from mathutils import Vector - a_corners = [obj_a.matrix_world @ Vector(c) for c in obj_a.bound_box] - b_corners = [obj_b.matrix_world @ Vector(c) for c in obj_b.bound_box] - - a_min = Vector((min(c.x for c in a_corners), min(c.y for c in a_corners), min(c.z for c in a_corners))) - a_max = Vector((max(c.x for c in a_corners), max(c.y for c in a_corners), max(c.z for c in a_corners))) - b_min = Vector((min(c.x for c in b_corners), min(c.y for c in b_corners), min(c.z for c in b_corners))) - b_max = Vector((max(c.x for c in b_corners), max(c.y for c in b_corners), max(c.z for c in b_corners))) - - overlap = (a_min.x < b_max.x and a_max.x > b_min.x and - a_min.y < b_max.y and a_max.y > b_min.y and - a_min.z < b_max.z and a_max.z > b_min.z) - return not overlap -``` - -This is a standard technique from real-time collision detection — use it whenever two objects must be close but not interpenetrating. - -## COMMON MISTAKES TO AVOID - -1. ❌ **Moving to Z=2 when user said "move up by 2"** — Read current position first -2. ❌ **Placing a rotated part at the unrotated height** — Rotation shifts the effective edges -3. ❌ **Assuming object size from creation params after scaling** — Actual size = param × scale -4. ❌ **Stacking without querying the lower object** — Never assume; always `get_object_info` -5. ❌ **Forgetting center-origin offset** — Bottom of a size=2 cube at origin is at Z=-1 -6. ❌ **Using `*` for matrix math** — Use `@` in Blender 2.8+ (PEP 465) -7. ❌ **Using absolute coords for relative instructions** — "3m right of table" ≠ x=3 -8. ❌ **Ignoring scale when computing proximity offsets** — A sphere(r=0.2, scale_x=1.3) extends 0.26m, NOT 0.2m. Always multiply: `actual_extent = radius × scale` -9. ❌ **Placing "beside" objects at a fixed offset from center instead of from surface** — "Beside" means surface-to-surface contact, not center-to-center distance -10. ❌ **Objects inside each other when meant to be touching** — Always compute both surfaces: `A_surface + B_half_width + margin`, never just `A_center + arbitrary_offset` -11. ❌ **Reversing an object's functional direction** — When placing an object "at" or "facing" a companion, the open/functional side must face the companion. Back geometry extends AWAY, never into the companion object. -12. ❌ **Skipping clearance validation after close placement** — After positioning any object near another, run an AABB overlap check to catch interpenetration before the user sees it -13. ❌ **Using local attachment coordinates as if they were already world-space** — always convert with `matrix_world @ Vector(...)` -14. ❌ **Using axis-aligned formulas on rotated or sloped supports** — derive contact from world-space bounding boxes or ray casts diff --git a/data/tool-guides/weight-painting-guide.md b/data/tool-guides/weight-painting-guide.md deleted file mode 100644 index 28400d8..0000000 --- a/data/tool-guides/weight-painting-guide.md +++ /dev/null @@ -1,188 +0,0 @@ ---- -title: "Weight Painting & Vertex Group Guide" -category: "weight-painting" -tags: ["weight painting", "vertex groups", "deformation", "auto-weights", "bone heat", "normalize", "smooth weights", "limit total", "game engine", "export", "bmesh", "vertex group elements", "inspect_weight_paint_readiness", "normalize_vertex_group_weights", "bind_mesh_to_armature", "transfer_vertex_group_weights", "project_vertex_group_weights"] -triggered_by: ["inspect_weight_paint_readiness", "normalize_vertex_group_weights", "bind_mesh_to_armature", "transfer_vertex_group_weights", "project_vertex_group_weights"] -description: "Research-backed weight painting guide covering Python vertex group architecture, bmesh high-performance assignment, post-binding cleanup pipeline, game engine bone limits (4 max), smoothing techniques, and bone heat weighting troubleshooting. Based on NotebookLM research with 10 cited sources." -blender_version: "4.0+ / 5.x" ---- - -# Weight Painting Guide — Blender Agent Best Practices - -> **Domain:** Weight Painting, Vertex Groups, Deformation Quality -> **Version:** Blender 4.x / 5.x (verified via NotebookLM research) -> **Purpose:** Research-backed guide for the agent. Use direct weight-paint inspection and cleanup tools first; reserve `execute_code` for unsupported custom assignment logic. - ---- - -## CRITICAL: Weight Data Architecture - -> **Source:** StackExchange "How to set Vertex Weights in Blender 4.0 with Python" - -Weights are NOT stored in vertex groups — they are stored as **Vertex Group Elements** -bound to each individual vertex. Understanding this architecture is essential: - -- `obj.vertex_groups` → list of vertex groups (by name/index) -- `obj.data.vertices[x].groups` → vertex group elements for vertex x -- Each element knows its `weight` and `group` index -- **Vertex groups don't know their vertices** — you must check each vertex's elements - ---- - -## 1. Assigning Weights via Python - -### Standard Method: `group.add()` -```python -import bpy - -obj = bpy.data.objects['CharacterMesh'] -# Get or create vertex group -vg = obj.vertex_groups.get('Bone_Name') -if not vg: - vg = obj.vertex_groups.new(name='Bone_Name') - -# Assign weight to specific vertices -# Syntax: group.add(vertex_indices, weight, mode) -# Modes: 'REPLACE', 'ADD', 'SUBTRACT' -vg.add([0, 1, 2, 3], 1.0, 'REPLACE') # full weight -vg.add([4, 5, 6], 0.5, 'REPLACE') # half weight -vg.add([7, 8, 9], 0.0, 'REPLACE') # zero weight -``` - -### High-Performance Method: bmesh Deform Layer -> **Source:** Blender DevTalk "Manipulate vertex groups via bmesh" - -For high-poly meshes, looping through vertex group elements is too slow. -Use bmesh's deform layer for direct dictionary-like access: - -```python -import bpy, bmesh - -obj = bpy.data.objects['CharacterMesh'] -bpy.context.view_layer.objects.active = obj -bpy.ops.object.mode_set(mode='EDIT') - -bm = bmesh.from_edit_mesh(obj.data) -deform_layer = bm.verts.layers.deform.verify() - -# Get the vertex group index -group_index = obj.vertex_groups['Bone_Name'].index - -# Assign weights via deform layer (fast) -for vert in bm.verts: - vert[deform_layer][group_index] = 1.0 # or calculated weight - -bmesh.update_edit_mesh(obj.data) -bpy.ops.object.mode_set(mode='OBJECT') -``` - ---- - -## 2. Automatic Weights (Binding Mesh to Armature) - -Use `bind_mesh_to_armature` after `inspect_rigging_data` and -`inspect_weight_paint_readiness`. The `AUTOMATIC` mode runs Blender's bone heat -weighting; `ENVELOPE`, `EMPTY`, and `NAME` expose the other bounded parent modes. -Do not replace existing skinning state unless inspection shows that it is stale. - -For a duplicate mesh with identical vertex count and vertex order, use -`transfer_vertex_group_weights` to copy known-good groups by vertex index. This -tool deliberately rejects different topology. For a remeshed or retopologized -target, use `project_vertex_group_weights`; it wraps Blender's Data Transfer -modifier with vertex-group weights and matching group names. - ---- - -## 3. Post-Binding Weight Cleanup (MANDATORY) - -> **Source:** Reddit "Mesh deforms differently in Unity than in Blender" - -Raw auto-weights are never production-quality. Always run the **direct-tool cleanup pipeline** after binding: - -1. `inspect_weight_paint_readiness` — Check mesh, armature, vertex groups, Armature modifier, and likely export risks. -2. `normalize_vertex_group_weights` — Cap bone influences per vertex, normalize sums, and remove tiny weights. -3. `inspect_rigging_data` — Verify armature, modifier, and vertex group state after cleanup. - -> **Fallback:** Use `execute_code` only for custom vertex-by-vertex assignments or specialized bmesh workflows that the direct tools cannot express. -> This guide focuses on the *weight data architecture* (Section 1-2) and *advanced techniques* (Sections 4-7) that the rigging guide does not cover. - ---- - -## 4. Game Engine Export Rules - -> **Source:** Reddit r/Unity3D "Mesh deforms differently in Unity than in Blender" - -| Engine | Max Bones/Vertex | Notes | -|--------|-----------------|-------| -| Unity | 4 (default 2) | Change in Project Settings → Quality → Skin Weights | -| Unreal | 4-8 | 4 recommended for mobile, 8 for desktop | -| Godot | 4 | Standard limit | - -**If you don't limit to 4 in Blender before export, the mesh WILL deform incorrectly in-engine.** - -Unity-specific: In FBX import → Rig tab → change Skin Weights to "Standard (4 Bone)". - ---- - -## 5. Smoothing Techniques - -> **Source:** StackExchange "Simple methods to smooth out weight paints" - -### Smooth Tool (Python) -```python -bpy.ops.object.vertex_group_smooth( - group_select_mode='ALL', # or 'BONE_SELECT', 'BONE_DEFORM' - factor=0.5, - repeat=3 -) -``` - -### Smooth Tool (Manual — for agent instructions) -1. Enter Weight Paint mode -2. Activate vertex selection (cube icon with yellow vertex) -3. Select vertices to smooth -4. Click **Smooth** in tools panel -5. In operator options: select **'Selected Pose Bones'** subset - -> **⚠️ WARNING:** Using 'All' subset causes weight leaking — a single blue vertex -> will expand to cover all selected vertices. Always use 'Selected Pose Bones' -> with 'only selected' source mode to prevent unintended weight spread. - -### Blur Brush -Paint over rough transitions manually using the Blur brush in Weight Paint mode. - -### Corrective Smooth Modifier -Non-destructive post-weighting smoothing: -```python -mod = mesh.modifiers.new(name='CorrSmooth', type='CORRECTIVE_SMOOTH') -mod.factor = 0.5 -mod.iterations = 5 -mod.smooth_type = 'SIMPLE' -``` - ---- - -## 6. Troubleshooting: "Bone Heat Weighting Failed" - -> **Source:** Reddit r/blender "Is there a remedy for bone heat weighting" - -| Fix | How | Why It Works | -|-----|-----|--------------| -| **Scale up 10-100x** | Select armature+mesh → scale → apply weights → scale back | Small meshes confuse the heat solver | -| **Merge by Distance** | Edit Mode → Select All → Mesh → Merge by Distance | Duplicate vertices cause ambiguity | -| **Decimate dense mesh** | Modifier → Decimate → apply | Vertex density overloads solver | -| **Data Transfer trick** | Remesh → bind clean copy → Data Transfer modifier to original | Bypasses bad topology entirely | -| **Fix symmetry** | Delete half armature → mirror | Asymmetric bones cause solver failure | -| **Separate loose parts** | Edit Mode → P → Separate by Loose Parts → parent individually | Intersecting meshes confuse heat map | - ---- - -## 7. Common Pitfalls - -| Problem | Cause | Fix (Research-backed) | -|---------|-------|----------------------| -| Mesh deforms wrong in Unity | >4 bone influences per vertex | `vertex_group_limit_total(limit=4)` before export | -| Weights leak to wrong areas | Smooth tool using 'All' subset | Use 'Selected Pose Bones' subset | -| Auto-weights fail | Mesh too small or non-manifold | Scale up 10x, merge by distance | -| Blocky deformation | No weight smoothing after auto-bind | Run smooth + normalize pipeline | -| Slow weight assignment | Looping vertex group elements on high-poly | Use bmesh deform layer | diff --git a/docs/client-setup.md b/docs/client-setup.md new file mode 100644 index 0000000..0e9d83d --- /dev/null +++ b/docs/client-setup.md @@ -0,0 +1,111 @@ +# MCP Client Setup + +## What Starts What + +ViperMesh for Blender has two local connections: + +1. An MCP client starts the ViperMesh Node server as a long-lived stdio + subprocess. +2. The Node server opens and reuses a serialized TCP connection to the Blender + addon on `127.0.0.1:9876`. + +The MCP client is responsible for the server lifecycle. Register the server +once in the client, then use the MCP tools exposed in that client session. +Running a separate `npm`, `npx`, or `tsx` command for each tool call creates a +new server process and discards the persistent connection. + +Docker is not required for clients that support local stdio MCP servers. + +## Native Stdio Setup + +Clone, install, and build the repository: + +```bash +git clone https://github.com/Ker102/vipermesh-blender.git +cd vipermesh-blender +npm install +npm run build +``` + +Use the built entry point from an absolute path. This avoids relying on a +client-specific working-directory option. + +### Codex + +```bash +codex mcp add vipermesh-blender -- node C:/absolute/path/to/vipermesh-blender/dist/public-portable-blender-mcp.js +``` + +Confirm the registration: + +```bash +codex mcp list +``` + +Open a new Codex task if the current task does not reload MCP registrations. + +### JSON-Configured Clients + +Clients such as Claude Desktop and other hosts commonly accept a command and +argument array: + +```json +{ + "mcpServers": { + "vipermesh-blender": { + "command": "node", + "args": [ + "C:/absolute/path/to/vipermesh-blender/dist/public-portable-blender-mcp.js" + ] + } + } +} +``` + +The configuration filename and top-level key vary by client. Use the client's +current MCP documentation, but keep the command itself equivalent. + +## Client Compatibility + +A client can use this release directly when it supports local MCP servers over +stdio and can launch a subprocess. MCP support alone does not guarantee that: + +- some clients support local stdio and remote Streamable HTTP; +- some support only remote server URLs; +- some require a plugin, extension, or administrator policy before local + process execution is allowed. + +This release is stdio-only. A remote-only client needs a separately hosted MCP +transport or a compatible gateway; it cannot connect directly to the Blender +addon's TCP protocol. + +## Docker MCP Toolkit + +Docker MCP Toolkit can centralize containerized MCP servers and connect its +stdio gateway to supported clients. It is an optional distribution route, not +a requirement for ViperMesh. + +ViperMesh does not currently publish a Docker MCP Catalog entry. The Blender +addon also remains loopback-only for local security. A container must reach +that host loopback service reliably, which varies with the Docker host, +networking mode, and client environment. + +For that reason, native stdio is the supported setup today. Do not expose the +Blender bridge on a public interface merely to make a container connect. Docker +Toolkit instructions will be promoted to a supported path after an end-to-end +Windows, macOS, and Linux connectivity test defines a secure configuration. + +Official references: + +- [MCP transports](https://modelcontextprotocol.io/specification/latest/basic/transports) +- [Docker MCP Toolkit](https://docs.docker.com/ai/mcp-catalog-and-toolkit/) + +## Verify The Session + +1. Start Blender and the ViperMesh local bridge. +2. Open a new session in the configured MCP client. +3. Call `bootstrap_vipermesh_session`. +4. Confirm `connection.connected` is `true` and `sessionModel` is + `persistent`. +5. Make two lightweight inspection calls. They should reuse one MCP process and + one Blender client rather than starting new shell commands. diff --git a/docs/portable-blender-mcp.md b/docs/portable-blender-mcp.md index 7a1cbac..32df1e8 100644 --- a/docs/portable-blender-mcp.md +++ b/docs/portable-blender-mcp.md @@ -1,5 +1,21 @@ # Portable Blender MCP Gateway +## Result Review And Existing Scenes + +Image-producing calls attach a PNG/JPEG directly when it is available on the MCP +server's filesystem and at most 3 MiB. Otherwise use the reported artifact path +with an image viewer. The `imageAttached` flag indicates transport availability; +`visualReviewRequired` means the agent must judge the actual output against the +task. Image-file health and successful commands are not visual quality scores. + +Stage helpers are optional convenience operations. Set `preservePresentation: +true` on finalize to keep the current camera, lighting and framing. `blendPath` +is optional; omit it for a render without saving a blend file. Saving through +finalize refuses an existing destination. Use a standalone save only when an +overwrite is intended and authorized. Explicit `spatialRelations` are rechecked +on finalize, and failed relations withhold final output. Diagnostic checks cannot +certify arbitrary mesh contact; inspect suspicious areas from revealing views. + The portable gateway lets trusted coding agents call ViperMesh Blender tools through standard MCP stdio transport, including `execute_code` for measuring fallback behavior during local testing. @@ -28,7 +44,7 @@ instead of reconnecting after every tool call. started. - The addon reachable at `127.0.0.1:9876`, unless overridden with `BLENDER_MCP_HOST` and `BLENDER_MCP_PORT`. -Local checked-in tool-guide search works without database or embedding +Local checked-in tool-skill search works without database or embedding credentials. ## Start The Gateway @@ -44,15 +60,16 @@ diagnostics are written to stderr. ## Coding Agent Configuration -Use an absolute repository path: +Use the built entry point from an absolute repository path: ```json { "mcpServers": { "vipermesh-blender": { - "command": "npm", - "args": ["run", "mcp"], - "cwd": "C:/absolute/path/to/vipermesh-blender" + "command": "node", + "args": [ + "C:/absolute/path/to/vipermesh-blender/dist/public-portable-blender-mcp.js" + ] } } } @@ -60,7 +77,8 @@ Use an absolute repository path: The exact configuration location depends on the coding agent. Restart or open a new agent session after registering the server if the client does not reload MCP -servers dynamically. +servers dynamically. See [MCP client setup](client-setup.md) for Codex and +client compatibility details. ## Available MCP Tools @@ -140,16 +158,22 @@ between decimation, voxel remesh, QuadriFlow, or custom fallback code. `search_3d_guidance` supports: -- `source: "local"` for deterministic search over `data/tool-guides`; +- `source: "local"` for deterministic search over the public agent skill + references in `skills/using-vipermesh-blender/references`; - `source: "semantic"` for a configured private ViperMesh semantic adapter; - `source: "all"` to combine both. Semantic retrieval is an optional private-product adapter. The public connector -ships deterministic local guide retrieval and does not require database or +ships deterministic local tool-skill retrieval and does not require database or embedding credentials. `get_3d_guidance_document` reads a Markdown basename from -`data/tool-guides`. Arbitrary filesystem paths and traversal are rejected. +`skills/using-vipermesh-blender/references`. Arbitrary filesystem paths and +traversal are rejected. + +The skill references describe capabilities, tradeoffs, and validation patterns. +They are recommendations, not universal scene recipes. The private ViperMesh +RAG corpus is not distributed in the public connector. ## Security Boundary diff --git a/docs/releasing.md b/docs/releasing.md index 408e163..aeaa13d 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -4,6 +4,21 @@ The public repository is generated from `config/public-blender-connector-files.json`. Never fork or copy the private repository history. +## Current Release + +- Repository: https://github.com/Ker102/vipermesh-blender +- Release target: https://github.com/Ker102/vipermesh-blender/releases/tag/v1.3.0 +- Source release target: `v1.3.0` +- Blender compatibility target: 5.2 +- CI: standalone typecheck, conformance tests, build, package validation, npm + audit, and addon Python compilation +- Historical live validation (v1.2.0): persistent stdio MCP discovery, scene calls, mutation, + staged preview/finalize, save, local guidance, and `execute_code` fallback +- Current launch checks: isolated package conformance, inline image transport, + render-only/preserved-presentation stages, spatial failure withholding, + dependency audit, Python compilation, and desktop/mobile site review. Full + live agent performance is evaluated in the next demo pilot. + ## Before Export - Run `npm run validate:public-blender-connector`. @@ -26,7 +41,7 @@ repository history. ## Release -1. Install dependencies and run `npm run typecheck` in the generated directory. +1. Install dependencies and run `npm run check` in the generated directory. 2. Package the addon as a release artifact. 3. Tag the connector version. 4. Publish the MCP package only after its package contents are inspected. diff --git a/lib/orchestration/tool-registry.ts b/lib/orchestration/tool-registry.ts index 8cc8cfa..818ce4f 100644 --- a/lib/orchestration/tool-registry.ts +++ b/lib/orchestration/tool-registry.ts @@ -69,7 +69,14 @@ export const TOOL_REGISTRY: ToolMetadata[] = [ description: "Save the current Blender scene to an explicit .blend filepath without generated Python. Use for review artifacts, handoff checkpoints, and export/package workflows.", category: "advanced", - parameters: "filepath: string (.blend), make_dirs?: boolean, check_existing?: boolean", + parameters: "filepath: string (.blend), make_dirs?: boolean, check_existing?: boolean, copy?: boolean", + }, + { + name: "restore_blend_checkpoint", + description: + "Restore a saved .blend checkpoint after a failed guarded mutation and return Blender to the original file path.", + category: "advanced", + parameters: "checkpoint_filepath: string (.blend), original_filepath: string (.blend)", }, { name: "get_viewport_screenshot", @@ -171,6 +178,14 @@ export const TOOL_REGISTRY: ToolMetadata[] = [ parameters: "relations: Array<{type, subject, reference?, other?, margin?, local_axis?, min_dot?}>, tolerance?: number", }, + { + name: "inspect_mesh_intersections", + description: + "Inspect evaluated mesh-surface intersections for explicit object pairs using Blender BVH geometry. Use when bounds-only clearance cannot prove clothing, collision, or assembly penetration.", + category: "inspection", + parameters: + "pairs: Array<{subject: string, reference: string}>, max_intersections?: number, epsilon?: number", + }, { name: "inspect_scene_grounding", description: @@ -340,6 +355,22 @@ export const TOOL_REGISTRY: ToolMetadata[] = [ category: "inspection", parameters: "(no parameters)", }, + { + name: "inspect_addon_capabilities", + description: + "Inspect registered Blender operator RNA for one enabled add-on without executing any operator. Returns typed inputs, current-context availability, provenance fingerprints, and inspection errors for personal capability onboarding.", + category: "inspection", + parameters: + "addon_module: string, operator_ids?: string[], max_operators?: number (default 100, max 250)", + }, + { + name: "invoke_addon_capability", + description: + "Dry-run or invoke one inspected third-party add-on operator. Real invocation requires the exact current fingerprint, approved=true, and an absolute .blend checkpoint path; dry_run defaults to true.", + category: "advanced", + parameters: + "addon_module: string, operator_id: string, arguments?: object, expected_source_fingerprint: string, dry_run?: boolean, approved?: boolean, checkpoint_filepath?: string", + }, { name: "create_material", description: @@ -479,6 +510,13 @@ export const TOOL_REGISTRY: ToolMetadata[] = [ parameters: "names: string[], file_format?: GLB|GLTF|FBX|OBJ|STL, max_dependencies?: number, check_texture_formats?: boolean", }, + { + name: "validate_gltf_roundtrip", + description: + "Temporarily import a GLB or glTF export, inventory meshes, armature bones, skinned meshes, materials, images, actions, and triangle count, then clean up the imported data by default.", + category: "advanced", + parameters: "filepath: string, cleanup?: boolean", + }, { name: "set_world_environment", description: @@ -687,6 +725,14 @@ export const TOOL_REGISTRY: ToolMetadata[] = [ parameters: "names?: string[], max_objects?: number, high_density_face_threshold?: number", }, + { + name: "compare_mesh_surfaces", + description: + "Compare source and candidate evaluated meshes using bounded bidirectional vertex and polygon-center surface deviation, nearest material-region correspondence, and coverage-aware UV seam position correspondence. Read-only and intended for topology or LOD evidence before artist acceptance.", + category: "inspection", + parameters: + "source_name: string, candidate_name: string, distance_tolerance?: number, seam_tolerance?: number, max_surface_samples?: number, max_attribute_samples?: number", + }, { name: "decimate_mesh", description: @@ -751,6 +797,14 @@ export const TOOL_REGISTRY: ToolMetadata[] = [ parameters: "names?: string[], max_objects?: number, max_vertices_sample?: number, max_influences?: number, weight_sum_tolerance?: number", }, + { + name: "inspect_pose_deformation", + description: + "Non-mutating stress pose inspection for evaluated rigged meshes. Reports sampled displacement, invalid coordinates, topology changes, and symmetric stretch/compression outliers while restoring the artist's current frame and subframe.", + category: "inspection", + parameters: + "names?: string[] (max 20), baseline_frame?: number, stress_frames?: number[] (max 24), max_objects?: number, max_vertices_sample?: number, max_edges_sample?: number, edge_ratio_threshold?: number, displacement_epsilon?: number", + }, { name: "normalize_vertex_group_weights", description: @@ -839,6 +893,14 @@ export const TOOL_REGISTRY: ToolMetadata[] = [ parameters: "names?: string[], include_materials?: boolean, include_actions?: boolean, max_keyframes_per_curve?: number, max_objects?: number, max_materials?: number, max_actions?: number", }, + { + name: "retarget_animation_clip", + description: + "Import a BVH or FBX motion clip, retarget an explicit source-to-target bone map through temporary constraints, bake a new target action, clean imported data, and export explicit objects as a GLB candidate with rollback on failure.", + category: "advanced", + parameters: + "filepath: string, target_armature: string, bone_map: Record, action_name: string, export_names: string[], output_filepath: string (.glb), source_armature?: string, root_motion?: IN_PLACE|PRESERVE, root_source_bone?: string, frame_start?: number, frame_end?: number, bake_step?: number, cleanup_imported?: boolean, overwrite?: boolean", + }, { name: "inspect_shape_keys", description: diff --git a/lib/portable-mcp/agent-context.ts b/lib/portable-mcp/agent-context.ts index f6f2fc4..76f0af5 100644 --- a/lib/portable-mcp/agent-context.ts +++ b/lib/portable-mcp/agent-context.ts @@ -6,12 +6,12 @@ You are operating Blender through the ViperMesh portable MCP gateway. 2. Inspect current scene state before mutation. Prefer get_scene_info or get_all_object_info in a single call. 3. Search search_3d_guidance for task-specific Blender and ViperMesh guidance before unfamiliar or high-risk work. 4. Discover direct tools with list_blender_tools. Prefer deterministic structured tools where they cover the operation. -5. For ordinary scene construction, prefer run_blender_scene_stage with separate build, inspect_preview, and finalize calls. Review the preview between stages. Use call_blender_tool_batch for other bounded, already-decided sequences. +5. Use run_blender_scene_stage when its optional build, inspect_preview, and finalize helpers fit the task. They can configure cameras and lighting: use standalone tools to preserve an artist's existing presentation. Batch only already-decided actions; inspect when the next decision depends on a result. 6. Keep execute_code available for genuinely custom geometry, procedural effects, unusual node setups, and other operations not covered by direct tools. Keep fallback scripts scoped to one logical cluster. -7. For reference reconstruction, use one blockout preview, one intended-camera assembly preview, then final acceptance. At each preview compare required-object presence, relative scale, orientation, support, occlusion, framing, and lighting; avoid redundant screenshots and full-scene dumps. -8. Verify contact, support, orientation, and clearance with inspect_scene_grounding and inspect_spatial_relations. Do not accept floating or interpenetrating props merely because one camera angle hides them. +7. For reference reconstruction, compare required-object presence, relative scale, orientation, support, occlusion, framing, and lighting. Choose useful previews according to uncertainty and task complexity; avoid fixed preview counts, redundant screenshots and full-scene dumps. +8. Verify consequential contact, support, orientation, and clearance with inspect_scene_grounding and explicit inspect_spatial_relations constraints. Bounds-based checks are diagnostic, not proof of exact mesh contact. Inspect suspicious contact and collisions from another angle; respect intentional suspension, overlaps or stylization. 9. Perform visual checks after major changes and before acceptance. Use a viewport capture when a usable 3D viewport exists; use a camera thumbnail or render artifact for autonomous/headless sessions. -10. Save the .blend and final visual artifact only after structural and visual validation pass. Standalone tools remain available for targeted repair between workflow stages. +10. Tool success and image-file health do not prove visual quality. Open and inspect the actual requested output; repair concrete defects and recheck affected relationships. Report unresolved failures instead of declaring them correct. Save a .blend only when requested, to an approved path; inspection/render/export-only tasks need no blend save. 11. Import multi-object local assets with one managed bottom-center root and placement transform in import_local_asset; request compact output instead of manually creating an Empty, parenting every child, and transforming children one by one. ` diff --git a/lib/portable-mcp/guidance.ts b/lib/portable-mcp/guidance.ts index 259d0f5..8ddbd8a 100644 --- a/lib/portable-mcp/guidance.ts +++ b/lib/portable-mcp/guidance.ts @@ -3,12 +3,37 @@ import path from "node:path" import { fileURLToPath } from "node:url" const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url)) -const TOOL_GUIDES_DIR = [ - path.resolve(MODULE_DIR, "..", "..", "data", "tool-guides"), - path.resolve(MODULE_DIR, "..", "data", "tool-guides"), - path.resolve(process.cwd(), "data", "tool-guides"), +const TOOL_SKILLS_DIR = [ + path.resolve( + MODULE_DIR, + "..", + "..", + "skills", + "using-vipermesh-blender", + "references" + ), + path.resolve( + MODULE_DIR, + "..", + "skills", + "using-vipermesh-blender", + "references" + ), + path.resolve( + process.cwd(), + "skills", + "using-vipermesh-blender", + "references" + ), ].find((candidate) => fs.existsSync(candidate)) ?? - path.resolve(MODULE_DIR, "..", "..", "data", "tool-guides") + path.resolve( + MODULE_DIR, + "..", + "..", + "skills", + "using-vipermesh-blender", + "references" + ) const MAX_GUIDANCE_DOCUMENT_BYTES = 64 * 1024 const DEFAULT_GUIDANCE_LIMIT = 5 const MAX_GUIDANCE_LIMIT = 20 @@ -74,10 +99,10 @@ function validateGuideFilename(filename: string) { function guidePath(filename: string) { validateGuideFilename(filename) - const resolved = path.resolve(TOOL_GUIDES_DIR, filename) - const relative = path.relative(TOOL_GUIDES_DIR, resolved) + const resolved = path.resolve(TOOL_SKILLS_DIR, filename) + const relative = path.relative(TOOL_SKILLS_DIR, resolved) if (relative.startsWith("..") || path.isAbsolute(relative)) { - throw new Error("Guidance filename resolves outside the tool-guide directory") + throw new Error("Guidance filename resolves outside the tool-skill directory") } return resolved } @@ -131,7 +156,7 @@ function searchLocalGuides(query: string, limit: number): GuidanceSearchItem[] { if (terms.length === 0) return [] return fs - .readdirSync(TOOL_GUIDES_DIR) + .readdirSync(TOOL_SKILLS_DIR) .filter((filename) => filename.endsWith(".md")) .map((filename) => { const content = fs.readFileSync(guidePath(filename), "utf8") @@ -149,7 +174,7 @@ function searchLocalGuides(query: string, limit: number): GuidanceSearchItem[] { id: `local:${filename}`, title: titleFromContent(filename, content), content: excerpt(content), - source: "tool-guides", + source: "tool-skills", metadata: { filename, retrieval: "local" }, })) } diff --git a/lib/portable-mcp/onboarding.ts b/lib/portable-mcp/onboarding.ts index 1204205..8ea05fa 100644 --- a/lib/portable-mcp/onboarding.ts +++ b/lib/portable-mcp/onboarding.ts @@ -7,7 +7,7 @@ export const VIPERMESH_MCP_INSTRUCTIONS = `ViperMesh for Blender uses one persis Do not launch npm, npx, tsx, or another MCP subprocess for individual Blender operations. Call bootstrap_vipermesh_session once before the first Blender mutation. Inspect the scene before editing, prefer deterministic Blender tools, group only already-decided operations, and retain execute_code for genuinely custom work. -Use a build, inspect/repair, and finalize workflow. Validate support, orientation, clearance, framing, and the final visual artifact before accepting a scene.` +Choose task-appropriate inspection and repair points. Validate requested relationships and inspect the actual output before accepting it; file-health checks do not judge appearance. Save only requested artifacts to approved paths.` export const VIPERMESH_CONNECTOR_MANUAL = `# ViperMesh for Blender connector manual @@ -36,8 +36,13 @@ geometry, procedural effects, unusual node graphs, and other uncovered work. ## Acceptance -Before saving, inspect required-object presence, relative scale, orientation, +Before completion, inspect required-object presence, relative scale, orientation, support, interpenetration, clearance, occlusion, camera framing, and lighting. Do not accept an object merely because one camera angle hides a structural -problem. Review the final rendered artifact before declaring completion. +problem. Open the requested artifact before declaring completion. Image-file +health checks do not establish appearance or correctness. Report unresolved +defects explicitly; do not assume a successful tool call met the user's goal. +Save a blend file only when requested and to an approved path. Rendering or +inspection does not require saving. Staged helpers may change camera/lighting; +use preservePresentation on finalize, or standalone tools, to retain them. ` diff --git a/lib/portable-mcp/preview.ts b/lib/portable-mcp/preview.ts new file mode 100644 index 0000000..28fd23a --- /dev/null +++ b/lib/portable-mcp/preview.ts @@ -0,0 +1,25 @@ +import { open } from "node:fs/promises" + +const MAX_IMAGE_BYTES = 3 * 1024 * 1024 + +export async function readPreviewImage(filepath: string) { + let file + try { + file = await open(filepath, "r") + const stats = await file.stat() + if (!stats.isFile() || stats.size === 0 || stats.size > MAX_IMAGE_BYTES) return undefined + const buffer = Buffer.alloc(stats.size) + const { bytesRead } = await file.read(buffer, 0, buffer.length, 0) + if (bytesRead !== buffer.length) return undefined + const mimeType = buffer.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])) + ? "image/png" + : buffer[0] === 255 && buffer[1] === 216 && buffer[2] === 255 + ? "image/jpeg" : undefined + if (!mimeType) return undefined + return { type: "image" as const, mimeType, data: buffer.toString("base64") } + } catch { + return undefined + } finally { + await file?.close() + } +} diff --git a/lib/portable-mcp/server.ts b/lib/portable-mcp/server.ts index 584fe70..af9db3b 100644 --- a/lib/portable-mcp/server.ts +++ b/lib/portable-mcp/server.ts @@ -2,6 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" import { z } from "zod" import type { PortableMcpService } from "./service" +import { readPreviewImage } from "./preview" import { VIPERMESH_CONNECTOR_MANUAL, VIPERMESH_CONNECTOR_MANUAL_URI, @@ -48,7 +49,7 @@ function toolError(error: unknown) { export function createPortableBlenderMcpServer(service: PortableMcpService) { const server = new McpServer({ name: "vipermesh-blender", - version: "1.2.0", + version: "1.3.0", }, { instructions: VIPERMESH_MCP_INSTRUCTIONS, }) @@ -155,7 +156,11 @@ export function createPortableBlenderMcpServer(service: PortableMcpService) { async ({ name, params }) => { try { const result = await service.callBlenderTool(name, params) - return structuredResult(result as Record) + const imagePath = name === "inspect_render_artifact" ? params.image_path + : name === "render_image" || name === "render_thumbnail_to_path" ? params.output_path : undefined + const preview = result.status === "success" && typeof imagePath === "string" ? await readPreviewImage(imagePath) : undefined + const response = structuredResult({ ...result, ...(imagePath ? { visualReviewRequired: true, imageAttached: !!preview } : {}) }) + return { ...response, content: [...response.content, ...(preview ? [preview] : [])] } } catch (error) { return toolError(error) } @@ -202,6 +207,7 @@ export function createPortableBlenderMcpServer(service: PortableMcpService) { previewPath: z.string().trim().min(1).max(2_048).optional(), renderPath: z.string().trim().min(1).max(2_048).optional(), blendPath: z.string().trim().min(1).max(2_048).optional(), + preservePresentation: z.boolean().optional(), preset: z.enum(["studio", "product", "indoor", "exterior", "night"]).optional(), cameraName: z.string().trim().min(1).max(256).optional(), resolutionX: z.number().int().min(64).max(8_192).optional(), @@ -212,7 +218,12 @@ export function createPortableBlenderMcpServer(service: PortableMcpService) { }, async (input) => { try { - return structuredResult(await service.runBlenderSceneStage(input)) + const result = await service.runBlenderSceneStage(input) + const imagePath = input.stage === "inspect_preview" ? input.previewPath : input.stage === "finalize" ? input.renderPath : undefined + const inspected = result.results.some((item) => item.name === "inspect_render_artifact" && item.success === true) + const preview = imagePath && inspected ? await readPreviewImage(imagePath) : undefined + const response = structuredResult({ ...result, visualReviewRequired: !!imagePath, imageAttached: !!preview }) + return { ...response, content: [...response.content, ...(preview ? [preview] : [])] } } catch (error) { return toolError(error) } @@ -241,7 +252,7 @@ export function createPortableBlenderMcpServer(service: PortableMcpService) { { title: "Search 3D Guidance", description: - "Search checked-in ViperMesh 3D tool guides and optionally the configured semantic guidance store.", + "Search checked-in ViperMesh 3D tool skills and optionally the configured semantic guidance store.", inputSchema: z.object({ query: z.string().trim().min(1).max(2_000), limit: z.number().int().min(1).max(20).optional(), @@ -263,7 +274,7 @@ export function createPortableBlenderMcpServer(service: PortableMcpService) { { title: "Get 3D Guidance Document", description: - "Read one approved Markdown document from the checked-in ViperMesh tool-guide directory.", + "Read one approved Markdown reference from the checked-in ViperMesh agent skill.", inputSchema: z.object({ filename: z.string().trim().min(1).max(255), }), diff --git a/lib/portable-mcp/service.ts b/lib/portable-mcp/service.ts index 9e9c64f..4569575 100644 --- a/lib/portable-mcp/service.ts +++ b/lib/portable-mcp/service.ts @@ -37,6 +37,9 @@ export interface PortableMcpServiceDependencies { source?: string similarity?: number }>> + authorizeCapabilityInvocation?: ( + params: Record + ) => Promise> } export interface ListBlenderToolsInput { @@ -62,6 +65,7 @@ export interface BlenderSceneWorkflowInput { previewPath?: string renderPath?: string blendPath?: string + preservePresentation?: boolean preset?: "studio" | "product" | "indoor" | "exterior" | "night" cameraName?: string resolutionX?: number @@ -112,6 +116,13 @@ export function createPortableMcpService( async function executeCommand(name: string, params: Record) { validateCommand(name, params) + if ( + name === "invoke_addon_capability" && + params.dry_run === false && + dependencies.authorizeCapabilityInvocation + ) { + params = await dependencies.authorizeCapabilityInvocation(params) + } try { return await client().execute({ type: name, params }) } catch (error) { @@ -252,9 +263,9 @@ export function createPortableMcpService( } else { if (!input.targetNames?.length) throw new Error("The finalize stage requires targetNames") if (!input.renderPath) throw new Error("The finalize stage requires renderPath") - if (!input.blendPath) throw new Error("The finalize stage requires blendPath") const cameraName = input.cameraName ?? "ViperMesh_Workflow_Camera" commands = [ + ...(!input.preservePresentation ? [ { name: "setup_studio_scene", params: { @@ -277,10 +288,15 @@ export function createPortableMcpService( set_dof_focus: false, }, }, + ] : []), { name: "validate_studio_scene", - params: { target_names: input.targetNames, camera_name: cameraName }, + params: { target_names: input.targetNames, require_lights: !input.preservePresentation, ...(input.cameraName || !input.preservePresentation ? { camera_name: cameraName } : {}) }, }, + ...(input.spatialRelations?.length ? [{ + name: "inspect_spatial_relations", + params: { relations: input.spatialRelations }, + }] : []), ] } @@ -292,7 +308,7 @@ export function createPortableMcpService( commands: [ { name: "render_image", params: { output_path: input.renderPath!, file_format: "PNG" } }, { name: "inspect_render_artifact", params: { image_path: input.renderPath! } }, - { name: "save_blend_file", params: { filepath: input.blendPath!, make_dirs: true, check_existing: false } }, + ...(input.blendPath ? [{ name: "save_blend_file", params: { filepath: input.blendPath, make_dirs: true, check_existing: true } }] : []), ], stopOnError: true, }) @@ -300,7 +316,7 @@ export function createPortableMcpService( } const ready = workflowReady(results) - const expectedCount = input.stage === "finalize" ? 6 : commands.length + const expectedCount = input.stage === "finalize" ? commands.length + 2 + (input.blendPath ? 1 : 0) : commands.length return { stage: input.stage, requested: expectedCount, @@ -364,11 +380,17 @@ function compactResponse(name: string, response: McpResponse) { "checked_count", "pass_count", "fail_count", "floating_count", "high_priority_floating_count", "low_priority_floating_count", "below_ground_count", "part_count", "object_count", "deleted_count", - "softened_count", "missing_targets", "warnings", "errors", + "softened_count", "missing_targets", "warnings", "errors", "camera", ] for (const key of keys) { if (key in result) summary[key] = result[key] } + if (Array.isArray(result.relations)) { + summary.failed_relations = result.relations.filter((relation) => relation && typeof relation === "object" && relation.passed === false).slice(0, 16) + } + for (const key of ["high_priority_floating_objects", "below_ground_objects"]) { + if (Array.isArray(result[key])) summary[key] = result[key].slice(0, 16) + } return summary } @@ -380,6 +402,8 @@ function workflowReady(results: Array<{ name: string; response: McpResponse }>) if ("ready" in result && result.ready === false) return false if ("high_priority_floating_count" in result && Number(result.high_priority_floating_count) > 0) return false if ("below_ground_count" in result && Number(result.below_ground_count) > 0) return false + if ("fail_count" in result && Number(result.fail_count) > 0) return false + if ("camera" in result && result.camera && typeof result.camera === "object" && "active" in result.camera && result.camera.active === false) return false return true }) } diff --git a/llms.txt b/llms.txt index 0903d37..78ab10a 100644 --- a/llms.txt +++ b/llms.txt @@ -2,8 +2,8 @@ > ViperMesh for Blender is an open-source local Blender addon and persistent > Model Context Protocol server for AI agents. It exposes deterministic 3D -> scene operations, structured inspection, bounded batching, local task -> guidance, and an explicit custom Python fallback. +> scene operations, structured inspection, bounded batching, portable tool +> skills, and an explicit custom Python fallback. Canonical repository: https://github.com/Ker102/vipermesh-blender License: MIT @@ -14,6 +14,8 @@ Transport: MCP over stdio, then a loopback TCP bridge to Blender - README: https://github.com/Ker102/vipermesh-blender#readme - Connector manual: https://github.com/Ker102/vipermesh-blender/blob/main/docs/portable-blender-mcp.md +- MCP client setup: https://github.com/Ker102/vipermesh-blender/blob/main/docs/client-setup.md +- Agent skill: https://github.com/Ker102/vipermesh-blender/blob/main/skills/using-vipermesh-blender/SKILL.md - Security boundary: https://github.com/Ker102/vipermesh-blender/blob/main/SECURITY.md - Releases: https://github.com/Ker102/vipermesh-blender/releases @@ -24,5 +26,6 @@ Transport: MCP over stdio, then a loopback TCP bridge to Blender - Agents should call bootstrap_vipermesh_session first. - Deterministic tools are preferred when they cover the task. - execute_code remains available for genuinely custom Blender work. +- Public tool skills provide adaptable patterns, not mandatory scene recipes. - The public connector does not include ViperMesh authentication, billing, private prompts, private RAG data, cloud model routing, or asset catalogs. diff --git a/package-lock.json b/package-lock.json index be73f3a..02a003a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "vipermesh-blender-mcp", - "version": "1.2.0", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vipermesh-blender-mcp", - "version": "1.2.0", + "version": "1.3.0", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/sdk": "^1.32.1", "zod": "^3.23.8" }, "bin": { @@ -519,12 +519,12 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", - "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "version": "1.32.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.32.1.tgz", + "integrity": "sha512-2DdE+SJDtzLEEWzY1ZjY7Q+VcPhcV1KisD3zI4u0XZyktsjHum1mwbMI+JaulUBi2OZk+KJAi2uPXzxichPkdw==", "license": "MIT", "dependencies": { - "@hono/node-server": "^1.19.9", + "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", @@ -1440,9 +1440,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", - "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", "funding": [ { "type": "github", @@ -1622,9 +1622,9 @@ } }, "node_modules/hono": { - "version": "4.12.32", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.32.tgz", - "integrity": "sha512-XcuyW9qE2kJn07PkecMOBd5Vq/hMy7mmGw+idz1yblbg9N17ijJODrvPkn7/dwL3Kulj8LcRJ69DLOWf91dRUg==", + "version": "4.13.13", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.13.tgz", + "integrity": "sha512-CQ46U0ZkAGmbT/4UxdzzGJpacP2IeKgY4a5/tOI9AABbpOMfK739wfDXmv1usCk+3RkKj1hQy4/fjhiwa2xlrA==", "license": "MIT", "peer": true, "engines": { @@ -1674,9 +1674,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.3.1.tgz", - "integrity": "sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==", + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.3.tgz", + "integrity": "sha512-A1kdq/tSb5QjvKvAMgIoEvDBIgL7qaqVP/jkvSwYYRZ9iEzvPpopxp2wQfu3SuZRHtpHNxMn8Fs0bS+gf5Xmwg==", "license": "MIT", "engines": { "node": ">= 12" @@ -2046,9 +2046,9 @@ } }, "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", "license": "MIT", "dependencies": { "forwarded": "0.2.0", @@ -2056,12 +2056,16 @@ }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/qs": { - "version": "6.15.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", - "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", "license": "BSD-3-Clause", "dependencies": { "es-define-property": "^1.0.1", diff --git a/package.json b/package.json index d1dd072..3961191 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "vipermesh-blender-mcp", - "version": "1.2.0", + "version": "1.3.0", "description": "Persistent Model Context Protocol server and deterministic Blender addon for AI agents", "license": "MIT", "private": false, "type": "module", "author": "ViperMesh contributors", - "homepage": "https://github.com/Ker102/vipermesh-blender#readme", + "homepage": "https://ker102.github.io/vipermesh-blender/", "repository": { "type": "git", "url": "git+https://github.com/Ker102/vipermesh-blender.git" @@ -26,7 +26,7 @@ "bin": { "vipermesh-blender": "./dist/public-portable-blender-mcp.js" }, - "files": ["addon", "data", "dist", "docs", "README.md", "LICENSE"], + "files": ["addon", "dist", "docs", "skills", "README.md", "LICENSE"], "scripts": { "mcp": "tsx scripts/public-portable-blender-mcp.ts", "build": "tsup scripts/public-portable-blender-mcp.ts --format esm --platform node --out-dir dist --clean", @@ -37,7 +37,7 @@ "prepublishOnly": "npm run check" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/sdk": "^1.32.1", "zod": "^3.23.8" }, "devDependencies": { diff --git a/scripts/validate-package.mjs b/scripts/validate-package.mjs index c5ff037..7a7f315 100644 --- a/scripts/validate-package.mjs +++ b/scripts/validate-package.mjs @@ -8,7 +8,11 @@ assert.ok(fs.existsSync(cliPath), `Missing built CLI: ${cliPath}`) const cli = fs.readFileSync(cliPath, "utf8") assert.ok(cli.startsWith("#!/usr/bin/env node"), "Built CLI must retain its Node shebang") -assert.ok(fs.existsSync("data/tool-guides/spatial-positioning-guide.md")) +assert.ok(fs.existsSync("skills/using-vipermesh-blender/SKILL.md")) +for (const reference of ["scene-operations", "spatial-validation", "visual-presentation", "geometry-materials-assets", "character-animation-export"]) { + assert.ok(fs.existsSync(`skills/using-vipermesh-blender/references/${reference}.md`), `Missing shipped reference: ${reference}`) +} +assert.ok(!fs.existsSync("data/tool-guides")) assert.ok(fs.existsSync("addon/vipermesh-addon.py")) assert.ok(fs.existsSync("NOTICE.md")) assert.ok(fs.existsSync("SECURITY.md")) diff --git a/site/asset-preview.png b/site/asset-preview.png new file mode 100644 index 0000000..27d8093 Binary files /dev/null and b/site/asset-preview.png differ diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..77cde62 --- /dev/null +++ b/site/index.html @@ -0,0 +1,43 @@ + + + + + ViperMesh for Blender | Open-source Blender MCP + + + + + + + + + + + + + +
ViperMesh / Blender
+
+
+ A stylized spacecraft asset +
+

Open source / MIT licensed / Local

ViperMesh
for Blender

Give your agent
the tools to work in 3D.

Connect an MCP-compatible agent to Blender. Inspect your scene, make structured edits and check the result through one persistent session.

+
+

An agent connection for your existing workspace

Structured operations

Assembly, transforms, materials, cameras, lighting, rendering, UVs, retopology, rigging and export. Discover relevant tools as the task develops.

Inspect, then improve

Check support, orientation and clearances. Review visual results and repair specific problems. Diagnostic checks support judgment; they cannot certify every scene.

Room for custom work

Keep Python execution for unusual geometry and uncovered operations. Batch known actions and keep decision points where new observations matter.

+

One session. One local connection.

Your MCP clientViperMesh serverBlender addon

The client launches the Node server once. The server keeps a serialized connection to the addon. No ViperMesh cloud account or Docker is needed for the local connector.

+

From download to first edit

Connect your agent.

Use the Blender version currently tested in the repository and a Node runtime that meets the package requirements. Keep Blender open with the local bridge running.

Client-specific setup and troubleshooting ↗
  1. Install the addon

    Download the versioned addon .py or .zip from the latest release. In Blender, install it from disk, enable it, and start the local bridge in the ViperMesh sidebar.

  2. Build the MCP server

    git clone https://github.com/Ker102/vipermesh-blender.git
    +cd vipermesh-blender
    +npm install
    +npm run build
  3. Add it to your MCP client

    Configure the client to launch node with the absolute path to dist/public-portable-blender-mcp.js. Start one server per session, not one per tool call.

    {
    +  "mcpServers": {
    +    "vipermesh-blender": {
    +      "command": "node",
    +      "args": ["/absolute/path/to/vipermesh-blender/dist/public-portable-blender-mcp.js"]
    +    }
    +  }
    +}

    JSON configuration above is a common pattern; your client's syntax may differ.

  4. Inspect before editing

    Ask your agent to call bootstrap_vipermesh_session, inspect the existing scene, then discover tools for your task. The repository includes a portable agent skill with adaptable operating guidance.

+

What to expect

Is it a 3D generation model?

The connector lets an agent operate Blender. It does not bundle a hosted text-to-3D model or promise production-ready assets. You can use local assets and your own Blender workflows.

Is it faster than UI automation?

Structured tools are designed to avoid repetitive navigation and generated Blender code. Performance depends on the model and task; we are preparing reproducible comparisons and make no universal speed or cost claim here.

Does it judge the final scene automatically?

Tools provide diagnostics and image-file checks. The agent still needs to inspect visual output and judge it against your request. A passing report can miss mesh-level collisions or an artistic mismatch.

What can an agent access?

This is a trusted local connector. Python fallback can execute arbitrary code inside Blender. Keep the bridge on loopback, connect trusted clients and review destructive actions. Read the repository security guidance before use.

How does this relate to ViperMesh Studio?

The public connector is usable independently. ViperMesh Studio is a broader AI workspace in development for artists and game creators, connecting generation, inspection and specialist workflows.

Join the Studio waitlist ↗
+

Start with your scene.

Try one useful edit. Inspect the result. Tell us where the connection helps and where it falls short.

Share feedback ↗
+
+ + diff --git a/site/robots.txt b/site/robots.txt new file mode 100644 index 0000000..e5b8291 --- /dev/null +++ b/site/robots.txt @@ -0,0 +1,3 @@ +User-agent: * +Allow: / +Sitemap: https://ker102.github.io/vipermesh-blender/sitemap.xml diff --git a/site/site.js b/site/site.js new file mode 100644 index 0000000..97291e4 --- /dev/null +++ b/site/site.js @@ -0,0 +1,12 @@ +for (const button of document.querySelectorAll("[data-copy]")) { + button.addEventListener("click", async () => { + const code = document.getElementById(button.dataset.copy) + const status = button.nextElementSibling + try { + await navigator.clipboard.writeText(code.textContent) + status.textContent = "Copied" + } catch { + status.textContent = "Select the commands above to copy." + } + }) +} diff --git a/site/sitemap.xml b/site/sitemap.xml new file mode 100644 index 0000000..661ce6e --- /dev/null +++ b/site/sitemap.xml @@ -0,0 +1,2 @@ + +https://ker102.github.io/vipermesh-blender/ diff --git a/site/styles.css b/site/styles.css new file mode 100644 index 0000000..703eb1a --- /dev/null +++ b/site/styles.css @@ -0,0 +1,54 @@ +:root { color-scheme: light; --ink:#172924; --muted:#52655d; --accent:#0a765a; } +* { box-sizing:border-box; } +html { scroll-behavior:smooth; } +body { margin:0; color:var(--ink); background:white; font:16px/1.6 Arial,Helvetica,sans-serif; letter-spacing:0; } +a { color:inherit; text-decoration:none; } a:hover { text-decoration:underline; text-underline-offset:5px; } +:focus-visible { outline:3px solid #e58b46; outline-offset:4px; } +.wrap { max-width:1208px; padding:0 36px; margin:auto; } +header { max-width:1280px; margin:auto; min-height:80px; padding:20px 36px; display:flex; justify-content:space-between; align-items:center; gap:20px; } +.brand { font-size:22px; font-weight:700; } .brand span { font-weight:400; color:var(--muted); } +nav, footer > div { display:flex; gap:24px; font-size:14px; } +.hero { position:relative; overflow:hidden; isolation:isolate; min-height:580px; background:#14231e; color:white; } +.hero > img { position:absolute; height:100%; width:100%; object-fit:contain; object-position:85% center; opacity:.9; } +.shade { position:absolute; inset:0; background:linear-gradient(90deg,#14231e 0%,rgba(20,35,30,.95) 27%,rgba(20,35,30,.55) 52%,rgba(20,35,30,0) 80%); } +.hero-content { position:relative; max-width:1208px; margin:auto; padding:52px 36px 60px; } +.eyebrow { font-size:12px; text-transform:uppercase; font-weight:700; color:var(--accent); margin:0 0 24px; } +.hero .eyebrow { color:#c4dacb; } +h1 { font-size:58px; line-height:1.05; margin:0 0 22px; } +.lead { font-size:30px; line-height:1.2; margin:0 0 20px; } +.hero-content > p:not(.eyebrow):not(.lead) { max-width:420px; color:#d9e5de; font-size:15px; } +.actions { display:flex; align-items:center; gap:24px; margin-top:30px; font-size:14px; } +.primary { padding:12px 20px; background:#d7f0e2; color:#163a2b; border-radius:4px; font-weight:700; } +.overview { padding-top:40px; padding-bottom:64px; } +.columns { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:40px; } +.columns > div { border-top:1px solid #cbdace; padding-top:20px; } +h2 { font-size:25px; line-height:1.2; margin:0 0 16px; } .columns h2 { font-size:20px; } +p { margin:0 0 16px; } .columns p, .setup p, .scope p { font-size:14px; color:var(--muted); } +.connection { background:#edf5f0; padding:44px 0; } +.flow { display:flex; align-items:center; gap:24px; font-size:23px; margin-bottom:24px; font-weight:700; } +.flow b { color:var(--accent); font-weight:400; } +.connection p:last-child { max-width:780px; color:var(--muted); font-size:14px; } +.setup { display:grid; grid-template-columns:minmax(0, .8fr) minmax(0,1.2fr); gap:80px; padding-top:64px; padding-bottom:64px; scroll-margin-top:24px; } +.setup > div > h2 { font-size:36px; } .setup a { color:var(--accent); font-size:14px; text-decoration:underline; } +ol { margin:0; padding-left:24px; } li { padding:0 0 24px 8px; } li::marker { color:var(--accent); font-weight:700; } +.setup > * { min-width:0; } +h3 { font-size:18px; line-height:1.3; margin:0 0 12px; } +pre { background:#f2f5f3; padding:18px; border-left:3px solid #82b79a; overflow:auto; font-size:12px; line-height:1.8; } +code { overflow-wrap:anywhere; font-family:Consolas,monospace; } pre code { overflow-wrap:normal; } +button { background:transparent; border:1px solid #a3baab; border-radius:4px; color:var(--accent); padding:8px 12px; cursor:pointer; font:600 12px Arial,sans-serif; } +.copy-status { font-size:12px; margin-left:12px; } +.scope { padding-top:24px; padding-bottom:60px; } +details { border-top:1px solid #d4dfd7; padding:20px 0; } summary { cursor:pointer; font-size:18px; font-weight:600; } details p { margin:16px 0 0; max-width:850px; } details a { color:var(--accent); } +.closing { padding:52px 0; background:#173a2d; color:white; } .closing h2 { font-size:32px; } .closing p { color:#c9dbd1; max-width:650px; font-size:15px; } .closing a { text-decoration:underline; color:#d8f0e4; } +footer { padding-top:28px !important; padding-bottom:28px !important; display:flex; gap:24px; justify-content:space-between; font-size:12px; } footer p { margin:0; font-weight:700; } footer span { color:var(--muted); font-weight:400; } footer > div { font-size:12px; flex-wrap:wrap; } +.skip { position:absolute; top:-100px; left:20px; z-index:10; background:white; padding:12px; } .skip:focus { top:12px; } +@media (min-width:1600px) { .hero > img { object-position:68% center; } } +@media (max-width:800px) { + header { padding:20px; min-height:68px; } .brand { font-size:18px; } nav { font-size:12px; gap:16px; } + .hero { min-height:550px; } .hero > img { object-fit:cover; object-position:65% center; opacity:.5; } .shade { background:rgba(15,32,25,.65); } + .hero-content { padding:44px 24px 56px; } h1 { font-size:42px; } .lead { font-size:27px; } + .wrap { padding-left:24px; padding-right:24px; } .columns, .setup { grid-template-columns:minmax(0,1fr); gap:28px; } + .overview { padding-top:32px; padding-bottom:40px; } .flow { flex-wrap:wrap; gap:12px; font-size:16px; } + .setup { padding-top:44px; padding-bottom:24px; } .actions { gap:18px; font-size:12px; } footer { flex-direction:column; } +} +@media (prefers-reduced-motion:reduce) { html { scroll-behavior:auto; } } diff --git a/skills/using-vipermesh-blender/SKILL.md b/skills/using-vipermesh-blender/SKILL.md new file mode 100644 index 0000000..b6ab425 --- /dev/null +++ b/skills/using-vipermesh-blender/SKILL.md @@ -0,0 +1,44 @@ +--- +name: using-vipermesh-blender +description: Operates Blender through the persistent ViperMesh MCP connector. Use for scene inspection, structured Blender edits, validation, rendering, rigging, animation, retopology, assets, and export. +--- + +# Using ViperMesh For Blender + +Use one MCP server process for the complete agent session. The MCP client should +start the server; do not launch `npm`, `npx`, `tsx`, or another server process +for individual Blender operations. + +## Start A Session + +1. Call `bootstrap_vipermesh_session`. +2. Inspect existing scene state before changing it. +3. Use `search_3d_guidance` when task semantics or an unfamiliar operation need clarification. +4. Use `list_blender_tools` with a relevant category or search term rather than + loading the complete registry. + +Prefer deterministic tools when they express the intended operation. Keep +`execute_code` available for custom geometry, procedural effects, unusual node +graphs, and other work that the structured tools do not cover well. + +Batch operations only when their inputs are already known and no intermediate +result changes the next decision. Preserve inspection and repair points between +meaningful stages. + +Before completion, inspect the requested output and validate the relationships +that matter to this task. A successful tool call or healthy image file is not a +quality verdict. Report unresolved defects. Save only requested artifacts to +approved paths; choose standalone tools when a stage would replace an artist's +camera, lighting, or framing. + +## References + +- Scene mutation and lifecycle: [references/scene-operations.md](references/scene-operations.md) +- Contact, orientation, and clearance: [references/spatial-validation.md](references/spatial-validation.md) +- Camera, lighting, previews, and acceptance: [references/visual-presentation.md](references/visual-presentation.md) +- Geometry, materials, and assets: [references/geometry-materials-assets.md](references/geometry-materials-assets.md) +- Characters, animation, and export: [references/character-animation-export.md](references/character-animation-export.md) + +These references provide useful patterns and checks, not mandatory recipes. +Adapt them to the user's goal, the current scene, the active render engine, and +the evidence available. diff --git a/skills/using-vipermesh-blender/references/character-animation-export.md b/skills/using-vipermesh-blender/references/character-animation-export.md new file mode 100644 index 0000000..ce1f5e8 --- /dev/null +++ b/skills/using-vipermesh-blender/references/character-animation-export.md @@ -0,0 +1,36 @@ +# Characters, Animation, And Export + +## Character Work + +Treat retopology, UVs, rig generation, binding, weight cleanup, and animation +readiness as separate concerns with explicit checks between them. A successful +operator call does not prove deformation quality or production readiness. + +Select decimation, voxel remesh, QuadriFlow, or custom topology work according +to the source mesh and target use. Preserve a source revision before destructive +topology changes. + +For rigging, verify scale, transforms, mesh integrity, armature alignment, +deform groups, weight normalization, and representative deformations. Automated +weights are a starting point whose adequacy depends on the mesh and motion. + +## Animation + +Inspect frame range, actions, constraints, drivers, root motion, and target rig +compatibility before editing. Retargeting and baking can be lossy, so keep a +recoverable source and validate representative poses or motion segments. + +## Export + +Choose format and options from the destination pipeline rather than a universal +preset. Before export, inspect: + +- intended object and collection inclusion +- transforms and scale +- topology and normals +- UVs, materials, and texture dependencies +- armature, weights, actions, and animation range +- modifiers or constraints that must be applied or preserved + +Validate the exported artifact when possible. A saved file is not sufficient +evidence that another application can consume it correctly. diff --git a/skills/using-vipermesh-blender/references/geometry-materials-assets.md b/skills/using-vipermesh-blender/references/geometry-materials-assets.md new file mode 100644 index 0000000..8eed7ec --- /dev/null +++ b/skills/using-vipermesh-blender/references/geometry-materials-assets.md @@ -0,0 +1,35 @@ +# Geometry, Materials, And Assets + +## Geometry Strategy + +Select an approach based on the requested form and downstream use: + +- primitives and assembly tools for blockouts and hard-surface structures +- curves for paths, cables, rails, and profile-driven forms +- modifiers for reversible repetition, smoothing, thickness, and deformation +- retopology tools for density reduction or topology conversion +- `execute_code` for custom procedural geometry that lacks a suitable direct + operation + +Avoid treating one method as universally superior. Preserve source geometry +when a destructive conversion would make iteration harder. + +## Materials + +Use structured material tools for common Principled BSDF and texture workflows. +Check texture paths, color space, UV dependence, and render-engine behavior when +the result matters beyond a preview. + +Material values should respond to the intended substance, scale, lighting, and +art direction. Defaults and physical ranges are useful references, not a reason +to override deliberate stylization. + +## Assets + +Search reusable assets before manually approximating complex objects whose +identity depends on detailed geometry. Import multi-object assets through a +managed root when possible, then inspect bounds, scale, orientation, and support +in the destination scene. + +An asset match is a candidate, not automatic acceptance. Verify that its +license, style, topology, materials, and functional direction fit the task. diff --git a/skills/using-vipermesh-blender/references/scene-operations.md b/skills/using-vipermesh-blender/references/scene-operations.md new file mode 100644 index 0000000..389a82d --- /dev/null +++ b/skills/using-vipermesh-blender/references/scene-operations.md @@ -0,0 +1,41 @@ +# Scene Operations + +## Choose The Smallest Useful Surface + +- Inspect before mutating an existing scene. +- Search or filter the registry around the current task. +- Prefer a named deterministic operation when it matches the requested change. +- Use `execute_code` when custom Blender logic is materially clearer or more + capable than composing available tools. + +## Group Calls Deliberately + +`call_blender_tool_batch` is useful for independent or already-decided actions, +such as creating a known blockout or applying several known transforms. Keep +calls separate when the next action depends on dimensions, contact, topology, +viewport feedback, or a render. + +`run_blender_scene_stage` can compact common build, preview, and finalize work. +Its stages remain optional and may configure the camera and lighting. Set +`preservePresentation: true` on finalize to keep the active presentation. +Standalone tools are appropriate for targeted +repairs and workflows that do not fit the staged shape. + +## Preserve User Work + +Treat existing objects, collections, modifiers, materials, animation, and file +paths as user-owned state. Prefer reversible edits and duplicate or save a +revision before destructive operations when recovery would be expensive. + +Use descriptive object and collection names when later operations depend on +identity. Do not reorganize a scene merely to make it match one preferred +hierarchy. + +## Finish With Evidence + +Before reporting completion, confirm that the requested objects and changes +exist and inspect high-risk structural relationships. Save a blend file only +when the user requests it, using their approved destination. Omit `blendPath` +for rendering-only work; inspection and export workflows can use standalone +tools without saving or overwriting a blend file. Produce and inspect the +requested visual or export artifact before claiming it is ready. diff --git a/skills/using-vipermesh-blender/references/spatial-validation.md b/skills/using-vipermesh-blender/references/spatial-validation.md new file mode 100644 index 0000000..4deec69 --- /dev/null +++ b/skills/using-vipermesh-blender/references/spatial-validation.md @@ -0,0 +1,33 @@ +# Spatial Validation + +## Read World-Space State + +Object origins and local dimensions are not reliable substitutes for evaluated +world-space bounds. Rotation, scale, parenting, modifiers, and evaluated +geometry can change the surfaces that matter. + +When placement is consequential, inspect the relevant objects and reason from +their world-space bounds, attachment points, or actual surface hits. + +## Express The Intended Relationship + +Choose validation that matches the user's meaning: + +- `supported_by` or `on_top_of` for physical support +- facing or orientation relations for functional direction +- clearance checks for required gaps +- containment checks for objects intended to be inside another object +- attachment-point alignment for parts that must meet precisely + +Broad vertical ordering such as "above" does not prove contact. A clean camera +angle also does not prove that an object is grounded or non-intersecting. + +## Use Recommendations, Not Fixed Layouts + +Reasonable scale, spacing, and orientation depend on the asset, camera, +animation, target platform, and artistic intent. Use reference dimensions or +clearance ranges as starting evidence when useful, then adapt them. + +After close placement, inspect from an angle that reveals depth and contact. +Repair floating, unintended penetration, reversed functional direction, and +support errors before final acceptance. diff --git a/skills/using-vipermesh-blender/references/visual-presentation.md b/skills/using-vipermesh-blender/references/visual-presentation.md new file mode 100644 index 0000000..35d6fed --- /dev/null +++ b/skills/using-vipermesh-blender/references/visual-presentation.md @@ -0,0 +1,50 @@ +# Visual Presentation + +## Build A Useful Feedback Loop + +Use previews to answer concrete questions: composition, relative scale, +orientation, visibility, material readability, contact, and lighting direction. +Avoid repeated screenshots that do not change a decision. + +For reference reconstruction, compare the largest visual anchors before +spending time on small details. Later passes can prioritize objects by +prominence in the intended camera. + +## Camera + +Focal length, camera height, perspective, and frame fill are creative and +task-dependent. Use framing tools and camera inspection to match the requested +result; do not force a universal interior, product, or cinematic preset. + +Check that required objects are visible and that important silhouettes are not +accidentally cropped or hidden. A camera can conceal structural mistakes, so +visual framing does not replace spatial validation. + +## Lighting + +Choose lights from the intended source, mood, material response, and render +engine. Studio presets are useful starting points for isolated assets, while +custom lights and world environments may fit scenes with explicit windows, +fixtures, outdoor conditions, or stylized direction. + +Judge exposure, color separation, shadow readability, and whether bright areas +erase material color. Numeric energy ranges are starting points rather than +scene-independent rules. + +## Acceptance + +Inspect the final render artifact, not only tool success responses. If a visible +problem remains, make a focused repair and re-render rather than rebuilding +unrelated scene elements. + +`inspect_render_artifact` checks image integrity, not whether the picture matches +the brief. Open the image with the client's image/viewer capability. If the client +cannot view it, state that visual quality remains unverified. + +For each required object or edit, compare evidence against the user's intent: +presence and proportions, functional facing direction, contact or attachment, +clearance, visibility, and material readability. Take a diagnostic side or top +view when the main camera hides a suspicious relationship. Bounds-based support +reports can miss hollow supports, rotated parts, handles and local mesh collisions. +Interpret them with actual geometry and stated intent, rather than treating a +passing report as complete acceptance. Recheck affected relationships after a repair. diff --git a/tests/conformance.ts b/tests/conformance.ts index 82a4b41..d3cbb92 100644 --- a/tests/conformance.ts +++ b/tests/conformance.ts @@ -1,5 +1,10 @@ import assert from "node:assert/strict" import fs from "node:fs" +import os from "node:os" +import path from "node:path" +import { Client } from "@modelcontextprotocol/sdk/client/index.js" +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js" +import { z } from "zod" import type { McpCommand, McpResponse } from "../lib/mcp/types" import { getBlenderAgentContext } from "../lib/portable-mcp/agent-context" @@ -7,13 +12,13 @@ import { VIPERMESH_CONNECTOR_MANUAL, VIPERMESH_MCP_INSTRUCTIONS, } from "../lib/portable-mcp/onboarding" -import { PORTABLE_MCP_TOOL_NAMES } from "../lib/portable-mcp/server" +import { PORTABLE_MCP_TOOL_NAMES, createPortableBlenderMcpServer } from "../lib/portable-mcp/server" import { createPortableMcpService } from "../lib/portable-mcp/service" import { getGuidanceDocument, search3dGuidance } from "../lib/portable-mcp/guidance" const addon = fs.readFileSync("addon/vipermesh-addon.py", "utf8") assert.match(addon, /"name": "ViperMesh for Blender"/) -assert.match(addon, /ADDON_VERSION = \(1, 2, 0\)/) +assert.match(addon, /ADDON_VERSION = \(1, 3, 0\)/) assert.ok(PORTABLE_MCP_TOOL_NAMES.includes("bootstrap_vipermesh_session")) assert.ok(PORTABLE_MCP_TOOL_NAMES.includes("call_blender_tool_batch")) @@ -22,13 +27,14 @@ assert.match(VIPERMESH_MCP_INSTRUCTIONS, /one persistent MCP server process/i) assert.match(VIPERMESH_CONNECTOR_MANUAL, /execute_code/) assert.match(getBlenderAgentContext().context, /Inspect/i) -const guide = getGuidanceDocument("spatial-positioning-guide.md") +const guide = getGuidanceDocument("spatial-validation.md") assert.match(guide.title, /spatial/i) const guidance = await search3dGuidance( { query: "grounding support placement", source: "local" }, { semanticSearch: async () => [] } ) assert.ok(guidance.results.length > 0) +assert.ok(guidance.results.every((result) => result.source === "tool-skills")) let clientCreations = 0 const service = createPortableMcpService({ @@ -54,4 +60,28 @@ await service.callBlenderTool("get_scene_info") assert.equal(clientCreations, 1, "A session must reuse one Blender client") await service.close() +const temp = fs.mkdtempSync(path.join(os.tmpdir(), "vipermesh-image-transport-")) +const pngPath = path.join(temp, "preview.png") +fs.writeFileSync(pngPath, Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jRZkAAAAASUVORK5CYII=", "base64")) +const server = createPortableBlenderMcpServer(service) +const client = new Client({ name: "preview-conformance", version: "1.0.0" }) +const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair() +const imageToolResult = z.object({ + content: z.array(z.object({ type: z.string(), mimeType: z.string().optional() })), + structuredContent: z.record(z.unknown()).optional(), +}) +try { + await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]) + const result = imageToolResult.parse(await client.callTool({ name: "call_blender_tool", arguments: { name: "inspect_render_artifact", params: { image_path: pngPath } } })) + assert.ok(result.content.some((item) => item.type === "image" && item.mimeType === "image/png"), "MCP clients must receive the actual preview image") + const final = imageToolResult.parse(await client.callTool({ name: "run_blender_scene_stage", arguments: { stage: "finalize", targetNames: ["Subject"], renderPath: pngPath, preservePresentation: true } })) + assert.ok(final.content.some((item) => item.type === "image")) + assert.equal(final.structuredContent?.visualReviewRequired, true) +} finally { + await client.close() + await server.close() + await service.close() + fs.rmSync(temp, { recursive: true, force: true }) +} + console.log("ViperMesh public connector conformance tests passed")