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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/routers/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ def registry_tool_lines(registry: MeshOpsRegistry) -> str:

- Always use tools to act on the scene — never just describe what you would do.
- If you need the current mesh path, call get_mesh_info first.
- To prepare a mesh for 3D printing, a game engine or the web, call prepare_for_printing, prepare_for_game or prepare_for_web ONCE: each runs every step itself, so never chain repair_mesh, arrange_mesh or decimate for these intents, and no suggest_fixes is needed first. Pass the size or triangle budget only if the user gave one. Report any remaining issue it lists.
- To repair or fix a mesh, call repair_mesh. Report the issues it could not fix, and warn the user when it says the textures were dropped.
- To center, ground, lay flat or resize a mesh, call arrange_mesh ONCE with every requested step — never one call per step.
- To answer questions about a mesh's quality or printability, call inspect_mesh — never guess. Before repairing a mesh, call suggest_fixes to pick the right operations.
Expand Down
22 changes: 22 additions & 0 deletions api/services/mesh_ops/agent_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,25 @@ def format_repair(data: Mapping[str, Any]) -> str:
if data.get("textures_dropped"):
lines.append("The textures and UVs were dropped: the repaired mesh is geometry only.")
return "\n".join(lines)


def format_preset(data: Mapping[str, Any]) -> str:
"""Render a prepare_for_* preset result: each step, then the outcome."""
lines = [f"- {step['summary']}" for step in data.get("steps") or [] if step.get("summary")]
remaining = data.get("remaining_issues") or []
if "watertight" in data:
if not data["watertight"]:
lines.append("The mesh is NOT watertight yet.")
else:
lines.append("The mesh is watertight" + ("." if remaining else " and ready to slice."))
if remaining:
lines.append("Still remaining: " + "; ".join(remaining) + ".")
if data.get("dimensions_mm"):
lines.append("Size: " + " × ".join(f"{d:.4g}" for d in data["dimensions_mm"]) + " mm.")
elif data.get("dimensions"):
lines.append("Size: " + " × ".join(f"{d:.4g}" for d in data["dimensions"]) + " m.")
faces = data.get("face_count")
lines.append(f"New mesh path: {data['path']}" + (f" ({faces:,} faces)." if faces else "."))
if data.get("textures_dropped"):
lines.append("The textures and UVs were dropped: the repaired mesh is geometry only.")
return "\n".join(lines)
123 changes: 122 additions & 1 deletion api/services/mesh_ops/builtin.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""Built-in mesh operation definitions."""

from .agent_format import format_fixes, format_inspection, format_repair
from .agent_format import format_fixes, format_inspection, format_preset, format_repair
from .inspection import inspect_mesh, suggest_fixes
from .operations import decimate_mesh, repair_mesh, smooth_mesh
from .presets import prepare_for_game, prepare_for_printing, prepare_for_web
from .repair import auto_repair_mesh
from .transforms import auto_orient, drop_to_ground, normalize_scale, recenter
from .types import AgentTool, MeshOp
Expand Down Expand Up @@ -177,6 +178,73 @@
)


DIMENSION_PARAM = {
"id": "dimension",
"label": "Dimension",
"type": "select",
"default": "longest",
"options": [
{"value": "longest", "label": "Longest side"},
{"value": "x", "label": "Width (X)"},
{"value": "y", "label": "Height (Y)"},
{"value": "z", "label": "Depth (Z)"},
],
"tooltip": "Which bounding-box dimension is set to the target size.",
}


PREPARE_FOR_PRINTING_PARAMS = (
{
"id": "size_mm",
"label": "Size (mm)",
"type": "float",
"default": 100.0,
"min": 1.0,
"max": 10000.0,
"step": 1.0,
"tooltip": "Printed size of the chosen dimension, in millimetres.",
},
DIMENSION_PARAM,
)

PREPARE_FOR_GAME_PARAMS = (
{
"id": "target_faces",
"label": "Poly Budget",
"type": "int",
"default": 20000,
"min": 100,
"max": 1000000,
"tooltip": "Maximum number of triangles the game asset may keep.",
},
{
"id": "size_m",
"label": "Size (m)",
"type": "float",
"default": 1.0,
"min": 0.01,
"max": 1000.0,
"step": 0.1,
"tooltip": "In-game size of the chosen dimension, in metres.",
},
DIMENSION_PARAM,
)

PREPARE_FOR_WEB_PARAMS = (
{
"id": "target_faces",
"label": "Poly Budget",
"type": "int",
"default": 5000,
"min": 100,
"max": 200000,
"tooltip": "Maximum number of triangles: fewer means a lighter download.",
},
)

DIMENSION_HINT = "Which side gets the size: longest, x (width), y (height) or z (depth)."


BUILTIN_MESH_OPS = (
MeshOp(
id="repair",
Expand Down Expand Up @@ -287,4 +355,57 @@
fn=normalize_scale,
category="transform",
),
MeshOp(
id="prepare_for_printing",
label="Prepare for 3D Printing",
params_schema=PREPARE_FOR_PRINTING_PARAMS,
fn=prepare_for_printing,
category="presets",
agent=AgentTool(
description=(
"Make a mesh ready for 3D printing in one call: repair it until it is "
"watertight and manifold, lay it flat, scale it in millimetres and rest it "
"on the build plate. Writes a new mesh without textures."
),
params={
"size_mm": "Printed size of `dimension`, in millimetres.",
"dimension": DIMENSION_HINT,
},
format_result=format_preset,
),
),
MeshOp(
id="prepare_for_game",
label="Prepare for Game",
params_schema=PREPARE_FOR_GAME_PARAMS,
fn=prepare_for_game,
category="presets",
agent=AgentTool(
description=(
"Make a mesh ready for a game engine in one call: decimate it to a "
"triangle budget, scale it in metres and recompute its normals."
),
params={
"target_faces": "Triangle budget of the game asset.",
"size_m": "In-game size of `dimension`, in metres.",
"dimension": DIMENSION_HINT,
},
format_result=format_preset,
),
),
MeshOp(
id="prepare_for_web",
label="Prepare for Web",
params_schema=PREPARE_FOR_WEB_PARAMS,
fn=prepare_for_web,
category="presets",
agent=AgentTool(
description=(
"Make a mesh light enough for the web in one call: decimate it "
"aggressively and recompute its normals. Keeps its textures and size."
),
params={"target_faces": "Triangle budget; lower means a lighter download."},
format_result=format_preset,
),
),
)
Loading
Loading