feat: Initial commit of FreeCAD MCP/tooling proj.

This commit is contained in:
Sean P. Kane
2026-01-03 16:48:59 -08:00
commit 8d1271995e
82 changed files with 26195 additions and 0 deletions
+579
View File
@@ -0,0 +1,579 @@
"""Integration tests for FreeCAD MCP GUI mode.
These tests verify that the MCP bridge works correctly when FreeCAD is running
in GUI mode (with full graphical interface). They test GUI-specific features
like visibility, display modes, colors, and camera operations.
Note: These tests require a running FreeCAD GUI server.
Start it with: just run-gui
To run these tests:
pytest tests/integration/test_gui_mode.py -v
"""
from __future__ import annotations
import tempfile
from pathlib import Path
from typing import TYPE_CHECKING, Any
import pytest
if TYPE_CHECKING:
import xmlrpc.client
from collections.abc import Generator
# Mark all tests in this module as integration tests and gui tests
pytestmark = [pytest.mark.integration, pytest.mark.gui]
# Note: xmlrpc_proxy fixture is defined in conftest.py
@pytest.fixture(scope="module")
def temp_dir() -> Generator[str, None, None]:
"""Create a temporary directory for test files."""
with tempfile.TemporaryDirectory() as tmpdir:
yield tmpdir
def execute_code(proxy: xmlrpc.client.ServerProxy, code: str) -> dict[str, Any]:
"""Execute Python code via the MCP bridge and return the result."""
result: dict[str, Any] = proxy.execute(code) # type: ignore[assignment]
assert result.get("success"), f"Execution failed: {result.get('error_traceback')}"
return result
class TestGUIConnection:
"""Tests for GUI mode connectivity and detection."""
def test_ping(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
"""Test that the server responds to ping."""
result: dict[str, Any] = xmlrpc_proxy.ping() # type: ignore[assignment]
assert result["pong"] is True
assert "timestamp" in result
def test_gui_mode_detected(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
"""Test that FreeCAD is running in GUI mode (GuiUp=True)."""
result = execute_code(
xmlrpc_proxy,
"""
import FreeCAD
_result_ = {"gui_up": FreeCAD.GuiUp}
""",
)
# In GUI mode, GuiUp should be True (1)
assert result["result"]["gui_up"] == 1 or result["result"]["gui_up"] is True
class TestGUIObjectCreation:
"""Tests for object creation in GUI mode."""
@pytest.fixture(autouse=True)
def setup_document(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
"""Create a fresh document for each test."""
execute_code(
xmlrpc_proxy,
"""
import FreeCAD
# Close any existing GUITestDoc
if "GUITestDoc" in FreeCAD.listDocuments():
FreeCAD.closeDocument("GUITestDoc")
doc = FreeCAD.newDocument("GUITestDoc")
_result_ = True
""",
)
def test_create_box_with_view_object(
self, xmlrpc_proxy: xmlrpc.client.ServerProxy
) -> None:
"""Test creating a box with ViewObject in GUI mode."""
result = execute_code(
xmlrpc_proxy,
"""
import FreeCAD
import Part
doc = FreeCAD.ActiveDocument
box = Part.makeBox(10, 20, 30)
obj = doc.addObject("Part::Feature", "GUIBox")
obj.Shape = box
doc.recompute()
has_view_object = hasattr(obj, "ViewObject") and obj.ViewObject is not None
_result_ = {
"name": obj.Name,
"volume": obj.Shape.Volume,
"has_view_object": has_view_object,
"valid": obj.Shape.isValid()
}
""",
)
assert result["result"]["name"] == "GUIBox"
assert abs(result["result"]["volume"] - 6000.0) < 0.01
assert result["result"]["has_view_object"] is True
assert result["result"]["valid"] is True
class TestVisibility:
"""Tests for object visibility operations in GUI mode."""
@pytest.fixture(autouse=True)
def setup_document(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
"""Create a document with test objects."""
execute_code(
xmlrpc_proxy,
"""
import FreeCAD
import Part
if "VisibilityTestDoc" in FreeCAD.listDocuments():
FreeCAD.closeDocument("VisibilityTestDoc")
doc = FreeCAD.newDocument("VisibilityTestDoc")
box = Part.makeBox(10, 10, 10)
obj = doc.addObject("Part::Feature", "VisBox")
obj.Shape = box
doc.recompute()
_result_ = True
""",
)
def test_hide_object(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
"""Test hiding an object."""
result = execute_code(
xmlrpc_proxy,
"""
import FreeCAD
doc = FreeCAD.ActiveDocument
obj = doc.getObject("VisBox")
# Hide the object
obj.ViewObject.Visibility = False
_result_ = {
"visible": obj.ViewObject.Visibility
}
""",
)
assert result["result"]["visible"] is False
def test_show_object(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
"""Test showing an object."""
result = execute_code(
xmlrpc_proxy,
"""
import FreeCAD
doc = FreeCAD.ActiveDocument
obj = doc.getObject("VisBox")
# First hide, then show
obj.ViewObject.Visibility = False
obj.ViewObject.Visibility = True
_result_ = {
"visible": obj.ViewObject.Visibility
}
""",
)
assert result["result"]["visible"] is True
class TestDisplayMode:
"""Tests for display mode operations in GUI mode."""
@pytest.fixture(autouse=True)
def setup_document(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
"""Create a document with test objects."""
execute_code(
xmlrpc_proxy,
"""
import FreeCAD
import Part
if "DisplayModeTestDoc" in FreeCAD.listDocuments():
FreeCAD.closeDocument("DisplayModeTestDoc")
doc = FreeCAD.newDocument("DisplayModeTestDoc")
box = Part.makeBox(10, 10, 10)
obj = doc.addObject("Part::Feature", "DisplayBox")
obj.Shape = box
doc.recompute()
_result_ = True
""",
)
def test_get_display_modes(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
"""Test getting available display modes."""
result = execute_code(
xmlrpc_proxy,
"""
import FreeCAD
doc = FreeCAD.ActiveDocument
obj = doc.getObject("DisplayBox")
modes = obj.ViewObject.listDisplayModes()
_result_ = {
"modes": list(modes),
"current": obj.ViewObject.DisplayMode
}
""",
)
# Typical modes: "Flat Lines", "Shaded", "Wireframe", etc.
assert len(result["result"]["modes"]) > 0
assert result["result"]["current"] is not None
def test_set_display_mode(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
"""Test setting display mode."""
result = execute_code(
xmlrpc_proxy,
"""
import FreeCAD
doc = FreeCAD.ActiveDocument
obj = doc.getObject("DisplayBox")
modes = obj.ViewObject.listDisplayModes()
# Set to a different mode if available
target_mode = "Wireframe" if "Wireframe" in modes else modes[0]
obj.ViewObject.DisplayMode = target_mode
_result_ = {
"mode_set": obj.ViewObject.DisplayMode,
"target_mode": target_mode
}
""",
)
assert result["result"]["mode_set"] == result["result"]["target_mode"]
class TestObjectColor:
"""Tests for object color operations in GUI mode."""
@pytest.fixture(autouse=True)
def setup_document(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
"""Create a document with test objects."""
execute_code(
xmlrpc_proxy,
"""
import FreeCAD
import Part
if "ColorTestDoc" in FreeCAD.listDocuments():
FreeCAD.closeDocument("ColorTestDoc")
doc = FreeCAD.newDocument("ColorTestDoc")
box = Part.makeBox(10, 10, 10)
obj = doc.addObject("Part::Feature", "ColorBox")
obj.Shape = box
doc.recompute()
_result_ = True
""",
)
def test_set_shape_color(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
"""Test setting object shape color."""
result = execute_code(
xmlrpc_proxy,
"""
import FreeCAD
doc = FreeCAD.ActiveDocument
obj = doc.getObject("ColorBox")
# Set color to red (RGB as tuple of floats 0-1)
obj.ViewObject.ShapeColor = (1.0, 0.0, 0.0)
color = obj.ViewObject.ShapeColor
_result_ = {
"r": color[0],
"g": color[1],
"b": color[2]
}
""",
)
assert result["result"]["r"] == pytest.approx(1.0, rel=0.01)
assert result["result"]["g"] == pytest.approx(0.0, abs=0.01)
assert result["result"]["b"] == pytest.approx(0.0, abs=0.01)
def test_set_transparency(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
"""Test setting object transparency."""
result = execute_code(
xmlrpc_proxy,
"""
import FreeCAD
doc = FreeCAD.ActiveDocument
obj = doc.getObject("ColorBox")
# Set 50% transparency
obj.ViewObject.Transparency = 50
_result_ = {
"transparency": obj.ViewObject.Transparency
}
""",
)
assert result["result"]["transparency"] == 50
class TestCameraOperations:
"""Tests for camera/view operations in GUI mode."""
@pytest.fixture(autouse=True)
def setup_document(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
"""Create a document with test objects."""
execute_code(
xmlrpc_proxy,
"""
import FreeCAD
import Part
if "CameraTestDoc" in FreeCAD.listDocuments():
FreeCAD.closeDocument("CameraTestDoc")
doc = FreeCAD.newDocument("CameraTestDoc")
box = Part.makeBox(10, 10, 10)
obj = doc.addObject("Part::Feature", "CameraBox")
obj.Shape = box
doc.recompute()
_result_ = True
""",
)
def test_fit_all(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
"""Test fit all view operation."""
result = execute_code(
xmlrpc_proxy,
"""
import FreeCAD
import FreeCADGui
doc = FreeCAD.ActiveDocument
view = FreeCADGui.ActiveDocument.ActiveView
# Fit all objects in view
view.fitAll()
_result_ = {"success": True}
""",
)
assert result["result"]["success"] is True
def test_set_view_direction(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None:
"""Test setting view direction (front, top, etc.)."""
result = execute_code(
xmlrpc_proxy,
"""
import FreeCAD
import FreeCADGui
doc = FreeCAD.ActiveDocument
view = FreeCADGui.ActiveDocument.ActiveView
# Set to front view
view.viewFront()
# Get camera orientation
cam = view.getCameraOrientation()
_result_ = {
"success": True,
"camera_orientation": [cam.Q[0], cam.Q[1], cam.Q[2], cam.Q[3]]
}
""",
)
assert result["result"]["success"] is True
class TestScreenshot:
"""Tests for screenshot functionality in GUI mode."""
def test_capture_screenshot(
self, xmlrpc_proxy: xmlrpc.client.ServerProxy, temp_dir: str
) -> None:
"""Test capturing a screenshot."""
# First create a document with objects
execute_code(
xmlrpc_proxy,
"""
import FreeCAD
import Part
if "ScreenshotTestDoc" in FreeCAD.listDocuments():
FreeCAD.closeDocument("ScreenshotTestDoc")
doc = FreeCAD.newDocument("ScreenshotTestDoc")
box = Part.makeBox(20, 20, 20)
obj = doc.addObject("Part::Feature", "ScreenshotBox")
obj.Shape = box
obj.ViewObject.ShapeColor = (0.0, 0.5, 1.0) # Blue color
doc.recompute()
_result_ = True
""",
)
screenshot_path = Path(temp_dir) / "test_screenshot.png"
result = execute_code(
xmlrpc_proxy,
f"""
import FreeCAD
import FreeCADGui
import os
doc = FreeCAD.ActiveDocument
view = FreeCADGui.ActiveDocument.ActiveView
# Fit objects in view
view.fitAll()
# Save screenshot
view.saveImage({screenshot_path!r}, 800, 600, "White")
_result_ = {{
"saved": os.path.exists({screenshot_path!r}),
"path": {screenshot_path!r}
}}
""",
)
assert result["result"]["saved"] is True
assert screenshot_path.exists()
# Verify file has content
assert screenshot_path.stat().st_size > 0
class TestComplexGUIWorkflow:
"""Tests for complex workflows that combine GUI and modeling features."""
def test_create_assembly_with_colors(
self, xmlrpc_proxy: xmlrpc.client.ServerProxy
) -> None:
"""Test creating multiple objects with different colors."""
result = execute_code(
xmlrpc_proxy,
"""
import FreeCAD
import Part
if "AssemblyTestDoc" in FreeCAD.listDocuments():
FreeCAD.closeDocument("AssemblyTestDoc")
doc = FreeCAD.newDocument("AssemblyTestDoc")
# Create base plate (gray)
base = Part.makeBox(100, 100, 5)
base_obj = doc.addObject("Part::Feature", "BasePlate")
base_obj.Shape = base
base_obj.ViewObject.ShapeColor = (0.5, 0.5, 0.5)
# Create pillar 1 (red)
pillar1 = Part.makeBox(10, 10, 50, FreeCAD.Vector(10, 10, 5))
pillar1_obj = doc.addObject("Part::Feature", "Pillar1")
pillar1_obj.Shape = pillar1
pillar1_obj.ViewObject.ShapeColor = (1.0, 0.0, 0.0)
# Create pillar 2 (green)
pillar2 = Part.makeBox(10, 10, 50, FreeCAD.Vector(80, 10, 5))
pillar2_obj = doc.addObject("Part::Feature", "Pillar2")
pillar2_obj.Shape = pillar2
pillar2_obj.ViewObject.ShapeColor = (0.0, 1.0, 0.0)
# Create pillar 3 (blue)
pillar3 = Part.makeBox(10, 10, 50, FreeCAD.Vector(80, 80, 5))
pillar3_obj = doc.addObject("Part::Feature", "Pillar3")
pillar3_obj.Shape = pillar3
pillar3_obj.ViewObject.ShapeColor = (0.0, 0.0, 1.0)
# Create pillar 4 (yellow)
pillar4 = Part.makeBox(10, 10, 50, FreeCAD.Vector(10, 80, 5))
pillar4_obj = doc.addObject("Part::Feature", "Pillar4")
pillar4_obj.Shape = pillar4
pillar4_obj.ViewObject.ShapeColor = (1.0, 1.0, 0.0)
# Create top plate (semi-transparent gray)
top = Part.makeBox(100, 100, 5, FreeCAD.Vector(0, 0, 55))
top_obj = doc.addObject("Part::Feature", "TopPlate")
top_obj.Shape = top
top_obj.ViewObject.ShapeColor = (0.5, 0.5, 0.5)
top_obj.ViewObject.Transparency = 30
doc.recompute()
_result_ = {
"object_count": len(doc.Objects),
"objects": [obj.Name for obj in doc.Objects],
"all_visible": all(obj.ViewObject.Visibility for obj in doc.Objects)
}
""",
)
assert result["result"]["object_count"] == 6
assert "BasePlate" in result["result"]["objects"]
assert "TopPlate" in result["result"]["objects"]
assert result["result"]["all_visible"] is True
def test_export_with_screenshot(
self, xmlrpc_proxy: xmlrpc.client.ServerProxy, temp_dir: str
) -> None:
"""Test creating model, setting view, taking screenshot, and exporting."""
step_path = Path(temp_dir) / "workflow_export.step"
screenshot_path = Path(temp_dir) / "workflow_screenshot.png"
result = execute_code(
xmlrpc_proxy,
f"""
import FreeCAD
import FreeCADGui
import Part
import os
if "WorkflowTestDoc" in FreeCAD.listDocuments():
FreeCAD.closeDocument("WorkflowTestDoc")
doc = FreeCAD.newDocument("WorkflowTestDoc")
# Create a simple bracket shape
# Base
base = Part.makeBox(50, 30, 5)
# Vertical support
vertical = Part.makeBox(5, 30, 40, FreeCAD.Vector(0, 0, 5))
# Top flange with hole
top_flange = Part.makeBox(20, 30, 5, FreeCAD.Vector(0, 0, 45))
hole = Part.makeCylinder(4, 10, FreeCAD.Vector(10, 15, 40))
# Combine and cut
bracket = base.fuse(vertical).fuse(top_flange).cut(hole)
bracket_obj = doc.addObject("Part::Feature", "Bracket")
bracket_obj.Shape = bracket
bracket_obj.ViewObject.ShapeColor = (0.2, 0.4, 0.8)
doc.recompute()
# Set isometric view
view = FreeCADGui.ActiveDocument.ActiveView
view.viewIsometric()
view.fitAll()
# Take screenshot
view.saveImage({screenshot_path!r}, 800, 600, "White")
# Export to STEP
bracket_obj.Shape.exportStep({step_path!r})
_result_ = {{
"bracket_valid": bracket_obj.Shape.isValid(),
"bracket_volume": bracket_obj.Shape.Volume,
"screenshot_exists": os.path.exists({screenshot_path!r}),
"step_exists": os.path.exists({step_path!r})
}}
""",
)
assert result["result"]["bracket_valid"] is True
assert result["result"]["bracket_volume"] > 0
assert result["result"]["screenshot_exists"] is True
assert result["result"]["step_exists"] is True
assert screenshot_path.exists()
assert step_path.exists()