Refactor (#37)

* refactor: remove macros

* refactor: additional cleanup

* chore: small cleanup

* refactor: update .coderabbit.yaml configuration

* docs: update CLAUDE.md with new documentation

* docs: table alignment
This commit is contained in:
Sean P. Kane
2026-01-17 21:24:37 -08:00
committed by GitHub
parent 62f6f4dbcf
commit 47e80f1bc7
46 changed files with 356 additions and 7425 deletions
+9 -9
View File
@@ -69,10 +69,12 @@ reviews:
This code runs inside FreeCAD's Python environment as a workbench addon. This code runs inside FreeCAD's Python environment as a workbench addon.
It cannot import packages from the project's virtualenv (mcp, pydantic). It cannot import packages from the project's virtualenv (mcp, pydantic).
Watch for accidental imports of project dependencies. Watch for accidental imports of project dependencies.
- path: "macros/**/*.FCMacro" - path: ".github/actions/**/*.yaml"
instructions: > instructions: >
FreeCAD macro files. These are standalone Python scripts. Custom GitHub Actions (composite actions). Check for:
They run in FreeCAD's environment with access to FreeCAD, Part, etc. - Proper input/output definitions
- Shell script correctness
- Cross-platform compatibility (x86_64/aarch64)
- path: "tests/**/*.py" - path: "tests/**/*.py"
instructions: > instructions: >
Test files. Ensure good test coverage and clear assertions. Test files. Ensure good test coverage and clear assertions.
@@ -105,14 +107,12 @@ reviews:
- The combination of '>=' in pyproject.toml + uv.lock is the modern standard - The combination of '>=' in pyproject.toml + uv.lock is the modern standard
- path: "README.md" - path: "README.md"
instructions: > instructions: >
INTENTIONAL NAMING: The repo is "freecad-robust-mcp-and-more" but Docker Main documentation for the FreeCAD Robust MCP Server addon.
image and PyPI package are "freecad-robust-mcp". This is documented and The PyPI package name is "freecad-robust-mcp".
intentional - do NOT flag as a naming mismatch or inconsistency.
- path: "Dockerfile" - path: "Dockerfile"
instructions: > instructions: >
INTENTIONAL NAMING: Image name "freecad-robust-mcp" differs from repo name Docker image for running FreeCAD MCP Server in containers.
"freecad-robust-mcp-and-more". This is intentional and documented in README. Image name is "freecad-robust-mcp".
Do NOT flag as a naming mismatch.
- path: ".mise.toml" - path: ".mise.toml"
instructions: > instructions: >
Tool version management via mise. All versions use fuzzy matching: Tool version management via mise. All versions use fuzzy matching:
-3
View File
@@ -78,9 +78,6 @@ site/
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Macros (not needed in container)
macros/
# Tests (not needed in runtime container) # Tests (not needed in runtime container)
tests/ tests/
# But allow CI test scripts for GUI testing container # But allow CI test scripts for GUI testing container
@@ -1,35 +0,0 @@
name: Macro Cut Object for Magnets Release
on:
push:
tags:
- 'macro-cut-object-for-magnets-v*'
workflow_dispatch:
workflow_call:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
release:
uses: ./.github/workflows/macro-release-reusable.yaml
with:
tag_prefix: 'macro-cut-object-for-magnets-v'
macro_dir: 'Cut_Object_for_Magnets'
macro_file: 'CutObjectForMagnets.FCMacro'
macro_name: 'Cut Object for Magnets'
archive_name: 'CutObjectForMagnets'
readme_file: 'README-CutObjectForMagnets.md'
wiki_url: 'https://wiki.freecad.org/Macro_Cut_Object_for_Magnets'
description: |
A FreeCAD macro that cuts 3D objects along a plane and automatically adds aligned magnet holes with surface collision detection. Perfect for creating 3D printed parts that snap together with embedded magnets.
**Features:**
- Cut along preset planes (XY, XZ, YZ) or model datum planes
- Automatic hole placement with even distribution
- Surface collision detection prevents holes from breaking through walls
- Configurable hole diameter, depth, and count
- Creates PartDesign::Body objects with parametric Hole features
permissions:
contents: write
@@ -1,35 +0,0 @@
name: Macro Multi Export Release
on:
push:
tags:
- 'macro-multi-export-v*'
workflow_dispatch:
workflow_call:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
release:
uses: ./.github/workflows/macro-release-reusable.yaml
with:
tag_prefix: 'macro-multi-export-v'
macro_dir: 'Multi_Export'
macro_file: 'MultiExport.FCMacro'
macro_name: 'Multi Export'
archive_name: 'MultiExport'
readme_file: 'README-MultiExport.md'
wiki_url: 'https://wiki.freecad.org/Macro_Multi_Export'
description: |
A FreeCAD macro that exports selected bodies to multiple file formats simultaneously with a user-friendly dialog.
**Features:**
- Export to STL, STEP, 3MF, OBJ, IGES, BREP, PLY, and AMF formats
- Batch export multiple objects at once
- Configurable mesh quality settings (tolerance and deflection)
- Real-time file preview before export
- Remember last-used export directory
permissions:
contents: write
@@ -1,312 +0,0 @@
name: Macro Release (Reusable)
# Reusable workflow for releasing individual FreeCAD macros
# Called by component-specific workflows with appropriate inputs
on:
workflow_dispatch:
inputs:
tag_prefix:
description: 'Tag prefix to match (e.g., macro-cut-object-for-magnets-v)'
required: true
type: string
macro_dir:
description: 'Macro directory relative to macros/ (e.g., Cut_Object_for_Magnets)'
required: true
type: string
macro_file:
description: 'Main macro filename (e.g., CutObjectForMagnets.FCMacro)'
required: true
type: string
macro_name:
description: 'Human-readable macro name (e.g., Cut Object for Magnets)'
required: true
type: string
archive_name:
description: 'Archive base name (e.g., CutObjectForMagnets)'
required: true
type: string
readme_file:
description: 'README filename (e.g., README-CutObjectForMagnets.md)'
required: true
type: string
wiki_url:
description: 'FreeCAD Wiki URL for this macro'
required: false
type: string
default: ''
description:
description: 'Short description for release notes'
required: true
type: string
workflow_call:
inputs:
tag_prefix:
description: 'Tag prefix to match (e.g., macro-cut-object-for-magnets-v)'
required: true
type: string
macro_dir:
description: 'Macro directory relative to macros/ (e.g., Cut_Object_for_Magnets)'
required: true
type: string
macro_file:
description: 'Main macro filename (e.g., CutObjectForMagnets.FCMacro)'
required: true
type: string
macro_name:
description: 'Human-readable macro name (e.g., Cut Object for Magnets)'
required: true
type: string
archive_name:
description: 'Archive base name (e.g., CutObjectForMagnets)'
required: true
type: string
readme_file:
description: 'README filename (e.g., README-CutObjectForMagnets.md)'
required: true
type: string
wiki_url:
description: 'FreeCAD Wiki URL for this macro'
required: false
type: string
default: ''
description:
description: 'Short description for release notes'
required: true
type: string
jobs:
validate-and-release:
name: Validate Tag and Create Release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Validate semantic version tag
id: version
env:
TAG_PREFIX: ${{ inputs.tag_prefix }}
run: |
TAG="${GITHUB_REF#refs/tags/}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
# Build regex pattern from tag prefix
PATTERN="^${TAG_PREFIX}([0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?)\$"
if [[ ! "$TAG" =~ $PATTERN ]]; then
echo "ERROR: Tag '$TAG' does not match expected format (${TAG_PREFIX}X.Y.Z)"
exit 1
fi
VERSION="${BASH_REMATCH[1]}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
# Check if this is a prerelease
if [[ "$VERSION" =~ -[a-zA-Z0-9.]+ ]]; then
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
else
echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
fi
echo "Parsed version: $VERSION"
- name: Verify macro version in files
env:
VERSION: ${{ steps.version.outputs.version }}
MACRO_DIR: ${{ inputs.macro_dir }}
MACRO_FILE: ${{ inputs.macro_file }}
MACRO_NAME: ${{ inputs.macro_name }}
run: |
MACRO_PATH="macros/${MACRO_DIR}/${MACRO_FILE}"
echo "Verifying version in source files matches tag: $VERSION"
# Check __Version__ in .FCMacro file
if [ -f "$MACRO_PATH" ]; then
MACRO_VERSION=$(grep -o '__Version__ = "[^"]*"' "$MACRO_PATH" | cut -d'"' -f2)
if [ "$MACRO_VERSION" != "$VERSION" ]; then
echo "ERROR: Version mismatch in $MACRO_PATH"
echo " Expected: $VERSION"
echo " Found: $MACRO_VERSION"
echo ""
echo "The version in source files must be updated before tagging."
echo "Run the appropriate bump command first."
exit 1
fi
echo "✓ $MACRO_PATH: $MACRO_VERSION"
else
echo "ERROR: Macro file not found: $MACRO_PATH"
exit 1
fi
# Check wiki-source.txt
WIKI_FILE="macros/${MACRO_DIR}/wiki-source.txt"
if [ -f "$WIKI_FILE" ]; then
WIKI_VERSION=$(grep -o '|Version=[^|]*' "$WIKI_FILE" | cut -d= -f2 | tr -d '\n')
if [ "$WIKI_VERSION" != "$VERSION" ]; then
echo "ERROR: Version mismatch in $WIKI_FILE"
echo " Expected: $VERSION"
echo " Found: $WIKI_VERSION"
echo ""
echo "Run the appropriate bump command first."
exit 1
fi
echo "✓ $WIKI_FILE: $WIKI_VERSION"
fi
# Check package.xml
PKG_VERSION=$(awk -v name="$MACRO_NAME" '
/<macro>/ { in_macro=1 }
/<\/macro>/ { in_macro=0; found_name=0 }
in_macro && index($0, name) > 0 { found_name=1 }
in_macro && found_name && /<version>/ {
gsub(/.*<version>/, ""); gsub(/<\/version>.*/, ""); print; exit
}
' package.xml)
if [ "$PKG_VERSION" != "$VERSION" ]; then
echo "ERROR: Version mismatch in package.xml ($MACRO_NAME section)"
echo " Expected: $VERSION"
echo " Found: $PKG_VERSION"
exit 1
fi
echo "✓ package.xml ($MACRO_NAME): $PKG_VERSION"
echo ""
echo "Version verification passed!"
- name: Create macro archive
env:
VERSION: ${{ steps.version.outputs.version }}
MACRO_DIR: ${{ inputs.macro_dir }}
MACRO_FILE: ${{ inputs.macro_file }}
ARCHIVE_NAME: ${{ inputs.archive_name }}
README_FILE: ${{ inputs.readme_file }}
run: |
# Create staging directory
mkdir -p "staging/${ARCHIVE_NAME}-${VERSION}"
# Copy macro files
cp "macros/${MACRO_DIR}/${MACRO_FILE}" "staging/${ARCHIVE_NAME}-${VERSION}/"
# Copy icon if exists
for ext in svg png; do
ICON_FILE="macros/${MACRO_DIR}/${ARCHIVE_NAME}.${ext}"
if [ -f "$ICON_FILE" ]; then
cp "$ICON_FILE" "staging/${ARCHIVE_NAME}-${VERSION}/"
fi
done
# Copy README
if [ -f "macros/${MACRO_DIR}/${README_FILE}" ]; then
cp "macros/${MACRO_DIR}/${README_FILE}" "staging/${ARCHIVE_NAME}-${VERSION}/README.md"
fi
# Copy LICENSE
cp LICENSE "staging/${ARCHIVE_NAME}-${VERSION}/"
# Create tar.gz archive
cd staging
tar -czvf "${ARCHIVE_NAME}-${VERSION}.tar.gz" "${ARCHIVE_NAME}-${VERSION}"
# Create zip archive
zip -r "${ARCHIVE_NAME}-${VERSION}.zip" "${ARCHIVE_NAME}-${VERSION}"
mv "${ARCHIVE_NAME}-${VERSION}.tar.gz" ../
mv "${ARCHIVE_NAME}-${VERSION}.zip" ../
cd ..
echo "Created archives:"
ls -la "${ARCHIVE_NAME}-${VERSION}."*
- name: Extract release notes section
id: changelog
env:
VERSION: ${{ steps.version.outputs.version }}
MACRO_NAME: ${{ inputs.macro_name }}
DESCRIPTION: ${{ inputs.description }}
MACRO_FILE: ${{ inputs.macro_file }}
MACRO_DIR: ${{ inputs.macro_dir }}
README_FILE: ${{ inputs.readme_file }}
WIKI_URL: ${{ inputs.wiki_url }}
run: |
RELEASE_NOTES="macros/${MACRO_DIR}/RELEASE_NOTES.md"
# Extract section for this version from RELEASE_NOTES.md
# Format: ## Version X.Y.Z (date)
CHANGELOG_CONTENT=$(awk -v version="$VERSION" '
BEGIN { found=0 }
/^## Version / {
if (found) exit
if (index($0, version) > 0) { found=1; next }
}
found { print }
' "$RELEASE_NOTES" 2>/dev/null || echo "")
# Build release body
cat > release_body.md << EOF
## ${MACRO_NAME} Macro v${VERSION}
${DESCRIPTION}
### Installation
Copy \`${MACRO_FILE}\` to your FreeCAD macro directory:
- **macOS**: \`~/Library/Application Support/FreeCAD/Macro/\`
- **Linux**: \`~/.local/share/FreeCAD/Macro/\`
- **Windows**: \`%APPDATA%/FreeCAD/Macro/\`
Optionally copy the icon file (\`.svg\` or \`.png\`) to the same location.
### Documentation
- [Full README](https://github.com/spkane/freecad-robust-mcp-and-more/blob/main/macros/${MACRO_DIR}/${README_FILE})
EOF
if [ -n "$WIKI_URL" ]; then
echo "- [FreeCAD Wiki Page](${WIKI_URL})" >> release_body.md
fi
if [ -n "$CHANGELOG_CONTENT" ]; then
{
echo ""
echo "### Changelog"
echo ""
echo "$CHANGELOG_CONTENT"
} >> release_body.md
fi
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
name: "${{ inputs.macro_name }} Macro v${{ steps.version.outputs.version }}"
tag_name: ${{ github.ref_name }}
prerelease: ${{ steps.version.outputs.is_prerelease == 'true' }}
generate_release_notes: true
body_path: release_body.md
files: |
${{ inputs.archive_name }}-${{ steps.version.outputs.version }}.tar.gz
${{ inputs.archive_name }}-${{ steps.version.outputs.version }}.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Generate summary
env:
VERSION: ${{ steps.version.outputs.version }}
MACRO_NAME: ${{ inputs.macro_name }}
ARCHIVE_NAME: ${{ inputs.archive_name }}
run: |
{
echo "## ${MACRO_NAME} Macro Release"
echo ""
echo "**Version:** ${VERSION}"
echo ""
echo "### Downloads"
echo ""
echo "- \`${ARCHIVE_NAME}-${VERSION}.tar.gz\`"
echo "- \`${ARCHIVE_NAME}-${VERSION}.zip\`"
} >> "$GITHUB_STEP_SUMMARY"
-422
View File
@@ -1,422 +0,0 @@
name: Integration Tests
on:
push:
branches: [main, master]
paths:
- "src/freecad_mcp/**/*.py"
- "macros/**/*.FCMacro"
- "macros/**/*.py"
- "addon/FreecadRobustMCPBridge/**/*.py"
- "tests/integration/**/*.py"
- ".github/workflows/macro-test.yaml"
- ".github/actions/setup-freecad/**"
pull_request:
branches: [main, master]
paths:
- "src/freecad_mcp/**/*.py"
- "macros/**/*.FCMacro"
- "macros/**/*.py"
- "addon/FreecadRobustMCPBridge/**/*.py"
- "tests/integration/**/*.py"
- ".github/workflows/macro-test.yaml"
- ".github/actions/setup-freecad/**"
workflow_dispatch:
workflow_call:
# Cancel in-progress runs for the same branch
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test-integration:
name: Integration Tests with FreeCAD
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Install mise
uses: jdx/mise-action@v3
- name: Setup FreeCAD
uses: ./.github/actions/setup-freecad
- name: Cache uv dependencies
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "**/uv.lock"
- name: Install dependencies
run: uv sync --all-extras
- name: Validate macro syntax
run: |
echo "Validating macro Python syntax..."
for macro in macros/**/*.FCMacro; do
echo "Checking: $macro"
python3 -m py_compile "$macro" || {
echo "ERROR: Syntax error in $macro"
exit 1
}
done
echo "All macros have valid Python syntax"
- name: Start FreeCAD headless with MCP bridge
run: |
echo "Using FreeCAD: freecadcmd"
# Start FreeCAD headless with MCP bridge in background
# Uses the workbench addon's blocking bridge script
# Use setsid to create a new process group for reliable cleanup
setsid freecadcmd addon/FreecadRobustMCPBridge/freecad_mcp_bridge/blocking_bridge.py > /tmp/freecad_bridge.log 2>&1 &
FREECAD_PID=$!
echo "FREECAD_PID=$FREECAD_PID" >> "$GITHUB_ENV"
# Wait for bridge to be ready (check XML-RPC port)
echo "Waiting for MCP bridge to start..."
for i in {1..60}; do
if curl -s --max-time 2 -X POST \
-H "Content-Type: text/xml" \
-d '<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName></methodCall>' \
http://localhost:9875 > /dev/null 2>&1; then
echo "MCP bridge is ready (took ${i}s)"
break
fi
if [ "$i" -eq 60 ]; then
echo "ERROR: MCP bridge did not start within 60s"
echo "=== Bridge log ==="
cat /tmp/freecad_bridge.log || true
kill "$FREECAD_PID" 2>/dev/null || true
exit 1
fi
sleep 1
done
# Log the instance ID for debugging (pytest tests verify bridge via conftest.py fixtures)
BRIDGE_INSTANCE_ID=$(grep -o 'FREECAD_MCP_BRIDGE_INSTANCE_ID=[^ ]*' /tmp/freecad_bridge.log | cut -d= -f2 | head -1)
if [ -n "$BRIDGE_INSTANCE_ID" ]; then
echo "Bridge Instance ID: $BRIDGE_INSTANCE_ID"
else
echo "Warning: Could not extract bridge instance ID from log"
cat /tmp/freecad_bridge.log || true
fi
- name: Run CutObjectForMagnets macro tests
env:
FREECAD_MODE: xmlrpc
# Note: Tests use boolean holes (not PartDesign::Hole) for CI compatibility.
# PartDesign::Hole has a CADKernelError bug in some headless AppImage environments.
run: |
uv run pytest tests/integration/test_cut_object_for_magnets.py -v --tb=short
- name: Run MultiExport macro tests
env:
FREECAD_MODE: xmlrpc
# Export functionality should work in headless mode
continue-on-error: true
run: |
uv run pytest tests/integration/test_multi_export.py -v --tb=short
- name: Run headless mode integration tests
env:
FREECAD_MODE: xmlrpc
# Tests for headless FreeCAD operations (primitives, booleans, exports, etc.)
run: |
uv run pytest tests/integration/test_headless_mode.py -v --tb=short
- name: Upload FreeCAD logs on failure
if: failure() || cancelled()
uses: actions/upload-artifact@v6
with:
name: freecad-headless-logs
path: /tmp/freecad_bridge.log
retention-days: 7
if-no-files-found: ignore
- name: Stop FreeCAD
if: always()
run: |
if [ -n "$FREECAD_PID" ]; then
# Kill process group (handles child processes spawned by FreeCAD/AppImage)
kill -- "-$FREECAD_PID" 2>/dev/null || kill "$FREECAD_PID" 2>/dev/null || true
fi
test-gui:
name: GUI Tests with FreeCAD + Xvfb
runs-on: ubuntu-latest
timeout-minutes: 30
# FreeCAD GUI requires a window manager to generate expose/configure events.
# Without a WM, the GUI binary hangs during Qt initialization.
# Solution: Xvfb + openbox window manager + xdotool for synthetic events.
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Install mise
uses: jdx/mise-action@v3
# Note: apt cache removed - caching /var/cache/apt/archives causes permission
# issues and is largely negated by the immediate sudo apt-get update
- name: Install Xvfb, openbox, and X11 dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
xvfb \
openbox \
xdotool \
libxkbcommon-x11-0 \
libxcb-icccm4 \
libxcb-image0 \
libxcb-keysyms1 \
libxcb-randr0 \
libxcb-render-util0 \
libxcb-xinerama0 \
libxcb-xfixes0 \
libxcb-shape0 \
libxcb-cursor0 \
x11-utils \
libegl1 \
libgl1-mesa-dri \
libgl1 \
mesa-utils \
fontconfig \
fonts-dejavu-core
# Rebuild font cache (required after installing fonts, especially from cache)
sudo fc-cache -f -v
- name: Setup FreeCAD
uses: ./.github/actions/setup-freecad
- name: Start Xvfb and openbox
run: |
echo "Starting Xvfb virtual display..."
# Use -nolisten tcp to prevent TCP listeners for security
Xvfb :99 -screen 0 1920x1080x24 -nolisten tcp &
XVFB_PID=$!
echo "XVFB_PID=$XVFB_PID" >> "$GITHUB_ENV"
echo "DISPLAY=:99" >> "$GITHUB_ENV"
sleep 2
# Verify Xvfb is running
if ! ps -p $XVFB_PID > /dev/null; then
echo "ERROR: Xvfb failed to start"
exit 1
fi
echo "Xvfb started on display :99 (PID: $XVFB_PID)"
# Start openbox window manager (required for FreeCAD GUI)
# FreeCAD needs a WM to generate expose/configure events
echo "Starting openbox window manager..."
DISPLAY=:99 openbox &
OPENBOX_PID=$!
echo "OPENBOX_PID=$OPENBOX_PID" >> "$GITHUB_ENV"
sleep 1
if ! ps -p $OPENBOX_PID > /dev/null; then
echo "WARNING: openbox may have failed to start"
else
echo "openbox started (PID: $OPENBOX_PID)"
fi
- name: Verify X11 display
env:
DISPLAY: ":99"
run: |
echo "Checking X11 display..."
xdpyinfo | head -10 || echo "xdpyinfo not available"
- name: Cache uv dependencies
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "**/uv.lock"
- name: Install dependencies
run: uv sync --all-extras
- name: Verify fontconfig setup
run: |
echo "Checking fontconfig configuration..."
# Verify the system fontconfig config exists
if [ -f /etc/fonts/fonts.conf ]; then
echo "System fontconfig found at /etc/fonts/fonts.conf"
else
echo "ERROR: /etc/fonts/fonts.conf not found!"
ls -la /etc/fonts/ || echo "/etc/fonts/ directory doesn't exist"
fi
# Test fontconfig is working
echo "Testing fc-list..."
fc-list | head -5 || echo "fc-list failed"
echo "Fontconfig setup verified."
- name: Start FreeCAD GUI with MCP bridge
env:
DISPLAY: ":99"
QT_QPA_PLATFORM: xcb
# Use software rendering for OpenGL (more reliable in CI)
LIBGL_ALWAYS_SOFTWARE: "1"
# Explicit fontconfig paths for AppImage compatibility
# The AppImage's bundled fontconfig may not find system config automatically
FONTCONFIG_FILE: /etc/fonts/fonts.conf
FONTCONFIG_PATH: /etc/fonts
# Set XDG_RUNTIME_DIR to avoid Qt warning
XDG_RUNTIME_DIR: /tmp/runtime-root
run: |
# Create XDG_RUNTIME_DIR
mkdir -p "$XDG_RUNTIME_DIR"
chmod 700 "$XDG_RUNTIME_DIR"
echo "Starting FreeCAD GUI with MCP bridge under Xvfb + openbox..."
echo "DISPLAY=$DISPLAY"
echo "QT_QPA_PLATFORM=$QT_QPA_PLATFORM"
echo "LIBGL_ALWAYS_SOFTWARE=$LIBGL_ALWAYS_SOFTWARE"
echo "XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR"
# Verify freecadcmd works (headless mode)
echo "=== Testing freecadcmd (headless) ==="
freecadcmd --version || echo "freecadcmd failed"
# Export fontconfig vars so they're available to backgrounded process
export FONTCONFIG_FILE FONTCONFIG_PATH
# Start FreeCAD GUI (not headless) with MCP bridge
# Uses blocking_bridge.py which blocks with run_forever() to keep process alive
# GUI features are available since we're using 'freecad' not 'freecadcmd'
# Use setsid to create a new process group for reliable cleanup
setsid freecad addon/FreecadRobustMCPBridge/freecad_mcp_bridge/blocking_bridge.py > /tmp/freecad_gui.log 2>&1 &
FREECAD_PID=$!
echo "FREECAD_PID=$FREECAD_PID" >> "$GITHUB_ENV"
echo "Waiting for MCP bridge to start..."
# Send xdotool events to help FreeCAD GUI initialize
# FreeCAD needs mouse/keyboard events to process its event queue
for i in {1..90}; do
# Send synthetic events to help FreeCAD initialize
xdotool mousemove $((400 + i*3)) $((300 + i*3)) click 1 key Escape 2>/dev/null || true
# Check if process is still running
if ! ps -p "$FREECAD_PID" > /dev/null 2>&1; then
echo "ERROR: FreeCAD process died (PID $FREECAD_PID)"
echo "=== FreeCAD GUI log ==="
cat /tmp/freecad_gui.log || echo "Log file is empty or missing"
exit 1
fi
if curl -s --max-time 2 -X POST \
-H "Content-Type: text/xml" \
-d '<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName></methodCall>' \
http://localhost:9875 > /dev/null 2>&1; then
echo "MCP bridge is ready (took ${i}s)"
break
fi
if [ "$i" -eq 90 ]; then
echo "ERROR: MCP bridge did not start within 90s"
echo "=== FreeCAD GUI log ==="
cat /tmp/freecad_gui.log || echo "Log file is empty or missing"
echo "=== Process status ==="
pgrep -a freecad || echo "No freecad processes found"
kill -- "-$FREECAD_PID" 2>/dev/null || kill "$FREECAD_PID" 2>/dev/null || true
exit 1
fi
# Show progress every 10 seconds
if [ $((i % 10)) -eq 0 ]; then
echo "Still waiting... (${i}s)"
# Show last few lines of log
tail -5 /tmp/freecad_gui.log 2>/dev/null || true
fi
sleep 1
done
# Verify GUI is available
sleep 2
BRIDGE_INSTANCE_ID=$(grep -o 'FREECAD_MCP_BRIDGE_INSTANCE_ID=[^ ]*' /tmp/freecad_gui.log | cut -d= -f2 | head -1)
echo "Bridge Instance ID: $BRIDGE_INSTANCE_ID"
# Check if GUI is up
if grep -q "GuiUp.*True\|GUI.*available" /tmp/freecad_gui.log 2>/dev/null; then
echo "FreeCAD GUI is available"
else
echo "Note: Could not confirm GUI status from log (may still work)"
fi
- name: Run GUI mode integration tests
env:
FREECAD_MODE: xmlrpc
DISPLAY: ":99"
# Tests for GUI-only features (screenshots, visibility, colors, camera, etc.)
# GUI tests under Xvfb can be flaky; don't fail the workflow during stabilization
continue-on-error: true
run: |
uv run pytest tests/integration/test_gui_mode.py -v --tb=short
- name: Upload FreeCAD logs on failure
if: failure() || cancelled()
uses: actions/upload-artifact@v6
with:
name: freecad-gui-logs
path: /tmp/freecad_gui.log
retention-days: 7
if-no-files-found: ignore
- name: Stop FreeCAD, openbox, and Xvfb
if: always()
run: |
if [ -n "$FREECAD_PID" ]; then
# Kill process group (handles child processes spawned by FreeCAD/AppImage)
kill -- "-$FREECAD_PID" 2>/dev/null || kill "$FREECAD_PID" 2>/dev/null || true
fi
if [ -n "$OPENBOX_PID" ]; then
kill "$OPENBOX_PID" 2>/dev/null || true
fi
if [ -n "$XVFB_PID" ]; then
kill "$XVFB_PID" 2>/dev/null || true
fi
lint-macros:
name: Lint Macro Files
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Install mise
uses: jdx/mise-action@v3
- name: Cache uv dependencies
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "**/uv.lock"
- name: Install dependencies
run: uv sync --all-extras
- name: Lint macro files with Ruff
run: |
# Run ruff on macro files (check only, don't fail on issues specific to FreeCAD macros)
uv run ruff check macros/ --select=E,F,W --ignore=F401,E402 || true
- name: Check macro documentation
run: |
# Ensure each macro directory has a README
for macro_dir in macros/*/; do
if [ -d "$macro_dir" ]; then
readme_found=false
for readme in "$macro_dir"README*.md; do
if [ -f "$readme" ]; then
readme_found=true
break
fi
done
if [ "$readme_found" = false ]; then
echo "WARNING: No README found in $macro_dir"
else
echo "OK: README found in $macro_dir"
fi
fi
done
-6
View File
@@ -53,9 +53,3 @@ jobs:
- name: Run type checking - name: Run type checking
run: uv run mypy src/ run: uv run mypy src/
# Note: FreeCAD integration tests are handled by the "Integration Tests" workflow
# (macro-test.yaml) which runs on every PR. That workflow provides:
# - Headless tests: test_headless_mode.py, test_cut_object_for_magnets.py, test_multi_export.py
# - GUI tests: test_gui_mode.py (uses Xvfb virtual display)
# See .github/workflows/macro-test.yaml for details.
+39 -58
View File
@@ -179,9 +179,6 @@ just freecad::run-headless # Run FreeCAD headless with MCP bridge
# Installation commands (for end users) # Installation commands (for end users)
just install::mcp-server # Install MCP server system-wide (via uv tool) just install::mcp-server # Install MCP server system-wide (via uv tool)
just install::mcp-bridge-workbench # Install FreeCAD workbench addon just install::mcp-bridge-workbench # Install FreeCAD workbench addon
just install::macro-all # Install all macros
just install::macro-cut # Install CutObjectForMagnets macro
just install::macro-export # Install MultiExport macro
just install::status # Check installation status just install::status # Check installation status
# Quality commands # Quality commands
@@ -195,7 +192,7 @@ just quality::scan # Run all secrets scanners
# Testing commands # Testing commands
just testing::unit # Run unit tests just testing::unit # Run unit tests
just testing::cov # Run tests with coverage just testing::cov # Run tests with coverage
just testing::fast # Run tests without slow markers just testing::quick # Run tests without slow markers
just testing::integration # Run integration tests just testing::integration # Run integration tests
just testing::integration-freecad-auto # Integration tests with auto FreeCAD startup just testing::integration-freecad-auto # Integration tests with auto FreeCAD startup
just testing::watch # Run tests in watch mode just testing::watch # Run tests in watch mode
@@ -231,8 +228,6 @@ just coderabbit::review-fix # Review with auto-fix suggestions
just release::status # Show unreleased changes across all components just release::status # Show unreleased changes across all components
just release::tag-mcp-server 1.0.0 # Release MCP server (PyPI + Docker) just release::tag-mcp-server 1.0.0 # Release MCP server (PyPI + Docker)
just release::tag-workbench 1.0.0 # Release Robust MCP Bridge workbench just release::tag-workbench 1.0.0 # Release Robust MCP Bridge workbench
just release::tag-macro-magnets 1.0.0 # Release Cut Object for Magnets macro
just release::tag-macro-export 1.0.0 # Release Multi Export macro
just release::list-tags # List all release tags just release::list-tags # List all release tags
just release::latest-versions # Show latest version of each component just release::latest-versions # Show latest version of each component
just release::delete-tag <tag> # Delete a release tag (local and remote) just release::delete-tag <tag> # Delete a release tag (local and remote)
@@ -246,10 +241,10 @@ just all-with-integration # Run all checks and integration tests
#### Just Module Structure #### Just Module Structure
| Module | Description | Key Commands | | Module | Description | Key Commands |
| --------------- | ------------------------------------- | --------------------------------------------------- | | --------------- | ------------------------------------- | ---------------------------------------------------- |
| `mcp` | MCP server commands | `run`, `run-debug`, `run-http` | | `mcp` | MCP server commands | `run`, `run-debug`, `run-http` |
| `freecad` | FreeCAD running commands | `run-gui`, `run-headless`, `run-gui-custom` | | `freecad` | FreeCAD running commands | `run-gui`, `run-headless`, `run-gui-custom` |
| `install` | User installation commands | `mcp-server`, `mcp-bridge-workbench`, `macro-all` | | `install` | User installation commands | `mcp-server`, `mcp-bridge-workbench`, `status` |
| `quality` | Code quality and linting | `check`, `lint`, `format`, `scan` | | `quality` | Code quality and linting | `check`, `lint`, `format`, `scan` |
| `testing` | Test execution | `unit`, `cov`, `integration-freecad-auto`, `watch` | | `testing` | Test execution | `unit`, `cov`, `integration-freecad-auto`, `watch` |
| `docker` | Docker build and run commands | `build`, `build-multi`, `run`, `clean-all` | | `docker` | Docker build and run commands | `build`, `build-multi`, `run`, `clean-all` |
@@ -521,6 +516,26 @@ just documentation::deploy-latest 1.0.0 # Deploy version and set as latest
**Note**: Local `deploy-*` commands modify the `gh-pages` branch locally. The GitHub Actions workflow handles actual deployment to GitHub Pages. **Note**: Local `deploy-*` commands modify the `gh-pages` branch locally. The GitHub Actions workflow handles actual deployment to GitHub Pages.
**Initial GitHub Pages Setup**:
This repo uses **mike** for versioned documentation, which requires a `gh-pages` branch:
1. **Create the gh-pages branch** (if it doesn't exist):
```bash
git checkout --orphan gh-pages
git reset --hard
git commit --allow-empty -m "Initialize gh-pages branch"
git push origin gh-pages
git checkout main
```
2. **Configure GitHub Pages** in repo Settings → Pages:
- Source: **Deploy from a branch**
- Branch: `gh-pages` / `/ (root)`
3. **First deployment**: Push to `main` or manually trigger the `docs.yaml` workflow
--- ---
## Testing Requirements ## Testing Requirements
@@ -587,7 +602,7 @@ class TestCalculateTotal:
```bash ```bash
just testing::unit # Run unit tests just testing::unit # Run unit tests
just testing::cov # Run tests with coverage report just testing::cov # Run tests with coverage report
just testing::fast # Run tests without slow markers just testing::quick # Run tests without slow markers
just testing::all # Run all tests including integration just testing::all # Run all tests including integration
uv run pytest tests/unit/ # Run specific test directory uv run pytest tests/unit/ # Run specific test directory
uv run pytest -k "test_name" # Run specific test by name uv run pytest -k "test_name" # Run specific test by name
@@ -737,10 +752,7 @@ project-root/
│ ├── workflows/ # GitHub Actions workflows │ ├── workflows/ # GitHub Actions workflows
│ │ ├── codeql.yaml # Security analysis │ │ ├── codeql.yaml # Security analysis
│ │ ├── docker.yaml # Docker build (CI) │ │ ├── docker.yaml # Docker build (CI)
│ │ ├── macro-cut-magnets-release.yaml # Macro release │ │ ├── docs.yaml # Documentation deployment
│ │ ├── macro-multi-export-release.yaml # Macro release
│ │ ├── macro-release-reusable.yaml # Shared macro release logic
│ │ ├── macro-test.yaml # Macro testing
│ │ ├── mcp-server-release.yaml # MCP server → PyPI/Docker │ │ ├── mcp-server-release.yaml # MCP server → PyPI/Docker
│ │ ├── mcp-workbench-release.yaml # Workbench → GitHub Release │ │ ├── mcp-workbench-release.yaml # Workbench → GitHub Release
│ │ ├── pre-commit.yaml # Pre-commit checks │ │ ├── pre-commit.yaml # Pre-commit checks
@@ -757,7 +769,6 @@ project-root/
│ ├── development/ # Developer guides │ ├── development/ # Developer guides
│ ├── getting-started/ # Installation, quickstart │ ├── getting-started/ # Installation, quickstart
│ ├── guide/ # User guides │ ├── guide/ # User guides
│ ├── macros/ # Macro documentation
│ ├── reference/ # API reference │ ├── reference/ # API reference
│ ├── variables.yaml # MkDocs macro variables │ ├── variables.yaml # MkDocs macro variables
│ └── index.md # Documentation home │ └── index.md # Documentation home
@@ -772,13 +783,6 @@ project-root/
│ ├── quality.just # Code quality commands │ ├── quality.just # Code quality commands
│ ├── release.just # Release/tagging commands │ ├── release.just # Release/tagging commands
│ └── testing.just # Test commands │ └── testing.just # Test commands
├── macros/ # FreeCAD macro source
│ ├── Cut_Object_for_Magnets/
│ │ ├── CutObjectForMagnets.FCMacro
│ │ └── README-CutObjectForMagnets.md
│ └── Multi_Export/
│ ├── MultiExport.FCMacro
│ └── README-MultiExport.md
├── src/ ├── src/
│ └── freecad_mcp/ # Main MCP server package │ └── freecad_mcp/ # Main MCP server package
│ ├── bridge/ # FreeCAD connection bridges │ ├── bridge/ # FreeCAD connection bridges
@@ -934,7 +938,7 @@ EOF
4. **Variable expansion**: `${VAR}` works inside heredocs for bash variables 4. **Variable expansion**: `${VAR}` works inside heredocs for bash variables
5. **Use `\\n` for newlines**: In heredoc strings that need literal `\n`, use `\\n` 5. **Use `\\n` for newlines**: In heredoc strings that need literal `\n`, use `\\n`
See the recipes in `just/freecad.just` (e.g., `run-gui`, `run-headless`, `install-cut-macro`) for working examples. See the recipes in `just/freecad.just` (e.g., `run-gui`, `run-headless`) for working examples.
--- ---
@@ -1320,23 +1324,19 @@ This project uses component-specific release workflows along with CI/CD pipeline
### CI Workflows ### CI Workflows
| Workflow | Trigger | Purpose | | Workflow | Trigger | Purpose |
| ------------------ | ------------------------------ | --------------------------------------------------------- | | ----------------- | ----------------------------- | --------------------------------------------------------- |
| `test.yaml` | Push, PR | Runs unit tests and integration tests on Ubuntu and macOS | | `test.yaml` | Push, PR | Runs unit tests and integration tests on Ubuntu and macOS |
| `pre-commit.yaml` | Push, PR | Runs all pre-commit hooks for code quality | | `pre-commit.yaml` | Push, PR | Runs all pre-commit hooks for code quality |
| `docker.yaml` | Push, PR | Builds Docker image to verify Dockerfile works | | `docker.yaml` | Push, PR | Builds Docker image to verify Dockerfile works |
| `macro-test.yaml` | Push, PR | Tests FreeCAD macros in headless Docker environment |
| `codeql.yaml` | Push, PR, scheduled | GitHub CodeQL security analysis | | `codeql.yaml` | Push, PR, scheduled | GitHub CodeQL security analysis |
| `docs.yaml` | Push to main, MCP server tags | Deploys versioned documentation to GitHub Pages | | `docs.yaml` | Push to main, MCP server tags | Deploys versioned documentation to GitHub Pages |
### Release Workflows ### Release Workflows
| Workflow | Trigger | Purpose | | Workflow | Trigger | Purpose |
| --------------------------------- | ---------------------------------------- | ------------------------------------------------------ | | ---------------------------- | ------------------------------ | ------------------------------------------------------ |
| `mcp-server-release.yaml` | Tag: `robust-mcp-server-v*` | Builds and publishes MCP server to PyPI and Docker Hub | | `mcp-server-release.yaml` | Tag: `robust-mcp-server-v*` | Builds and publishes MCP server to PyPI and Docker Hub |
| `mcp-workbench-release.yaml` | Tag: `robust-mcp-workbench-v*` | Creates GitHub Release with workbench addon archive | | `mcp-workbench-release.yaml` | Tag: `robust-mcp-workbench-v*` | Creates GitHub Release with workbench addon archive |
| `macro-cut-magnets-release.yaml` | Tag: `macro-cut-object-for-magnets-v*` | Creates GitHub Release with macro archive |
| `macro-multi-export-release.yaml` | Tag: `macro-multi-export-v*` | Creates GitHub Release with macro archive |
| `macro-release-reusable.yaml` | Called by macro release workflows | Shared logic for macro releases (DRY) |
### Release Workflow Features ### Release Workflow Features
@@ -1366,11 +1366,9 @@ This project uses component-specific release workflows along with CI/CD pipeline
This project uses **component-specific versioning**. Each component has its own git tag and release workflow: This project uses **component-specific versioning**. Each component has its own git tag and release workflow:
| Component | Tag Format | Releases To | | Component | Tag Format | Releases To |
| ---------------------------- | ------------------------------------- | ------------------------------------------ | | ----------------- | ----------------------------- | ------------------------------------------ |
| MCP Server | `robust-mcp-server-vX.Y.Z` | PyPI/TestPyPI*, Docker Hub, GitHub Release | | MCP Server | `robust-mcp-server-vX.Y.Z` | PyPI/TestPyPI*, Docker Hub, GitHub Release |
| Robust MCP Bridge Workbench | `robust-mcp-workbench-vX.Y.Z` | GitHub Release (archive) | | Robust MCP Bridge | `robust-mcp-workbench-vX.Y.Z` | GitHub Release (archive) |
| Cut Object for Magnets Macro | `macro-cut-object-for-magnets-vX.Y.Z` | GitHub Release (archive) |
| Multi Export Macro | `macro-multi-export-vX.Y.Z` | GitHub Release (archive) |
*Stable releases (`X.Y.Z`) publish to PyPI; non-stable releases (alpha, beta, rc) publish to TestPyPI only. *Stable releases (`X.Y.Z`) publish to PyPI; non-stable releases (alpha, beta, rc) publish to TestPyPI only.
@@ -1379,11 +1377,9 @@ This project uses **component-specific versioning**. Each component has its own
Each component has its own `RELEASE_NOTES.md` file. Release workflows automatically extract the relevant section for GitHub Releases. Each component has its own `RELEASE_NOTES.md` file. Release workflows automatically extract the relevant section for GitHub Releases.
| Component | Release Notes File | | Component | Release Notes File |
| ---------------------------- | ----------------------------------------------------- | | ----------------- | ----------------------------------------------- |
| MCP Server | `src/freecad_mcp/RELEASE_NOTES.md` | | MCP Server | `src/freecad_mcp/RELEASE_NOTES.md` |
| Robust MCP Bridge Workbench | `addon/FreecadRobustMCPBridge/RELEASE_NOTES.md` | | Robust MCP Bridge | `addon/FreecadRobustMCPBridge/RELEASE_NOTES.md` |
| Cut Object for Magnets Macro | `macros/Cut_Object_for_Magnets/RELEASE_NOTES.md` |
| Multi Export Macro | `macros/Multi_Export/RELEASE_NOTES.md` |
**Before releasing a component:** **Before releasing a component:**
@@ -1392,8 +1388,6 @@ Each component has its own `RELEASE_NOTES.md` file. Release workflows automatica
```bash ```bash
just release::draft-notes mcp-server just release::draft-notes mcp-server
just release::draft-notes workbench just release::draft-notes workbench
just release::draft-notes macro-magnets
just release::draft-notes macro-export
``` ```
2. **Edit the component's RELEASE_NOTES.md** file, adding a new version section at the top: 2. **Edit the component's RELEASE_NOTES.md** file, adding a new version section at the top:
@@ -1438,12 +1432,10 @@ just release::draft-notes mcp-server
# See "Release Notes Management" above for format # See "Release Notes Management" above for format
``` ```
**Step 3: Bump versions** (for workbench/macros only - MCP server auto-bumps from tag): **Step 3: Bump versions** (for workbench only - MCP server auto-bumps from tag):
```bash ```bash
just release::bump-workbench 1.0.0 just release::bump-workbench 1.0.0
just release::bump-macro-magnets 1.0.0
just release::bump-macro-export 1.0.0
``` ```
**Step 4: Commit and push** your RELEASE_NOTES.md and version bump changes. **Step 4: Commit and push** your RELEASE_NOTES.md and version bump changes.
@@ -1456,10 +1448,6 @@ just release::tag-mcp-server 1.0.0
# Release the Robust MCP Bridge workbench # Release the Robust MCP Bridge workbench
just release::tag-workbench 1.0.0 just release::tag-workbench 1.0.0
# Release macros
just release::tag-macro-magnets 1.0.0
just release::tag-macro-export 1.0.0
``` ```
**Utility commands:** **Utility commands:**
@@ -1508,26 +1496,21 @@ All versions follow SemVer 2.0:
4. Creates archive (tar.gz + zip) 4. Creates archive (tar.gz + zip)
5. Creates GitHub release with archives 5. Creates GitHub release with archives
### Package.xml Per-Component Versioning ### Package.xml Versioning
The `package.xml` file uses per-content versioning as supported by FreeCAD: The `package.xml` file contains metadata for the FreeCAD addon:
```xml ```xml
<content> <content>
<workbench> <workbench>
<name>MCP Bridge</name> <name>Robust MCP Bridge</name>
<version>1.0.0</version> <version>1.0.0</version>
... ...
</workbench> </workbench>
<macro>
<name>Multi Export</name>
<version>0.8.0</version>
...
</macro>
</content> </content>
``` ```
Each component can have a different version, and the release workflows automatically update these when a component is released. The release workflow automatically updates the version when the workbench is released.
--- ---
@@ -1544,11 +1527,9 @@ Each component has its own `RELEASE_NOTES.md` file that is updated before releas
**Release notes files:** **Release notes files:**
| Component | File | | Component | File |
| ---------------------------- | ------------------------------------------------ | | ----------------- | ----------------------------------------------- |
| MCP Server | `src/freecad_mcp/RELEASE_NOTES.md` | | MCP Server | `src/freecad_mcp/RELEASE_NOTES.md` |
| Robust MCP Bridge Workbench | `addon/FreecadRobustMCPBridge/RELEASE_NOTES.md` | | Robust MCP Bridge | `addon/FreecadRobustMCPBridge/RELEASE_NOTES.md` |
| Cut Object for Magnets Macro | `macros/Cut_Object_for_Magnets/RELEASE_NOTES.md` |
| Multi Export Macro | `macros/Multi_Export/RELEASE_NOTES.md` |
--- ---
+22 -199
View File
@@ -1,26 +1,23 @@
# FreeCAD Robust MCP Suite # FreeCAD Robust MCP Server
[![CI Tests](https://github.com/spkane/freecad-robust-mcp-and-more/actions/workflows/test.yaml/badge.svg)](https://github.com/spkane/freecad-robust-mcp-and-more/actions/workflows/test.yaml) [![CI Tests](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/test.yaml/badge.svg)](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/test.yaml)
[![Integration Tests](https://github.com/spkane/freecad-robust-mcp-and-more/actions/workflows/macro-test.yaml/badge.svg)](https://github.com/spkane/freecad-robust-mcp-and-more/actions/workflows/macro-test.yaml) [![Docker Build](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/docker.yaml/badge.svg)](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/docker.yaml)
[![Docker Build](https://github.com/spkane/freecad-robust-mcp-and-more/actions/workflows/docker.yaml/badge.svg)](https://github.com/spkane/freecad-robust-mcp-and-more/actions/workflows/docker.yaml) [![Pre-commit](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/pre-commit.yaml/badge.svg)](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/pre-commit.yaml)
[![Pre-commit](https://github.com/spkane/freecad-robust-mcp-and-more/actions/workflows/pre-commit.yaml/badge.svg)](https://github.com/spkane/freecad-robust-mcp-and-more/actions/workflows/pre-commit.yaml) [![CodeQL](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/codeql.yaml/badge.svg)](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/codeql.yaml)
[![CodeQL](https://github.com/spkane/freecad-robust-mcp-and-more/actions/workflows/codeql.yaml/badge.svg)](https://github.com/spkane/freecad-robust-mcp-and-more/actions/workflows/codeql.yaml)
[![PyPI Version](https://img.shields.io/pypi/v/freecad-robust-mcp)](https://pypi.org/project/freecad-robust-mcp/) [![PyPI Version](https://img.shields.io/pypi/v/freecad-robust-mcp)](https://pypi.org/project/freecad-robust-mcp/)
[![Python Versions](https://img.shields.io/pypi/pyversions/freecad-robust-mcp)](https://pypi.org/project/freecad-robust-mcp/) [![Python Versions](https://img.shields.io/pypi/pyversions/freecad-robust-mcp)](https://pypi.org/project/freecad-robust-mcp/)
[![Docker Image Version](https://img.shields.io/docker/v/spkane/freecad-robust-mcp?sort=semver&label=docker)](https://hub.docker.com/r/spkane/freecad-robust-mcp) [![Docker Image Version](https://img.shields.io/docker/v/spkane/freecad-robust-mcp?sort=semver&label=docker)](https://hub.docker.com/r/spkane/freecad-robust-mcp)
[![Documentation](https://img.shields.io/badge/docs-latest-blue.svg)](https://spkane.github.io/freecad-robust-mcp-and-more/) [![Documentation](https://img.shields.io/badge/docs-latest-blue.svg)](https://spkane.github.io/freecad-addon-robust-mcp-server/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that enables integration between AI assistants (Claude, GPT, and other MCP-compatible tools) and [FreeCAD](https://www.freecadweb.org/), allowing AI-assisted development and debugging of 3D models, macros, and workbenches. An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that enables integration between AI assistants (Claude, GPT, and other MCP-compatible tools) and [FreeCAD](https://www.freecadweb.org/), allowing AI-assisted development and debugging of 3D models, macros, and workbenches.
> Also includes standalone FreeCAD macros for common tasks.
## Table of Contents ## Table of Contents
<!--TOC--> <!--TOC-->
- [FreeCAD Robust MCP Suite](#freecad-robust-mcp-suite) - [FreeCAD Robust MCP Server](#freecad-robust-mcp-server)
- [Table of Contents](#table-of-contents) - [Table of Contents](#table-of-contents)
- [Features](#features) - [Features](#features)
- [Requirements](#requirements) - [Requirements](#requirements)
@@ -59,10 +56,6 @@ An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that
- [Export/Import (7 tools)](#exportimport-7-tools) - [Export/Import (7 tools)](#exportimport-7-tools)
- [Macro Management (6 tools)](#macro-management-6-tools) - [Macro Management (6 tools)](#macro-management-6-tools)
- [Parts Library (2 tools)](#parts-library-2-tools) - [Parts Library (2 tools)](#parts-library-2-tools)
- [FreeCAD Macros](#freecad-macros)
- [Downloading Macros](#downloading-macros)
- [CutObjectForMagnets](#cutobjectformagnets)
- [MultiExport](#multiexport)
- [For Developers](#for-developers) - [For Developers](#for-developers)
- [Robust MCP Server Development](#robust-mcp-server-development) - [Robust MCP Server Development](#robust-mcp-server-development)
- [Prerequisites](#prerequisites) - [Prerequisites](#prerequisites)
@@ -74,9 +67,6 @@ An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that
- [Headless Mode (for automation/CI)](#headless-mode-for-automationci) - [Headless Mode (for automation/CI)](#headless-mode-for-automationci)
- [Running Tests](#running-tests) - [Running Tests](#running-tests)
- [Code Quality](#code-quality) - [Code Quality](#code-quality)
- [Macro Development](#macro-development)
- [CutObjectForMagnets Macro](#cutobjectformagnets-macro)
- [MultiExport Macro](#multiexport-macro)
- [Architecture](#architecture) - [Architecture](#architecture)
- [Acknowledgements](#acknowledgements) - [Acknowledgements](#acknowledgements)
- [Related Projects](#related-projects) - [Related Projects](#related-projects)
@@ -90,7 +80,6 @@ An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that
- **Multiple Connection Modes**: XML-RPC (recommended), JSON-RPC socket, or embedded - **Multiple Connection Modes**: XML-RPC (recommended), JSON-RPC socket, or embedded
- **GUI & Headless Support**: Full modeling in headless mode, plus screenshots/colors in GUI mode - **GUI & Headless Support**: Full modeling in headless mode, plus screenshots/colors in GUI mode
- **Macro Development**: Create, edit, run, and template FreeCAD macros via MCP - **Macro Development**: Create, edit, run, and template FreeCAD macros via MCP
- **Standalone Macros**: Useful FreeCAD macros that work independently of the Robust MCP Server
## Requirements ## Requirements
@@ -101,20 +90,20 @@ An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that
## For Users ## For Users
This section covers installation and usage for end users who want to use the Robust MCP Server with AI assistants or the standalone FreeCAD macros. This section covers installation and usage for end users who want to use the Robust MCP Server with AI assistants.
### Quick Links ### Quick Links
| Resource | Description | | Resource | Description |
| --------------------------------------------------------------------------------- | ------------------------------------------------------------ | | ------------------------------------------------------------------------------------- | --------------------------------------------- |
| [**Documentation**](https://spkane.github.io/freecad-robust-mcp-and-more/) | Full documentation, guides, and API reference | | [**Documentation**](https://spkane.github.io/freecad-addon-robust-mcp-server/) | Full documentation, guides, and API reference |
| [Docker Hub](https://hub.docker.com/r/spkane/freecad-robust-mcp) | Pre-built Docker images for easy deployment | | [Docker Hub](https://hub.docker.com/r/spkane/freecad-robust-mcp) | Pre-built Docker images for easy deployment |
| [PyPI](https://pypi.org/project/freecad-robust-mcp/) | Python package for pip installation | | [PyPI](https://pypi.org/project/freecad-robust-mcp/) | Python package for pip installation |
| [GitHub Releases](https://github.com/spkane/freecad-robust-mcp-and-more/releases) | Release archives, changelogs, and standalone macro downloads | | [GitHub Releases](https://github.com/spkane/freecad-addon-robust-mcp-server/releases) | Release archives and changelogs |
## Robust MCP Server ## Robust MCP Server
> **Note**: Since this repository has more than just the Robust MCP Server in it, the Linux container and PyPi projects releases are both simply named `freecad-robust-mcp` which differs from the name of this git repository. > **Note**: The Linux container and PyPI package are both named `freecad-robust-mcp` which differs slightly from this git repository name.
### Installation ### Installation
@@ -127,8 +116,8 @@ pip install freecad-robust-mcp
#### Using mise and just (from source) #### Using mise and just (from source)
```bash ```bash
git clone https://github.com/spkane/freecad-robust-mcp-and-more.git git clone https://github.com/spkane/freecad-addon-robust-mcp-server.git
cd freecad-robust-mcp-and-more cd freecad-addon-robust-mcp-server
# Install mise via the Official mise installer script (if not already installed) # Install mise via the Official mise installer script (if not already installed)
curl https://mise.run | sh curl https://mise.run | sh
@@ -147,8 +136,8 @@ Run the Robust MCP Server in a container. This is useful for isolated environmen
docker pull spkane/freecad-robust-mcp docker pull spkane/freecad-robust-mcp
# Or build locally # Or build locally
git clone https://github.com/spkane/freecad-robust-mcp-and-more.git git clone https://github.com/spkane/freecad-addon-robust-mcp-server.git
cd freecad-robust-mcp-and-more cd freecad-addon-robust-mcp-server
docker build -t freecad-robust-mcp . docker build -t freecad-robust-mcp .
# Or use just commands (if you have mise/just installed) # Or use just commands (if you have mise/just installed)
@@ -205,7 +194,7 @@ If installed from source with mise/uv:
"mcpServers": { "mcpServers": {
"freecad": { "freecad": {
"command": "/path/to/mise/shims/uv", "command": "/path/to/mise/shims/uv",
"args": ["run", "--project", "/path/to/freecad-robust-mcp-and-more", "freecad-mcp"], "args": ["run", "--project", "/path/to/freecad-addon-robust-mcp-server", "freecad-mcp"],
"env": { "env": {
"FREECAD_MODE": "xmlrpc" "FREECAD_MODE": "xmlrpc"
} }
@@ -491,132 +480,6 @@ The Robust MCP Server provides **83 tools** organized into categories. Tools mar
--- ---
## FreeCAD Macros
This project includes standalone FreeCAD macros that can be used independently of the Robust MCP Server. These are useful for FreeCAD users who want the macros without setting up the full MCP integration.
### Downloading Macros
Pre-packaged macro archives are available with each release:
1. Go to the [Releases page](https://github.com/spkane/freecad-robust-mcp-and-more/releases)
1. Download the macro archive for your platform:
- `freecad-macros-X.Y.Z.tar.gz` (Linux/macOS)
- `freecad-macros-X.Y.Z.zip` (Windows)
1. Extract and copy the `.FCMacro` files to your FreeCAD macro directory:
- **macOS**: `~/Library/Application Support/FreeCAD/Macro/`
- **Linux**: `~/.local/share/FreeCAD/Macro/`
- **Windows**: `%APPDATA%/FreeCAD/Macro/`
1. **(Optional)** Copy the `.svg` icon files to the same directory for custom icons in FreeCAD's macro menu
### CutObjectForMagnets
Intelligently cuts 3D objects along a plane and automatically places magnet holes with built-in surface penetration detection. Perfect for creating multi-part prints that snap together with magnets.
**Features:**
- Smart surface detection - skips holes that would penetrate outer surfaces
- Supports preset planes (XY, XZ, YZ) or custom datum planes for angled cuts
- Count-based hole placement with even distribution
- Dual-part validation ensures alignment
- Non-destructive (original object is hidden, not deleted)
**Installation:**
```bash
# If you have the source:
just install-cut-macro
# Or manually copy CutObjectForMagnets.FCMacro from macros/Cut_Object_for_Magnets/
# to your FreeCAD macro directory:
# macOS: ~/Library/Application Support/FreeCAD/Macro/
# Linux: ~/.local/share/FreeCAD/Macro/
# Windows: %APPDATA%/FreeCAD/Macro/
```
**Usage:**
1. Open your model in FreeCAD
1. Select the object to cut
1. Go to **Macro -> Macros... -> CutObjectForMagnets -> Execute**
1. Configure the cut plane and magnet hole parameters
1. Click **Execute Cut**
**Parameters:**
- **Plane:** XY, XZ, YZ, or a model datum plane
- **Offset:** Distance from origin (for preset planes)
- **Hole diameter:** Size of magnet holes (e.g., 6.2mm for 6mm magnets)
- **Hole depth:** How deep holes go into each piece
- **Number of holes:** Total holes to create (evenly distributed)
- **Edge clearance:** Distance from hole edge to outer surface
See [macros/Cut_Object_for_Magnets/README-CutObjectForMagnets.md](macros/Cut_Object_for_Magnets/README-CutObjectForMagnets.md) for detailed documentation.
**Uninstall:**
```bash
just uninstall-cut-macro
```
### MultiExport
Export selected FreeCAD objects to multiple file formats simultaneously. Supports 8 formats with a convenient checkbox dialog and smart defaults.
**Features:**
- Export to 8 formats: STL, STEP, 3MF, OBJ, IGES, BREP, PLY, AMF
- Smart defaults: STL, STEP, and 3MF pre-selected
- Intelligent path defaults based on document location
- Configurable mesh quality (tolerance and deflection)
- Real-time file preview before export
- Multi-object batch export support
**Installation:**
```bash
# If you have the source:
just install::macro-export
# Or manually copy MultiExport.FCMacro from macros/Multi_Export/
# to your FreeCAD macro directory:
# macOS: ~/Library/Application Support/FreeCAD/Macro/
# Linux: ~/.local/share/FreeCAD/Macro/
# Windows: %APPDATA%/FreeCAD/Macro/
```
**Usage:**
1. Select one or more objects in FreeCAD (Ctrl+click for multiple)
1. Go to **Macro -> Macros... -> MultiExport -> Execute**
1. Choose export formats (checkboxes)
1. Set output directory and base filename
1. Adjust mesh quality if needed
1. Click **Export**
**Supported Formats:**
| Format | Extension | Default | Description |
| ------ | --------- | ------- | ------------------------------------ |
| STL | `.stl` | Yes | Standard 3D printing format |
| STEP | `.step` | Yes | CAD interchange (preserves geometry) |
| 3MF | `.3mf` | Yes | Modern 3D printing with metadata |
| OBJ | `.obj` | No | 3D graphics and game engines |
| IGES | `.iges` | No | Legacy CAD interchange |
| BREP | `.brep` | No | OpenCASCADE native format |
| PLY | `.ply` | No | Polygon file for 3D scanning |
| AMF | `.amf` | No | Additive manufacturing format |
See [macros/Multi_Export/README-MultiExport.md](macros/Multi_Export/README-MultiExport.md) for detailed documentation.
**Uninstall:**
```bash
just freecad::uninstall-export-macro
```
---
## For Developers ## For Developers
This section covers development setup, contributing, and working with the codebase. This section covers development setup, contributing, and working with the codebase.
@@ -632,8 +495,8 @@ This section covers development setup, contributing, and working with the codeba
```bash ```bash
# Clone the repository # Clone the repository
git clone https://github.com/spkane/freecad-robust-mcp-and-more.git git clone https://github.com/spkane/freecad-addon-robust-mcp-server.git
cd freecad-robust-mcp-and-more cd freecad-addon-robust-mcp-server
# Install mise via the Official mise installer script (if not already installed) # Install mise via the Official mise installer script (if not already installed)
curl https://mise.run | sh curl https://mise.run | sh
@@ -662,7 +525,7 @@ Create a `.mcp.json` file in the project directory:
"mcpServers": { "mcpServers": {
"freecad": { "freecad": {
"command": "/path/to/mise/shims/uv", "command": "/path/to/mise/shims/uv",
"args": ["run", "--project", "/path/to/freecad-robust-mcp-and-more", "freecad-mcp"], "args": ["run", "--project", "/path/to/freecad-addon-robust-mcp-server", "freecad-mcp"],
"env": { "env": {
"FREECAD_MODE": "xmlrpc", "FREECAD_MODE": "xmlrpc",
"FREECAD_SOCKET_HOST": "localhost", "FREECAD_SOCKET_HOST": "localhost",
@@ -677,9 +540,9 @@ Create a `.mcp.json` file in the project directory:
**Replace the paths with your actual paths:** **Replace the paths with your actual paths:**
| Placeholder | Description | Example | | Placeholder | Description | Example |
| -------------------------------------- | ------------------------------ | ------------------------------------------ | | ------------------------------------------- | ------------------------------- | ---------------------------------------------- |
| `/path/to/mise/shims/uv` | Full path to uv via mise shims | `~/.local/share/mise/shims/uv` | | `/path/to/mise/shims/uv` | Full path to uv via mise shims | `~/.local/share/mise/shims/uv` |
| `/path/to/freecad-robust-mcp-and-more` | Project directory | `/home/me/dev/freecad-robust-mcp-and-more` | | `/path/to/freecad-addon-robust-mcp-server` | Project directory | `/home/me/dev/freecad-addon-robust-mcp-server` |
| `/path/to/mise/shims` | mise shims directory for PATH | `~/.local/share/mise/shims` | | `/path/to/mise/shims` | mise shims directory for PATH | `~/.local/share/mise/shims` |
**Finding your mise shims path:** **Finding your mise shims path:**
@@ -789,46 +652,6 @@ just quality::secrets
--- ---
## Macro Development
### CutObjectForMagnets Macro
**Location:** `macros/Cut_Object_for_Magnets/`
**Installation for development:**
```bash
just install::macro-cut
```
**Uninstall:**
```bash
just install::uninstall-macro-cut
```
See [macros/Cut_Object_for_Magnets/README-CutObjectForMagnets.md](macros/Cut_Object_for_Magnets/README-CutObjectForMagnets.md) for detailed documentation on the macro's internals.
### MultiExport Macro
**Location:** `macros/Multi_Export/`
**Installation for development:**
```bash
just install::macro-export
```
**Uninstall:**
```bash
just install::uninstall-macro-export
```
See [macros/Multi_Export/README-MultiExport.md](macros/Multi_Export/README-MultiExport.md) for detailed documentation on the macro's internals.
---
## Architecture ## Architecture
See the [detailed architecture document](docs/development/architecture-detailed.md) for design documentation covering: See the [detailed architecture document](docs/development/architecture-detailed.md) for design documentation covering:
+83
View File
@@ -259,6 +259,89 @@ Embedded mode receives **minimal testing**:
--- ---
## Future Considerations
### Bundled vs. Separate Server Architecture
The current architecture keeps the MCP server separate from the FreeCAD addon/workbench. This section documents the trade-offs and potential future directions.
#### Current Approach: Separate Components
```text
┌─────────────────┐ XML-RPC/Socket ┌─────────────────┐
│ MCP Server │◄──────────────────────►│ FreeCAD │
│ (separate venv) │ │ (+ Workbench) │
└─────────────────┘ └─────────────────┘
```
**Advantages:**
- Server runs in its own Python environment with full control over dependencies
- Allows remote server scenarios (AI/server on powerful machine, FreeCAD on workstation)
- Server can be updated independently of the addon
- No dependency conflicts with FreeCAD's embedded Python
- Easier testing and development
**Disadvantages:**
- Users must install two components separately
- More complex setup process
- Need to manage version compatibility between server and workbench
#### Potential Future: Bundled Server in Addon
```text
┌─────────────────────────────────────────┐
│ FreeCAD Addon │
│ ┌─────────────┐ ┌─────────────────┐ │
│ │ Workbench │◄──►│ Bundled Server │ │
│ │ (GUI) │ │ (subprocess) │ │
│ └─────────────┘ └─────────────────┘ │
└─────────────────────────────────────────┘
```
A bundled approach could:
- Provide single-install experience from FreeCAD Addon Manager
- Auto-start server when workbench loads
- Still support "Remote Server" mode for advanced users via preferences
**Implementation considerations:**
1. **Dependency management**: Server requires `fastmcp`, `httpx`, `uvicorn`, etc. These may conflict with FreeCAD's Python. Options:
- Bundle dependencies in addon (vendor them)
- Use subprocess with bundled `requirements.txt` and pip install on first run
- Create a minimal server that uses only stdlib
2. **Startup modes**:
```python
if preferences.use_remote_server:
connect_to(preferences.server_url)
else:
# Start bundled server in subprocess
subprocess.Popen([sys.executable, "-m", "robust_mcp_server"])
```
3. **Hybrid approach**: Default to bundled local server, but expose preferences for remote server URL (host:port) for advanced deployments.
#### Decision
Currently maintaining separate components because:
- Cleaner separation of concerns
- Proven reliability across platforms
- Easier to develop and test independently
- Remote server use case, while less common, is valuable for some workflows
May revisit bundling in a future major version when:
- Dependency requirements stabilize
- User feedback indicates strong preference for single-install
- A clean subprocess-based bundling approach is validated
---
## Next Steps ## Next Steps
- [Contributing](contributing.md) - How to contribute - [Contributing](contributing.md) - How to contribute
-1
View File
@@ -89,7 +89,6 @@ freecad-robust-mcp-and-more/
│ └── server.py # Main server entry point │ └── server.py # Main server entry point
├── addon/ # FreeCAD workbench addon ├── addon/ # FreeCAD workbench addon
│ └── FreecadRobustMCPBridge/ # Workbench files │ └── FreecadRobustMCPBridge/ # Workbench files
├── macros/ # Standalone FreeCAD macros
├── tests/ # Test suite ├── tests/ # Test suite
│ ├── unit/ # Unit tests │ ├── unit/ # Unit tests
│ └── integration/ # Integration tests │ └── integration/ # Integration tests
+11 -136
View File
@@ -1,6 +1,6 @@
# Release Process # Release Process
This project uses **component-specific versioning**. Each component (MCP Server, Workbench, Macros) has its own version and release cycle, allowing independent updates without affecting other components. This project uses **component-specific versioning**. Each component (MCP Server, Workbench) has its own version and release cycle, allowing independent updates without affecting other components.
## Quick Start ## Quick Start
@@ -9,46 +9,39 @@ The complete release workflow in order:
```bash ```bash
# 1. Pre-release checks # 1. Pre-release checks
just release::status # Check which components have unreleased changes just release::status # Check which components have unreleased changes
just release::changes-since mcp-server # View specific changes (or workbench, macro-magnets, macro-export) just release::changes-since mcp-server # View specific changes (or workbench)
just all # Run all quality checks (must pass) just all # Run all quality checks (must pass)
# 2. Update release notes # 2. Update release notes
just release::draft-notes mcp-server # Generate draft notes from commits just release::draft-notes mcp-server # Generate draft notes from commits
# Then edit the component's RELEASE_NOTES.md file (see "Release Notes Files" below) # Then edit the component's RELEASE_NOTES.md file (see "Release Notes Files" below)
# 3. Version bump (workbench & macros only - MCP Server uses setuptools-scm) # 3. Version bump (workbench only - MCP Server uses setuptools-scm)
just release::bump-workbench 1.0.0 # or bump-macro-magnets, bump-macro-export just release::bump-workbench 1.0.0
# 4. Commit changes # 4. Commit changes
git add -A git add -A
git commit -m "chore: bump workbench to 1.0.0" # or appropriate component/message git commit -m "chore: bump workbench to 1.0.0" # or appropriate component/message
# 5. Create & push tag (triggers CI/CD automatically) # 5. Create & push tag (triggers CI/CD automatically)
just release::tag-workbench 1.0.0 # or tag-mcp-server, tag-macro-magnets, tag-macro-export just release::tag-workbench 1.0.0 # or tag-mcp-server
# 6. Monitor release at GitHub Actions, then verify # 6. Monitor release at GitHub Actions, then verify
just release::list-tags just release::list-tags
just release::latest-versions just release::latest-versions
# 7. Update FreeCAD wiki (macros only)
just release::wiki-update macro-magnets # Copies content to clipboard & opens wiki edit page
``` ```
| Component | Bump Command | Tag Command | | Component | Bump Command | Tag Command |
| ------------- | ---------------------------------------- | --------------------------------------- | | ---------- | ------------------------------------ | ------------------------------------ |
| MCP Server | *(none - uses setuptools-scm)* | `just release::tag-mcp-server X.Y.Z` | | MCP Server | *(none - uses setuptools-scm)* | `just release::tag-mcp-server X.Y.Z` |
| Workbench | `just release::bump-workbench X.Y.Z` | `just release::tag-workbench X.Y.Z` | | Workbench | `just release::bump-workbench X.Y.Z` | `just release::tag-workbench X.Y.Z` |
| Magnets Macro | `just release::bump-macro-magnets X.Y.Z` | `just release::tag-macro-magnets X.Y.Z` |
| Export Macro | `just release::bump-macro-export X.Y.Z` | `just release::tag-macro-export X.Y.Z` |
## Components and Their Release Targets ## Components and Their Release Targets
| Component | Tag Format | Releases To | | Component | Tag Format | Releases To |
| ---------------------------- | ------------------------------------- | ------------------------------------------ | | --------------------------- | ----------------------------- | ------------------------------------------ |
| MCP Server | `robust-mcp-server-vX.Y.Z` | PyPI/TestPyPI*, Docker Hub, GitHub Release | | MCP Server | `robust-mcp-server-vX.Y.Z` | PyPI/TestPyPI*, Docker Hub, GitHub Release |
| Robust MCP Bridge Workbench | `robust-mcp-workbench-vX.Y.Z` | GitHub Release (archive) | | Robust MCP Bridge Workbench | `robust-mcp-workbench-vX.Y.Z` | GitHub Release (archive) |
| Cut Object for Magnets Macro | `macro-cut-object-for-magnets-vX.Y.Z` | GitHub Release (archive) |
| Multi Export Macro | `macro-multi-export-vX.Y.Z` | GitHub Release (archive) |
*Stable releases (`X.Y.Z`) publish to PyPI; non-stable releases (alpha, beta, rc) publish to TestPyPI only. *Stable releases (`X.Y.Z`) publish to PyPI; non-stable releases (alpha, beta, rc) publish to TestPyPI only.
@@ -110,10 +103,6 @@ just release::changes-since mcp-server
# For Workbench # For Workbench
just release::changes-since workbench just release::changes-since workbench
# For Macros
just release::changes-since macro-magnets
just release::changes-since macro-export
``` ```
### 3. Run All Quality Checks ### 3. Run All Quality Checks
@@ -137,11 +126,9 @@ Each component has its own `RELEASE_NOTES.md` file. Release workflows automatica
#### Release Notes Files #### Release Notes Files
| Component | Release Notes File | | Component | Release Notes File |
| ---------------------------- | ----------------------------------------------------- | | --------------------------- | ----------------------------------------------- |
| MCP Server | `src/freecad_mcp/RELEASE_NOTES.md` | | MCP Server | `src/freecad_mcp/RELEASE_NOTES.md` |
| Robust MCP Bridge Workbench | `addon/FreecadRobustMCPBridge/RELEASE_NOTES.md` | | Robust MCP Bridge Workbench | `addon/FreecadRobustMCPBridge/RELEASE_NOTES.md` |
| Cut Object for Magnets Macro | `macros/Cut_Object_for_Magnets/RELEASE_NOTES.md` |
| Multi Export Macro | `macros/Multi_Export/RELEASE_NOTES.md` |
#### Draft Release Notes #### Draft Release Notes
@@ -151,8 +138,6 @@ Use the `draft-notes` command to generate a starting point from conventional com
# Generate draft notes for a component # Generate draft notes for a component
just release::draft-notes mcp-server just release::draft-notes mcp-server
just release::draft-notes workbench just release::draft-notes workbench
just release::draft-notes macro-magnets
just release::draft-notes macro-export
``` ```
This categorizes commits by type (feat, fix, refactor, etc.) to help you write the release notes. This categorizes commits by type (feat, fix, refactor, etc.) to help you write the release notes.
@@ -256,61 +241,13 @@ just release::tag-workbench 1.0.0
3. Creates archive (tar.gz and zip) 3. Creates archive (tar.gz and zip)
4. Creates GitHub Release with the archives 4. Creates GitHub Release with the archives
### Macro Releases
Each macro can be released independently.
**Cut Object for Magnets Macro:**
```bash
# 1. Bump version in source files
just release::bump-macro-magnets 1.0.0
# 2. Review and commit the changes
git diff # Review changes
git add -A
git commit -m "chore: bump Cut Object for Magnets macro to 1.0.0"
# 3. Create and push the release tag
just release::tag-macro-magnets 1.0.0
```
**Multi Export Macro:**
```bash
# 1. Bump version in source files
just release::bump-macro-export 1.0.0
# 2. Review and commit the changes
git diff # Review changes
git add -A
git commit -m "chore: bump Multi Export macro to 1.0.0"
# 3. Create and push the release tag
just release::tag-macro-export 1.0.0
```
**Files updated by macro bump commands:**
- `macros/<MacroDir>/<Macro>.FCMacro` (`__Version__` and `__Date__`)
- `macros/<MacroDir>/README-<Macro>.md` (`**Version:**`)
- `macros/<MacroDir>/wiki-source.txt` (`|Version=` and `|Date=`)
- `package.xml` (macro section: `<version>` and `<date>`)
**What happens automatically:**
1. GitHub Actions validates the tag format
2. Verifies version in source files matches tag
3. Creates archive (tar.gz and zip)
4. Creates GitHub Release with the archives
## Verifying a Release ## Verifying a Release
### Check GitHub Actions ### Check GitHub Actions
After pushing a tag, monitor the release workflow: After pushing a tag, monitor the release workflow:
1. Go to [GitHub Actions](https://github.com/spkane/freecad-robust-mcp-and-more/actions) 1. Go to [GitHub Actions](https://github.com/spkane/freecad-addon-robust-mcp-server/actions)
2. Find the workflow run triggered by your tag 2. Find the workflow run triggered by your tag
3. Verify all steps complete successfully 3. Verify all steps complete successfully
@@ -326,7 +263,7 @@ pip index versions freecad-robust-mcp
docker pull spkane/freecad-robust-mcp:1.0.0 docker pull spkane/freecad-robust-mcp:1.0.0
``` ```
**For Workbench/Macros:** **For Workbench:**
- Check the GitHub Releases page for the archive downloads - Check the GitHub Releases page for the archive downloads
- Verify the `package.xml` version was updated - Verify the `package.xml` version was updated
@@ -361,7 +298,6 @@ To see what a release tag would look like without creating it:
```bash ```bash
just release::dry-run-tag mcp-server 1.0.0 just release::dry-run-tag mcp-server 1.0.0
just release::dry-run-tag workbench 1.0.0 just release::dry-run-tag workbench 1.0.0
just release::dry-run-tag macro-magnets 1.0.0
``` ```
## Troubleshooting ## Troubleshooting
@@ -416,56 +352,6 @@ just release::tag-mcp-server 1.0.0
- **MCP Server**: Release when there are significant new features or important bug fixes - **MCP Server**: Release when there are significant new features or important bug fixes
- **Workbench**: Release in sync with server changes that affect the bridge protocol - **Workbench**: Release in sync with server changes that affect the bridge protocol
- **Macros**: Release independently when macro functionality changes
## Updating the FreeCAD Wiki
After releasing a macro, you should update its FreeCAD wiki page. The `wiki-source.txt` files are automatically updated by the `bump-macro-*` commands with the new version and date.
### Wiki Update Commands
```bash
# Check differences between local and live wiki
just release::wiki-diff macro-magnets
just release::wiki-diff macro-export
# View the wiki source content locally
just release::wiki-show macro-magnets
just release::wiki-show macro-export
# Update the wiki (copies to clipboard and opens edit page)
just release::wiki-update macro-magnets
just release::wiki-update macro-export
```
### Wiki Update Workflow
The `wiki-update` command provides a safe, assisted workflow:
1. Copies the updated wiki-source.txt content to your clipboard
2. Opens the FreeCAD wiki edit page in your browser
3. Displays step-by-step instructions
**Manual steps after running the command:**
1. Log in to your FreeCAD wiki account if prompted
2. Select all content in the edit box (Ctrl+A / Cmd+A)
3. Paste the new content (Ctrl+V / Cmd+V)
4. Add an edit summary like "Update to version X.Y.Z"
5. Click "Show preview" to verify changes
6. Click "Save changes" when satisfied
!!! note "Wiki Account Required"
You need a FreeCAD wiki account to edit pages. Register at [wiki.freecad.org](https://wiki.freecad.org) if you don't have one.
### Macro Shortcuts
The wiki commands accept multiple aliases for convenience:
| Macro | Aliases |
| ------------------------ | -------------------------------- |
| Cut Object for Magnets | `macro-magnets`, `magnets`, `cut`|
| Multi Export | `macro-export`, `export`, `multi`|
## Quick Reference ## Quick Reference
@@ -479,13 +365,9 @@ just release::changes-since mcp-server
# Draft release notes from commits # Draft release notes from commits
just release::draft-notes mcp-server just release::draft-notes mcp-server
just release::draft-notes workbench just release::draft-notes workbench
just release::draft-notes macro-magnets
just release::draft-notes macro-export
# Bump versions (for workbench and macros) # Bump versions (for workbench only)
just release::bump-workbench 1.0.0 just release::bump-workbench 1.0.0
just release::bump-macro-magnets 1.0.0
just release::bump-macro-export 1.0.0
# Commit version changes # Commit version changes
git add -A && git commit -m "chore: bump <component> to X.Y.Z" git add -A && git commit -m "chore: bump <component> to X.Y.Z"
@@ -493,8 +375,6 @@ git add -A && git commit -m "chore: bump <component> to X.Y.Z"
# Create releases (verifies versions, creates and pushes tag) # Create releases (verifies versions, creates and pushes tag)
just release::tag-mcp-server 1.0.0 just release::tag-mcp-server 1.0.0
just release::tag-workbench 1.0.0 just release::tag-workbench 1.0.0
just release::tag-macro-magnets 1.0.0
just release::tag-macro-export 1.0.0
# List existing releases # List existing releases
just release::list-tags just release::list-tags
@@ -503,11 +383,6 @@ just release::latest-versions
# Extract changelog for a version (used by CI) # Extract changelog for a version (used by CI)
just release::extract-changelog mcp-server 1.0.0 just release::extract-changelog mcp-server 1.0.0
# Update FreeCAD wiki for macros (after release)
just release::wiki-diff macro-magnets # Check differences
just release::wiki-update macro-magnets # Copy to clipboard & open edit page
just release::wiki-update macro-export
# Delete a tag if needed # Delete a tag if needed
just release::delete-tag <full-tag-name> just release::delete-tag <full-tag-name>
``` ```
+5 -73
View File
@@ -1,78 +1,10 @@
# FreeCAD Macros # MCP Macro Tools
This project includes standalone FreeCAD macros that work independently of the MCP server, plus MCP tools for creating and managing macros programmatically. The MCP server provides tools for working with FreeCAD macros programmatically.
--- ---
## Included Macros ## Available Tools
### CutObjectForMagnets
Cut an object along a plane and add aligned magnet holes with surface collision detection. Perfect for creating 3D printed parts that snap together with embedded magnets.
**Features:**
- Interactive plane selection via GUI
- Automatic magnet hole placement with configurable grid
- Surface collision detection to avoid invalid hole positions
- Configurable magnet dimensions and tolerances
**Usage:**
1. Select an object in FreeCAD
1. Run the macro
1. Define the cutting plane interactively
1. Configure magnet parameters
1. The macro creates two halves with aligned magnet holes
See [CutObjectForMagnets documentation](https://github.com/spkane/freecad-robust-mcp-and-more/tree/main/macros/Cut_Object_for_Magnets) for detailed usage.
### MultiExport
Export selected bodies to multiple file formats simultaneously with configurable mesh options.
**Supported Formats:**
- STL (ASCII and Binary)
- STEP
- 3MF
- OBJ
- IGES
- BREP
- PLY
- AMF
**Usage:**
1. Select one or more bodies/parts
1. Run the macro
1. Select output formats and configure mesh options
1. Choose output directory
1. All exports are created with consistent naming
See [MultiExport documentation](https://github.com/spkane/freecad-robust-mcp-and-more/tree/main/macros/Multi_Export) for detailed usage.
---
## Installing Macros
### Via FreeCAD Addon Manager
When you install the "FreeCAD Robust MCP Suite" addon, the macros are installed automatically.
### Manual Installation
1. Download macros from [GitHub Releases](https://github.com/spkane/freecad-robust-mcp-and-more/releases)
1. Copy `.FCMacro` files to your macro directory:
- **Linux:** `~/.local/share/FreeCAD/Macro/`
- **macOS:** `~/Library/Application Support/FreeCAD/Macro/`
- **Windows:** `%APPDATA%\FreeCAD\Macro\`
---
## MCP Macro Tools
The MCP server provides tools for working with macros programmatically:
### list_macros ### list_macros
@@ -98,7 +30,7 @@ run_macro(
**Example prompt:** **Example prompt:**
```text ```text
"Run the MultiExport macro" "Run the ExportSTL macro"
``` ```
### create_macro ### create_macro
@@ -130,7 +62,7 @@ read_macro(macro_name: str) -> dict
**Example prompt:** **Example prompt:**
```text ```text
"Show me the code for the MultiExport macro" "Show me the code for my custom macro"
``` ```
### delete_macro ### delete_macro
+3 -3
View File
@@ -237,9 +237,9 @@ Gets list of available FreeCAD macros.
```json ```json
[ [
{ {
"name": "MultiExport", "name": "ExportSTL",
"path": "/home/user/.local/share/FreeCAD/Macro/MultiExport.FCMacro", "path": "/home/user/.local/share/FreeCAD/Macro/ExportSTL.FCMacro",
"description": "Export objects to multiple formats", "description": "Export selected objects to STL",
"is_system": false "is_system": false
} }
] ]
+2 -7
View File
@@ -255,11 +255,6 @@ The workbench uses a **queue-based thread safety system** to ensure FreeCAD oper
--- ---
## Included Macros ## Macro Tools
The addon bundle also includes standalone FreeCAD macros: The MCP server provides tools for working with FreeCAD macros. See [Macros](macros.md) for details on using macro tools.
- **MultiExport** - Export objects to multiple formats simultaneously
- **CutObjectForMagnets** - Cut objects with aligned magnet holes for 3D printing
See [Macros](macros.md) for details.
+7 -5
View File
@@ -2,7 +2,7 @@
Welcome to the FreeCAD Robust MCP Suite documentation. Welcome to the FreeCAD Robust MCP Suite documentation.
This project provides an [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server, FreeCAD workbench, and standalone macros that enable integration between AI assistants (Claude, GPT, and other MCP-compatible tools) and [FreeCAD](https://www.freecadweb.org/), allowing AI-assisted development and debugging of 3D models, macros, and workbenches. This project provides an [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server and FreeCAD workbench that enable integration between AI assistants (Claude, GPT, and other MCP-compatible tools) and [FreeCAD](https://www.freecadweb.org/), allowing AI-assisted development and debugging of 3D models, macros, and workbenches.
--- ---
@@ -12,7 +12,6 @@ This project provides an [MCP (Model Context Protocol)](https://modelcontextprot
- **Multiple Connection Modes** - XML-RPC (recommended), JSON-RPC socket, or embedded (Linux only) - **Multiple Connection Modes** - XML-RPC (recommended), JSON-RPC socket, or embedded (Linux only)
- **GUI & Headless Support** - Full modeling in headless mode, plus screenshots/colors in GUI mode - **GUI & Headless Support** - Full modeling in headless mode, plus screenshots/colors in GUI mode
- **Macro Development** - Create, edit, run, and template FreeCAD macros via MCP - **Macro Development** - Create, edit, run, and template FreeCAD macros via MCP
- **Standalone Macros** - Useful FreeCAD macros that work independently of the Robust MCP Server
--- ---
@@ -64,10 +63,13 @@ The Robust MCP Server works with FreeCAD in both GUI and headless mode:
## FreeCAD Macros ## FreeCAD Macros
This project includes standalone FreeCAD macros: The MCP server provides tools for working with FreeCAD macros:
- **[CutObjectForMagnets](guide/macros.md#cutobjectformagnets)** - Cuts objects along planes with automatic magnet hole placement - **List macros** - Discover available macros in your FreeCAD installation
- **[MultiExport](guide/macros.md#multiexport)** - Export objects to multiple formats simultaneously - **Run macros** - Execute macros with parameter passing
- **Create macros** - Generate new macros from templates or custom code
See [Macros Guide](guide/macros.md) for details on using macros with the MCP server.
--- ---
-3
View File
@@ -196,8 +196,5 @@ run-gui-custom freecad_path:
# ============================================================================= # =============================================================================
# These are kept for backwards compatibility but will be removed in a future version. # These are kept for backwards compatibility but will be removed in a future version.
# Use the new install:: module commands instead: # Use the new install:: module commands instead:
# just install::macro-cut (was: just freecad::install-cut-macro)
# just install::macro-export (was: just freecad::install-export-macro)
# just install::macro-all (was: just freecad::install-all-macros)
# just install::mcp-bridge-workbench (was: just freecad::install-workbench) # just install::mcp-bridge-workbench (was: just freecad::install-workbench)
# just install::status (was: just freecad::mcp-status) # just install::status (was: just freecad::mcp-status)
+48 -170
View File
@@ -4,7 +4,6 @@
# This module installs components for end users: # This module installs components for end users:
# - Robust MCP Server (as a uv tool, available system-wide) # - Robust MCP Server (as a uv tool, available system-wide)
# - Robust MCP Bridge Workbench (FreeCAD addon) # - Robust MCP Bridge Workbench (FreeCAD addon)
# - FreeCAD Macros (CutObjectForMagnets, MultiExport)
# #
# For developer setup (Python dependencies in virtualenv), use: just dev::install-deps # For developer setup (Python dependencies in virtualenv), use: just dev::install-deps
@@ -320,132 +319,6 @@ uninstall-mcp-bridge-workbench:
echo "Workbench not found at: $ADDON_DEST" echo "Workbench not found at: $ADDON_DEST"
fi fi
# =============================================================================
# Macro Installation
# =============================================================================
# Install the CutObjectForMagnets macro to FreeCAD's macro directory
macro-cut:
#!/usr/bin/env bash
set -euo pipefail
PROJECT_DIR="{{project_root}}"
# Set FreeCAD directories
eval "$(just install::_freecad-dirs)"
# Verify source exists before copying
MACRO_SRC="${PROJECT_DIR}/macros/Cut_Object_for_Magnets/CutObjectForMagnets.FCMacro"
if [[ ! -f "$MACRO_SRC" ]]; then
echo "Error: Macro source file not found: $MACRO_SRC" >&2
exit 1
fi
mkdir -p "$MACRO_DIR"
# Remove existing installation if present (clean install)
if [[ -f "$MACRO_DIR/CutObjectForMagnets.FCMacro" ]]; then
echo "Removing existing CutObjectForMagnets macro..."
rm -f "$MACRO_DIR/CutObjectForMagnets.FCMacro"
rm -f "$MACRO_DIR/CutObjectForMagnets.svg"
fi
# Copy the macro file
cp "$MACRO_SRC" "$MACRO_DIR/"
# Optionally copy the icon if it exists
ICON_SRC="${PROJECT_DIR}/macros/Cut_Object_for_Magnets/CutObjectForMagnets.svg"
if [[ -f "$ICON_SRC" ]]; then
cp "$ICON_SRC" "$MACRO_DIR/"
fi
echo "CutObjectForMagnets macro installed to: $MACRO_DIR"
echo ""
echo "To use:"
echo " 1. Start FreeCAD"
echo " 2. Select an object to cut"
echo " 3. Go to: Macro → Macros → CutObjectForMagnets → Execute"
echo ""
echo "For angled cuts:"
echo " 1. Create a datum plane at your desired angle (Part Design → Create datum plane)"
echo " 2. Select 'Model Plane' in the macro dialog"
echo " 3. Choose your datum plane from the dropdown"
# Uninstall the CutObjectForMagnets macro
uninstall-macro-cut:
#!/usr/bin/env bash
set -euo pipefail
# Set FreeCAD directories
eval "$(just install::_freecad-dirs)"
rm -f "$MACRO_DIR/CutObjectForMagnets.FCMacro"
rm -f "$MACRO_DIR/CutObjectForMagnets.svg"
echo "CutObjectForMagnets macro uninstalled"
# Install the MultiExport macro to FreeCAD's macro directory
macro-export:
#!/usr/bin/env bash
set -euo pipefail
PROJECT_DIR="{{project_root}}"
# Set FreeCAD directories
eval "$(just install::_freecad-dirs)"
# Verify source exists before copying
MACRO_SRC="${PROJECT_DIR}/macros/Multi_Export/MultiExport.FCMacro"
if [[ ! -f "$MACRO_SRC" ]]; then
echo "Error: Macro source file not found: $MACRO_SRC" >&2
exit 1
fi
mkdir -p "$MACRO_DIR"
# Remove existing installation if present (clean install)
if [[ -f "$MACRO_DIR/MultiExport.FCMacro" ]]; then
echo "Removing existing MultiExport macro..."
rm -f "$MACRO_DIR/MultiExport.FCMacro"
rm -f "$MACRO_DIR/MultiExport.svg"
fi
# Copy the macro file
cp "$MACRO_SRC" "$MACRO_DIR/"
# Optionally copy the icon if it exists
ICON_SRC="${PROJECT_DIR}/macros/Multi_Export/MultiExport.svg"
if [[ -f "$ICON_SRC" ]]; then
cp "$ICON_SRC" "$MACRO_DIR/"
fi
echo "MultiExport macro installed to: $MACRO_DIR"
echo ""
echo "To use:"
echo " 1. Start FreeCAD"
echo " 2. Select one or more objects to export"
echo " 3. Go to: Macro → Macros → MultiExport → Execute"
echo " 4. Choose export formats (STL, STEP, 3MF selected by default)"
echo " 5. Set output directory and filename"
echo " 6. Click Export"
# Uninstall the MultiExport macro
uninstall-macro-export:
#!/usr/bin/env bash
set -euo pipefail
# Set FreeCAD directories
eval "$(just install::_freecad-dirs)"
rm -f "$MACRO_DIR/MultiExport.FCMacro"
rm -f "$MACRO_DIR/MultiExport.svg"
echo "MultiExport macro uninstalled"
# Install all macros to FreeCAD's macro directory
macro-all: macro-cut macro-export
@echo "All macros installed successfully!"
# Uninstall all macros from FreeCAD's macro directory
uninstall-macro-all: uninstall-macro-cut uninstall-macro-export
@echo "All macros uninstalled successfully!"
# ============================================================================= # =============================================================================
# Status Check # Status Check
# ============================================================================= # =============================================================================
@@ -521,23 +394,6 @@ status:
' 2>/dev/null || echo "unknown" ' 2>/dev/null || echo "unknown"
} }
# Helper function to extract __Version__ from macro files (handles single/double quotes)
# Uses environment variable to pass file path safely to Python (avoids shell interpolation)
extract_macro_version() {
local file="$1"
MACRO_FILE="$file" python3 -c '
import os
import re
try:
macro_file = os.environ.get("MACRO_FILE", "")
content = open(macro_file).read()
match = re.search(r"__Version__\s*=\s*[\"'"'"']([^\"'"'"']+)[\"'"'"']", content)
print(match.group(1) if match else "unknown")
except Exception:
print("unknown")
' 2>/dev/null || echo "unknown"
}
# Check workbench # Check workbench
if [[ -d "$MOD_DIR/FreecadRobustMCPBridge" ]]; then if [[ -d "$MOD_DIR/FreecadRobustMCPBridge" ]]; then
WB_VERSION="unknown" WB_VERSION="unknown"
@@ -555,32 +411,6 @@ status:
fi fi
echo "" echo ""
# Check macros
echo "Macros:"
if [[ -f "$MACRO_DIR/CutObjectForMagnets.FCMacro" ]]; then
CUT_VERSION=$(extract_macro_version "$MACRO_DIR/CutObjectForMagnets.FCMacro")
CUT_MOD_TIME=$(get_mod_time "$MACRO_DIR/CutObjectForMagnets.FCMacro")
echo " ✓ CutObjectForMagnets: INSTALLED"
echo " Version: $CUT_VERSION"
echo " Updated: $CUT_MOD_TIME"
else
echo " ✗ CutObjectForMagnets: NOT INSTALLED"
echo " Install: just install::macro-cut"
fi
if [[ -f "$MACRO_DIR/MultiExport.FCMacro" ]]; then
EXPORT_VERSION=$(extract_macro_version "$MACRO_DIR/MultiExport.FCMacro")
EXPORT_MOD_TIME=$(get_mod_time "$MACRO_DIR/MultiExport.FCMacro")
echo " ✓ MultiExport: INSTALLED"
echo " Version: $EXPORT_VERSION"
echo " Updated: $EXPORT_MOD_TIME"
else
echo " ✗ MultiExport: NOT INSTALLED"
echo " Install: just install::macro-export"
fi
echo ""
# Check for legacy installations # Check for legacy installations
LEGACY_COUNT=0 LEGACY_COUNT=0
@@ -602,3 +432,51 @@ status:
fi fi
echo "==========================================" echo "=========================================="
# =============================================================================
# Convenience Commands
# =============================================================================
# Uninstall all components (MCP server and workbench)
uninstall:
#!/usr/bin/env bash
set -euo pipefail
echo "Uninstalling all Robust MCP components..."
echo ""
just install::uninstall-mcp-server
echo ""
just install::uninstall-mcp-bridge-workbench
echo ""
echo "All components uninstalled."
# Clean up everything (uninstall all + remove legacy installations)
cleanup:
#!/usr/bin/env bash
set -euo pipefail
# Set FreeCAD directories
eval "$(just install::_freecad-dirs)"
echo "Cleaning up all Robust MCP installations..."
echo ""
# Uninstall current components
just install::uninstall
echo ""
echo "Removing legacy installations..."
# Remove legacy MCPBridge if present
if [[ -d "$MOD_DIR/MCPBridge" ]]; then
rm -rf "$MOD_DIR/MCPBridge"
echo " Removed: $MOD_DIR/MCPBridge"
fi
# Remove legacy macro if present
if [[ -f "$MACRO_DIR/StartMCPBridge.FCMacro" ]]; then
rm "$MACRO_DIR/StartMCPBridge.FCMacro"
echo " Removed: $MACRO_DIR/StartMCPBridge.FCMacro"
fi
echo ""
echo "Cleanup complete."
+10 -329
View File
@@ -13,8 +13,6 @@
# This project uses component-specific git tags for releases: # This project uses component-specific git tags for releases:
# - robust-mcp-server-vX.Y.Z (triggers PyPI, Docker, GitHub release) # - robust-mcp-server-vX.Y.Z (triggers PyPI, Docker, GitHub release)
# - robust-mcp-workbench-vX.Y.Z (triggers workbench archive release) # - robust-mcp-workbench-vX.Y.Z (triggers workbench archive release)
# - macro-cut-object-for-magnets-vX.Y.Z
# - macro-multi-export-vX.Y.Z
# #
# Version Format (SemVer 2.0): # Version Format (SemVer 2.0):
# - X.Y.Z - Stable release # - X.Y.Z - Stable release
@@ -103,102 +101,6 @@ bump-workbench version:
echo " 2. Commit: git add -A && git commit -m 'chore: bump workbench to $VERSION'" echo " 2. Commit: git add -A && git commit -m 'chore: bump workbench to $VERSION'"
echo " 3. Tag and release: just release::tag-workbench $VERSION" echo " 3. Tag and release: just release::tag-workbench $VERSION"
# Private helper recipe for bumping macro versions
# Parameters: macro_dir, macro_name, macro_file_basename, readme_basename, tag_command, version
[private]
_bump-macro macro_dir macro_name macro_file_basename readme_basename tag_command version:
#!/usr/bin/env bash
set -euo pipefail
VERSION="{{version}}"
TODAY=$(date +%Y-%m-%d)
MACRO_DIR="{{project_root}}/macros/{{macro_dir}}"
MACRO_NAME="{{macro_name}}"
# Validate version format (SemVer 2.0)
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
echo "ERROR: Version '$VERSION' is not valid semver (X.Y.Z or X.Y.Z-prerelease)"
echo "Examples: 1.0.0, 1.0.0-alpha, 1.0.0-beta.1, 1.0.0-rc.1"
exit 1
fi
echo "Bumping $MACRO_NAME Macro to version: $VERSION (date: $TODAY)"
echo ""
# Update __Version__ and __Date__ in .FCMacro file
MACRO_FILE="$MACRO_DIR/{{macro_file_basename}}"
if [ -f "$MACRO_FILE" ]; then
sed "s/^__Version__ = [\"'].*[\"']/__Version__ = \"${VERSION}\"/" "$MACRO_FILE" > "$MACRO_FILE.tmp" && mv "$MACRO_FILE.tmp" "$MACRO_FILE"
sed "s/^__Date__ = [\"'].*[\"']/__Date__ = \"${TODAY}\"/" "$MACRO_FILE" > "$MACRO_FILE.tmp" && mv "$MACRO_FILE.tmp" "$MACRO_FILE"
echo "Updated $MACRO_FILE:"
grep -E "^__(Version|Date)__" "$MACRO_FILE"
else
echo "ERROR: File not found: $MACRO_FILE"
exit 1
fi
# Update README version line
README_FILE="$MACRO_DIR/{{readme_basename}}"
if [ -f "$README_FILE" ]; then
sed "s/^\*\*Version:\*\* .*/\*\*Version:\*\* ${VERSION}/" "$README_FILE" > "$README_FILE.tmp" && mv "$README_FILE.tmp" "$README_FILE"
echo ""
echo "Updated $README_FILE:"
grep "Version:" "$README_FILE" | head -1
else
echo "WARNING: File not found: $README_FILE"
fi
# Update wiki-source.txt version and date
WIKI_FILE="$MACRO_DIR/wiki-source.txt"
if [ -f "$WIKI_FILE" ]; then
sed "s/|Version=.*/|Version=${VERSION}/" "$WIKI_FILE" > "$WIKI_FILE.tmp" && mv "$WIKI_FILE.tmp" "$WIKI_FILE"
sed "s/|Date=.*/|Date=${TODAY}/" "$WIKI_FILE" > "$WIKI_FILE.tmp" && mv "$WIKI_FILE.tmp" "$WIKI_FILE"
echo ""
echo "Updated $WIKI_FILE:"
grep -E "^\|Version=|\|Date=" "$WIKI_FILE"
else
echo "WARNING: File not found: $WIKI_FILE"
fi
# Update package.xml macro version
PACKAGE_XML="{{project_root}}/package.xml"
if [ -f "$PACKAGE_XML" ]; then
awk -v name="$MACRO_NAME" -v version="$VERSION" -v date="$TODAY" '
/<macro>/ { in_macro=1 }
/<\/macro>/ { in_macro=0; found_name=0 }
in_macro && /<name>.*<\/name>/ {
if (index($0, name) > 0) found_name=1
}
in_macro && found_name && /<version>/ {
gsub(/<version>[^<]*<\/version>/, "<version>" version "</version>")
}
in_macro && found_name && /<date>/ {
gsub(/<date>[^<]*<\/date>/, "<date>" date "</date>")
}
{ print }
' "$PACKAGE_XML" > "$PACKAGE_XML.tmp" && mv "$PACKAGE_XML.tmp" "$PACKAGE_XML"
echo ""
echo "Updated $PACKAGE_XML ($MACRO_NAME section):"
grep -A4 ">$MACRO_NAME<" "$PACKAGE_XML"
else
echo "ERROR: File not found: $PACKAGE_XML"
exit 1
fi
echo ""
echo "Version bump complete!"
echo ""
echo "Next steps:"
echo " 1. Review changes: git diff"
echo " 2. Commit: git add -A && git commit -m 'chore: bump $MACRO_NAME macro to $VERSION'"
echo " 3. Tag and release: just release::{{tag_command}} $VERSION"
# Bump the Cut Object for Magnets macro version in all source files
bump-macro-magnets version: (_bump-macro "Cut_Object_for_Magnets" "Cut Object for Magnets" "CutObjectForMagnets.FCMacro" "README-CutObjectForMagnets.md" "tag-macro-magnets" version)
# Bump the Multi Export macro version in all source files
bump-macro-export version: (_bump-macro "Multi_Export" "Multi Export" "MultiExport.FCMacro" "README-MultiExport.md" "tag-macro-export" version)
# ============================================================================= # =============================================================================
# Tag Creation Commands # Tag Creation Commands
# ============================================================================= # =============================================================================
@@ -235,7 +137,7 @@ tag-mcp-server version:
git push origin "$TAG" git push origin "$TAG"
echo "" echo ""
echo "Tag $TAG created and pushed!" echo "Tag $TAG created and pushed!"
echo "Watch the release at: https://github.com/spkane/freecad-robust-mcp-and-more/actions" echo "Watch the release at: https://github.com/spkane/freecad-addon-robust-mcp-server/actions"
# Create and push a release tag for the Robust MCP Bridge workbench # Create and push a release tag for the Robust MCP Bridge workbench
tag-workbench version: tag-workbench version:
@@ -314,101 +216,7 @@ tag-workbench version:
git push origin "$TAG" git push origin "$TAG"
echo "" echo ""
echo "Tag $TAG created and pushed!" echo "Tag $TAG created and pushed!"
echo "Watch the release at: https://github.com/spkane/freecad-robust-mcp-and-more/actions" echo "Watch the release at: https://github.com/spkane/freecad-addon-robust-mcp-server/actions"
# Private helper recipe for tagging macro releases
# Parameters: macro_dir, macro_name, macro_file_basename, tag_prefix, bump_command, tag_message, version
[private]
_tag-macro macro_dir macro_name macro_file_basename tag_prefix bump_command tag_message version:
#!/usr/bin/env bash
set -euo pipefail
# Source shared release helper functions
. "{{project_root}}/scripts/release-helpers.sh"
VERSION="{{version}}"
TAG="{{tag_prefix}}v{{version}}"
MACRO_DIR="{{project_root}}/macros/{{macro_dir}}"
MACRO_NAME="{{macro_name}}"
# Run all pre-release checks (version format, main branch, up-to-date, clean tree)
pre_release_checks "$VERSION" || exit 1
# Verify version in source files matches tag version
echo "Verifying version in source files..."
# Check .FCMacro file
MACRO_FILE="$MACRO_DIR/{{macro_file_basename}}"
MACRO_VERSION=$(grep -o '__Version__ = "[^"]*"' "$MACRO_FILE" | cut -d'"' -f2)
if [ "$MACRO_VERSION" != "$VERSION" ]; then
echo "ERROR: Version mismatch in $MACRO_FILE"
echo " Expected: $VERSION"
echo " Found: $MACRO_VERSION"
echo ""
echo "Run 'just release::{{bump_command}} $VERSION' first, then commit the changes."
exit 1
fi
echo "✓ $MACRO_FILE: $MACRO_VERSION"
# Check wiki-source.txt
WIKI_FILE="$MACRO_DIR/wiki-source.txt"
if [ -f "$WIKI_FILE" ]; then
WIKI_VERSION=$(grep -o '|Version=[^|]*' "$WIKI_FILE" | cut -d= -f2 | tr -d '\n')
if [ "$WIKI_VERSION" != "$VERSION" ]; then
echo "ERROR: Version mismatch in $WIKI_FILE"
echo " Expected: $VERSION"
echo " Found: $WIKI_VERSION"
echo ""
echo "Run 'just release::{{bump_command}} $VERSION' first, then commit the changes."
exit 1
fi
echo "✓ $WIKI_FILE: $WIKI_VERSION"
fi
# Check package.xml
PACKAGE_XML="{{project_root}}/package.xml"
PKG_VERSION=$(awk -v name="$MACRO_NAME" '
/<macro>/ { in_macro=1 }
/<\/macro>/ { in_macro=0; found_name=0 }
in_macro && index($0, name) > 0 { found_name=1 }
in_macro && found_name && /<version>/ {
gsub(/.*<version>/, ""); gsub(/<\/version>.*/, ""); print; exit
}
' "$PACKAGE_XML")
if [ "$PKG_VERSION" != "$VERSION" ]; then
echo "ERROR: Version mismatch in $PACKAGE_XML ($MACRO_NAME section)"
echo " Expected: $VERSION"
echo " Found: $PKG_VERSION"
echo ""
echo "Run 'just release::{{bump_command}} $VERSION' first, then commit the changes."
exit 1
fi
echo "Version verification passed!"
echo ""
echo "Creating tag: $TAG"
echo ""
echo "This will trigger:"
echo " - GitHub release with macro archive"
echo ""
read -p "Continue? [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 1
fi
git tag -a "$TAG" -m "{{tag_message}} v{{version}}"
git push origin "$TAG"
echo ""
echo "Tag $TAG created and pushed!"
echo "Watch the release at: https://github.com/spkane/freecad-robust-mcp-and-more/actions"
# Create and push a release tag for the Cut Object for Magnets macro
tag-macro-magnets version: (_tag-macro "Cut_Object_for_Magnets" "Cut Object for Magnets" "CutObjectForMagnets.FCMacro" "macro-cut-object-for-magnets-" "bump-macro-magnets" "Release Cut Object for Magnets Macro" version)
# Create and push a release tag for the Multi Export macro
tag-macro-export version: (_tag-macro "Multi_Export" "Multi Export" "MultiExport.FCMacro" "macro-multi-export-" "bump-macro-export" "Release Multi Export Macro" version)
# ============================================================================= # =============================================================================
# Tag Information Commands # Tag Information Commands
@@ -422,12 +230,6 @@ list-tags:
echo "" echo ""
echo "=== Robust MCP Bridge Workbench Releases ===" echo "=== Robust MCP Bridge Workbench Releases ==="
git tag -l 'robust-mcp-workbench-v*' --sort=-v:refname | head -10 git tag -l 'robust-mcp-workbench-v*' --sort=-v:refname | head -10
echo ""
echo "=== Cut Object for Magnets Macro Releases ==="
git tag -l 'macro-cut-object-for-magnets-v*' --sort=-v:refname | head -10
echo ""
echo "=== Multi Export Macro Releases ==="
git tag -l 'macro-multi-export-v*' --sort=-v:refname | head -10
# Show the latest version of each component # Show the latest version of each component
latest-versions: latest-versions:
@@ -436,12 +238,8 @@ latest-versions:
echo "" echo ""
SERVER_TAG=$(git tag -l 'robust-mcp-server-v*' --sort=-v:refname | head -n1) SERVER_TAG=$(git tag -l 'robust-mcp-server-v*' --sort=-v:refname | head -n1)
WORKBENCH_TAG=$(git tag -l 'robust-mcp-workbench-v*' --sort=-v:refname | head -n1) WORKBENCH_TAG=$(git tag -l 'robust-mcp-workbench-v*' --sort=-v:refname | head -n1)
MAGNETS_TAG=$(git tag -l 'macro-cut-object-for-magnets-v*' --sort=-v:refname | head -n1)
EXPORT_TAG=$(git tag -l 'macro-multi-export-v*' --sort=-v:refname | head -n1)
echo " Robust MCP Server: ${SERVER_TAG:-none}" echo " Robust MCP Server: ${SERVER_TAG:-none}"
echo " MCP Bridge Workbench: ${WORKBENCH_TAG:-none}" echo " MCP Bridge Workbench: ${WORKBENCH_TAG:-none}"
echo " Macro Magnets: ${MAGNETS_TAG:-none}"
echo " Macro Export: ${EXPORT_TAG:-none}"
# Show commits since the last release of a component # Show commits since the last release of a component
changes-since component: changes-since component:
@@ -457,17 +255,9 @@ changes-since component:
PREFIX="robust-mcp-workbench-v" PREFIX="robust-mcp-workbench-v"
PATHS="addon/FreecadRobustMCPBridge" PATHS="addon/FreecadRobustMCPBridge"
;; ;;
macro-magnets|magnets)
PREFIX="macro-cut-object-for-magnets-v"
PATHS="macros/Cut_Object_for_Magnets"
;;
macro-export|export)
PREFIX="macro-multi-export-v"
PATHS="macros/Multi_Export"
;;
*) *)
echo "Unknown component: {{component}}" echo "Unknown component: {{component}}"
echo "Valid: mcp-server, workbench, macro-magnets, macro-export" echo "Valid: mcp-server, server, workbench"
exit 1 exit 1
;; ;;
esac esac
@@ -601,16 +391,6 @@ show-release-info tag:
echo "Component: Robust MCP Bridge Workbench" echo "Component: Robust MCP Bridge Workbench"
echo "Version: $VERSION" echo "Version: $VERSION"
;; ;;
macro-cut-object-for-magnets-v*)
VERSION="${TAG#macro-cut-object-for-magnets-v}"
echo "Component: Cut Object for Magnets Macro"
echo "Version: $VERSION"
;;
macro-multi-export-v*)
VERSION="${TAG#macro-multi-export-v}"
echo "Component: Multi Export Macro"
echo "Version: $VERSION"
;;
*) *)
echo "Component: Unknown (not a recognized release tag format)" echo "Component: Unknown (not a recognized release tag format)"
;; ;;
@@ -640,14 +420,6 @@ rollback-release tag:
COMPONENT="workbench" COMPONENT="workbench"
VERSION="${TAG#robust-mcp-workbench-v}" VERSION="${TAG#robust-mcp-workbench-v}"
;; ;;
macro-cut-object-for-magnets-v*)
COMPONENT="macro-magnets"
VERSION="${TAG#macro-cut-object-for-magnets-v}"
;;
macro-multi-export-v*)
COMPONENT="macro-export"
VERSION="${TAG#macro-multi-export-v}"
;;
*) *)
COMPONENT="unknown" COMPONENT="unknown"
;; ;;
@@ -712,20 +484,6 @@ rollback-release tag:
echo "Rollback will fully clean up this release." echo "Rollback will fully clean up this release."
echo "" echo ""
;; ;;
macro-magnets)
echo "Component: Cut Object for Magnets Macro v$VERSION"
echo ""
echo "This release only creates a GitHub Release with archive files."
echo "Rollback will fully clean up this release."
echo ""
;;
macro-export)
echo "Component: Multi Export Macro v$VERSION"
echo ""
echo "This release only creates a GitHub Release with archive files."
echo "Rollback will fully clean up this release."
echo ""
;;
*) *)
echo "WARNING: Unrecognized tag format. Proceeding with generic rollback." echo "WARNING: Unrecognized tag format. Proceeding with generic rollback."
echo "" echo ""
@@ -858,7 +616,7 @@ rollback-release tag:
echo " □ Docker Hub: Delete 'latest' tag (if this was the latest release)" echo " □ Docker Hub: Delete 'latest' tag (if this was the latest release)"
echo " URL: https://hub.docker.com/repository/docker/spkane/freecad-robust-mcp/tags" echo " URL: https://hub.docker.com/repository/docker/spkane/freecad-robust-mcp/tags"
;; ;;
workbench|macro-magnets|macro-export) workbench)
echo "COMPLETED (automated):" echo "COMPLETED (automated):"
echo " ✓ GitHub Release deleted" echo " ✓ GitHub Release deleted"
echo " ✓ Git tag deleted (local and remote)" echo " ✓ Git tag deleted (local and remote)"
@@ -965,30 +723,6 @@ status:
echo "Robust MCP Bridge Workbench: up to date (${WORKBENCH_TAG:-no releases})" echo "Robust MCP Bridge Workbench: up to date (${WORKBENCH_TAG:-no releases})"
fi fi
echo "" echo ""
# Macro Magnets
MAGNETS_CHANGES=$(count_changes "macro-cut-object-for-magnets-v" "macros/Cut_Object_for_Magnets")
MAGNETS_TAG=$(git tag -l 'macro-cut-object-for-magnets-v*' --sort=-v:refname | head -1)
if [ "$MAGNETS_CHANGES" -gt 0 ]; then
echo "Macro (Cut Object for Magnets): $MAGNETS_CHANGES unreleased commit(s)"
echo " Latest: ${MAGNETS_TAG:-none}"
echo " View: just release::changes-since macro-magnets"
else
echo "Macro (Cut Object for Magnets): up to date (${MAGNETS_TAG:-no releases})"
fi
echo ""
# Macro Export
EXPORT_CHANGES=$(count_changes "macro-multi-export-v" "macros/Multi_Export")
EXPORT_TAG=$(git tag -l 'macro-multi-export-v*' --sort=-v:refname | head -1)
if [ "$EXPORT_CHANGES" -gt 0 ]; then
echo "Macro (Multi Export): $EXPORT_CHANGES unreleased commit(s)"
echo " Latest: ${EXPORT_TAG:-none}"
echo " View: just release::changes-since macro-export"
else
echo "Macro (Multi Export): up to date (${EXPORT_TAG:-no releases})"
fi
echo ""
echo "==========================================" echo "=========================================="
# ============================================================================= # =============================================================================
@@ -1011,19 +745,9 @@ draft-notes component:
PATHS="addon/FreecadRobustMCPBridge" PATHS="addon/FreecadRobustMCPBridge"
COMPONENT_NAME="Robust MCP Bridge Workbench" COMPONENT_NAME="Robust MCP Bridge Workbench"
;; ;;
macro-magnets|magnets)
PREFIX="macro-cut-object-for-magnets-v"
PATHS="macros/Cut_Object_for_Magnets"
COMPONENT_NAME="Cut Object for Magnets Macro"
;;
macro-export|export)
PREFIX="macro-multi-export-v"
PATHS="macros/Multi_Export"
COMPONENT_NAME="Multi Export Macro"
;;
*) *)
echo "Unknown component: {{component}}" echo "Unknown component: {{component}}"
echo "Valid: mcp-server, workbench, macro-magnets, macro-export" echo "Valid: mcp-server, server, workbench"
exit 1 exit 1
;; ;;
esac esac
@@ -1089,14 +813,9 @@ extract-changelog component version:
workbench) workbench)
RELEASE_NOTES="{{project_root}}/addon/FreecadRobustMCPBridge/RELEASE_NOTES.md" RELEASE_NOTES="{{project_root}}/addon/FreecadRobustMCPBridge/RELEASE_NOTES.md"
;; ;;
macro-magnets|magnets)
RELEASE_NOTES="{{project_root}}/macros/Cut_Object_for_Magnets/RELEASE_NOTES.md"
;;
macro-export|export)
RELEASE_NOTES="{{project_root}}/macros/Multi_Export/RELEASE_NOTES.md"
;;
*) *)
echo "Unknown component: {{component}}" echo "Unknown component: {{component}}"
echo "Valid: mcp-server, server, workbench"
exit 1 exit 1
;; ;;
esac esac
@@ -1142,19 +861,9 @@ dry-run-tag component version:
echo "Would create tag: $TAG" echo "Would create tag: $TAG"
echo "Triggers: GitHub Release with workbench archive" echo "Triggers: GitHub Release with workbench archive"
;; ;;
macro-magnets|magnets)
TAG="macro-cut-object-for-magnets-v{{version}}"
echo "Would create tag: $TAG"
echo "Triggers: GitHub Release with macro archive"
;;
macro-export|export)
TAG="macro-multi-export-v{{version}}"
echo "Would create tag: $TAG"
echo "Triggers: GitHub Release with macro archive"
;;
*) *)
echo "Unknown component: {{component}}" echo "Unknown component: {{component}}"
echo "Valid: mcp-server, workbench, macro-magnets, macro-export" echo "Valid: mcp-server, server, workbench"
exit 1 exit 1
;; ;;
esac esac
@@ -1174,19 +883,9 @@ wiki-update component:
WIKI_PAGE="Robust_MCP_Bridge_Workbench" WIKI_PAGE="Robust_MCP_Bridge_Workbench"
COMPONENT_NAME="Robust MCP Bridge Workbench" COMPONENT_NAME="Robust MCP Bridge Workbench"
;; ;;
macro-magnets|magnets|cut)
WIKI_SOURCE="{{project_root}}/macros/Cut_Object_for_Magnets/wiki-source.txt"
WIKI_PAGE="Macro_Cut_Object_for_Magnets"
COMPONENT_NAME="Cut Object for Magnets"
;;
macro-export|export|multi)
WIKI_SOURCE="{{project_root}}/macros/Multi_Export/wiki-source.txt"
WIKI_PAGE="Macro_Multi_Export"
COMPONENT_NAME="Multi Export"
;;
*) *)
echo "Unknown component: {{component}}" echo "Unknown component: {{component}}"
echo "Valid options: workbench (or bridge), macro-magnets (or magnets, cut), macro-export (or export, multi)" echo "Valid options: workbench (or bridge)"
exit 1 exit 1
;; ;;
esac esac
@@ -1302,17 +1001,9 @@ wiki-show component:
WIKI_SOURCE="{{project_root}}/addon/FreecadRobustMCPBridge/wiki-source.txt" WIKI_SOURCE="{{project_root}}/addon/FreecadRobustMCPBridge/wiki-source.txt"
COMPONENT_NAME="Robust MCP Bridge Workbench" COMPONENT_NAME="Robust MCP Bridge Workbench"
;; ;;
macro-magnets|magnets|cut)
WIKI_SOURCE="{{project_root}}/macros/Cut_Object_for_Magnets/wiki-source.txt"
COMPONENT_NAME="Cut Object for Magnets"
;;
macro-export|export|multi)
WIKI_SOURCE="{{project_root}}/macros/Multi_Export/wiki-source.txt"
COMPONENT_NAME="Multi Export"
;;
*) *)
echo "Unknown component: {{component}}" echo "Unknown component: {{component}}"
echo "Valid options: workbench (or bridge), macro-magnets (or magnets, cut), macro-export (or export, multi)" echo "Valid options: workbench (or bridge)"
exit 1 exit 1
;; ;;
esac esac
@@ -1345,19 +1036,9 @@ wiki-diff component:
WIKI_PAGE="Robust_MCP_Bridge_Workbench" WIKI_PAGE="Robust_MCP_Bridge_Workbench"
COMPONENT_NAME="Robust MCP Bridge Workbench" COMPONENT_NAME="Robust MCP Bridge Workbench"
;; ;;
macro-magnets|magnets|cut)
WIKI_SOURCE="{{project_root}}/macros/Cut_Object_for_Magnets/wiki-source.txt"
WIKI_PAGE="Macro_Cut_Object_for_Magnets"
COMPONENT_NAME="Cut Object for Magnets"
;;
macro-export|export|multi)
WIKI_SOURCE="{{project_root}}/macros/Multi_Export/wiki-source.txt"
WIKI_PAGE="Macro_Multi_Export"
COMPONENT_NAME="Multi Export"
;;
*) *)
echo "Unknown component: {{component}}" echo "Unknown component: {{component}}"
echo "Valid options: workbench (or bridge), macro-magnets (or magnets, cut), macro-export (or export, multi)" echo "Valid options: workbench (or bridge)"
exit 1 exit 1
;; ;;
esac esac
+12 -8
View File
@@ -17,7 +17,7 @@ cov:
uv run pytest tests/unit --cov=freecad_mcp --cov-report=term-missing --cov-report=html:htmlcov uv run pytest tests/unit --cov=freecad_mcp --cov-report=term-missing --cov-report=html:htmlcov
# Run tests without slow markers (excludes integration tests) # Run tests without slow markers (excludes integration tests)
fast: quick:
uv run pytest {{project_root}}/tests/unit -m "not slow" uv run pytest {{project_root}}/tests/unit -m "not slow"
# Run only integration tests (requires running FreeCAD Robust MCP Bridge) # Run only integration tests (requires running FreeCAD Robust MCP Bridge)
@@ -154,6 +154,16 @@ integration-freecad-auto:
# Cleanup is handled by trap # Cleanup is handled by trap
exit $TEST_EXIT_CODE exit $TEST_EXIT_CODE
# =============================================================================
# Test Setup
# =============================================================================
# Check if test dependencies are installed (fails if any are missing)
check-deps:
@uv run python -c "import pytest" 2>/dev/null && echo "✓ pytest installed" || { echo "✗ pytest not installed"; exit 1; }
@uv run python -c "import pytest_cov" 2>/dev/null && echo "✓ pytest-cov installed" || { echo "✗ pytest-cov not installed"; exit 1; }
@uv run python -c "import pytest_asyncio" 2>/dev/null && echo "✓ pytest-asyncio installed" || { echo "✗ pytest-asyncio not installed"; exit 1; }
# ============================================================================= # =============================================================================
# Just Command Tests # Just Command Tests
# ============================================================================= # =============================================================================
@@ -314,8 +324,6 @@ release-test:
echo " Edit the RELEASE_NOTES.md file for each component:" echo " Edit the RELEASE_NOTES.md file for each component:"
echo " - MCP Server: src/freecad_mcp/RELEASE_NOTES.md" echo " - MCP Server: src/freecad_mcp/RELEASE_NOTES.md"
echo " - Workbench: addon/FreecadRobustMCPBridge/RELEASE_NOTES.md" echo " - Workbench: addon/FreecadRobustMCPBridge/RELEASE_NOTES.md"
echo " - Cut Macro: macros/Cut_Object_for_Magnets/RELEASE_NOTES.md"
echo " - Export Macro: macros/Multi_Export/RELEASE_NOTES.md"
echo "" echo ""
echo " Add a new version section at the top:" echo " Add a new version section at the top:"
echo " ## Version X.Y.Z (YYYY-MM-DD)" echo " ## Version X.Y.Z (YYYY-MM-DD)"
@@ -329,11 +337,9 @@ release-test:
echo " Tip: Use 'just release::draft-notes <component>' to generate draft notes" echo " Tip: Use 'just release::draft-notes <component>' to generate draft notes"
echo " from git commits since the last release." echo " from git commits since the last release."
echo "" echo ""
echo "2. BUMP VERSIONS (for workbench/macros only - MCP server auto-bumps from tag):" echo "2. BUMP VERSIONS (for workbench only - MCP server auto-bumps from tag):"
echo "" echo ""
echo " just release::bump-workbench <version>" echo " just release::bump-workbench <version>"
echo " just release::bump-macro-magnets <version>"
echo " just release::bump-macro-export <version>"
echo "" echo ""
echo "3. COMMIT AND PUSH your RELEASE_NOTES.md and version bump changes" echo "3. COMMIT AND PUSH your RELEASE_NOTES.md and version bump changes"
echo "" echo ""
@@ -341,8 +347,6 @@ release-test:
echo "" echo ""
echo " just release::tag-mcp-server <version>" echo " just release::tag-mcp-server <version>"
echo " just release::tag-workbench <version>" echo " just release::tag-workbench <version>"
echo " just release::tag-macro-magnets <version>"
echo " just release::tag-macro-export <version>"
echo "" echo ""
echo "NOTE: The MCP server version is determined by the git tag, so no bump needed." echo "NOTE: The MCP server version is determined by the git tag, so no bump needed."
echo "NOTE: Release workflows extract notes from each component's RELEASE_NOTES.md." echo "NOTE: Release workflows extract notes from each component's RELEASE_NOTES.md."
File diff suppressed because it is too large Load Diff
@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<!-- Background -->
<rect width="64" height="64" fill="#2c3e50" rx="4"/>
<!-- Top piece of cut object -->
<g id="top-piece">
<!-- Main body -->
<rect x="12" y="8" width="40" height="18" fill="#3498db" stroke="#2980b9" stroke-width="1.5"/>
<!-- Connector holes (visible from bottom) -->
<circle cx="24" cy="26" r="3" fill="#1a252f"/>
<circle cx="40" cy="26" r="3" fill="#1a252f"/>
</g>
<!-- Cut line indicator -->
<line x1="8" y1="32" x2="56" y2="32" stroke="#e74c3c" stroke-width="2" stroke-dasharray="4,2"/>
<polygon points="54,32 50,29 50,35" fill="#e74c3c"/>
<!-- Bottom piece of cut object -->
<g id="bottom-piece">
<!-- Main body -->
<rect x="12" y="38" width="40" height="18" fill="#95a5a6" stroke="#7f8c8d" stroke-width="1.5"/>
<!-- Connector holes (visible from top) -->
<circle cx="24" cy="38" r="3" fill="#1a252f"/>
<circle cx="40" cy="38" r="3" fill="#1a252f"/>
<!-- Depth indicators (showing holes go into the part) -->
<ellipse cx="24" cy="38" rx="3" ry="1.5" fill="#34495e" opacity="0.6"/>
<ellipse cx="40" cy="38" rx="3" ry="1.5" fill="#34495e" opacity="0.6"/>
</g>
<!-- Small connector/magnet indicators -->
<circle cx="24" cy="26" r="2" fill="#95a5a6" stroke="#7f8c8d" stroke-width="0.5"/>
<circle cx="40" cy="26" r="2" fill="#95a5a6" stroke="#7f8c8d" stroke-width="0.5"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 674 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 706 B

@@ -1,632 +0,0 @@
# Cut Object for Magnets - FreeCAD Macro
![Macro Icon](CutObjectForMagnets.svg)
**Version:** 0.6.1
**FreeCAD Version:** 0.19 or later
**License:** MIT
> **Documentation:** [https://spkane.github.io/freecad-robust-mcp-and-more/](https://spkane.github.io/freecad-robust-mcp-and-more/)
## Overview
This FreeCAD macro intelligently cuts 3D objects along a plane and automatically places magnet holes (for magnets, dowels, pins, etc.) with built-in surface penetration detection. Unlike simple cutting tools, this macro ensures magnet holes won't accidentally break through the outer surface of your object.
Perfect for creating multi-part prints that snap together with magnets or alignment pins!
## Quick Visual Guide
### Method 1: Preset Planes (Simple)
```text
Select Object → Run Macro → Choose XY/XZ/YZ → Set Offset → Execute
```
Use for: Axis-aligned cuts, quick splits, standard orientations
### Method 2: Model Planes (Advanced - ANY Angle)
```text
Create Datum Plane (angled as needed) → Select Object → Run Macro →
Choose "Model Plane" → Select Your Plane → Execute
```
Use for: Angled cuts, following model geometry, complex orientations
### Quick Tip: Select Both Object AND Plane Together
```text
Select Object + Plane (Ctrl+Click) → Run Macro → Plane auto-selected!
```
The macro automatically detects when you select both an object and a plane together.
---
## Features
- **Smart Surface Detection** - Automatically skips holes that would penetrate the object's outer surface
- **Smart Repositioning** - When a hole would penetrate, tries nearby positions (further inward or along perimeter)
- **Dual-Part Validation** - Validates each hole position works for BOTH parts before creating any holes
- **Minimum Spacing** - Ensures holes are at least 2x diameter apart (one hole width between them)
- **Automatic Alignment** - Magnet holes on both pieces are perfectly aligned
- **Flexible Plane Selection** - Use preset planes (XY/XZ/YZ) OR any datum plane/face from your model
- **Angled Cuts** - Cut along any angle by selecting a datum plane
- **Even Distribution** - Holes are evenly spaced around the perimeter of the cut face
- **Count-Based Placement** - Specify exact number of holes (default: 6) for precise control
- **Safety Clearance** - Maintains minimum distance from outer surfaces
- **Visual Feedback** - Progress bar and status messages
- **Non-Destructive** - Original object is hidden, not deleted
---
## Installation
### macOS Installation (Detailed)
#### Step 1: Locate Your FreeCAD Macros Folder
The macros folder location on macOS:
```text
~/Library/Application Support/FreeCAD/Macro/
```
**How to access it:**
**Option A - Finder (Recommended):**
1. Open **Finder**
1. Press `Cmd + Shift + G` (Go to Folder)
1. Paste: `~/Library/Application Support/FreeCAD/Macro/`
1. Press **Enter**
**Option B - Terminal:**
```bash
mkdir -p ~/Library/Application\ Support/FreeCAD/Macro/
open ~/Library/Application\ Support/FreeCAD/Macro/
```
**Option C - From FreeCAD:**
1. Open FreeCAD
1. Go to **Macro → Macros...**
1. Note the path shown at the top of the dialog
1. Click **User macros location** to open in Finder
#### Step 2: Install the Macro File
1. Save the macro file as `CutObjectForMagnets.FCMacro` in the macros folder
1. (Optional) Save the icon as `CutObjectForMagnets.svg` in the same folder
#### Step 3: Verify Installation
1. Open FreeCAD
1. Go to **Macro → Macros...**
1. You should see "CutObjectForMagnets" in the list
1. (Optional) Click **Edit** to view the macro code
#### Step 4: Create a Toolbar Button (Optional but Recommended)
1. Go to **Macro → Macros...**
1. Select **CutObjectForMagnets**
1. Click **Create** (toolbar button icon)
1. Choose the icon file (`CutObjectForMagnets.svg`) if you saved it
1. The macro will now appear in your toolbar for quick access
---
### Linux Installation
```bash
# Create macros directory if it doesn't exist
mkdir -p ~/.FreeCAD/Macro/
# Copy macro file
cp CutObjectForMagnets.FCMacro ~/.FreeCAD/Macro/
# (Optional) Copy icon
cp CutObjectForMagnets.svg ~/.FreeCAD/Macro/
```
---
### Windows Installation
1. Navigate to: `%APPDATA%\FreeCAD\Macro\`
1. Copy `CutObjectForMagnets.FCMacro` to this folder
1. (Optional) Copy `CutObjectForMagnets.svg` to the same folder
---
## How to Use
### Quick Start Guide
#### Step 1: Prepare Your Model
1. Open your model in FreeCAD (or create/import one)
1. Ensure the object is a **solid** (not a shell or surface)
1. **Select the object** in the 3D view or tree view
**Tip:** If you imported an STL, it should already be a valid solid. If you created the model in FreeCAD, make sure you created a solid object in the Part workbench.
#### Step 2: Launch the Macro
- **From Macro Menu:** Macro → Macros... → Select "CutObjectForMagnets" → Execute
- **From Toolbar:** Click the macro icon (if you created a toolbar button)
#### Step 3: Configure Cut Plane
The dialog will open with several configuration sections.
**Plane Type Settings:**
You have two options for defining your cut plane:
**Option 1: Preset Plane** (Simple, axis-aligned cuts)
- **Plane:** Choose orientation
- `XY` - Horizontal cut (most common)
- `XZ` - Vertical cut (front-to-back)
- `YZ` - Vertical cut (left-to-right)
- **Offset:** Position of the cut plane from origin (in mm)
- `0` - Cut through the origin
- Positive values move the plane in the positive axis direction
- Negative values move it in the negative axis direction
**Finding the Right Offset:**
1. Note your object's bounding box dimensions (visible in FreeCAD)
1. If your object is centered at origin and you want to cut in half:
- Use offset = `0`
1. If your object is positioned elsewhere:
- Look at the coordinate where you want to cut
- Use that value as the offset
**Option 2: Model Plane** (Advanced, angled cuts)
Select any datum plane or planar face from your model:
**Using Datum Planes:**
1. Create a datum plane first (if not already in model):
- Go to **Part Design** workbench
- Click **Create datum plane** (or Part → Datum → Datum Plane)
- Position and angle the plane where you want to cut
- Exit datum plane creation
1. In the macro dialog:
- Select **"Model Plane"** from Plane Type
- Choose your datum plane from the dropdown
- The offset field is disabled (plane position defines the cut)
**Using Object Faces:**
- Any planar face on any object in your document can be used as a cut plane
- Select from the dropdown: "Face: [ObjectName] (Face1, Face2, etc.)"
- Useful for cutting along existing geometry
**Examples of Model Plane Cuts:**
- Cut at 45° angle - Create datum plane rotated 45° around X or Y axis
- Cut following surface contour - Select a planar face on reference geometry
- Cut perpendicular to cylinder axis - Use cylinder end face
- Complex multi-angle cuts - Create multiple datum planes for different cuts
#### Step 4: Configure Magnet Holes
**Hole Parameters:**
- **Diameter:** The diameter of your magnet (mm)
- For 6mm magnets: Use `6.2mm` (adds 0.2mm clearance)
- For 3mm dowels: Use `3.1mm` (adds 0.1mm clearance)
- For friction fit: Use exact diameter or slightly smaller
- **Depth:** How deep holes go into each piece (mm)
- For magnets: Use magnet thickness + 0.5mm
- For dowel pins: Usually half the dowel length
- **Important:** Each piece gets this depth
- **Number of Holes:** Total number of magnet holes to create (default: 6)
- Holes are evenly distributed around the perimeter of the cut face
- More holes = stronger joint, but requires more magnets
- Recommended: `4-6` for small objects, `8-12` for large objects
- **Edge Clearance (Preferred):** Ideal distance from hole edge to outer surface (mm)
- Default: `2mm` - holes are initially placed with this clearance
- This is the distance the macro tries to maintain for optimal part strength
- Larger values = more material around holes = stronger parts
- **Edge Clearance (Minimum):** Absolute minimum acceptable clearance (mm)
- Default: `0.5mm` - the smallest allowable clearance
- When smart repositioning can't place a hole at preferred clearance, it will try progressively smaller clearances down to this minimum
- Prevents holes from getting too close to outer surfaces
- For thin-walled objects: Increase this value to prevent breakthrough
#### Step 5: Execute the Cut
1. Review all parameters
1. Click **"Execute Cut"**
1. Watch the progress bar
1. Check status messages for any skipped holes
#### Step 6: Review Results
The macro creates two new objects:
- `[ObjectName]_Bottom` - Lower piece with holes
- `[ObjectName]_Top` - Upper piece with holes
Your original object is **hidden** (not deleted) - you can show it again from the tree view if needed.
---
## Parameter Reference Guide
### Understanding Edge Clearance
The macro uses a **dual clearance system** to balance ideal hole placement with flexibility:
- **Preferred Clearance (2mm default):** Initial hole placement uses this value
- **Minimum Clearance (0.5mm default):** Fallback when preferred clearance fails
**How it works:**
1. Holes are first placed using the preferred clearance
1. If a hole fails the safety check at preferred clearance, smart repositioning kicks in
1. The macro tries progressively smaller clearances (from preferred down to minimum)
1. Only if all clearance levels fail does the macro try moving the hole position
The safety check ensures:
```text
hole_radius + depth + clearance < distance_to_nearest_surface
```
**Visual Example:**
```text
[Outer Surface]
|
clearance (preferred: 2mm, min: 0.5mm)
|
[Hole boundary] ← Must not touch outer surface
|
actual hole
```
If a hole would violate even the minimum clearance, it's automatically skipped and you'll see a warning in the console.
### Calculating Depth for Magnets
For magnets that should be flush or recessed:
```text
depth = magnet_thickness + recess_amount
Examples:
- 2mm thick magnet, flush: depth = 2.5mm (0.5mm tolerance)
- 3mm thick magnet, 0.5mm recess: depth = 3.5mm + 0.5mm = 4mm
```
**Important:** Total depth in both pieces should accommodate the magnet fully:
```text
total_depth = depth_bottom + depth_top
total_depth should be >= magnet_thickness
```
### Recommended Hole Count Guidelines
| Object Size | Recommended Holes | Notes |
| ----------------- | ----------------- | ---------------------------------- |
| Small (\<50mm) | 4-6 | Fewer holes for small surfaces |
| Medium (50-150mm) | 6-10 | Default of 6 works well |
| Large (>150mm) | 10-16 | More holes for stronger connection |
| Thin-walled | 4-6 | Use smaller diameter holes |
---
## Common Use Cases
### Case 1: Large Print Split for Bed Size
**Scenario:** 300mm diameter object, need to split for 250mm print bed
**Settings:**
```text
Plane: XY
Offset: 0 (if centered) or half the object height
Diameter: 6.2mm (for 6×2mm magnets)
Depth: 2.5mm
Number of Holes: 12
Clearance: 3mm
```
**Result:** Two pieces that stack vertically with 12 evenly-spaced magnets for alignment
---
### Case 2: Modular Terrain Tiles
**Scenario:** 100×100mm terrain tiles with magnetic edges
**Settings:**
```text
Plane: XY or YZ (depending on desired split)
Offset: 50mm (half the tile dimension)
Diameter: 3.2mm (for 3×1mm magnets)
Depth: 1.5mm
Number of Holes: 6
Clearance: 2mm
```
**Result:** Tiles that connect magnetically at edges with 6 evenly-spaced magnets
---
### Case 3: Dowel Pin Alignment for Large Parts
**Scenario:** Splitting a large miniature or sculpture
**Settings:**
```text
Plane: XY
Offset: [at desired split point]
Diameter: 3.1mm (for 3mm brass pins)
Depth: 8mm (16mm pins, 8mm per side)
Number of Holes: 4
Clearance: 3mm
```
**Result:** Precise alignment with 4 evenly-spaced removable pins
---
### Case 4: Angled Cut with Datum Plane (45° Wedge)
**Scenario:** Splitting a wedge-shaped console at a 45° angle
**Preparation:**
1. Switch to **Part Design** workbench
1. Create datum plane:
- Click **Create datum plane** (or Part → Datum → Create datum plane)
- In the dialog:
- Attachment mode: "Translate origin"
- Angle: Rotate 45° around X-axis
- Position: Move to desired cut location
- Click **OK**
1. Name it "CutPlane45" (optional but helpful)
**Settings:**
```text
Plane Type: Model Plane
Model Plane: Plane: CutPlane45
Diameter: 6.2mm (for 6×2mm magnets)
Depth: 3mm
Number of Holes: 8
Clearance: 3mm
```
**Result:** Clean 45° angled cut with 8 evenly-spaced magnet holes
**Pro Tip:** You can visually see where the cut will be by looking at the datum plane position before running the macro!
---
## Troubleshooting
### Error: "Please select an object to cut"
**Solution:** Click on your object in the 3D view before running the macro.
---
### Error: "Selected object does not have a shape"
**Solution:** You selected a group or annotation. Select the actual 3D object (should be under Part or Body in the tree).
---
### Error: "Failed to cut object"
**Possible causes:**
1. **Invalid mesh** - The object has geometry errors
- Try: Edit → Preferences → Part Design → "Automatically refine model after boolean operation"
- Or manually: Part → Refine Shape
1. **Offset is outside object bounds** - The cut plane doesn't intersect the object
- Check your object's position and bounding box
- Adjust offset to actually pass through the object
1. **Model plane doesn't intersect** - The selected datum plane or face doesn't pass through the object
- Verify the plane position in the 3D view
- Create a new datum plane that actually cuts through the object
---
### Error: "No planes available"
**Solution:** You selected "Model Plane" but no datum planes or planar faces exist in the document.
**Fix:**
1. Create a datum plane:
- **Part Design → Create datum plane**
- Position and angle as needed
- Click OK
1. Or switch to "Preset Plane" mode
1. Re-run the macro
---
### Warning: "Skipping hole at [position] - would penetrate surface"
**This is normal!** The macro is protecting you from holes that would break through.
**If too many holes are skipped:**
1. **Increase edge clearance** (e.g., from 2mm to 4mm)
1. **Increase spacing** (fewer holes, but safer)
1. **Reduce hole depth** (less likely to penetrate)
1. **Check your object** - might be too thin for the hole size
---
### Error: "No valid hole positions found"
**Possible causes:**
1. **Cut surface too small** - Not enough room for even one hole
- Solution: Reduce spacing or hole diameter
1. **Object too thin** - All positions would penetrate
- Solution: Reduce hole depth or increase clearance less aggressively
1. **Complex geometry** - Cut surface is irregular
- Solution: Manually place holes in FreeCAD after cutting
---
## Advanced Tips & Best Practices
### Creating Perfect Datum Planes
**For Angled Cuts:**
1. **Simple Rotation Method:**
```text
Part Design → Create datum plane
- Reference: XY plane (or any base plane)
- Attachment offset → Rotation:
- Around X: Tilts forward/back
- Around Y: Tilts left/right
- Around Z: Spins horizontally
- Position: Translation Z/Y/X to move the plane
```
1. **Align to Edges/Vertices:**
```text
Part Design → Create datum plane
- Attachment mode: "Three points"
- Select 3 points on your model
- Plane will pass through all three points
```
### Choosing the Right Clearance
The dual clearance system gives you fine-grained control:
**Preferred Clearance (default 2mm):**
- Sets how far from edges holes are initially placed
- Increase for stronger parts (more material around holes)
- Decrease if you need holes closer to edges
**Minimum Clearance (default 0.5mm):**
- The absolute minimum acceptable distance from edges
- Increase for thin-walled objects to prevent breakthrough
- Should always be ≤ preferred clearance
**Guidelines by object type:**
- **Thin-walled objects** (2-4mm walls): Preferred=2mm, Minimum=1mm
- **Thick objects** (>10mm walls): Preferred=3mm, Minimum=0.5mm (defaults work well)
- **Irregular shapes:** Preferred=4-5mm, Minimum=2mm to be safe
### Magnet Installation Tips
After printing and cutting:
1. Test fit magnets - they should slide in smoothly
1. If too tight: Use a drill bit to clean out the holes slightly
1. If too loose: Use CA glue or epoxy to secure
1. **Check polarity!** - Mark magnet orientation before gluing
---
## Technical Details
### How Surface Penetration Detection Works
For each potential hole position, the macro:
1. Creates a test cylinder with radius = `(diameter/2) + clearance`
1. Extends the cylinder to depth = `hole_depth + clearance`
1. Performs boolean intersection with the part
1. If intersection volume < 99% of test cylinder volume → hole would penetrate → skip it
This ensures holes only go where they're safe!
### Coordinate System
- **Origin (0,0,0):** FreeCAD document origin
- **Cut planes** pass through a point at the specified offset:
- XY plane: Point = (0, 0, offset)
- XZ plane: Point = (0, offset, 0)
- YZ plane: Point = (offset, 0, 0)
### Hole Direction
- **Bottom piece:** Holes point UP (toward cut plane)
- **Top piece:** Holes point DOWN (toward cut plane)
- Both sets align perfectly at the cut interface
---
## Version History
### v0.5.0-beta (2026-01-05)
- Initial release
- XY, XZ, YZ preset plane support
- Model plane support (datum planes and planar faces)
- Angled cut capability via datum planes
- Surface penetration detection
- Configurable hole parameters
- Progress tracking
- Dual-part validation
- Minimum spacing enforcement (2x diameter)
- Smart hole repositioning
- Object selection combo box
---
## License
MIT License - Free to use, modify, and distribute.
---
## Credits
Created for the FreeCAD community to make multi-part 3D printing easier and more reliable.
**Inspiration:** PrusaSlicer's cut tool, but with reliable export and better surface detection.
@@ -1,42 +0,0 @@
# Cut Object for Magnets Macro Release Notes
## Version 0.6.1 (2026-01-12)
Release notes for changes between v0.5.0-beta and v0.6.1.
### Added
- **FreeCAD Addon Manager metadata**: Macro now includes standard metadata fields for better Addon Manager integration
- **FreeCAD Wiki page**: Official wiki documentation at [Macro Cut Object for Magnets](https://wiki.freecad.org/Macro_Cut_Object_for_Magnets)
- **Example images**: Added screenshots showing the dialog and example output
### Changed
- **Version tracking**: Version now managed via `__Version__` metadata field instead of inline comment
### Fixed
- **Ring-shaped objects**: Improved handling of inset point calculation for hollow/ring-shaped cut faces where the inset point could land in the hole
- **Code formatting**: Minor cleanup for consistent code style
### No Functional Changes
The core cutting algorithm, magnet hole placement, and collision detection remain unchanged from the beta release. This macro is stable for production use.
### Installation
**Via FreeCAD Addon Manager:**
1. Open FreeCAD
2. Go to Macro > Macros...
3. Click "Download" tab
4. Search for "Cut Object for Magnets"
5. Click Install
**Manual Installation:**
Copy `CutObjectForMagnets.FCMacro` to your FreeCAD macro directory:
- **macOS**: `~/Library/Application Support/FreeCAD/Macro/`
- **Linux**: `~/.local/share/FreeCAD/Macro/`
- **Windows**: `%APPDATA%/FreeCAD/Macro/`
@@ -1,168 +0,0 @@
<languages/>
<translate>
<!--T:1-->
{{Macro
|Name=Macro Cut Object for Magnets
|Icon=Macro_Cut_Object_For_Magnets.png
|Description=Cut an object along a plane and add aligned magnet holes with surface collision detection. Creates two parts with perfectly aligned holes for embedding magnets that allow the parts to snap together.
|Author=Sean P. Kane
|Version=0.6.1
|Date=2026-01-12
|FCVersion=0.19+
|Download=[https://wiki.freecad.org/images/thumb/e/e3/Macro_Cut_Object_For_Magnets.png/48px-Macro_Cut_Object_For_Magnets.png ToolBar Icon]
|SeeAlso=[[Part_Slice|Part Slice]], [[PartDesign_Hole|PartDesign Hole]]
}}
==Description== <!--T:2-->
<!--T:3-->
This macro cuts a 3D object along a specified plane and automatically adds aligned magnet holes to both resulting pieces. The holes are positioned with intelligent collision detection to ensure they don't break through the outer walls of the object.
<!--T:4-->
This is particularly useful for:
* Creating multi-part prints that snap together with embedded magnets
* Splitting large objects for smaller 3D printer beds while maintaining alignment
* Adding magnetic closure mechanisms to enclosures and cases
<!--T:5-->
'''Key Features:'''
* Cut along preset planes (XY, XZ, YZ) or model datum planes
* Automatic hole placement with even distribution along the cut edge
* Surface collision detection prevents holes from breaking through walls
* Configurable hole diameter, depth, and count
* Preferred and minimum edge clearance settings
* Smart repositioning of holes when initial placement fails safety checks
* Creates PartDesign::Body objects with parametric Hole features
* Supports re-cutting already-cut objects (preserves existing holes)
* Automatically separates the two parts for easy viewing
==Usage== <!--T:6-->
<!--T:7-->
# Open a document with the object you want to cut
# Optionally select the object and/or a datum plane before running the macro
# Run the macro from '''Macro → Macros → CutObjectForMagnets → Execute'''
# In the dialog:
#* Select the body to cut from the dropdown
#* Choose the cut plane type (Preset or Model Plane)
#* For preset planes, set the offset from origin
#* Configure magnet hole parameters:
#** '''Diameter''': Hole diameter (should match your magnet size)
#** '''Depth''': Hole depth from cut surface
#** '''Number of Holes''': Total holes to create
#** '''Edge Clearance (Preferred)''': Ideal distance from hole edge to object surface
#** '''Edge Clearance (Minimum)''': Minimum acceptable clearance
# Click "Execute Cut"
[[File:MacroCutObjectForMagnetsDialog.png|400px|center]]
==Example== <!--T:8-->
This image shows a vase object in its original form, and after being cut multiple times with the macro. Magnets can then be inserted to object, and the object can be re-assembled. This example was to create a "breakable" vase for a theatrical stage performance, but there are many other potential uses as well.
[[File:CutObjectForMagnetsExample.png|400px|center]]
==How It Works== <!--T:9-->
<!--T:10-->
'''Cutting Process:'''
# Creates a large half-space box aligned with the cutting plane
# Uses boolean operations to split the object into two parts
# Creates PartDesign::Body containers for each half
<!--T:11-->
'''Hole Placement Algorithm:'''
# Calculates hole positions evenly distributed along the cut face perimeter
# For each position, validates against BOTH parts (not just one)
# Uses preferred clearance first, falls back to minimum if needed
# Checks for hole-to-hole spacing (minimum 2x diameter)
# Repositions holes that fail safety checks using multiple strategies:
#* Reduced clearance at same position
#* Increased inset from edge
#* Alternative positions along the perimeter
<!--T:12-->
'''Safety Features:'''
* Creates test cylinders to check if holes would break through walls
* Validates each hole against both the top and bottom parts
* Skips holes that cannot be safely placed
* Reports repositioned and skipped holes in the console
==Parameters== <!--T:13-->
<!--T:14-->
{| class="wikitable"
! Parameter !! Description !! Default
|-
| Plane Type || "Preset Plane" (XY/XZ/YZ) or "Model Plane" (datum plane) || Preset Plane
|-
| Offset || Distance from origin for preset planes || 0 mm
|-
| Diameter || Magnet hole diameter || 3 mm
|-
| Depth || Hole depth from cut surface || 3 mm
|-
| Number of Holes || Total holes to create || 6
|-
| Edge Clearance (Preferred) || Ideal distance from hole edge to object surface || 2 mm
|-
| Edge Clearance (Minimum) || Minimum acceptable clearance (used during repositioning) || 0.5 mm
|}
==Requirements== <!--T:15-->
<!--T:16-->
* FreeCAD 0.19 or later
* An object with a solid shape (Part or PartDesign body)
* For model plane mode: a datum plane or object with planar faces
==Installation== <!--T:17-->
<!--T:18-->
# Download the macro file: [[Media:CutObjectForMagnets.FCMacro|CutObjectForMagnets.FCMacro]]
# Copy the file to your FreeCAD macro directory:
#* '''macOS''': {{FileName|~/Library/Application Support/FreeCAD/Macro/}}
#* '''Linux''': {{FileName|~/.local/share/FreeCAD/Macro/}}
#* '''Windows''': {{FileName|%APPDATA%/FreeCAD/Macro/}}
# Optionally, download the toolbar icon and place it in the same directory
==Tips== <!--T:19-->
<!--T:20-->
* '''Magnet sizing''': Measure your magnets carefully. Common sizes are 3mm, 5mm, and 6mm diameter.
* '''Add tolerance''': Consider adding 0.1-0.2mm to the diameter for easier magnet insertion.
* '''Depth planning''': Set hole depth slightly deeper than magnet height to ensure flush or recessed fit.
* '''Edge clearance''': For thin-walled objects, reduce the preferred clearance but keep minimum clearance high enough to prevent wall breakthrough.
* '''Re-cutting''': The macro detects existing magnet holes and preserves them when cutting already-cut parts.
==Source Code== <!--T:21-->
<!--T:22-->
The full source code is hosted on GitHub:
* [https://github.com/spkane/freecad-robust-mcp-and-more/blob/main/macros/Cut_Object_for_Magnets/CutObjectForMagnets.FCMacro CutObjectForMagnets.FCMacro on GitHub]
==Script== <!--T:23-->
<!--T:24-->
ToolBar Icon [[Image:Macro_Cut_Object_For_Magnets.png]]
</translate>
'''Macro_Cut_Object_For_Magnets.FCMacro'''
- https://github.com/spkane/freecad-robust-mcp-and-more/blob/main/macros/Cut_Object_for_Magnets/CutObjectForMagnets.FCMacro
==Links== <!--T:25-->
<!--T:26-->
* [https://spkane.github.io/freecad-robust-mcp-and-more/ Full Documentation] - Complete guides and tutorials
* [https://github.com/spkane/freecad-robust-mcp-and-more GitHub Repository] - Source code and issue tracker
* [[Part_Slice|Part Slice]] - FreeCAD's built-in slice tool
* [[PartDesign_Hole|PartDesign Hole]] - Parametric hole feature documentation
* [[PartDesign_Body|PartDesign Body]] - Body container documentation
[[Category:Macros{{#translation:}}]]
[[Category:User Documentation{{#translation:}}]]
[[Category:Addons {{#translation:}}]]
Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

-722
View File
@@ -1,722 +0,0 @@
"""FreeCAD Macro: Multi-Format Export.
SPDX-License-Identifier: MIT
Copyright (c) 2025 Sean P. Kane (GitHub: spkane)
Export selected bodies to multiple file formats simultaneously with a
user-friendly dialog for format selection and output configuration.
Requirements:
- FreeCAD 0.21 or later
- One or more objects selected in the 3D view
Usage:
1. Select the object(s) to export
2. Run the macro
3. Select desired export formats (STL, STEP, 3MF selected by default)
4. Choose output directory and base filename
5. Click "Export"
"""
# FreeCAD Addon Manager metadata
__Name__ = "Multi Export"
__Comment__ = "Export selected bodies to multiple file formats (STL, STEP, 3MF, OBJ, IGES, BREP, PLY, AMF) simultaneously"
__Author__ = "Sean P. Kane"
__Version__ = "0.6.1"
__Date__ = "2026-01-12"
__License__ = "MIT"
__Web__ = "https://github.com/spkane/freecad-robust-mcp-and-more"
__Wiki__ = "https://github.com/spkane/freecad-robust-mcp-and-more#readme"
__Icon__ = ""
__Help__ = "Select one or more objects, run the macro, choose export formats and output location, then click Export."
__Status__ = "Beta"
__Requires__ = "FreeCAD 0.21+"
__Communication__ = "https://github.com/spkane/freecad-robust-mcp-and-more/issues"
__Files__ = ""
import os
import FreeCAD as App
import FreeCADGui as Gui
import Mesh
import Part
from PySide import QtGui
class ExportFormat:
"""Represents an export format with its properties."""
def __init__(
self,
name: str,
extension: str,
description: str,
default_enabled: bool = False,
):
"""Initialize an export format.
Args:
name: Display name for the format
extension: File extension (without dot)
description: Brief description of the format
default_enabled: Whether this format is enabled by default
"""
self.name = name
self.extension = extension
self.description = description
self.default_enabled = default_enabled
# Define available export formats
EXPORT_FORMATS = [
ExportFormat(
"STL",
"stl",
"Stereolithography - common for 3D printing",
default_enabled=True,
),
ExportFormat(
"STEP",
"step",
"Standard for Exchange of Product Data - CAD interchange",
default_enabled=True,
),
ExportFormat(
"3MF",
"3mf",
"3D Manufacturing Format - modern 3D printing format",
default_enabled=True,
),
ExportFormat(
"OBJ",
"obj",
"Wavefront OBJ - 3D graphics and game engines",
default_enabled=False,
),
ExportFormat(
"IGES",
"iges",
"Initial Graphics Exchange Specification - legacy CAD format",
default_enabled=False,
),
ExportFormat(
"BREP",
"brep",
"OpenCASCADE native format - preserves exact geometry",
default_enabled=False,
),
ExportFormat(
"PLY",
"ply",
"Polygon File Format - 3D scanning and printing",
default_enabled=False,
),
ExportFormat(
"AMF",
"amf",
"Additive Manufacturing Format - XML-based 3D printing",
default_enabled=False,
),
]
class MultiExportDialog(QtGui.QDialog):
"""Dialog for configuring multi-format export options."""
def __init__(self, selected_objects: list, parent=None):
"""Initialize the export dialog.
Args:
selected_objects: List of FreeCAD objects to export
parent: Parent widget
"""
super(MultiExportDialog, self).__init__(parent)
self.selected_objects = selected_objects
self.format_checkboxes = {}
self.setWindowTitle("Multi-Format Export")
self.setModal(True)
self.setMinimumWidth(500)
self.setup_ui()
self.populate_defaults()
def setup_ui(self):
"""Initialize the user interface."""
layout = QtGui.QVBoxLayout()
# Objects to export section
objects_group = QtGui.QGroupBox("Objects to Export")
objects_layout = QtGui.QVBoxLayout()
self.objects_list = QtGui.QListWidget()
self.objects_list.setMaximumHeight(100)
self.objects_list.setSelectionMode(QtGui.QAbstractItemView.NoSelection)
for obj in self.selected_objects:
item = QtGui.QListWidgetItem(f"{obj.Label} ({_get_object_type(obj)})")
self.objects_list.addItem(item)
objects_layout.addWidget(self.objects_list)
objects_group.setLayout(objects_layout)
layout.addWidget(objects_group)
# Export formats section
formats_group = QtGui.QGroupBox("Export Formats")
formats_layout = QtGui.QGridLayout()
for i, fmt in enumerate(EXPORT_FORMATS):
checkbox = QtGui.QCheckBox(f"{fmt.name} (.{fmt.extension})")
checkbox.setChecked(fmt.default_enabled)
checkbox.setToolTip(fmt.description)
self.format_checkboxes[fmt.extension] = checkbox
row = i // 2
col = i % 2
formats_layout.addWidget(checkbox, row, col)
# Quick selection buttons
button_row = (len(EXPORT_FORMATS) + 1) // 2
select_all_btn = QtGui.QPushButton("Select All")
select_all_btn.clicked.connect(self._select_all_formats)
select_none_btn = QtGui.QPushButton("Select None")
select_none_btn.clicked.connect(self._select_no_formats)
select_defaults_btn = QtGui.QPushButton("Reset Defaults")
select_defaults_btn.clicked.connect(self._reset_default_formats)
btn_layout = QtGui.QHBoxLayout()
btn_layout.addWidget(select_all_btn)
btn_layout.addWidget(select_none_btn)
btn_layout.addWidget(select_defaults_btn)
btn_layout.addStretch()
formats_layout.addLayout(btn_layout, button_row, 0, 1, 2)
formats_group.setLayout(formats_layout)
layout.addWidget(formats_group)
# Output configuration section
output_group = QtGui.QGroupBox("Output Configuration")
output_layout = QtGui.QFormLayout()
# Directory selection
dir_layout = QtGui.QHBoxLayout()
self.directory_edit = QtGui.QLineEdit()
self.directory_edit.setReadOnly(True)
browse_btn = QtGui.QPushButton("Browse...")
browse_btn.clicked.connect(self._browse_directory)
dir_layout.addWidget(self.directory_edit)
dir_layout.addWidget(browse_btn)
output_layout.addRow("Directory:", dir_layout)
# Base filename
self.filename_edit = QtGui.QLineEdit()
self.filename_edit.setToolTip(
"Base filename without extension. Each format will be appended."
)
output_layout.addRow("Base Filename:", self.filename_edit)
# Preview of output files
self.preview_label = QtGui.QLabel("")
self.preview_label.setWordWrap(True)
self.preview_label.setStyleSheet("color: gray; font-size: 11px;")
output_layout.addRow("Will create:", self.preview_label)
# Connect signals for preview updates
self.filename_edit.textChanged.connect(self._update_preview)
self.directory_edit.textChanged.connect(self._update_preview)
for checkbox in self.format_checkboxes.values():
checkbox.stateChanged.connect(self._update_preview)
output_group.setLayout(output_layout)
layout.addWidget(output_group)
# Mesh options section (for STL, OBJ, PLY, 3MF, AMF)
mesh_group = QtGui.QGroupBox("Mesh Options (STL, OBJ, PLY, 3MF, AMF)")
mesh_layout = QtGui.QFormLayout()
self.tolerance_spin = QtGui.QDoubleSpinBox()
self.tolerance_spin.setRange(0.001, 10.0)
self.tolerance_spin.setValue(0.1)
self.tolerance_spin.setDecimals(3)
self.tolerance_spin.setSuffix(" mm")
self.tolerance_spin.setToolTip(
"Lower values create finer meshes but larger files"
)
mesh_layout.addRow("Surface Tolerance:", self.tolerance_spin)
self.deflection_spin = QtGui.QDoubleSpinBox()
self.deflection_spin.setRange(0.001, 10.0)
self.deflection_spin.setValue(0.1)
self.deflection_spin.setDecimals(3)
self.deflection_spin.setSuffix(" mm")
self.deflection_spin.setToolTip("Angular deflection for curved surfaces")
mesh_layout.addRow("Angular Deflection:", self.deflection_spin)
mesh_group.setLayout(mesh_layout)
layout.addWidget(mesh_group)
# Status label
self.status_label = QtGui.QLabel("")
self.status_label.setWordWrap(True)
layout.addWidget(self.status_label)
# Progress bar (hidden initially)
self.progress_bar = QtGui.QProgressBar()
self.progress_bar.setVisible(False)
layout.addWidget(self.progress_bar)
# Buttons
button_box = QtGui.QDialogButtonBox()
self.export_btn = button_box.addButton(
"Export", QtGui.QDialogButtonBox.AcceptRole
)
cancel_btn = button_box.addButton(QtGui.QDialogButtonBox.Cancel)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
layout.addWidget(button_box)
self.setLayout(layout)
def populate_defaults(self):
"""Populate default values based on the current document."""
doc = App.ActiveDocument
# Default directory: same as the FreeCAD document
if doc and doc.FileName:
default_dir = os.path.dirname(doc.FileName)
else:
default_dir = os.path.expanduser("~")
self.directory_edit.setText(default_dir)
# Default filename: based on selected objects or document name
if len(self.selected_objects) == 1:
# Single object: use its label
default_name = _sanitize_filename(self.selected_objects[0].Label)
elif doc and doc.FileName:
# Multiple objects: use document name
doc_name = os.path.splitext(os.path.basename(doc.FileName))[0]
default_name = _sanitize_filename(doc_name)
elif doc:
default_name = _sanitize_filename(doc.Label)
else:
default_name = "export"
self.filename_edit.setText(default_name)
self._update_preview()
def _select_all_formats(self):
"""Select all export formats."""
for checkbox in self.format_checkboxes.values():
checkbox.setChecked(True)
def _select_no_formats(self):
"""Deselect all export formats."""
for checkbox in self.format_checkboxes.values():
checkbox.setChecked(False)
def _reset_default_formats(self):
"""Reset to default format selection."""
for fmt in EXPORT_FORMATS:
self.format_checkboxes[fmt.extension].setChecked(fmt.default_enabled)
def _browse_directory(self):
"""Open directory browser dialog."""
current_dir = self.directory_edit.text() or os.path.expanduser("~")
directory = QtGui.QFileDialog.getExistingDirectory(
self,
"Select Export Directory",
current_dir,
QtGui.QFileDialog.ShowDirsOnly,
)
if directory:
self.directory_edit.setText(directory)
def _update_preview(self):
"""Update the preview of files to be created."""
selected_formats = self.get_selected_formats()
base_name = self.filename_edit.text().strip()
if not selected_formats:
self.preview_label.setText("No formats selected")
return
if not base_name:
self.preview_label.setText("Enter a filename")
return
files = [f"{base_name}.{ext}" for ext in selected_formats]
if len(files) > 4:
preview_text = ", ".join(files[:4]) + f", ... (+{len(files) - 4} more)"
else:
preview_text = ", ".join(files)
self.preview_label.setText(preview_text)
def get_selected_formats(self) -> list[str]:
"""Get list of selected format extensions."""
return [
ext
for ext, checkbox in self.format_checkboxes.items()
if checkbox.isChecked()
]
def get_export_parameters(self) -> dict:
"""Get all export parameters from the dialog."""
return {
"directory": self.directory_edit.text(),
"base_filename": self.filename_edit.text().strip(),
"formats": self.get_selected_formats(),
"mesh_tolerance": self.tolerance_spin.value(),
"mesh_deflection": self.deflection_spin.value(),
}
def set_status(self, message: str, is_error: bool = False):
"""Update status message."""
if is_error:
self.status_label.setStyleSheet("color: red;")
else:
self.status_label.setStyleSheet("color: green;")
self.status_label.setText(message)
QtGui.QApplication.processEvents()
def set_progress(self, value: int, maximum: int = 100):
"""Update progress bar."""
if not self.progress_bar.isVisible():
self.progress_bar.setVisible(True)
self.progress_bar.setMaximum(maximum)
self.progress_bar.setValue(value)
QtGui.QApplication.processEvents()
class MultiExporter:
"""Handles exporting objects to multiple formats."""
def __init__(self, objects: list, params: dict):
"""Initialize the exporter.
Args:
objects: List of FreeCAD objects to export
params: Export parameters from the dialog
"""
self.objects = objects
self.params = params
self.exported_files = []
self.errors = []
def export_all(self, progress_callback=None) -> tuple[list[str], list[str]]:
"""Export objects to all selected formats.
Args:
progress_callback: Optional callback for progress updates
Returns:
Tuple of (list of exported files, list of errors)
"""
formats = self.params["formats"]
total_exports = len(formats)
if total_exports == 0:
return [], ["No formats selected"]
for i, fmt in enumerate(formats):
if progress_callback:
progress = int((i / total_exports) * 100)
progress_callback(progress, f"Exporting {fmt.upper()}...")
try:
filepath = self._export_format(fmt)
self.exported_files.append(filepath)
App.Console.PrintMessage(f"Exported: {filepath}\n")
except Exception as e:
error_msg = f"Failed to export {fmt.upper()}: {e!s}"
self.errors.append(error_msg)
App.Console.PrintError(f"{error_msg}\n")
if progress_callback:
progress_callback(100, "Export complete!")
return self.exported_files, self.errors
def _export_format(self, extension: str) -> str:
"""Export to a specific format.
Args:
extension: File extension (format identifier)
Returns:
Path to the exported file
"""
directory = self.params["directory"]
base_name = self.params["base_filename"]
filepath = os.path.join(directory, f"{base_name}.{extension}")
# Get the shape(s) to export
shapes = []
for obj in self.objects:
if hasattr(obj, "Shape"):
shapes.append(obj.Shape)
elif hasattr(obj, "Mesh"):
shapes.append(obj.Mesh)
if not shapes:
raise ValueError("No exportable shapes found")
# Combine shapes if multiple
if len(shapes) == 1:
combined_shape = shapes[0]
else:
# For Part shapes, make a compound
combined_shape = Part.makeCompound(shapes)
# Export based on format
if extension == "stl":
self._export_mesh(combined_shape, filepath, "stl")
elif extension == "step":
self._export_step(combined_shape, filepath)
elif extension == "3mf":
self._export_mesh(combined_shape, filepath, "3mf")
elif extension == "obj":
self._export_mesh(combined_shape, filepath, "obj")
elif extension == "iges":
self._export_iges(combined_shape, filepath)
elif extension == "brep":
self._export_brep(combined_shape, filepath)
elif extension == "ply":
self._export_mesh(combined_shape, filepath, "ply")
elif extension == "amf":
self._export_mesh(combined_shape, filepath, "amf")
else:
raise ValueError(f"Unsupported format: {extension}")
return filepath
def _export_mesh(self, shape, filepath: str, format_type: str):
"""Export shape as a mesh format (STL, OBJ, PLY, 3MF, AMF).
Args:
shape: Part.Shape to export
filepath: Output file path
format_type: Mesh format type
"""
tolerance = self.params["mesh_tolerance"]
deflection = self.params["mesh_deflection"]
# Create mesh from shape
mesh = Mesh.Mesh()
if hasattr(shape, "tessellate"):
# Part.Shape - tessellate it
vertices, facets = shape.tessellate(tolerance)
mesh_data = []
for facet in facets:
triangle = [
vertices[facet[0]],
vertices[facet[1]],
vertices[facet[2]],
]
mesh_data.append(triangle)
mesh.addFacets(mesh_data)
elif hasattr(shape, "Facets"):
# Already a Mesh
mesh = shape
else:
raise ValueError("Cannot create mesh from object")
# Export the mesh
mesh.write(filepath)
def _export_step(self, shape, filepath: str):
"""Export shape as STEP format.
Args:
shape: Part.Shape to export
filepath: Output file path
"""
shape.exportStep(filepath)
def _export_iges(self, shape, filepath: str):
"""Export shape as IGES format.
Args:
shape: Part.Shape to export
filepath: Output file path
"""
shape.exportIges(filepath)
def _export_brep(self, shape, filepath: str):
"""Export shape as BREP format.
Args:
shape: Part.Shape to export
filepath: Output file path
"""
shape.exportBrep(filepath)
def _get_object_type(obj) -> str:
"""Get a human-readable type description for an object."""
if hasattr(obj, "TypeId"):
type_id = obj.TypeId
if "Part::" in type_id:
return type_id.replace("Part::", "")
if "PartDesign::" in type_id:
return type_id.replace("PartDesign::", "")
if "Mesh::" in type_id:
return "Mesh"
return type_id
if hasattr(obj, "Shape"):
return "Shape"
return ""
def _sanitize_filename(name: str) -> str:
"""Sanitize a string for use as a filename.
Args:
name: The original name
Returns:
Sanitized filename (without extension)
"""
# Replace problematic characters
invalid_chars = '<>:"/\\|?*'
result = name
for char in invalid_chars:
result = result.replace(char, "_")
# Remove leading/trailing whitespace and dots
result = result.strip(" .")
# Ensure non-empty
return result if result else "export"
def _get_exportable_objects(selection: list) -> list:
"""Filter selection to only include exportable objects.
Args:
selection: List of selected FreeCAD objects
Returns:
List of objects that can be exported
"""
exportable = []
for obj in selection:
# Check for Part shapes
if hasattr(obj, "Shape") and hasattr(obj.Shape, "Volume"):
if obj.Shape.Volume > 0.001:
exportable.append(obj)
# Check for Mesh objects
elif hasattr(obj, "Mesh"):
exportable.append(obj)
return exportable
def main():
"""Main macro entry point."""
# Check for active document
if not App.ActiveDocument:
QtGui.QMessageBox.warning(
None, "No Document", "Please open or create a document first."
)
return
# Get current selection
selection = Gui.Selection.getSelection()
if not selection:
QtGui.QMessageBox.warning(
None,
"No Selection",
"Please select one or more objects to export.\n\n"
"You can select multiple objects by holding Ctrl/Cmd while clicking.",
)
return
# Filter to exportable objects
exportable = _get_exportable_objects(selection)
if not exportable:
QtGui.QMessageBox.warning(
None,
"No Exportable Objects",
"None of the selected objects can be exported.\n\n"
"Please select objects with solid shapes (Part or PartDesign bodies).",
)
return
# Show dialog
dialog = MultiExportDialog(exportable)
if dialog.exec_() != QtGui.QDialog.Accepted:
return
params = dialog.get_export_parameters()
# Validate parameters
if not params["formats"]:
QtGui.QMessageBox.warning(
None,
"No Formats Selected",
"Please select at least one export format.",
)
return
if not params["base_filename"]:
QtGui.QMessageBox.warning(
None,
"No Filename",
"Please enter a base filename for the exports.",
)
return
if not os.path.isdir(params["directory"]):
QtGui.QMessageBox.warning(
None,
"Invalid Directory",
f"The directory does not exist:\n{params['directory']}",
)
return
# Perform export
try:
exporter = MultiExporter(exportable, params)
def progress_update(value, message=""):
dialog.set_status(message)
dialog.set_progress(value)
exported_files, errors = exporter.export_all(progress_update)
# Show results
if exported_files:
success_msg = f"Successfully exported {len(exported_files)} file(s):\n"
for f in exported_files[:5]:
success_msg += f"\n • {os.path.basename(f)}"
if len(exported_files) > 5:
success_msg += f"\n ... and {len(exported_files) - 5} more"
if errors:
success_msg += f"\n\nWarnings ({len(errors)}):\n"
for e in errors[:3]:
success_msg += f"\n • {e}"
dialog.set_status(success_msg)
App.Console.PrintMessage(
f"Multi-export complete: {len(exported_files)} files\n"
)
else:
error_msg = "Export failed:\n" + "\n".join(errors)
dialog.set_status(error_msg, is_error=True)
App.Console.PrintError(f"Multi-export failed: {errors}\n")
except Exception as e:
dialog.set_status(f"Export error: {e!s}", is_error=True)
App.Console.PrintError(f"Multi-export error: {e!s}\n")
import traceback
traceback.print_exc()
if __name__ == "__main__":
main()
-47
View File
@@ -1,47 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<!-- Background -->
<rect width="64" height="64" fill="#2c3e50" rx="4"/>
<!-- 3D Object representation (cube) -->
<g id="source-object" transform="translate(8, 8)">
<!-- Cube top face -->
<polygon points="16,4 28,10 16,16 4,10" fill="#3498db" stroke="#2980b9" stroke-width="1"/>
<!-- Cube right face -->
<polygon points="28,10 28,22 16,28 16,16" fill="#2980b9" stroke="#1a5276" stroke-width="1"/>
<!-- Cube left face -->
<polygon points="4,10 16,16 16,28 4,22" fill="#5dade2" stroke="#2980b9" stroke-width="1"/>
</g>
<!-- Export arrow -->
<g id="export-arrow">
<line x1="32" y1="26" x2="32" y2="38" stroke="#ecf0f1" stroke-width="2"/>
<polygon points="32,42 26,36 38,36" fill="#ecf0f1"/>
</g>
<!-- Multiple file format outputs -->
<!-- STL file icon -->
<g id="stl-file" transform="translate(6, 44)">
<rect x="0" y="0" width="14" height="16" fill="#27ae60" stroke="#1e8449" stroke-width="1" rx="1"/>
<rect x="0" y="0" width="14" height="4" fill="#1e8449" rx="1"/>
<text x="7" y="12" font-family="Arial, sans-serif" font-size="5" fill="white" text-anchor="middle" font-weight="bold">STL</text>
</g>
<!-- STEP file icon -->
<g id="step-file" transform="translate(25, 44)">
<rect x="0" y="0" width="14" height="16" fill="#e67e22" stroke="#d35400" stroke-width="1" rx="1"/>
<rect x="0" y="0" width="14" height="4" fill="#d35400" rx="1"/>
<text x="7" y="12" font-family="Arial, sans-serif" font-size="4" fill="white" text-anchor="middle" font-weight="bold">STEP</text>
</g>
<!-- 3MF file icon -->
<g id="3mf-file" transform="translate(44, 44)">
<rect x="0" y="0" width="14" height="16" fill="#9b59b6" stroke="#8e44ad" stroke-width="1" rx="1"/>
<rect x="0" y="0" width="14" height="4" fill="#8e44ad" rx="1"/>
<text x="7" y="12" font-family="Arial, sans-serif" font-size="4" fill="white" text-anchor="middle" font-weight="bold">3MF</text>
</g>
<!-- Checkmarks on files to indicate selection -->
<g id="checkmarks" fill="#2ecc71" stroke="#27ae60" stroke-width="0.5">
<circle cx="17" cy="47" r="3" fill="#2ecc71"/>
<path d="M15.5,47 L16.5,48 L18.5,46" stroke="white" stroke-width="1" fill="none"/>
<circle cx="36" cy="47" r="3" fill="#2ecc71"/>
<path d="M34.5,47 L35.5,48 L37.5,46" stroke="white" stroke-width="1" fill="none"/>
<circle cx="55" cy="47" r="3" fill="#2ecc71"/>
<path d="M53.5,47 L54.5,48 L56.5,46" stroke="white" stroke-width="1" fill="none"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

-298
View File
@@ -1,298 +0,0 @@
# Multi-Format Export - FreeCAD Macro
![Macro Icon](MultiExport.svg)
**Version:** 0.6.1
**FreeCAD Version:** 0.19 or later
**License:** MIT
> **Documentation:** [https://spkane.github.io/freecad-robust-mcp-and-more/](https://spkane.github.io/freecad-robust-mcp-and-more/)
## Overview
This FreeCAD macro exports selected objects to multiple file formats simultaneously with a single click. It features a user-friendly dialog for selecting export formats, configuring output options, and previewing the files that will be created.
Perfect for:
- Preparing files for 3D printing (STL, 3MF)
- CAD interchange with colleagues or clients (STEP, IGES)
- Asset creation for games or visualization (OBJ, PLY)
- Backup exports in multiple formats
## Features
- **Multi-Format Export** - Export to up to 8 different formats at once
- **Smart Defaults** - STL, STEP, and 3MF selected by default (most common formats)
- **Intelligent Path Defaults** - Uses document location and object names automatically
- **Mesh Quality Control** - Adjustable tolerance and deflection for mesh formats
- **Preview Mode** - See exactly what files will be created before exporting
- **Progress Feedback** - Real-time progress bar and status messages
- **Batch Export** - Export multiple selected objects together
---
## Supported Formats
| Format | Extension | Default | Description |
| ------ | --------- | ------- | ---------------------------------------- |
| STL | `.stl` | Yes | Stereolithography - standard 3D printing |
| STEP | `.step` | Yes | CAD interchange - preserves geometry |
| 3MF | `.3mf` | Yes | Modern 3D printing with metadata |
| OBJ | `.obj` | No | Wavefront - 3D graphics and games |
| IGES | `.iges` | No | Legacy CAD interchange |
| BREP | `.brep` | No | OpenCASCADE native - exact geometry |
| PLY | `.ply` | No | Polygon file - 3D scanning |
| AMF | `.amf` | No | Additive manufacturing format |
---
## Installation
### macOS
1. Open **Finder**
1. Press `Cmd + Shift + G` (Go to Folder)
1. Paste: `~/Library/Application Support/FreeCAD/Macro/`
1. Press **Enter**
1. Copy `MultiExport.FCMacro` to this folder
1. (Optional) Copy `MultiExport.svg` for the icon
**From FreeCAD:**
1. Go to **Macro → Macros...**
1. Note the path shown at the top of the dialog
1. Click **User macros location** to open in Finder
1. Copy files there
### Linux
```bash
mkdir -p ~/.FreeCAD/Macro/
cp MultiExport.FCMacro ~/.FreeCAD/Macro/
cp MultiExport.svg ~/.FreeCAD/Macro/ # Optional icon
```
### Windows
1. Navigate to: `%APPDATA%\FreeCAD\Macro\`
1. Copy `MultiExport.FCMacro` to this folder
1. (Optional) Copy `MultiExport.svg` to the same folder
---
## How to Use
### Quick Start
1. **Select objects** in the 3D view (Ctrl+click for multiple)
1. **Run the macro:** Macro → Macros... → MultiExport → Execute
1. **Choose formats** (STL, STEP, 3MF are pre-selected)
1. **Set output location** and filename
1. **Click Export**
### Detailed Steps
#### Step 1: Select Objects
- Click on one or more objects in the 3D view
- Hold `Ctrl` (or `Cmd` on macOS) to select multiple objects
- Selected objects appear in the "Objects to Export" list
#### Step 2: Choose Export Formats
The dialog shows all available formats with checkboxes:
- **STL** - Checked by default (3D printing standard)
- **STEP** - Checked by default (CAD interchange)
- **3MF** - Checked by default (modern 3D printing)
Quick selection buttons:
- **Select All** - Check all formats
- **Select None** - Uncheck all formats
- **Reset Defaults** - Return to STL + STEP + 3MF
#### Step 3: Configure Output
**Directory:**
- Defaults to the same folder as the current FreeCAD document
- Click **Browse...** to choose a different location
**Base Filename:**
- Defaults to the selected object's label (single object)
- Or the document name (multiple objects)
- Enter any name (without extension)
- Extensions are added automatically based on selected formats
**Preview:**
- Shows the files that will be created
- Updates in real-time as you change options
#### Step 4: Mesh Options (Optional)
For mesh formats (STL, OBJ, PLY, 3MF, AMF):
**Surface Tolerance:**
- Lower values = finer mesh = larger files
- Default: 0.1mm (good balance)
- For fine details: 0.01-0.05mm
- For quick exports: 0.2-0.5mm
**Angular Deflection:**
- Controls curved surface approximation
- Default: 0.1mm
- Lower values = smoother curves
#### Step 5: Export
- Click **Export**
- Watch the progress bar
- Review results in the status area
---
## Parameter Reference
### Output Configuration
| Parameter | Default | Description |
| ------------- | ----------------- | ---------------------------- |
| Directory | Document location | Where to save exported files |
| Base Filename | Object/Doc label | Filename without extension |
### Mesh Settings
| Parameter | Default | Range | Description |
| ------------------ | ------- | ---------- | ----------------------------- |
| Surface Tolerance | 0.1 mm | 0.001-10mm | Mesh fineness (lower = finer) |
| Angular Deflection | 0.1 mm | 0.001-10mm | Curved surface approximation |
---
## Use Cases
### Case 1: 3D Printing Preparation
**Scenario:** Export a model for printing on different slicers
**Settings:**
- Formats: STL + 3MF
- Tolerance: 0.1mm (standard quality)
**Result:** Files ready for Cura, PrusaSlicer, Bambu Studio, etc.
### Case 2: Sharing with CAD Users
**Scenario:** Send design to colleague using SolidWorks
**Settings:**
- Formats: STEP + IGES
- (Mesh settings don't apply to these formats)
**Result:** Editable CAD files that preserve exact geometry
### Case 3: Game Asset Export
**Scenario:** Create 3D model for Unity/Unreal
**Settings:**
- Formats: OBJ + PLY
- Tolerance: 0.05mm (good detail)
**Result:** Standard mesh formats for game engines
### Case 4: Complete Backup
**Scenario:** Archive a project in all formats
**Settings:**
- Click **Select All** for all formats
- Directory: Your backup location
**Result:** Complete export in 8 formats for future compatibility
---
## Troubleshooting
### Error: "No Selection"
**Solution:** Select one or more objects in the 3D view before running the macro.
### Error: "No Exportable Objects"
**Cause:** Selected items are not solid objects (groups, annotations, etc.)
**Solution:** Select actual Part or PartDesign bodies with solid geometry.
### Error: "Invalid Directory"
**Solution:** The chosen directory doesn't exist. Click Browse and select a valid folder.
### Warning: Format export failed
**Possible causes:**
1. **Object has invalid geometry** - Try Part → Check Geometry first
1. **Disk full** - Check available disk space
1. **Permission denied** - Choose a folder you have write access to
---
## Technical Details
### Export Methods
| Format | Export Method |
| ------ | -------------------------------- |
| STL | Mesh tessellation → Mesh.write() |
| STEP | Part.Shape.exportStep() |
| 3MF | Mesh tessellation → Mesh.write() |
| OBJ | Mesh tessellation → Mesh.write() |
| IGES | Part.Shape.exportIges() |
| BREP | Part.Shape.exportBrep() |
| PLY | Mesh tessellation → Mesh.write() |
| AMF | Mesh tessellation → Mesh.write() |
### Multiple Objects
When multiple objects are selected:
- **Mesh formats:** Objects are combined into a single compound mesh
- **CAD formats:** Objects are combined into a Part.Compound
---
## Version History
### v0.5.0-beta (2026-01-05)
- Initial release
- 8 export format support (STL, STEP, 3MF, OBJ, IGES, BREP, PLY, AMF)
- Smart defaults (STL, STEP, 3MF pre-selected)
- Intelligent path and filename defaults
- Configurable mesh quality settings
- Real-time file preview
- Progress bar and status feedback
- Multi-object batch export
---
## License
MIT License - Free to use, modify, and distribute.
---
## Credits
Created to simplify the FreeCAD export workflow and eliminate repetitive export operations.
-45
View File
@@ -1,45 +0,0 @@
# Multi Export Macro Release Notes
## Version 0.6.1 (2026-01-12)
Release notes for changes between v0.5.0-beta and v0.6.1.
### Added
- **FreeCAD Addon Manager metadata**: Macro now includes standard metadata fields for better Addon Manager integration
- **FreeCAD Wiki page**: Official wiki documentation at [Macro Multi Export](https://wiki.freecad.org/Macro_Multi_Export)
- **Dialog screenshot**: Added screenshot showing the export dialog
### Changed
- **Minimum FreeCAD version**: Updated requirement from FreeCAD 0.19 to FreeCAD 0.21 (for better 3MF support)
- **Version tracking**: Version now managed via `__Version__` metadata field instead of inline comment
### No Functional Changes
The core export functionality remains unchanged from the beta release:
- Multi-format export (STL, STEP, 3MF, OBJ, IGES, BREP, PLY, AMF)
- Configurable mesh tolerance for tessellated formats
- Batch export with progress feedback
- Format-specific options
This macro is stable for production use.
### Installation
**Via FreeCAD Addon Manager:**
1. Open FreeCAD
2. Go to Macro > Macros...
3. Click "Download" tab
4. Search for "Multi Export"
5. Click Install
**Manual Installation:**
Copy `MultiExport.FCMacro` to your FreeCAD macro directory:
- **macOS**: `~/Library/Application Support/FreeCAD/Macro/`
- **Linux**: `~/.local/share/FreeCAD/Macro/`
- **Windows**: `%APPDATA%/FreeCAD/Macro/`
-101
View File
@@ -1,101 +0,0 @@
<languages/>
<translate>
<!--T:1-->
{{Macro
|Name=Macro Multi Export
|Icon=Macro_Multi_Export.png
|Description=Export selected bodies to multiple file formats (STL, STEP, 3MF, OBJ, IGES, BREP, PLY, AMF) simultaneously with a user-friendly dialog for format selection and output configuration.
|Author=Sean P. Kane
|Version=0.6.1
|Date=2026-01-12
|FCVersion=0.21+
|Download=[https://wiki.freecad.org/images/thumb/1/19/Macro_Multi_Export.png/48px-Macro_Multi_Export.png ToolBar Icon]
|SeeAlso=[[Std_Export|Std Export]], [[Import_Export|Import Export]]
}}
==Description== <!--T:2-->
<!--T:3-->
This macro provides a convenient way to export selected FreeCAD objects to multiple file formats at once. Instead of exporting to each format individually, you can select all desired formats in a single dialog and export them simultaneously.
<!--T:4-->
'''Supported Export Formats:'''
* STL - Stereolithography (common for 3D printing)
* STEP - Standard for Exchange of Product Data (CAD interchange)
* 3MF - 3D Manufacturing Format (modern 3D printing format)
* OBJ - Wavefront OBJ (3D graphics and game engines)
* IGES - Initial Graphics Exchange Specification (legacy CAD format)
* BREP - OpenCASCADE native format (preserves exact geometry)
* PLY - Polygon File Format (3D scanning and printing)
* AMF - Additive Manufacturing Format (XML-based 3D printing)
<!--T:5-->
'''Features:'''
* Export to multiple formats in a single operation
* User-friendly dialog for format selection
* Configurable output directory and base filename
* Mesh tolerance settings for STL/OBJ/PLY/3MF/AMF exports
* Preview of files to be created
* Quick select buttons: "Select All", "Select None", "Reset Defaults"
* Default formats: STL, STEP, and 3MF are pre-selected
==Usage== <!--T:6-->
<!--T:7-->
# Select one or more objects in the 3D view that you want to export
# Run the macro from '''Macro → Macros → MultiExport → Execute'''
# In the dialog:
#* Review the selected objects in the "Objects to Export" section
#* Check the formats you want to export to (STL, STEP, 3MF are selected by default)
#* Choose an output directory using the "Browse..." button
#* Enter a base filename (the format extension will be appended automatically)
#* Optionally adjust mesh tolerance settings for mesh-based formats
# Click "Export" to create the files
[[File:Macro_Multi_Export_dialog.png|400px|center]]
==Requirements== <!--T:8-->
<!--T:9-->
* FreeCAD 0.21 or later
* Objects with solid shapes (Part or PartDesign bodies)
==Installation== <!--T:10-->
<!--T:11-->
# Download the macro file: [[Media:MultiExport.FCMacro|MultiExport.FCMacro]]
# Copy the file to your FreeCAD macro directory:
#* '''macOS''': {{FileName|~/Library/Application Support/FreeCAD/Macro/}}
#* '''Linux''': {{FileName|~/.local/share/FreeCAD/Macro/}}
#* '''Windows''': {{FileName|%APPDATA%/FreeCAD/Macro/}}
# Optionally, download the toolbar icon and place it in the same directory
==Source Code== <!--T:12-->
<!--T:13-->
The full source code is hosted on GitHub:
* [https://github.com/spkane/freecad-robust-mcp-and-more/blob/main/macros/Multi_Export/MultiExport.FCMacro MultiExport.FCMacro on GitHub]
==Script== <!--T:14-->
<!--T:15-->
ToolBar Icon [[Image:Macro_Multi_Export.png]]
</translate>
'''Macro_Multi_Export.FCMacro'''
- https://github.com/spkane/freecad-robust-mcp-and-more/blob/main/macros/Multi_Export/MultiExport.FCMacro
==Links== <!--T:16-->
<!--T:17-->
* [https://spkane.github.io/freecad-robust-mcp-and-more/ Full Documentation] - Complete guides and tutorials
* [https://github.com/spkane/freecad-robust-mcp-and-more GitHub Repository] - Source code and issue tracker
* [[Std_Export|Std Export]] - FreeCAD's built-in export function
* [[Import_Export|Import Export]] - Overview of FreeCAD import/export formats
[[Category:Macros{{#translation:}}]]
[[Category:User Documentation{{#translation:}}]]
[[Category:Addons {{#translation:}}]]
+5 -5
View File
@@ -1,8 +1,8 @@
site_name: FreeCAD Robust MCP Suite site_name: FreeCAD Robust MCP Suite
site_description: FreeCAD Robust MCP Suite - MCP Server, Bridge Workbench, and Macros for AI assistant integration site_description: FreeCAD Robust MCP Suite - MCP Server, Bridge Workbench, and Macros for AI assistant integration
site_url: https://github.com/spkane/freecad-robust-mcp-and-more site_url: https://github.com/spkane/freecad-addon-robust-mcp-server
repo_url: https://github.com/spkane/freecad-robust-mcp-and-more repo_url: https://github.com/spkane/freecad-addon-robust-mcp-server
repo_name: spkane/freecad-robust-mcp-and-more repo_name: spkane/freecad-addon-robust-mcp-server
theme: theme:
name: material name: material
@@ -112,7 +112,7 @@ extra:
default: latest default: latest
social: social:
- icon: fontawesome/brands/github - icon: fontawesome/brands/github
link: https://github.com/spkane/freecad-robust-mcp-and-more link: https://github.com/spkane/freecad-addon-robust-mcp-server
- icon: fontawesome/brands/docker - icon: fontawesome/brands/docker
link: https://hub.docker.com/r/spkane/freecad-robust-mcp link: https://hub.docker.com/r/spkane/freecad-robust-mcp
- icon: fontawesome/brands/python - icon: fontawesome/brands/python
@@ -127,7 +127,7 @@ nav:
- User Guide: - User Guide:
- Connection Modes: guide/connection-modes.md - Connection Modes: guide/connection-modes.md
- Robust MCP Bridge Workbench: guide/workbench.md - Robust MCP Bridge Workbench: guide/workbench.md
- FreeCAD Macros: guide/macros.md - MCP Macro Tools: guide/macros.md
- Tools Overview: guide/tools.md - Tools Overview: guide/tools.md
- MCP Resources: guide/resources.md - MCP Resources: guide/resources.md
- Detailed User Guide: USER_GUIDE.md - Detailed User Guide: USER_GUIDE.md
+6 -30
View File
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<package format="1" xmlns="https://wiki.freecad.org/Package_Metadata"> <package format="1" xmlns="https://wiki.freecad.org/Package_Metadata">
<name>FreeCAD Robust MCP Suite</name> <name>FreeCAD Robust MCP Bridge</name>
<description>A collection of FreeCAD macros plus a robust MCP (Model Context Protocol) bridge workbench for AI assistant integration.</description> <description>A robust MCP (Model Context Protocol) bridge workbench for AI assistant integration with FreeCAD.</description>
<icon>addon/FreecadRobustMCPBridge/FreecadRobustMCPBridge.svg</icon> <icon>addon/FreecadRobustMCPBridge/FreecadRobustMCPBridge.svg</icon>
<!-- Note: Each component has its own version below. This top-level date is for reference only. --> <!-- Note: Each component has its own version below. This top-level date is for reference only. -->
@@ -15,16 +15,12 @@
<freecadmin>0.21</freecadmin> <freecadmin>0.21</freecadmin>
<url type="repository" branch="main">https://github.com/spkane/freecad-robust-mcp-and-more</url> <url type="repository" branch="main">https://github.com/spkane/freecad-addon-robust-mcp-server</url>
<url type="bugtracker">https://github.com/spkane/freecad-robust-mcp-and-more/issues</url> <url type="bugtracker">https://github.com/spkane/freecad-addon-robust-mcp-server/issues</url>
<url type="readme">https://github.com/spkane/freecad-robust-mcp-and-more/blob/main/README.md</url> <url type="readme">https://github.com/spkane/freecad-addon-robust-mcp-server/blob/main/README.md</url>
<url type="documentation">https://github.com/spkane/freecad-robust-mcp-and-more#readme</url> <url type="documentation">https://github.com/spkane/freecad-addon-robust-mcp-server#readme</url>
<tag>workbench</tag> <tag>workbench</tag>
<tag>macro</tag>
<tag>export</tag>
<tag>3D printing</tag>
<tag>magnets</tag>
<tag>MCP</tag> <tag>MCP</tag>
<tag>AI</tag> <tag>AI</tag>
<tag>automation</tag> <tag>automation</tag>
@@ -46,26 +42,6 @@
<freecadmin>0.21</freecadmin> <freecadmin>0.21</freecadmin>
</workbench> </workbench>
<macro>
<name>Multi Export</name>
<version>0.6.1</version>
<date>2026-01-12</date>
<description>Export selected bodies to multiple file formats (STL, STEP, 3MF, OBJ, IGES, BREP, PLY, AMF) simultaneously with configurable mesh options.</description>
<subdirectory>./macros/Multi_Export/</subdirectory>
<file>MultiExport.FCMacro</file>
<icon>MultiExport.svg</icon>
</macro>
<macro>
<name>Cut Object for Magnets</name>
<version>0.6.1</version>
<date>2026-01-12</date>
<description>Cut an object along a plane and add aligned magnet holes with surface collision detection. Perfect for creating 3D printed parts that snap together with embedded magnets.</description>
<subdirectory>./macros/Cut_Object_for_Magnets/</subdirectory>
<file>CutObjectForMagnets.FCMacro</file>
<icon>CutObjectForMagnets.svg</icon>
</macro>
</content> </content>
</package> </package>
-2
View File
@@ -335,7 +335,5 @@ major_version_zero = true
# Note: This project uses component-specific tags for releases: # Note: This project uses component-specific tags for releases:
# - robust-mcp-server-vX.Y.Z (MCP server: PyPI, Docker, GitHub release) # - robust-mcp-server-vX.Y.Z (MCP server: PyPI, Docker, GitHub release)
# - robust-mcp-workbench-vX.Y.Z (FreeCAD workbench) # - robust-mcp-workbench-vX.Y.Z (FreeCAD workbench)
# - macro-cut-object-for-magnets-vX.Y.Z
# - macro-multi-export-vX.Y.Z
# Use `just release::tag-<component> <version>` to create release tags. # Use `just release::tag-<component> <version>` to create release tags.
# Commitizen is used for commit message validation and changelog generation. # Commitizen is used for commit message validation and changelog generation.
File diff suppressed because it is too large Load Diff
-396
View File
@@ -1,396 +0,0 @@
"""Integration tests for the MultiExport macro.
These tests verify the MultiExporter class functionality including:
- Exporting to STL format
- Exporting to STEP format
- Exporting to multiple formats simultaneously
- Handling mesh tolerance settings
Note: These tests require a running FreeCAD Robust MCP Bridge.
Start it with: just freecad::run-gui or just freecad::run-headless
To run these tests:
pytest tests/integration/test_multi_export.py -v
"""
from __future__ import annotations
import os
import tempfile
from typing import TYPE_CHECKING, Any
import pytest
if TYPE_CHECKING:
import xmlrpc.client
# Mark all tests in this module as integration tests
pytestmark = pytest.mark.integration
def execute_code(proxy: xmlrpc.client.ServerProxy, code: str) -> dict[str, Any]:
"""Execute Python code via the MCP bridge and return the result."""
result = proxy.execute(code) # type: ignore[union-attr]
assert isinstance(result, dict), f"Unexpected result type: {type(result)}"
assert result.get("success"), f"Execution failed: {result.get('error_traceback')}"
return result
# The MultiExporter class code embedded for testing
MULTI_EXPORTER_CODE = '''
import os
import FreeCAD as App
import Part
import Mesh
class MultiExporter:
"""Handles exporting objects to multiple formats."""
def __init__(self, objects, params):
"""Initialize the exporter."""
self.objects = objects
self.params = params
self.exported_files = []
self.errors = []
def export_all(self):
"""Export objects to all selected formats."""
formats = self.params["formats"]
if not formats:
return [], ["No formats selected"]
for fmt in formats:
try:
filepath = self._export_format(fmt)
self.exported_files.append(filepath)
except Exception as e:
self.errors.append(f"Failed to export {fmt.upper()}: {str(e)}")
return self.exported_files, self.errors
def _export_format(self, extension):
"""Export to a specific format."""
directory = self.params["directory"]
base_name = self.params["base_filename"]
filepath = os.path.join(directory, f"{base_name}.{extension}")
shapes = []
for obj in self.objects:
if hasattr(obj, "Shape"):
shapes.append(obj.Shape)
if not shapes:
raise ValueError("No exportable shapes found")
if len(shapes) == 1:
combined_shape = shapes[0]
else:
combined_shape = Part.makeCompound(shapes)
if extension == "stl":
self._export_mesh(combined_shape, filepath)
elif extension == "step":
combined_shape.exportStep(filepath)
elif extension == "brep":
combined_shape.exportBrep(filepath)
else:
raise ValueError(f"Unsupported format: {extension}")
return filepath
def _export_mesh(self, shape, filepath):
"""Export shape as mesh format."""
tolerance = self.params.get("mesh_tolerance", 0.1)
mesh = Mesh.Mesh()
vertices, facets = shape.tessellate(tolerance)
mesh_data = []
for facet in facets:
triangle = [vertices[facet[0]], vertices[facet[1]], vertices[facet[2]]]
mesh_data.append(triangle)
mesh.addFacets(mesh_data)
mesh.write(filepath)
'''
@pytest.fixture
def temp_export_dir():
"""Create a temporary directory for export tests."""
with tempfile.TemporaryDirectory() as tmpdir:
yield tmpdir
class TestMultiExporter:
"""Tests for the MultiExporter functionality."""
def test_export_stl(self, xmlrpc_proxy, temp_export_dir):
"""Test exporting a simple box to STL format."""
code = f"""
{MULTI_EXPORTER_CODE}
# Create a new document and simple box
doc = App.newDocument("TestExportSTL")
box = doc.addObject("Part::Box", "TestBox")
box.Length = 20
box.Width = 20
box.Height = 10
doc.recompute()
# Set up export parameters
params = {{
"directory": "{temp_export_dir}",
"base_filename": "test_box",
"formats": ["stl"],
"mesh_tolerance": 0.1,
}}
# Create exporter and export
exporter = MultiExporter([box], params)
exported_files, errors = exporter.export_all()
# Clean up
App.closeDocument("TestExportSTL")
_result_ = {{
"exported_files": exported_files,
"errors": errors,
"file_exists": os.path.exists(exported_files[0]) if exported_files else False,
}}
"""
result = execute_code(xmlrpc_proxy, code)
data = result.get("result", {})
assert len(data["exported_files"]) == 1
assert len(data["errors"]) == 0
assert data["file_exists"] is True
assert data["exported_files"][0].endswith(".stl")
def test_export_step(self, xmlrpc_proxy, temp_export_dir):
"""Test exporting a simple cylinder to STEP format."""
code = f"""
{MULTI_EXPORTER_CODE}
# Create a new document and simple cylinder
doc = App.newDocument("TestExportSTEP")
cylinder = doc.addObject("Part::Cylinder", "TestCylinder")
cylinder.Radius = 10
cylinder.Height = 30
doc.recompute()
# Set up export parameters
params = {{
"directory": "{temp_export_dir}",
"base_filename": "test_cylinder",
"formats": ["step"],
"mesh_tolerance": 0.1,
}}
# Create exporter and export
exporter = MultiExporter([cylinder], params)
exported_files, errors = exporter.export_all()
# Clean up
App.closeDocument("TestExportSTEP")
_result_ = {{
"exported_files": exported_files,
"errors": errors,
"file_exists": os.path.exists(exported_files[0]) if exported_files else False,
}}
"""
result = execute_code(xmlrpc_proxy, code)
data = result.get("result", {})
assert len(data["exported_files"]) == 1
assert len(data["errors"]) == 0
assert data["file_exists"] is True
assert data["exported_files"][0].endswith(".step")
def test_export_multiple_formats(self, xmlrpc_proxy, temp_export_dir):
"""Test exporting to multiple formats simultaneously."""
code = f"""
{MULTI_EXPORTER_CODE}
# Create a new document and simple sphere
doc = App.newDocument("TestExportMulti")
sphere = doc.addObject("Part::Sphere", "TestSphere")
sphere.Radius = 15
doc.recompute()
# Set up export parameters for multiple formats
params = {{
"directory": "{temp_export_dir}",
"base_filename": "test_sphere",
"formats": ["stl", "step", "brep"],
"mesh_tolerance": 0.1,
}}
# Create exporter and export
exporter = MultiExporter([sphere], params)
exported_files, errors = exporter.export_all()
# Check which files exist
files_exist = [os.path.exists(f) for f in exported_files]
# Clean up
App.closeDocument("TestExportMulti")
_result_ = {{
"exported_files": exported_files,
"errors": errors,
"files_exist": files_exist,
"count": len(exported_files),
}}
"""
result = execute_code(xmlrpc_proxy, code)
data = result.get("result", {})
assert data["count"] == 3
assert len(data["errors"]) == 0
assert all(data["files_exist"])
# Verify each format is present
extensions = [os.path.splitext(f)[1] for f in data["exported_files"]]
assert ".stl" in extensions
assert ".step" in extensions
assert ".brep" in extensions
def test_export_multiple_objects(self, xmlrpc_proxy, temp_export_dir):
"""Test exporting multiple objects as a compound."""
code = f"""
{MULTI_EXPORTER_CODE}
# Create a new document with multiple objects
doc = App.newDocument("TestExportMultiObj")
box = doc.addObject("Part::Box", "Box1")
box.Length = 10
box.Width = 10
box.Height = 10
cylinder = doc.addObject("Part::Cylinder", "Cyl1")
cylinder.Radius = 5
cylinder.Height = 20
cylinder.Placement.Base = App.Vector(20, 0, 0)
doc.recompute()
# Set up export parameters
params = {{
"directory": "{temp_export_dir}",
"base_filename": "test_compound",
"formats": ["stl"],
"mesh_tolerance": 0.1,
}}
# Create exporter with multiple objects
exporter = MultiExporter([box, cylinder], params)
exported_files, errors = exporter.export_all()
# Check file size (compound should be larger than single object)
file_size = os.path.getsize(exported_files[0]) if exported_files else 0
# Clean up
App.closeDocument("TestExportMultiObj")
_result_ = {{
"exported_files": exported_files,
"errors": errors,
"file_exists": os.path.exists(exported_files[0]) if exported_files else False,
"file_size": file_size,
}}
"""
result = execute_code(xmlrpc_proxy, code)
data = result.get("result", {})
assert len(data["exported_files"]) == 1
assert len(data["errors"]) == 0
assert data["file_exists"] is True
# Compound file should have some reasonable size
assert data["file_size"] > 100
def test_export_with_custom_tolerance(self, xmlrpc_proxy, temp_export_dir):
"""Test that mesh tolerance affects output file size.
Note: FreeCAD's tessellate function has internal limits, so we need
to use a very fine tolerance (0.01) to actually see more triangles
compared to the default tessellation.
"""
code = f"""
{MULTI_EXPORTER_CODE}
# Create a sphere (curved surface shows tolerance effect best)
doc = App.newDocument("TestTolerance")
sphere = doc.addObject("Part::Sphere", "TestSphere")
sphere.Radius = 20
doc.recompute()
# Export with coarse tolerance (uses default tessellation)
params_coarse = {{
"directory": "{temp_export_dir}",
"base_filename": "sphere_coarse",
"formats": ["stl"],
"mesh_tolerance": 1.0,
}}
exporter_coarse = MultiExporter([sphere], params_coarse)
coarse_files, _ = exporter_coarse.export_all()
coarse_size = os.path.getsize(coarse_files[0]) if coarse_files else 0
# Export with very fine tolerance (0.01 required to see difference)
params_fine = {{
"directory": "{temp_export_dir}",
"base_filename": "sphere_fine",
"formats": ["stl"],
"mesh_tolerance": 0.01,
}}
exporter_fine = MultiExporter([sphere], params_fine)
fine_files, _ = exporter_fine.export_all()
fine_size = os.path.getsize(fine_files[0]) if fine_files else 0
# Clean up
App.closeDocument("TestTolerance")
_result_ = {{
"coarse_size": coarse_size,
"fine_size": fine_size,
"fine_is_larger": fine_size > coarse_size,
}}
"""
result = execute_code(xmlrpc_proxy, code)
data = result.get("result", {})
# Fine tolerance (0.01) should produce larger file (more triangles)
assert data["fine_is_larger"] is True
assert data["fine_size"] > data["coarse_size"]
def test_export_empty_formats_list(self, xmlrpc_proxy, temp_export_dir):
"""Test that empty formats list returns appropriate error."""
code = f"""
{MULTI_EXPORTER_CODE}
doc = App.newDocument("TestEmptyFormats")
box = doc.addObject("Part::Box", "TestBox")
doc.recompute()
params = {{
"directory": "{temp_export_dir}",
"base_filename": "test",
"formats": [],
"mesh_tolerance": 0.1,
}}
exporter = MultiExporter([box], params)
exported_files, errors = exporter.export_all()
App.closeDocument("TestEmptyFormats")
_result_ = {{
"exported_files": exported_files,
"errors": errors,
}}
"""
result = execute_code(xmlrpc_proxy, code)
data = result.get("result", {})
assert len(data["exported_files"]) == 0
assert len(data["errors"]) == 1
assert "No formats selected" in data["errors"][0]
+22 -6
View File
@@ -23,13 +23,9 @@ class TestInstallSyntax:
"install::uninstall-mcp-server", "install::uninstall-mcp-server",
"install::mcp-bridge-workbench", "install::mcp-bridge-workbench",
"install::uninstall-mcp-bridge-workbench", "install::uninstall-mcp-bridge-workbench",
"install::macro-cut",
"install::uninstall-macro-cut",
"install::macro-export",
"install::uninstall-macro-export",
"install::macro-all",
"install::uninstall-macro-all",
"install::status", "install::status",
"install::uninstall",
"install::cleanup",
] ]
@pytest.mark.just_syntax @pytest.mark.just_syntax
@@ -77,3 +73,23 @@ class TestInstallRuntime:
assert uninstall_result.success, ( assert uninstall_result.success, (
f"MCP server uninstall failed: {uninstall_result.stderr}" f"MCP server uninstall failed: {uninstall_result.stderr}"
) )
@pytest.mark.just_runtime
def test_uninstall_runs(self, just: JustRunner) -> None:
"""Uninstall command should run without error.
This command uninstalls all components. It may complete with warnings
if nothing is installed, but should not error.
"""
result = just.run("install::uninstall", timeout=120)
# Command should succeed even if nothing was installed
assert result.success, f"Uninstall failed: {result.stderr}"
@pytest.mark.just_runtime
def test_cleanup_runs(self, just: JustRunner) -> None:
"""Cleanup command should run without error.
This command cleans up caches and temporary files.
"""
result = just.run("install::cleanup", timeout=60)
assert result.success, f"Cleanup failed: {result.stderr}"
+9 -5
View File
@@ -63,11 +63,17 @@ class TestModulesExist:
# Map of modules to their expected commands (subset for validation) # Map of modules to their expected commands (subset for validation)
EXPECTED_COMMANDS: ClassVar[dict[str, list[str]]] = { EXPECTED_COMMANDS: ClassVar[dict[str, list[str]]] = {
"quality": ["check", "format", "lint", "typecheck", "security"], "quality": ["check", "format", "lint", "typecheck", "security"],
"testing": ["unit", "cov", "fast", "integration"], "testing": ["unit", "cov", "quick", "integration"],
"dev": ["install-deps", "install-pre-commit", "clean"], "dev": ["install-deps", "install-pre-commit", "clean"],
"docker": ["build", "run", "clean"], "docker": ["build", "run", "clean"],
"documentation": ["build", "serve", "open"], "documentation": ["build", "serve", "open"],
"install": ["mcp-server", "mcp-bridge-workbench", "status"], "install": [
"mcp-server",
"mcp-bridge-workbench",
"status",
"uninstall",
"cleanup",
],
"mcp": ["run", "check"], "mcp": ["run", "check"],
"freecad": ["run-gui", "run-headless"], "freecad": ["run-gui", "run-headless"],
"release": ["status", "list-tags", "latest-versions"], "release": ["status", "list-tags", "latest-versions"],
@@ -110,7 +116,7 @@ class TestSyntaxValidation:
# Testing commands # Testing commands
"testing::unit", "testing::unit",
"testing::cov", "testing::cov",
"testing::fast", "testing::quick",
"testing::verbose", "testing::verbose",
# Dev commands # Dev commands
"dev::install-deps", "dev::install-deps",
@@ -131,8 +137,6 @@ class TestSyntaxValidation:
# Install commands # Install commands
"install::mcp-server", "install::mcp-server",
"install::mcp-bridge-workbench", "install::mcp-bridge-workbench",
"install::macro-cut",
"install::macro-export",
"install::status", "install::status",
# MCP commands # MCP commands
"mcp::check", "mcp::check",
+7 -59
View File
@@ -44,13 +44,9 @@ class TestReleaseSyntax:
RELEASE_COMMANDS: ClassVar[list[tuple[str, list[str]]]] = [ RELEASE_COMMANDS: ClassVar[list[tuple[str, list[str]]]] = [
# Version bump commands # Version bump commands
("bump-workbench", ["0.0.1-test"]), ("bump-workbench", ["0.0.1-test"]),
("bump-macro-magnets", ["0.0.1-test"]),
("bump-macro-export", ["0.0.1-test"]),
# Tag commands (require version argument) # Tag commands (require version argument)
("tag-mcp-server", ["0.0.1-test"]), ("tag-mcp-server", ["0.0.1-test"]),
("tag-workbench", ["0.0.1-test"]), ("tag-workbench", ["0.0.1-test"]),
("tag-macro-magnets", ["0.0.1-test"]),
("tag-macro-export", ["0.0.1-test"]),
# Info commands # Info commands
("list-tags", []), ("list-tags", []),
("latest-versions", []), ("latest-versions", []),
@@ -63,9 +59,9 @@ class TestReleaseSyntax:
# Tag management # Tag management
("delete-tag", ["test-tag-v0.0.1"]), ("delete-tag", ["test-tag-v0.0.1"]),
# Wiki commands # Wiki commands
("wiki-update", ["magnets"]), ("wiki-update", ["workbench"]),
("wiki-show", ["magnets"]), ("wiki-show", ["workbench"]),
("wiki-diff", ["magnets"]), ("wiki-diff", ["workbench"]),
] ]
@pytest.mark.just_syntax @pytest.mark.just_syntax
@@ -106,7 +102,7 @@ class TestReleaseReadOnly:
@pytest.mark.just_runtime @pytest.mark.just_runtime
@pytest.mark.parametrize( @pytest.mark.parametrize(
"component", "component",
["mcp-server", "workbench", "macro-magnets", "macro-export"], ["mcp-server", "workbench"],
) )
def test_changes_since_works(self, just: JustRunner, component: str) -> None: def test_changes_since_works(self, just: JustRunner, component: str) -> None:
"""changes-since should work for each component.""" """changes-since should work for each component."""
@@ -121,7 +117,7 @@ class TestReleaseReadOnly:
@pytest.mark.just_runtime @pytest.mark.just_runtime
@pytest.mark.parametrize( @pytest.mark.parametrize(
"component", "component",
["mcp-server", "workbench", "macro-magnets", "macro-export"], ["mcp-server", "workbench"],
) )
def test_draft_notes_works(self, just: JustRunner, component: str) -> None: def test_draft_notes_works(self, just: JustRunner, component: str) -> None:
"""draft-notes should work for each component.""" """draft-notes should work for each component."""
@@ -135,8 +131,6 @@ class TestReleaseReadOnly:
[ [
("mcp-server", "1.0.0"), ("mcp-server", "1.0.0"),
("workbench", "1.0.0"), ("workbench", "1.0.0"),
("macro-magnets", "1.0.0"),
("macro-export", "1.0.0"),
], ],
) )
def test_dry_run_tag_shows_info( def test_dry_run_tag_shows_info(
@@ -148,10 +142,9 @@ class TestReleaseReadOnly:
assert "Would create tag" in result.stdout assert "Would create tag" in result.stdout
@pytest.mark.just_runtime @pytest.mark.just_runtime
@pytest.mark.parametrize("macro", ["magnets", "export"]) def test_wiki_show_works(self, just: JustRunner) -> None:
def test_wiki_show_works(self, just: JustRunner, macro: str) -> None:
"""wiki-show should display wiki source content.""" """wiki-show should display wiki source content."""
result = just.run("release::wiki-show", macro, timeout=10) result = just.run("release::wiki-show", "workbench", timeout=10)
assert result.success, f"wiki-show failed: {result.stderr}" assert result.success, f"wiki-show failed: {result.stderr}"
assert "Wiki Source" in result.stdout assert "Wiki Source" in result.stdout
@@ -175,15 +168,6 @@ class TestReleaseBumpCommands:
/ "addon/FreecadRobustMCPBridge/freecad_mcp_bridge/__init__.py", / "addon/FreecadRobustMCPBridge/freecad_mcp_bridge/__init__.py",
PROJECT_ROOT / "addon/FreecadRobustMCPBridge/wiki-source.txt", PROJECT_ROOT / "addon/FreecadRobustMCPBridge/wiki-source.txt",
PROJECT_ROOT / "package.xml", PROJECT_ROOT / "package.xml",
# Cut Object for Magnets macro files
PROJECT_ROOT / "macros/Cut_Object_for_Magnets/CutObjectForMagnets.FCMacro",
PROJECT_ROOT
/ "macros/Cut_Object_for_Magnets/README-CutObjectForMagnets.md",
PROJECT_ROOT / "macros/Cut_Object_for_Magnets/wiki-source.txt",
# Multi Export macro files
PROJECT_ROOT / "macros/Multi_Export/MultiExport.FCMacro",
PROJECT_ROOT / "macros/Multi_Export/README-MultiExport.md",
PROJECT_ROOT / "macros/Multi_Export/wiki-source.txt",
] ]
backups: dict[Path, str] = {} backups: dict[Path, str] = {}
@@ -214,38 +198,6 @@ class TestReleaseBumpCommands:
content = init_file.read_text() content = init_file.read_text()
assert "99.99.99-test" in content assert "99.99.99-test" in content
@pytest.mark.just_runtime
@pytest.mark.just_release
def test_bump_macro_magnets_modifies_files(
self, just: JustRunner, backup_and_restore_files: None
) -> None:
"""bump-macro-magnets should modify version files."""
result = just.run("release::bump-macro-magnets", "99.99.99-test", timeout=30)
assert result.success, f"bump-macro-magnets failed: {result.stderr}"
assert "Version bump complete" in result.stdout
# Verify version was updated
macro_file = (
PROJECT_ROOT / "macros/Cut_Object_for_Magnets/CutObjectForMagnets.FCMacro"
)
content = macro_file.read_text()
assert "99.99.99-test" in content
@pytest.mark.just_runtime
@pytest.mark.just_release
def test_bump_macro_export_modifies_files(
self, just: JustRunner, backup_and_restore_files: None
) -> None:
"""bump-macro-export should modify version files."""
result = just.run("release::bump-macro-export", "99.99.99-test", timeout=30)
assert result.success, f"bump-macro-export failed: {result.stderr}"
assert "Version bump complete" in result.stdout
# Verify version was updated
macro_file = PROJECT_ROOT / "macros/Multi_Export/MultiExport.FCMacro"
content = macro_file.read_text()
assert "99.99.99-test" in content
class TestReleaseTagCommands: class TestReleaseTagCommands:
"""Tests for tag creation commands. """Tests for tag creation commands.
@@ -347,10 +299,6 @@ class TestReleaseValidation:
"mcp-server", "mcp-server",
"server", "server",
"workbench", "workbench",
"macro-magnets",
"magnets",
"macro-export",
"export",
], ],
) )
def test_changes_since_component_aliases( def test_changes_since_component_aliases(
+6 -5
View File
@@ -22,11 +22,12 @@ class TestTestingSyntax:
TESTING_COMMANDS: ClassVar[list[str]] = [ TESTING_COMMANDS: ClassVar[list[str]] = [
"testing::unit", "testing::unit",
"testing::cov", "testing::cov",
"testing::fast", "testing::quick",
"testing::integration", "testing::integration",
"testing::verbose", "testing::verbose",
"testing::all", "testing::all",
"testing::watch", "testing::watch",
"testing::check-deps",
"testing::integration-freecad-auto", "testing::integration-freecad-auto",
"testing::just-syntax", "testing::just-syntax",
"testing::just-runtime", "testing::just-runtime",
@@ -73,11 +74,11 @@ class TestTestingRuntime:
assert_command_executed(result, "testing::unit") assert_command_executed(result, "testing::unit")
@pytest.mark.just_runtime @pytest.mark.just_runtime
def test_fast_command_recognizes_markers(self, just: JustRunner) -> None: def test_quick_command_recognizes_markers(self, just: JustRunner) -> None:
"""Fast test command should recognize the 'not slow' marker.""" """Quick test command should recognize the 'not slow' marker."""
result = just.run( result = just.run(
"testing::fast", "testing::quick",
timeout=60, timeout=60,
env={"PYTEST_ADDOPTS": "--collect-only -q"}, env={"PYTEST_ADDOPTS": "--collect-only -q"},
) )
assert_command_executed(result, "testing::fast") assert_command_executed(result, "testing::quick")
+4 -4
View File
@@ -373,9 +373,9 @@ class TestFreecadResources:
"""freecad://macros should return macro list.""" """freecad://macros should return macro list."""
mock_macros = [ mock_macros = [
MacroInfo( MacroInfo(
name="MultiExport", name="ExportSTL",
path="/home/user/.local/share/FreeCAD/Macro/MultiExport.FCMacro", path="/home/user/.local/share/FreeCAD/Macro/ExportSTL.FCMacro",
description="Export to multiple formats", description="Export objects to STL",
is_system=False, is_system=False,
), ),
MacroInfo( MacroInfo(
@@ -392,7 +392,7 @@ class TestFreecadResources:
data = json.loads(result) data = json.loads(result)
assert len(data) == 2 assert len(data) == 2
assert data[0]["name"] == "MultiExport" assert data[0]["name"] == "ExportSTL"
assert data[0]["is_system"] is False assert data[0]["is_system"] is False
assert data[1]["is_system"] is True assert data[1]["is_system"] is True
+6 -6
View File
@@ -58,9 +58,9 @@ class TestMacroTools:
"""list_macros should return macro info.""" """list_macros should return macro info."""
mock_macros = [ mock_macros = [
MacroInfo( MacroInfo(
name="MultiExport", name="ExportSTL",
path="/home/user/.FreeCAD/Macro/MultiExport.FCMacro", path="/home/user/.FreeCAD/Macro/ExportSTL.FCMacro",
description="Export to multiple formats", description="Export objects to STL",
is_system=False, is_system=False,
), ),
MacroInfo( MacroInfo(
@@ -76,7 +76,7 @@ class TestMacroTools:
result = await list_macros() result = await list_macros()
assert len(result) == 2 assert len(result) == 2
assert result[0]["name"] == "MultiExport" assert result[0]["name"] == "ExportSTL"
assert result[0]["is_system"] is False assert result[0]["is_system"] is False
assert result[1]["name"] == "SystemMacro" assert result[1]["name"] == "SystemMacro"
assert result[1]["is_system"] is True assert result[1]["is_system"] is True
@@ -96,11 +96,11 @@ class TestMacroTools:
) )
run_macro = register_tools["run_macro"] run_macro = register_tools["run_macro"]
result = await run_macro(macro_name="MultiExport") result = await run_macro(macro_name="ExportSTL")
assert result["success"] is True assert result["success"] is True
assert result["stdout"] == "Exported 3 objects\n" assert result["stdout"] == "Exported 3 objects\n"
mock_bridge.run_macro.assert_called_once_with("MultiExport", None) mock_bridge.run_macro.assert_called_once_with("ExportSTL", None)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_run_macro_with_args(self, register_tools, mock_bridge): async def test_run_macro_with_args(self, register_tools, mock_bridge):