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
@@ -69,10 +69,12 @@ reviews:
|
||||
This code runs inside FreeCAD's Python environment as a workbench addon.
|
||||
It cannot import packages from the project's virtualenv (mcp, pydantic).
|
||||
Watch for accidental imports of project dependencies.
|
||||
- path: "macros/**/*.FCMacro"
|
||||
- path: ".github/actions/**/*.yaml"
|
||||
instructions: >
|
||||
FreeCAD macro files. These are standalone Python scripts.
|
||||
They run in FreeCAD's environment with access to FreeCAD, Part, etc.
|
||||
Custom GitHub Actions (composite actions). Check for:
|
||||
- Proper input/output definitions
|
||||
- Shell script correctness
|
||||
- Cross-platform compatibility (x86_64/aarch64)
|
||||
- path: "tests/**/*.py"
|
||||
instructions: >
|
||||
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
|
||||
- path: "README.md"
|
||||
instructions: >
|
||||
INTENTIONAL NAMING: The repo is "freecad-robust-mcp-and-more" but Docker
|
||||
image and PyPI package are "freecad-robust-mcp". This is documented and
|
||||
intentional - do NOT flag as a naming mismatch or inconsistency.
|
||||
Main documentation for the FreeCAD Robust MCP Server addon.
|
||||
The PyPI package name is "freecad-robust-mcp".
|
||||
- path: "Dockerfile"
|
||||
instructions: >
|
||||
INTENTIONAL NAMING: Image name "freecad-robust-mcp" differs from repo name
|
||||
"freecad-robust-mcp-and-more". This is intentional and documented in README.
|
||||
Do NOT flag as a naming mismatch.
|
||||
Docker image for running FreeCAD MCP Server in containers.
|
||||
Image name is "freecad-robust-mcp".
|
||||
- path: ".mise.toml"
|
||||
instructions: >
|
||||
Tool version management via mise. All versions use fuzzy matching:
|
||||
|
||||
@@ -78,9 +78,6 @@ site/
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Macros (not needed in container)
|
||||
macros/
|
||||
|
||||
# Tests (not needed in runtime container)
|
||||
tests/
|
||||
# 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"
|
||||
@@ -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
|
||||
@@ -53,9 +53,3 @@ jobs:
|
||||
|
||||
- name: Run type checking
|
||||
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.
|
||||
|
||||
@@ -179,9 +179,6 @@ just freecad::run-headless # Run FreeCAD headless with MCP bridge
|
||||
# Installation commands (for end users)
|
||||
just install::mcp-server # Install MCP server system-wide (via uv tool)
|
||||
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
|
||||
|
||||
# Quality commands
|
||||
@@ -195,7 +192,7 @@ just quality::scan # Run all secrets scanners
|
||||
# Testing commands
|
||||
just testing::unit # Run unit tests
|
||||
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-freecad-auto # Integration tests with auto FreeCAD startup
|
||||
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::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-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::latest-versions # Show latest version of each component
|
||||
just release::delete-tag <tag> # Delete a release tag (local and remote)
|
||||
@@ -246,14 +241,14 @@ just all-with-integration # Run all checks and integration tests
|
||||
#### Just Module Structure
|
||||
|
||||
| Module | Description | Key Commands |
|
||||
| --------------- | ------------------------------------- | --------------------------------------------------- |
|
||||
| --------------- | ------------------------------------- | ---------------------------------------------------- |
|
||||
| `mcp` | MCP server commands | `run`, `run-debug`, `run-http` |
|
||||
| `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` |
|
||||
| `testing` | Test execution | `unit`, `cov`, `integration-freecad-auto`, `watch` |
|
||||
| `docker` | Docker build and run commands | `build`, `build-multi`, `run`, `clean-all` |
|
||||
| `documentation` | Documentation building and deployment | `build`, `serve`, `serve-versioned`, `list-versions`|
|
||||
| `documentation` | Documentation building and deployment | `build`, `serve`, `serve-versioned`, `list-versions` |
|
||||
| `dev` | Development utilities | `install-deps`, `update-deps`, `clean` |
|
||||
| `release` | Release and tagging | `status`, `tag-mcp-server`, `delete-tag` |
|
||||
| `coderabbit` | AI code reviews (local) | `install`, `login`, `review`, `review-fix` |
|
||||
@@ -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.
|
||||
|
||||
**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
|
||||
@@ -587,7 +602,7 @@ class TestCalculateTotal:
|
||||
```bash
|
||||
just testing::unit # Run unit tests
|
||||
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
|
||||
uv run pytest tests/unit/ # Run specific test directory
|
||||
uv run pytest -k "test_name" # Run specific test by name
|
||||
@@ -737,10 +752,7 @@ project-root/
|
||||
│ ├── workflows/ # GitHub Actions workflows
|
||||
│ │ ├── codeql.yaml # Security analysis
|
||||
│ │ ├── docker.yaml # Docker build (CI)
|
||||
│ │ ├── macro-cut-magnets-release.yaml # Macro release
|
||||
│ │ ├── macro-multi-export-release.yaml # Macro release
|
||||
│ │ ├── macro-release-reusable.yaml # Shared macro release logic
|
||||
│ │ ├── macro-test.yaml # Macro testing
|
||||
│ │ ├── docs.yaml # Documentation deployment
|
||||
│ │ ├── mcp-server-release.yaml # MCP server → PyPI/Docker
|
||||
│ │ ├── mcp-workbench-release.yaml # Workbench → GitHub Release
|
||||
│ │ ├── pre-commit.yaml # Pre-commit checks
|
||||
@@ -757,7 +769,6 @@ project-root/
|
||||
│ ├── development/ # Developer guides
|
||||
│ ├── getting-started/ # Installation, quickstart
|
||||
│ ├── guide/ # User guides
|
||||
│ ├── macros/ # Macro documentation
|
||||
│ ├── reference/ # API reference
|
||||
│ ├── variables.yaml # MkDocs macro variables
|
||||
│ └── index.md # Documentation home
|
||||
@@ -772,13 +783,6 @@ project-root/
|
||||
│ ├── quality.just # Code quality commands
|
||||
│ ├── release.just # Release/tagging 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/
|
||||
│ └── freecad_mcp/ # Main MCP server package
|
||||
│ ├── bridge/ # FreeCAD connection bridges
|
||||
@@ -934,7 +938,7 @@ EOF
|
||||
4. **Variable expansion**: `${VAR}` works inside heredocs for bash variables
|
||||
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
|
||||
|
||||
| Workflow | Trigger | Purpose |
|
||||
| ------------------ | ------------------------------ | --------------------------------------------------------- |
|
||||
| ----------------- | ----------------------------- | --------------------------------------------------------- |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `docs.yaml` | Push to main, MCP server tags | Deploys versioned documentation to GitHub Pages |
|
||||
|
||||
### Release Workflows
|
||||
|
||||
| Workflow | Trigger | Purpose |
|
||||
| --------------------------------- | ---------------------------------------- | ------------------------------------------------------ |
|
||||
| ---------------------------- | ------------------------------ | ------------------------------------------------------ |
|
||||
| `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 |
|
||||
| `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
|
||||
|
||||
@@ -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:
|
||||
|
||||
| Component | Tag Format | Releases To |
|
||||
| ---------------------------- | ------------------------------------- | ------------------------------------------ |
|
||||
| ----------------- | ----------------------------- | ------------------------------------------ |
|
||||
| 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) |
|
||||
| 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) |
|
||||
| Robust MCP Bridge | `robust-mcp-workbench-vX.Y.Z` | GitHub Release (archive) |
|
||||
|
||||
*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.
|
||||
|
||||
| Component | Release Notes File |
|
||||
| ---------------------------- | ----------------------------------------------------- |
|
||||
| ----------------- | ----------------------------------------------- |
|
||||
| MCP Server | `src/freecad_mcp/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` |
|
||||
| Robust MCP Bridge | `addon/FreecadRobustMCPBridge/RELEASE_NOTES.md` |
|
||||
|
||||
**Before releasing a component:**
|
||||
|
||||
@@ -1392,8 +1388,6 @@ Each component has its own `RELEASE_NOTES.md` file. Release workflows automatica
|
||||
```bash
|
||||
just release::draft-notes mcp-server
|
||||
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:
|
||||
@@ -1438,12 +1432,10 @@ just release::draft-notes mcp-server
|
||||
# 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
|
||||
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.
|
||||
@@ -1456,10 +1448,6 @@ just release::tag-mcp-server 1.0.0
|
||||
|
||||
# Release the Robust MCP Bridge workbench
|
||||
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:**
|
||||
@@ -1508,26 +1496,21 @@ All versions follow SemVer 2.0:
|
||||
4. Creates archive (tar.gz + zip)
|
||||
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
|
||||
<content>
|
||||
<workbench>
|
||||
<name>MCP Bridge</name>
|
||||
<name>Robust MCP Bridge</name>
|
||||
<version>1.0.0</version>
|
||||
...
|
||||
</workbench>
|
||||
<macro>
|
||||
<name>Multi Export</name>
|
||||
<version>0.8.0</version>
|
||||
...
|
||||
</macro>
|
||||
</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:**
|
||||
|
||||
| Component | File |
|
||||
| ---------------------------- | ------------------------------------------------ |
|
||||
| ----------------- | ----------------------------------------------- |
|
||||
| MCP Server | `src/freecad_mcp/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` |
|
||||
| Robust MCP Bridge | `addon/FreecadRobustMCPBridge/RELEASE_NOTES.md` |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
# FreeCAD Robust MCP Suite
|
||||
# FreeCAD Robust MCP Server
|
||||
|
||||
[](https://github.com/spkane/freecad-robust-mcp-and-more/actions/workflows/test.yaml)
|
||||
[](https://github.com/spkane/freecad-robust-mcp-and-more/actions/workflows/macro-test.yaml)
|
||||
[](https://github.com/spkane/freecad-robust-mcp-and-more/actions/workflows/docker.yaml)
|
||||
[](https://github.com/spkane/freecad-robust-mcp-and-more/actions/workflows/pre-commit.yaml)
|
||||
[](https://github.com/spkane/freecad-robust-mcp-and-more/actions/workflows/codeql.yaml)
|
||||
[](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/test.yaml)
|
||||
[](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/docker.yaml)
|
||||
[](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/pre-commit.yaml)
|
||||
[](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/codeql.yaml)
|
||||
[](https://pypi.org/project/freecad-robust-mcp/)
|
||||
[](https://pypi.org/project/freecad-robust-mcp/)
|
||||
|
||||
[](https://hub.docker.com/r/spkane/freecad-robust-mcp)
|
||||
[](https://spkane.github.io/freecad-robust-mcp-and-more/)
|
||||
[](https://spkane.github.io/freecad-addon-robust-mcp-server/)
|
||||
[](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.
|
||||
|
||||
> Also includes standalone FreeCAD macros for common tasks.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
<!--TOC-->
|
||||
|
||||
- [FreeCAD Robust MCP Suite](#freecad-robust-mcp-suite)
|
||||
- [FreeCAD Robust MCP Server](#freecad-robust-mcp-server)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Features](#features)
|
||||
- [Requirements](#requirements)
|
||||
@@ -59,10 +56,6 @@ An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that
|
||||
- [Export/Import (7 tools)](#exportimport-7-tools)
|
||||
- [Macro Management (6 tools)](#macro-management-6-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)
|
||||
- [Robust MCP Server Development](#robust-mcp-server-development)
|
||||
- [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)
|
||||
- [Running Tests](#running-tests)
|
||||
- [Code Quality](#code-quality)
|
||||
- [Macro Development](#macro-development)
|
||||
- [CutObjectForMagnets Macro](#cutobjectformagnets-macro)
|
||||
- [MultiExport Macro](#multiexport-macro)
|
||||
- [Architecture](#architecture)
|
||||
- [Acknowledgements](#acknowledgements)
|
||||
- [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
|
||||
- **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
|
||||
- **Standalone Macros**: Useful FreeCAD macros that work independently of the Robust MCP Server
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -101,20 +90,20 @@ An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that
|
||||
|
||||
## 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
|
||||
|
||||
| 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 |
|
||||
| [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
|
||||
|
||||
> **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
|
||||
|
||||
@@ -127,8 +116,8 @@ pip install freecad-robust-mcp
|
||||
#### Using mise and just (from source)
|
||||
|
||||
```bash
|
||||
git clone https://github.com/spkane/freecad-robust-mcp-and-more.git
|
||||
cd freecad-robust-mcp-and-more
|
||||
git clone https://github.com/spkane/freecad-addon-robust-mcp-server.git
|
||||
cd freecad-addon-robust-mcp-server
|
||||
|
||||
# Install mise via the Official mise installer script (if not already installed)
|
||||
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
|
||||
|
||||
# Or build locally
|
||||
git clone https://github.com/spkane/freecad-robust-mcp-and-more.git
|
||||
cd freecad-robust-mcp-and-more
|
||||
git clone https://github.com/spkane/freecad-addon-robust-mcp-server.git
|
||||
cd freecad-addon-robust-mcp-server
|
||||
docker build -t freecad-robust-mcp .
|
||||
|
||||
# Or use just commands (if you have mise/just installed)
|
||||
@@ -205,7 +194,7 @@ If installed from source with mise/uv:
|
||||
"mcpServers": {
|
||||
"freecad": {
|
||||
"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": {
|
||||
"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
|
||||
|
||||
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
|
||||
# Clone the repository
|
||||
git clone https://github.com/spkane/freecad-robust-mcp-and-more.git
|
||||
cd freecad-robust-mcp-and-more
|
||||
git clone https://github.com/spkane/freecad-addon-robust-mcp-server.git
|
||||
cd freecad-addon-robust-mcp-server
|
||||
|
||||
# Install mise via the Official mise installer script (if not already installed)
|
||||
curl https://mise.run | sh
|
||||
@@ -662,7 +525,7 @@ Create a `.mcp.json` file in the project directory:
|
||||
"mcpServers": {
|
||||
"freecad": {
|
||||
"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": {
|
||||
"FREECAD_MODE": "xmlrpc",
|
||||
"FREECAD_SOCKET_HOST": "localhost",
|
||||
@@ -677,9 +540,9 @@ Create a `.mcp.json` file in the project directory:
|
||||
**Replace the paths with your actual paths:**
|
||||
|
||||
| Placeholder | Description | Example |
|
||||
| -------------------------------------- | ------------------------------ | ------------------------------------------ |
|
||||
| ------------------------------------------- | ------------------------------- | ---------------------------------------------- |
|
||||
| `/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` |
|
||||
|
||||
**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
|
||||
|
||||
See the [detailed architecture document](docs/development/architecture-detailed.md) for design documentation covering:
|
||||
|
||||
@@ -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
|
||||
|
||||
- [Contributing](contributing.md) - How to contribute
|
||||
|
||||
@@ -89,7 +89,6 @@ freecad-robust-mcp-and-more/
|
||||
│ └── server.py # Main server entry point
|
||||
├── addon/ # FreeCAD workbench addon
|
||||
│ └── FreecadRobustMCPBridge/ # Workbench files
|
||||
├── macros/ # Standalone FreeCAD macros
|
||||
├── tests/ # Test suite
|
||||
│ ├── unit/ # Unit tests
|
||||
│ └── integration/ # Integration tests
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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
|
||||
|
||||
@@ -9,46 +9,39 @@ The complete release workflow in order:
|
||||
```bash
|
||||
# 1. Pre-release checks
|
||||
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)
|
||||
|
||||
# 2. Update release notes
|
||||
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)
|
||||
|
||||
# 3. Version bump (workbench & macros only - MCP Server uses setuptools-scm)
|
||||
just release::bump-workbench 1.0.0 # or bump-macro-magnets, bump-macro-export
|
||||
# 3. Version bump (workbench only - MCP Server uses setuptools-scm)
|
||||
just release::bump-workbench 1.0.0
|
||||
|
||||
# 4. Commit changes
|
||||
git add -A
|
||||
git commit -m "chore: bump workbench to 1.0.0" # or appropriate component/message
|
||||
|
||||
# 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
|
||||
just release::list-tags
|
||||
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 |
|
||||
| ------------- | ---------------------------------------- | --------------------------------------- |
|
||||
| ---------- | ------------------------------------ | ------------------------------------ |
|
||||
| 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` |
|
||||
| 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
|
||||
|
||||
| Component | Tag Format | Releases To |
|
||||
| ---------------------------- | ------------------------------------- | ------------------------------------------ |
|
||||
| --------------------------- | ----------------------------- | ------------------------------------------ |
|
||||
| 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) |
|
||||
| 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.
|
||||
|
||||
@@ -110,10 +103,6 @@ just release::changes-since mcp-server
|
||||
|
||||
# For 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
|
||||
@@ -137,11 +126,9 @@ Each component has its own `RELEASE_NOTES.md` file. Release workflows automatica
|
||||
#### Release Notes Files
|
||||
|
||||
| Component | Release Notes File |
|
||||
| ---------------------------- | ----------------------------------------------------- |
|
||||
| --------------------------- | ----------------------------------------------- |
|
||||
| MCP Server | `src/freecad_mcp/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
|
||||
|
||||
@@ -151,8 +138,6 @@ Use the `draft-notes` command to generate a starting point from conventional com
|
||||
# Generate draft notes for a component
|
||||
just release::draft-notes mcp-server
|
||||
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.
|
||||
@@ -256,61 +241,13 @@ just release::tag-workbench 1.0.0
|
||||
3. Creates archive (tar.gz and zip)
|
||||
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
|
||||
|
||||
### Check GitHub Actions
|
||||
|
||||
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
|
||||
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
|
||||
```
|
||||
|
||||
**For Workbench/Macros:**
|
||||
**For Workbench:**
|
||||
|
||||
- Check the GitHub Releases page for the archive downloads
|
||||
- Verify the `package.xml` version was updated
|
||||
@@ -361,7 +298,6 @@ To see what a release tag would look like without creating it:
|
||||
```bash
|
||||
just release::dry-run-tag mcp-server 1.0.0
|
||||
just release::dry-run-tag workbench 1.0.0
|
||||
just release::dry-run-tag macro-magnets 1.0.0
|
||||
```
|
||||
|
||||
## 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
|
||||
- **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
|
||||
|
||||
@@ -479,13 +365,9 @@ just release::changes-since mcp-server
|
||||
# Draft release notes from commits
|
||||
just release::draft-notes mcp-server
|
||||
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-macro-magnets 1.0.0
|
||||
just release::bump-macro-export 1.0.0
|
||||
|
||||
# Commit version changes
|
||||
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)
|
||||
just release::tag-mcp-server 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
|
||||
just release::list-tags
|
||||
@@ -503,11 +383,6 @@ just release::latest-versions
|
||||
# Extract changelog for a version (used by CI)
|
||||
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
|
||||
just release::delete-tag <full-tag-name>
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
### 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:
|
||||
## Available Tools
|
||||
|
||||
### list_macros
|
||||
|
||||
@@ -98,7 +30,7 @@ run_macro(
|
||||
**Example prompt:**
|
||||
|
||||
```text
|
||||
"Run the MultiExport macro"
|
||||
"Run the ExportSTL macro"
|
||||
```
|
||||
|
||||
### create_macro
|
||||
@@ -130,7 +62,7 @@ read_macro(macro_name: str) -> dict
|
||||
**Example prompt:**
|
||||
|
||||
```text
|
||||
"Show me the code for the MultiExport macro"
|
||||
"Show me the code for my custom macro"
|
||||
```
|
||||
|
||||
### delete_macro
|
||||
|
||||
@@ -237,9 +237,9 @@ Gets list of available FreeCAD macros.
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "MultiExport",
|
||||
"path": "/home/user/.local/share/FreeCAD/Macro/MultiExport.FCMacro",
|
||||
"description": "Export objects to multiple formats",
|
||||
"name": "ExportSTL",
|
||||
"path": "/home/user/.local/share/FreeCAD/Macro/ExportSTL.FCMacro",
|
||||
"description": "Export selected objects to STL",
|
||||
"is_system": false
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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:
|
||||
|
||||
- **MultiExport** - Export objects to multiple formats simultaneously
|
||||
- **CutObjectForMagnets** - Cut objects with aligned magnet holes for 3D printing
|
||||
|
||||
See [Macros](macros.md) for details.
|
||||
The MCP server provides tools for working with FreeCAD macros. See [Macros](macros.md) for details on using macro tools.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
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)
|
||||
- **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
|
||||
- **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
|
||||
|
||||
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
|
||||
- **[MultiExport](guide/macros.md#multiexport)** - Export objects to multiple formats simultaneously
|
||||
- **List macros** - Discover available macros in your FreeCAD installation
|
||||
- **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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -196,8 +196,5 @@ run-gui-custom freecad_path:
|
||||
# =============================================================================
|
||||
# These are kept for backwards compatibility but will be removed in a future version.
|
||||
# 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::status (was: just freecad::mcp-status)
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
# This module installs components for end users:
|
||||
# - Robust MCP Server (as a uv tool, available system-wide)
|
||||
# - Robust MCP Bridge Workbench (FreeCAD addon)
|
||||
# - FreeCAD Macros (CutObjectForMagnets, MultiExport)
|
||||
#
|
||||
# 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"
|
||||
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
|
||||
# =============================================================================
|
||||
@@ -521,23 +394,6 @@ status:
|
||||
' 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
|
||||
if [[ -d "$MOD_DIR/FreecadRobustMCPBridge" ]]; then
|
||||
WB_VERSION="unknown"
|
||||
@@ -555,32 +411,6 @@ status:
|
||||
fi
|
||||
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
|
||||
LEGACY_COUNT=0
|
||||
|
||||
@@ -602,3 +432,51 @@ status:
|
||||
fi
|
||||
|
||||
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."
|
||||
|
||||
@@ -13,8 +13,6 @@
|
||||
# This project uses component-specific git tags for releases:
|
||||
# - robust-mcp-server-vX.Y.Z (triggers PyPI, Docker, GitHub 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):
|
||||
# - 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 " 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
|
||||
# =============================================================================
|
||||
@@ -235,7 +137,7 @@ tag-mcp-server 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"
|
||||
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
|
||||
tag-workbench version:
|
||||
@@ -314,101 +216,7 @@ tag-workbench 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"
|
||||
|
||||
# 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)
|
||||
echo "Watch the release at: https://github.com/spkane/freecad-addon-robust-mcp-server/actions"
|
||||
|
||||
# =============================================================================
|
||||
# Tag Information Commands
|
||||
@@ -422,12 +230,6 @@ list-tags:
|
||||
echo ""
|
||||
echo "=== Robust MCP Bridge Workbench Releases ==="
|
||||
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
|
||||
latest-versions:
|
||||
@@ -436,12 +238,8 @@ latest-versions:
|
||||
echo ""
|
||||
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)
|
||||
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 " 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
|
||||
changes-since component:
|
||||
@@ -457,17 +255,9 @@ changes-since component:
|
||||
PREFIX="robust-mcp-workbench-v"
|
||||
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 "Valid: mcp-server, workbench, macro-magnets, macro-export"
|
||||
echo "Valid: mcp-server, server, workbench"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -601,16 +391,6 @@ show-release-info tag:
|
||||
echo "Component: Robust MCP Bridge Workbench"
|
||||
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)"
|
||||
;;
|
||||
@@ -640,14 +420,6 @@ rollback-release tag:
|
||||
COMPONENT="workbench"
|
||||
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"
|
||||
;;
|
||||
@@ -712,20 +484,6 @@ rollback-release tag:
|
||||
echo "Rollback will fully clean up this release."
|
||||
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 ""
|
||||
@@ -858,7 +616,7 @@ rollback-release tag:
|
||||
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"
|
||||
;;
|
||||
workbench|macro-magnets|macro-export)
|
||||
workbench)
|
||||
echo "COMPLETED (automated):"
|
||||
echo " ✓ GitHub Release deleted"
|
||||
echo " ✓ Git tag deleted (local and remote)"
|
||||
@@ -965,30 +723,6 @@ status:
|
||||
echo "Robust MCP Bridge Workbench: up to date (${WORKBENCH_TAG:-no releases})"
|
||||
fi
|
||||
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 "=========================================="
|
||||
|
||||
# =============================================================================
|
||||
@@ -1011,19 +745,9 @@ draft-notes component:
|
||||
PATHS="addon/FreecadRobustMCPBridge"
|
||||
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 "Valid: mcp-server, workbench, macro-magnets, macro-export"
|
||||
echo "Valid: mcp-server, server, workbench"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -1089,14 +813,9 @@ extract-changelog component version:
|
||||
workbench)
|
||||
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 "Valid: mcp-server, server, workbench"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -1142,19 +861,9 @@ dry-run-tag component version:
|
||||
echo "Would create tag: $TAG"
|
||||
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 "Valid: mcp-server, workbench, macro-magnets, macro-export"
|
||||
echo "Valid: mcp-server, server, workbench"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -1174,19 +883,9 @@ wiki-update component:
|
||||
WIKI_PAGE="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 "Valid options: workbench (or bridge), macro-magnets (or magnets, cut), macro-export (or export, multi)"
|
||||
echo "Valid options: workbench (or bridge)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -1302,17 +1001,9 @@ wiki-show component:
|
||||
WIKI_SOURCE="{{project_root}}/addon/FreecadRobustMCPBridge/wiki-source.txt"
|
||||
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 "Valid options: workbench (or bridge), macro-magnets (or magnets, cut), macro-export (or export, multi)"
|
||||
echo "Valid options: workbench (or bridge)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -1345,19 +1036,9 @@ wiki-diff component:
|
||||
WIKI_PAGE="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 "Valid options: workbench (or bridge), macro-magnets (or magnets, cut), macro-export (or export, multi)"
|
||||
echo "Valid options: workbench (or bridge)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -17,7 +17,7 @@ cov:
|
||||
uv run pytest tests/unit --cov=freecad_mcp --cov-report=term-missing --cov-report=html:htmlcov
|
||||
|
||||
# Run tests without slow markers (excludes integration tests)
|
||||
fast:
|
||||
quick:
|
||||
uv run pytest {{project_root}}/tests/unit -m "not slow"
|
||||
|
||||
# Run only integration tests (requires running FreeCAD Robust MCP Bridge)
|
||||
@@ -154,6 +154,16 @@ integration-freecad-auto:
|
||||
# Cleanup is handled by trap
|
||||
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
|
||||
# =============================================================================
|
||||
@@ -314,8 +324,6 @@ release-test:
|
||||
echo " Edit the RELEASE_NOTES.md file for each component:"
|
||||
echo " - MCP Server: src/freecad_mcp/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 " Add a new version section at the top:"
|
||||
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 " from git commits since the last release."
|
||||
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 " just release::bump-workbench <version>"
|
||||
echo " just release::bump-macro-magnets <version>"
|
||||
echo " just release::bump-macro-export <version>"
|
||||
echo ""
|
||||
echo "3. COMMIT AND PUSH your RELEASE_NOTES.md and version bump changes"
|
||||
echo ""
|
||||
@@ -341,8 +347,6 @@ release-test:
|
||||
echo ""
|
||||
echo " just release::tag-mcp-server <version>"
|
||||
echo " just release::tag-workbench <version>"
|
||||
echo " just release::tag-macro-magnets <version>"
|
||||
echo " just release::tag-macro-export <version>"
|
||||
echo ""
|
||||
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."
|
||||
|
||||
@@ -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 |
|
Before Width: | Height: | Size: 674 KiB |
|
Before Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 706 B |
@@ -1,632 +0,0 @@
|
||||
# Cut Object for Magnets - FreeCAD Macro
|
||||
|
||||

|
||||
|
||||
**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:}}]]
|
||||
|
Before Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
@@ -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()
|
||||
@@ -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 |
@@ -1,298 +0,0 @@
|
||||
# Multi-Format Export - FreeCAD Macro
|
||||
|
||||

|
||||
|
||||
**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.
|
||||
@@ -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/`
|
||||
@@ -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:}}]]
|
||||
@@ -1,8 +1,8 @@
|
||||
site_name: FreeCAD Robust MCP Suite
|
||||
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
|
||||
repo_url: https://github.com/spkane/freecad-robust-mcp-and-more
|
||||
repo_name: spkane/freecad-robust-mcp-and-more
|
||||
site_url: https://github.com/spkane/freecad-addon-robust-mcp-server
|
||||
repo_url: https://github.com/spkane/freecad-addon-robust-mcp-server
|
||||
repo_name: spkane/freecad-addon-robust-mcp-server
|
||||
|
||||
theme:
|
||||
name: material
|
||||
@@ -112,7 +112,7 @@ extra:
|
||||
default: latest
|
||||
social:
|
||||
- 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
|
||||
link: https://hub.docker.com/r/spkane/freecad-robust-mcp
|
||||
- icon: fontawesome/brands/python
|
||||
@@ -127,7 +127,7 @@ nav:
|
||||
- User Guide:
|
||||
- Connection Modes: guide/connection-modes.md
|
||||
- Robust MCP Bridge Workbench: guide/workbench.md
|
||||
- FreeCAD Macros: guide/macros.md
|
||||
- MCP Macro Tools: guide/macros.md
|
||||
- Tools Overview: guide/tools.md
|
||||
- MCP Resources: guide/resources.md
|
||||
- Detailed User Guide: USER_GUIDE.md
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<package format="1" xmlns="https://wiki.freecad.org/Package_Metadata">
|
||||
|
||||
<name>FreeCAD Robust MCP Suite</name>
|
||||
<description>A collection of FreeCAD macros plus a robust MCP (Model Context Protocol) bridge workbench for AI assistant integration.</description>
|
||||
<name>FreeCAD Robust MCP Bridge</name>
|
||||
<description>A robust MCP (Model Context Protocol) bridge workbench for AI assistant integration with FreeCAD.</description>
|
||||
<icon>addon/FreecadRobustMCPBridge/FreecadRobustMCPBridge.svg</icon>
|
||||
|
||||
<!-- Note: Each component has its own version below. This top-level date is for reference only. -->
|
||||
@@ -15,16 +15,12 @@
|
||||
|
||||
<freecadmin>0.21</freecadmin>
|
||||
|
||||
<url type="repository" branch="main">https://github.com/spkane/freecad-robust-mcp-and-more</url>
|
||||
<url type="bugtracker">https://github.com/spkane/freecad-robust-mcp-and-more/issues</url>
|
||||
<url type="readme">https://github.com/spkane/freecad-robust-mcp-and-more/blob/main/README.md</url>
|
||||
<url type="documentation">https://github.com/spkane/freecad-robust-mcp-and-more#readme</url>
|
||||
<url type="repository" branch="main">https://github.com/spkane/freecad-addon-robust-mcp-server</url>
|
||||
<url type="bugtracker">https://github.com/spkane/freecad-addon-robust-mcp-server/issues</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-addon-robust-mcp-server#readme</url>
|
||||
|
||||
<tag>workbench</tag>
|
||||
<tag>macro</tag>
|
||||
<tag>export</tag>
|
||||
<tag>3D printing</tag>
|
||||
<tag>magnets</tag>
|
||||
<tag>MCP</tag>
|
||||
<tag>AI</tag>
|
||||
<tag>automation</tag>
|
||||
@@ -46,26 +42,6 @@
|
||||
<freecadmin>0.21</freecadmin>
|
||||
</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>
|
||||
|
||||
</package>
|
||||
|
||||
@@ -335,7 +335,5 @@ major_version_zero = true
|
||||
# Note: This project uses component-specific tags for releases:
|
||||
# - robust-mcp-server-vX.Y.Z (MCP server: PyPI, Docker, GitHub release)
|
||||
# - 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.
|
||||
# Commitizen is used for commit message validation and changelog generation.
|
||||
|
||||
@@ -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]
|
||||
@@ -23,13 +23,9 @@ class TestInstallSyntax:
|
||||
"install::uninstall-mcp-server",
|
||||
"install::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::uninstall",
|
||||
"install::cleanup",
|
||||
]
|
||||
|
||||
@pytest.mark.just_syntax
|
||||
@@ -77,3 +73,23 @@ class TestInstallRuntime:
|
||||
assert uninstall_result.success, (
|
||||
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}"
|
||||
|
||||
@@ -63,11 +63,17 @@ class TestModulesExist:
|
||||
# Map of modules to their expected commands (subset for validation)
|
||||
EXPECTED_COMMANDS: ClassVar[dict[str, list[str]]] = {
|
||||
"quality": ["check", "format", "lint", "typecheck", "security"],
|
||||
"testing": ["unit", "cov", "fast", "integration"],
|
||||
"testing": ["unit", "cov", "quick", "integration"],
|
||||
"dev": ["install-deps", "install-pre-commit", "clean"],
|
||||
"docker": ["build", "run", "clean"],
|
||||
"documentation": ["build", "serve", "open"],
|
||||
"install": ["mcp-server", "mcp-bridge-workbench", "status"],
|
||||
"install": [
|
||||
"mcp-server",
|
||||
"mcp-bridge-workbench",
|
||||
"status",
|
||||
"uninstall",
|
||||
"cleanup",
|
||||
],
|
||||
"mcp": ["run", "check"],
|
||||
"freecad": ["run-gui", "run-headless"],
|
||||
"release": ["status", "list-tags", "latest-versions"],
|
||||
@@ -110,7 +116,7 @@ class TestSyntaxValidation:
|
||||
# Testing commands
|
||||
"testing::unit",
|
||||
"testing::cov",
|
||||
"testing::fast",
|
||||
"testing::quick",
|
||||
"testing::verbose",
|
||||
# Dev commands
|
||||
"dev::install-deps",
|
||||
@@ -131,8 +137,6 @@ class TestSyntaxValidation:
|
||||
# Install commands
|
||||
"install::mcp-server",
|
||||
"install::mcp-bridge-workbench",
|
||||
"install::macro-cut",
|
||||
"install::macro-export",
|
||||
"install::status",
|
||||
# MCP commands
|
||||
"mcp::check",
|
||||
|
||||
@@ -44,13 +44,9 @@ class TestReleaseSyntax:
|
||||
RELEASE_COMMANDS: ClassVar[list[tuple[str, list[str]]]] = [
|
||||
# Version bump commands
|
||||
("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-mcp-server", ["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
|
||||
("list-tags", []),
|
||||
("latest-versions", []),
|
||||
@@ -63,9 +59,9 @@ class TestReleaseSyntax:
|
||||
# Tag management
|
||||
("delete-tag", ["test-tag-v0.0.1"]),
|
||||
# Wiki commands
|
||||
("wiki-update", ["magnets"]),
|
||||
("wiki-show", ["magnets"]),
|
||||
("wiki-diff", ["magnets"]),
|
||||
("wiki-update", ["workbench"]),
|
||||
("wiki-show", ["workbench"]),
|
||||
("wiki-diff", ["workbench"]),
|
||||
]
|
||||
|
||||
@pytest.mark.just_syntax
|
||||
@@ -106,7 +102,7 @@ class TestReleaseReadOnly:
|
||||
@pytest.mark.just_runtime
|
||||
@pytest.mark.parametrize(
|
||||
"component",
|
||||
["mcp-server", "workbench", "macro-magnets", "macro-export"],
|
||||
["mcp-server", "workbench"],
|
||||
)
|
||||
def test_changes_since_works(self, just: JustRunner, component: str) -> None:
|
||||
"""changes-since should work for each component."""
|
||||
@@ -121,7 +117,7 @@ class TestReleaseReadOnly:
|
||||
@pytest.mark.just_runtime
|
||||
@pytest.mark.parametrize(
|
||||
"component",
|
||||
["mcp-server", "workbench", "macro-magnets", "macro-export"],
|
||||
["mcp-server", "workbench"],
|
||||
)
|
||||
def test_draft_notes_works(self, just: JustRunner, component: str) -> None:
|
||||
"""draft-notes should work for each component."""
|
||||
@@ -135,8 +131,6 @@ class TestReleaseReadOnly:
|
||||
[
|
||||
("mcp-server", "1.0.0"),
|
||||
("workbench", "1.0.0"),
|
||||
("macro-magnets", "1.0.0"),
|
||||
("macro-export", "1.0.0"),
|
||||
],
|
||||
)
|
||||
def test_dry_run_tag_shows_info(
|
||||
@@ -148,10 +142,9 @@ class TestReleaseReadOnly:
|
||||
assert "Would create tag" in result.stdout
|
||||
|
||||
@pytest.mark.just_runtime
|
||||
@pytest.mark.parametrize("macro", ["magnets", "export"])
|
||||
def test_wiki_show_works(self, just: JustRunner, macro: str) -> None:
|
||||
def test_wiki_show_works(self, just: JustRunner) -> None:
|
||||
"""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 "Wiki Source" in result.stdout
|
||||
|
||||
@@ -175,15 +168,6 @@ class TestReleaseBumpCommands:
|
||||
/ "addon/FreecadRobustMCPBridge/freecad_mcp_bridge/__init__.py",
|
||||
PROJECT_ROOT / "addon/FreecadRobustMCPBridge/wiki-source.txt",
|
||||
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] = {}
|
||||
@@ -214,38 +198,6 @@ class TestReleaseBumpCommands:
|
||||
content = init_file.read_text()
|
||||
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:
|
||||
"""Tests for tag creation commands.
|
||||
@@ -347,10 +299,6 @@ class TestReleaseValidation:
|
||||
"mcp-server",
|
||||
"server",
|
||||
"workbench",
|
||||
"macro-magnets",
|
||||
"magnets",
|
||||
"macro-export",
|
||||
"export",
|
||||
],
|
||||
)
|
||||
def test_changes_since_component_aliases(
|
||||
|
||||
@@ -22,11 +22,12 @@ class TestTestingSyntax:
|
||||
TESTING_COMMANDS: ClassVar[list[str]] = [
|
||||
"testing::unit",
|
||||
"testing::cov",
|
||||
"testing::fast",
|
||||
"testing::quick",
|
||||
"testing::integration",
|
||||
"testing::verbose",
|
||||
"testing::all",
|
||||
"testing::watch",
|
||||
"testing::check-deps",
|
||||
"testing::integration-freecad-auto",
|
||||
"testing::just-syntax",
|
||||
"testing::just-runtime",
|
||||
@@ -73,11 +74,11 @@ class TestTestingRuntime:
|
||||
assert_command_executed(result, "testing::unit")
|
||||
|
||||
@pytest.mark.just_runtime
|
||||
def test_fast_command_recognizes_markers(self, just: JustRunner) -> None:
|
||||
"""Fast test command should recognize the 'not slow' marker."""
|
||||
def test_quick_command_recognizes_markers(self, just: JustRunner) -> None:
|
||||
"""Quick test command should recognize the 'not slow' marker."""
|
||||
result = just.run(
|
||||
"testing::fast",
|
||||
"testing::quick",
|
||||
timeout=60,
|
||||
env={"PYTEST_ADDOPTS": "--collect-only -q"},
|
||||
)
|
||||
assert_command_executed(result, "testing::fast")
|
||||
assert_command_executed(result, "testing::quick")
|
||||
|
||||
@@ -373,9 +373,9 @@ class TestFreecadResources:
|
||||
"""freecad://macros should return macro list."""
|
||||
mock_macros = [
|
||||
MacroInfo(
|
||||
name="MultiExport",
|
||||
path="/home/user/.local/share/FreeCAD/Macro/MultiExport.FCMacro",
|
||||
description="Export to multiple formats",
|
||||
name="ExportSTL",
|
||||
path="/home/user/.local/share/FreeCAD/Macro/ExportSTL.FCMacro",
|
||||
description="Export objects to STL",
|
||||
is_system=False,
|
||||
),
|
||||
MacroInfo(
|
||||
@@ -392,7 +392,7 @@ class TestFreecadResources:
|
||||
data = json.loads(result)
|
||||
|
||||
assert len(data) == 2
|
||||
assert data[0]["name"] == "MultiExport"
|
||||
assert data[0]["name"] == "ExportSTL"
|
||||
assert data[0]["is_system"] is False
|
||||
assert data[1]["is_system"] is True
|
||||
|
||||
|
||||
@@ -58,9 +58,9 @@ class TestMacroTools:
|
||||
"""list_macros should return macro info."""
|
||||
mock_macros = [
|
||||
MacroInfo(
|
||||
name="MultiExport",
|
||||
path="/home/user/.FreeCAD/Macro/MultiExport.FCMacro",
|
||||
description="Export to multiple formats",
|
||||
name="ExportSTL",
|
||||
path="/home/user/.FreeCAD/Macro/ExportSTL.FCMacro",
|
||||
description="Export objects to STL",
|
||||
is_system=False,
|
||||
),
|
||||
MacroInfo(
|
||||
@@ -76,7 +76,7 @@ class TestMacroTools:
|
||||
result = await list_macros()
|
||||
|
||||
assert len(result) == 2
|
||||
assert result[0]["name"] == "MultiExport"
|
||||
assert result[0]["name"] == "ExportSTL"
|
||||
assert result[0]["is_system"] is False
|
||||
assert result[1]["name"] == "SystemMacro"
|
||||
assert result[1]["is_system"] is True
|
||||
@@ -96,11 +96,11 @@ class TestMacroTools:
|
||||
)
|
||||
|
||||
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["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
|
||||
async def test_run_macro_with_args(self, register_tools, mock_bridge):
|
||||
|
||||