* chore: rename workbench to FreecadRobustMCPBridge * fix: stdio cleanup and bug fix for JSON RPC * fix: update FreeCAD MCP bridge and tests * test: Fix GUI Integration tests and other issues * fix: unit tests in CI and a few other things * docs: generate GitHub pages site and link to it. * fix: General code improvements and DRY refactoring * chore: General cleanup * chore: small fixes * docs: cleanup * docs: fixes * docs: small corrections * docs: fix * test: release check * bump: workbench v0.6.0 * chore: bump macros to 0.6.0 * docs: Release improvements * refactor: improve DRYness of code
723 lines
23 KiB
Plaintext
723 lines
23 KiB
Plaintext
"""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.0"
|
|
__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()
|