mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 22:04:56 +00:00
feat: Add CadQuery wrapper library for parametric CAD modeling
Python wrapper around CadQuery for simplified 3D CAD operations with clean API for creating shapes, performing boolean operations, and exporting to various formats. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
4
cadquerywrapper/.gitignore
vendored
Normal file
4
cadquerywrapper/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# Ignore log files
|
||||
cadquerywrapper.log
|
||||
__pycache__/
|
||||
.venv/
|
||||
341
cadquerywrapper/AGENTS.md
Normal file
341
cadquerywrapper/AGENTS.md
Normal file
@@ -0,0 +1,341 @@
|
||||
# AGENTS.md
|
||||
|
||||
## 🧠 Project Overview
|
||||
|
||||
This project is a Python-based application. Agents working in this directory must adhere to Python 3.11+ best practices, with clear module separation, test coverage, security hygiene, and readable documentation.
|
||||
|
||||
---
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
Agents must follow the directory structure below:
|
||||
|
||||
```
|
||||
project-root/
|
||||
│
|
||||
├── src/ # Application source code
|
||||
│ └── <package_name>/ # Python modules/packages
|
||||
│
|
||||
├── tests/ # Unit + integration tests
|
||||
│ └── __init__.py
|
||||
│
|
||||
├── scripts/ # Utility and CLI scripts
|
||||
│
|
||||
├── docs/ # Documentation (Markdown or reStructuredText)
|
||||
│
|
||||
├── .github/ # GitHub Actions workflows
|
||||
│
|
||||
├── pyproject.toml # Project metadata & tool configs (preferred)
|
||||
├── requirements.txt # Legacy dependency spec (optional)
|
||||
├── .env.example # Environment variable template
|
||||
└── AGENTS.md # Agent configuration (this file)
|
||||
```
|
||||
|
||||
Do not create files outside this structure unless explicitly instructed.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing & Validation
|
||||
|
||||
Before submitting changes, agents **must** ensure all tests and linters pass:
|
||||
|
||||
```bash
|
||||
# Set up environment (once)
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -e .[dev]
|
||||
|
||||
# Run unit tests
|
||||
pytest --cov=src --cov-report=term --cov-report=xml
|
||||
|
||||
# Run linters
|
||||
ruff check src/ tests/
|
||||
mypy src/
|
||||
black --check src/
|
||||
```
|
||||
|
||||
### ✅ Required Quality Gates
|
||||
|
||||
- Code coverage: ≥ 90%
|
||||
- Type coverage: 100% via `mypy`
|
||||
- No linter errors (ruff + mypy)
|
||||
- No black formatting issues
|
||||
|
||||
Agents must **not** commit code that fails these checks.
|
||||
|
||||
---
|
||||
|
||||
## 🧹 Code Style
|
||||
|
||||
- Use **PEP8 + PEP257** standards.
|
||||
- Prefer `pyproject.toml` for tool configuration (Black, Ruff, Mypy, etc.).
|
||||
- Function names: `snake_case`
|
||||
- Class names: `PascalCase`
|
||||
- Constant names: `UPPER_SNAKE_CASE`
|
||||
- Use f-strings, not `%` or `.format()` for string interpolation.
|
||||
- Keep function length under 50 lines where possible.
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Security Guidelines
|
||||
|
||||
Agents must:
|
||||
|
||||
- Never commit `.env` files, secrets, or tokens.
|
||||
- Use `dotenv` to load secrets during local execution.
|
||||
- Avoid using `eval()`, `exec()`, or subprocess calls unless sandboxed and essential.
|
||||
- Avoid external internet access during execution unless configured via API keys.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing Philosophy
|
||||
|
||||
- Use `pytest` with fixtures, mocking (`unittest.mock` or `pytest-mock`), and parameterized tests.
|
||||
- Aim for 1:1 parity between modules and test modules.
|
||||
- Include boundary cases and exception tests.
|
||||
|
||||
---
|
||||
|
||||
## 🧾 Commit & PR Conventions
|
||||
|
||||
Agents must format commits using **Conventional Commits**:
|
||||
|
||||
```
|
||||
feat: add user authentication module
|
||||
fix: correct path handling in file parser
|
||||
docs: update README with usage example
|
||||
```
|
||||
|
||||
Pull Requests should include:
|
||||
|
||||
- A concise title (≤ 72 chars)
|
||||
- A summary of what changed and why
|
||||
- Before/after behavior (if applicable)
|
||||
- Testing evidence (CI, screenshots, coverage)
|
||||
|
||||
---
|
||||
|
||||
## 📦 Dependency Management
|
||||
|
||||
- Use **`pyproject.toml`** (via Poetry or PEP 621 standard)
|
||||
- Prefer `pip install .[dev]` over `requirements.txt`
|
||||
- Dev dependencies go under `[tool.poetry.group.dev.dependencies]` or similar
|
||||
- Avoid `pip freeze` output for lockfiles — use `poetry.lock` or `requirements.lock` if necessary
|
||||
|
||||
---
|
||||
|
||||
## 🤖 Agent Behavior Summary
|
||||
|
||||
- Respect the file and folder structure
|
||||
- Never break existing tests
|
||||
- Follow Pythonic style and formatting
|
||||
- Automate validation where possible
|
||||
- Explain large changes via commit messages and PR descriptions
|
||||
- Use comments where logic is non-obvious
|
||||
|
||||
---
|
||||
|
||||
Happy coding! 🐍
|
||||
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Setup Script Requirement
|
||||
|
||||
Every project must include a `setup.sh` script at the root level that ensures:
|
||||
|
||||
- Python 3.11+ is installed
|
||||
- Poetry is installed (via `install.python-poetry.org`)
|
||||
- All dependencies are installed using `poetry install`
|
||||
|
||||
If this script does not exist, the agent must create it. It should be idempotent and safe to re-run.
|
||||
|
||||
Run it like this:
|
||||
|
||||
```bash
|
||||
chmod +x setup.sh
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🖥 OS-Specific Setup Scripts (Expanded)
|
||||
|
||||
Agents must maintain the following OS-specific setup scripts:
|
||||
|
||||
- `setup_linux.sh`
|
||||
- `setup_mac.sh`
|
||||
- `setup_windows.ps1`
|
||||
|
||||
These scripts must:
|
||||
- Ensure Python 3.11+ is present (install if missing)
|
||||
- Install Poetry (if not already installed)
|
||||
- Run `poetry install` to install dependencies
|
||||
- Be idempotent and safe to re-run
|
||||
|
||||
A user must be able to simply clone the repo, run the appropriate script, and be ready to work.
|
||||
|
||||
Refer to `README.md` for user-friendly execution instructions.
|
||||
|
||||
---
|
||||
|
||||
## 🖥 OS-Specific Setup Scripts (Expanded)
|
||||
|
||||
Agents must maintain the following **OS-specific setup scripts** in the project root:
|
||||
|
||||
- `setup_linux.sh` – for Ubuntu, Debian, Fedora, Arch, etc.
|
||||
- `setup_mac.sh` – for macOS (Intel/ARM); must auto-install Homebrew if missing
|
||||
- `setup_windows.ps1` – for Windows 10+ using PowerShell
|
||||
|
||||
Each script must:
|
||||
- Ensure Python 3.11+ is installed
|
||||
- Install Poetry if missing
|
||||
- Run `poetry install` to fetch dependencies
|
||||
- Be idempotent and safe to re-run
|
||||
- Provide clear terminal output
|
||||
|
||||
These scripts should allow a contributor to clone the project, run one script, and be ready to develop.
|
||||
|
||||
---
|
||||
|
||||
### 🐧 setup_linux.sh
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "🔍 Checking Python 3.11+..."
|
||||
if ! python3 --version | grep -q "3.11"; then
|
||||
echo "Installing Python 3.11..."
|
||||
if command -v apt &>/dev/null; then
|
||||
sudo apt update
|
||||
sudo apt install -y python3.11 python3.11-venv python3.11-dev
|
||||
sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1
|
||||
elif command -v dnf &>/dev/null; then
|
||||
sudo dnf install -y python3.11
|
||||
elif command -v pacman &>/dev/null; then
|
||||
sudo pacman -S --noconfirm python
|
||||
else
|
||||
echo "❌ Unsupported Linux package manager."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "✅ Python version: $(python3 --version)"
|
||||
|
||||
echo "🔍 Checking for Poetry..."
|
||||
if ! command -v poetry &>/dev/null; then
|
||||
echo "📦 Installing Poetry..."
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
else
|
||||
echo "✅ Poetry is installed: $(poetry --version)"
|
||||
fi
|
||||
|
||||
echo "📦 Installing dependencies..."
|
||||
poetry install
|
||||
|
||||
echo "✅ Linux setup complete. Use 'poetry shell' to activate environment."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🍎 setup_mac.sh
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "🔍 Checking Python 3.11+..."
|
||||
if ! python3 --version | grep -q "3.11"; then
|
||||
echo "Installing Python 3.11 using Homebrew..."
|
||||
if ! command -v brew &>/dev/null; then
|
||||
echo "📦 Installing Homebrew..."
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
export PATH="/opt/homebrew/bin:$PATH"
|
||||
fi
|
||||
brew install python@3.11
|
||||
brew link python@3.11 --force
|
||||
fi
|
||||
|
||||
echo "✅ Python version: $(python3 --version)"
|
||||
|
||||
echo "🔍 Checking for Poetry..."
|
||||
if ! command -v poetry &>/dev/null; then
|
||||
echo "📦 Installing Poetry..."
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
else
|
||||
echo "✅ Poetry is installed: $(poetry --version)"
|
||||
fi
|
||||
|
||||
echo "📦 Installing dependencies..."
|
||||
poetry install
|
||||
|
||||
echo "✅ macOS setup complete. Use 'poetry shell' to activate environment."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🪟 setup_windows.ps1
|
||||
|
||||
```powershell
|
||||
# PowerShell script
|
||||
Write-Host "🔍 Checking Python 3.11+..."
|
||||
$pythonVersion = python --version
|
||||
if (-not ($pythonVersion -like "*3.11*")) {
|
||||
Write-Host "Installing Python 3.11..."
|
||||
Invoke-WebRequest -Uri "https://www.python.org/ftp/python/3.11.5/python-3.11.5-amd64.exe" -OutFile "python_installer.exe"
|
||||
Start-Process -Wait -FilePath "./python_installer.exe" -ArgumentList "/quiet InstallAllUsers=1 PrependPath=1"
|
||||
Remove-Item "python_installer.exe"
|
||||
}
|
||||
|
||||
Write-Host "✅ Python version: $(python --version)"
|
||||
|
||||
Write-Host "🔍 Checking for Poetry..."
|
||||
if (-not (Get-Command poetry -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "📦 Installing Poetry..."
|
||||
(Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | python -
|
||||
$env:Path += ";$env:USERPROFILE\.poetry\bin"
|
||||
}
|
||||
|
||||
Write-Host "📦 Installing dependencies..."
|
||||
poetry install
|
||||
|
||||
Write-Host "✅ Windows setup complete. Run 'poetry shell' to activate environment."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 📘 Instructing Users in README.md
|
||||
|
||||
Also ensure `README.md` includes this section:
|
||||
|
||||
```markdown
|
||||
## 🖥 Setup Instructions
|
||||
|
||||
Run the setup script for your operating system:
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
chmod +x setup_linux.sh
|
||||
./setup_linux.sh
|
||||
```
|
||||
|
||||
**macOS:**
|
||||
```bash
|
||||
chmod +x setup_mac.sh
|
||||
./setup_mac.sh
|
||||
```
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
.\setup_windows.ps1
|
||||
```
|
||||
|
||||
After installation:
|
||||
```bash
|
||||
poetry shell
|
||||
```
|
||||
```
|
||||
9
cadquerywrapper/CODE_STYLE.md
Normal file
9
cadquerywrapper/CODE_STYLE.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Code Style Guidelines
|
||||
|
||||
This project follows these conventions:
|
||||
|
||||
- Keep code PEP 8 compliant. Use `black` formatting with default settings.
|
||||
- Type annotations are required for all public functions and classes.
|
||||
- Do not use monkey patching. Any changes to third party libraries must be done via subclassing or wrapper functions instead of modifying objects at runtime.
|
||||
|
||||
These rules apply to all code contributions.
|
||||
83
cadquerywrapper/README.md
Normal file
83
cadquerywrapper/README.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# CadQueryWrapper
|
||||
|
||||
CadQueryWrapper is a lightweight wrapper around [CadQuery/cadquery](https://github.com/CadQuery/cadquery). It provides a small validator for checking model parameters against 3D printer rules.
|
||||
|
||||
## Installation
|
||||
|
||||
If Python is not available on your system, run the helper script. Use the
|
||||
Bash version on Linux and macOS or the batch version on Windows:
|
||||
|
||||
```bash
|
||||
./install_python.sh
|
||||
```
|
||||
|
||||
```cmd
|
||||
install_python.bat
|
||||
```
|
||||
Both scripts are self-contained and do not require Python to run.
|
||||
|
||||
Then install the runtime dependencies with:
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
For running the test suite use the development requirements instead:
|
||||
|
||||
```bash
|
||||
pip install -r requirements-dev.txt
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```python
|
||||
import cadquery as cq
|
||||
from cadquerywrapper import CadQueryWrapper, ValidationError
|
||||
|
||||
# create a CadQuery model
|
||||
wp = cq.Workplane().box(1, 1, 1)
|
||||
|
||||
# load rules and create a wrapper using the workplane
|
||||
wrapper = CadQueryWrapper("cadquerywrapper/rules/bambu_printability_rules.json", wp)
|
||||
|
||||
# validate using default rules
|
||||
try:
|
||||
wrapper.validate()
|
||||
except ValidationError as exc:
|
||||
print("Model invalid:", exc)
|
||||
|
||||
# exporting will raise ValidationError if parameters fail
|
||||
wrapper.export_stl("out.stl")
|
||||
```
|
||||
|
||||
See `examples/Ex001_Simple_Block.py` for a complete script that validates and
|
||||
saves a simple block model using ``CadQueryWrapper``.
|
||||
|
||||
## Code Style
|
||||
See [CODE_STYLE.md](CODE_STYLE.md) for contribution guidelines. Monkey patching is prohibited.
|
||||
|
||||
## 🖥 Setup Instructions
|
||||
|
||||
Run the setup script for your operating system:
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
chmod +x setup_linux.sh
|
||||
./setup_linux.sh
|
||||
```
|
||||
|
||||
**macOS:**
|
||||
```bash
|
||||
chmod +x setup_mac.sh
|
||||
./setup_mac.sh
|
||||
```
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
.\setup_windows.ps1
|
||||
```
|
||||
|
||||
After installation:
|
||||
```bash
|
||||
poetry shell
|
||||
```
|
||||
16
cadquerywrapper/cadquerywrapper/__init__.py
Normal file
16
cadquerywrapper/cadquerywrapper/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""CadQueryWrapper package."""
|
||||
|
||||
from .validator import ValidationError, Validator, load_rules, validate
|
||||
from .save_validator import SaveValidator
|
||||
from .project import CadQueryWrapper
|
||||
from .logger import get_logger
|
||||
|
||||
__all__ = [
|
||||
"Validator",
|
||||
"SaveValidator",
|
||||
"ValidationError",
|
||||
"load_rules",
|
||||
"validate",
|
||||
"CadQueryWrapper",
|
||||
"get_logger",
|
||||
]
|
||||
20
cadquerywrapper/cadquerywrapper/logger.py
Normal file
20
cadquerywrapper/cadquerywrapper/logger.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
_LOG_PATH = Path("cadquerywrapper.log")
|
||||
|
||||
# Create logger for the package
|
||||
logger = logging.getLogger("cadquerywrapper")
|
||||
if not logger.handlers:
|
||||
handler = logging.FileHandler(_LOG_PATH)
|
||||
formatter = logging.Formatter(
|
||||
"%(asctime)s - %(levelname)s - %(name)s - %(message)s"
|
||||
)
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
def get_logger(name: str | None = None) -> logging.Logger:
|
||||
"""Return a logger writing to ``cadquerywrapper.log``."""
|
||||
return logger if name is None else logger.getChild(name)
|
||||
100
cadquerywrapper/cadquerywrapper/project.py
Normal file
100
cadquerywrapper/cadquerywrapper/project.py
Normal file
@@ -0,0 +1,100 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
import cadquery as cq
|
||||
|
||||
from .save_validator import SaveValidator
|
||||
from .validator import ValidationError, Validator
|
||||
|
||||
|
||||
class CadQueryWrapper:
|
||||
"""Main interface combining :class:`Validator` and :class:`SaveValidator`."""
|
||||
|
||||
def __init__(self, rules: dict | str | Path, workplane: Any):
|
||||
logger.debug("Initializing CadQueryWrapper with rules %s", rules)
|
||||
self.validator = Validator(rules)
|
||||
self.saver = SaveValidator(self.validator)
|
||||
self.workplane = workplane
|
||||
self.attach_model(self.workplane, {})
|
||||
|
||||
@staticmethod
|
||||
def attach_model(workplane: Any, model: dict) -> None:
|
||||
"""Attach printability model data to ``workplane``."""
|
||||
logger.debug("Attaching model %s to object %s", model, workplane)
|
||||
SaveValidator.attach_model(workplane, model)
|
||||
|
||||
def validate(self) -> None:
|
||||
"""Validate the attached model using :class:`SaveValidator`."""
|
||||
logger.debug("Validating object %s", self.workplane)
|
||||
self.saver._validate_obj(self.workplane)
|
||||
|
||||
def export(self, obj: Any | None = None, *args: Any, **kwargs: Any) -> Any:
|
||||
"""Delegate to :meth:`SaveValidator.export`."""
|
||||
obj = obj or self.workplane
|
||||
logger.debug("Exporting %s", obj)
|
||||
return self.saver.export(obj, *args, **kwargs)
|
||||
|
||||
def cq_export(self, obj: Any | None = None, *args: Any, **kwargs: Any) -> Any:
|
||||
"""Delegate to :meth:`SaveValidator.cq_export`."""
|
||||
obj = obj or self.workplane
|
||||
logger.debug("cq_export %s", obj)
|
||||
return self.saver.cq_export(obj, *args, **kwargs)
|
||||
|
||||
def export_stl(
|
||||
self, shape: cq.Shape | None = None, *args: Any, **kwargs: Any
|
||||
) -> None:
|
||||
"""Delegate to :meth:`SaveValidator.export_stl`."""
|
||||
shape = shape or self.workplane
|
||||
logger.debug("export_stl %s", shape)
|
||||
self.saver.export_stl(shape, *args, **kwargs)
|
||||
|
||||
def export_step(
|
||||
self, shape: cq.Shape | None = None, *args: Any, **kwargs: Any
|
||||
) -> None:
|
||||
"""Delegate to :meth:`SaveValidator.export_step`."""
|
||||
shape = shape or self.workplane
|
||||
logger.debug("export_step %s", shape)
|
||||
self.saver.export_step(shape, *args, **kwargs)
|
||||
|
||||
def export_bin(
|
||||
self, shape: cq.Shape | None = None, *args: Any, **kwargs: Any
|
||||
) -> None:
|
||||
"""Delegate to :meth:`SaveValidator.export_bin`."""
|
||||
shape = shape or self.workplane
|
||||
logger.debug("export_bin %s", shape)
|
||||
self.saver.export_bin(shape, *args, **kwargs)
|
||||
|
||||
def export_brep(
|
||||
self, shape: cq.Shape | None = None, *args: Any, **kwargs: Any
|
||||
) -> None:
|
||||
"""Delegate to :meth:`SaveValidator.export_brep`."""
|
||||
shape = shape or self.workplane
|
||||
logger.debug("export_brep %s", shape)
|
||||
self.saver.export_brep(shape, *args, **kwargs)
|
||||
|
||||
def assembly_export(
|
||||
self, assembly: cq.Assembly | None = None, *args: Any, **kwargs: Any
|
||||
) -> None:
|
||||
"""Delegate to :meth:`SaveValidator.assembly_export`."""
|
||||
|
||||
assembly = assembly or self.workplane
|
||||
logger.debug("assembly_export %s", assembly)
|
||||
self.saver.assembly_export(assembly, *args, **kwargs)
|
||||
|
||||
def assembly_save(
|
||||
self, assembly: cq.Assembly | None = None, *args: Any, **kwargs: Any
|
||||
) -> None:
|
||||
"""Delegate to :meth:`SaveValidator.assembly_save`."""
|
||||
|
||||
assembly = assembly or self.workplane
|
||||
logger.debug("assembly_save %s", assembly)
|
||||
self.saver.assembly_save(assembly, *args, **kwargs)
|
||||
|
||||
|
||||
__all__ = ["CadQueryWrapper"]
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"printer": "Bambu Labs",
|
||||
"nozzle_diameter_mm": 0.4,
|
||||
"layer_height_mm": 0.2,
|
||||
"rules": {
|
||||
"minimum_wall_thickness_mm": 0.8,
|
||||
"minimum_embossed_detail_height_mm": 0.2,
|
||||
"minimum_engraved_detail_depth_mm": 0.2,
|
||||
"minimum_text_height_mm": 4.0,
|
||||
"minimum_text_line_thickness_mm": 0.5,
|
||||
"minimum_hole_diameter_mm": 2.0,
|
||||
"hole_size_tolerance_mm": 0.2,
|
||||
"bridge_max_length_mm": 5.0,
|
||||
"overhang_max_angle_deg": 45,
|
||||
"minimum_clearance_between_parts_mm": 0.3,
|
||||
"minimum_feature_size_mm": 0.4,
|
||||
"recommended_support_gap_mm": 0.2,
|
||||
"minimum_chamfer_or_fillet_mm": 0.5,
|
||||
"max_model_size_mm": {
|
||||
"X": 256,
|
||||
"Y": 256,
|
||||
"Z": 256
|
||||
},
|
||||
"maximum_file_triangle_count": 1000000,
|
||||
"manifold_geometry_required": true,
|
||||
"no_intersecting_geometry": true,
|
||||
"no_open_edges": true,
|
||||
"preferred_file_format": "STL",
|
||||
"alternate_file_formats": ["3MF", "OBJ"]
|
||||
},
|
||||
"notes": [
|
||||
"Values are tuned for typical Bambu Lab P1P, P1S, X1, or A1 with 0.4 mm nozzle.",
|
||||
"Minimum feature sizes are physical—anything smaller may not print or adhere properly.",
|
||||
"Bridge length and overhang angle assume no support and average cooling.",
|
||||
"Clearance depends on material shrinkage—tune for specific filament types.",
|
||||
"For press-fit or moving parts, increase clearance to 0.4–0.5 mm."
|
||||
]
|
||||
}
|
||||
234
cadquerywrapper/cadquerywrapper/save_validator.py
Normal file
234
cadquerywrapper/cadquerywrapper/save_validator.py
Normal file
@@ -0,0 +1,234 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
import trimesh
|
||||
|
||||
import cadquery as cq
|
||||
|
||||
from .validator import (
|
||||
ValidationError,
|
||||
Validator,
|
||||
assembly_has_intersections,
|
||||
is_manifold,
|
||||
shape_has_open_edges,
|
||||
validate,
|
||||
)
|
||||
|
||||
|
||||
class SaveValidator:
|
||||
"""Wrapper around CadQuery save functions that performs validation."""
|
||||
|
||||
def __init__(
|
||||
self, rules: dict | str | Path | Validator, obj: Any | None = None
|
||||
) -> None:
|
||||
if isinstance(rules, Validator):
|
||||
logger.debug("Initializing SaveValidator with Validator instance")
|
||||
self.validator = rules
|
||||
else:
|
||||
logger.debug("Initializing SaveValidator with rules: %s", rules)
|
||||
self.validator = Validator(rules)
|
||||
|
||||
if obj is not None:
|
||||
logger.debug("Attaching empty model to object %s", obj)
|
||||
self.attach_model(obj, {})
|
||||
|
||||
@staticmethod
|
||||
def attach_model(workplane: Any, model: dict) -> None:
|
||||
"""Attach printability model data to a CadQuery object."""
|
||||
logger.debug("Attaching model %s to object %s", model, workplane)
|
||||
setattr(workplane, "_printability_model", model)
|
||||
|
||||
def _validate_obj(self, obj: Any) -> None:
|
||||
logger.debug("Validating object %s", obj)
|
||||
model = getattr(obj, "_printability_model", None)
|
||||
if model is None:
|
||||
logger.debug("No model attached; skipping validation")
|
||||
return
|
||||
|
||||
combined_model = dict(model)
|
||||
max_size = self.validator.rules.get("rules", {}).get("max_model_size_mm")
|
||||
if max_size is not None:
|
||||
try:
|
||||
bbox_obj = obj.val().BoundingBox()
|
||||
except Exception: # pragma: no cover - val() may not exist
|
||||
bbox_obj = None
|
||||
if hasattr(obj, "BoundingBox"):
|
||||
try:
|
||||
bbox_obj = obj.BoundingBox()
|
||||
except Exception: # pragma: no cover - bounding box failure
|
||||
bbox_obj = None
|
||||
if bbox_obj is not None:
|
||||
combined_model["max_model_size_mm"] = {
|
||||
"X": bbox_obj.xlen,
|
||||
"Y": bbox_obj.ylen,
|
||||
"Z": bbox_obj.zlen,
|
||||
}
|
||||
|
||||
errors = validate(combined_model, self.validator.rules)
|
||||
if errors:
|
||||
logger.debug("Validation errors: %s", errors)
|
||||
raise ValidationError("; ".join(errors))
|
||||
|
||||
rules = self.validator.rules.get("rules", {})
|
||||
if rules.get("manifold_geometry_required"):
|
||||
if not is_manifold(obj):
|
||||
logger.debug("Non-manifold geometry detected")
|
||||
raise ValidationError("Non-manifold geometry detected")
|
||||
|
||||
if rules.get("no_open_edges"):
|
||||
if shape_has_open_edges(obj):
|
||||
logger.debug("Object contains open edges")
|
||||
raise ValidationError("Object contains open edges")
|
||||
|
||||
if rules.get("no_intersecting_geometry"):
|
||||
if assembly_has_intersections(obj):
|
||||
logger.debug("Intersecting geometry detected")
|
||||
raise ValidationError("Intersecting geometry detected")
|
||||
|
||||
min_clear = rules.get("minimum_clearance_between_parts_mm")
|
||||
if min_clear is not None and hasattr(obj, "solids"):
|
||||
from .validator import assembly_minimum_clearance
|
||||
|
||||
clearance = assembly_minimum_clearance(obj)
|
||||
if clearance is not None and clearance < min_clear:
|
||||
logger.debug("Clearance %s below minimum %s", clearance, min_clear)
|
||||
raise ValidationError(
|
||||
f"Clearance {clearance} below minimum {min_clear}"
|
||||
)
|
||||
|
||||
max_overhang = rules.get("overhang_max_angle_deg")
|
||||
if max_overhang is not None:
|
||||
from .validator import shape_max_overhang_angle
|
||||
|
||||
angle = shape_max_overhang_angle(obj)
|
||||
if angle is not None and angle > max_overhang:
|
||||
logger.debug(
|
||||
"Overhang angle %s exceeds maximum %s", angle, max_overhang
|
||||
)
|
||||
raise ValidationError(
|
||||
f"Overhang angle {angle} exceeds maximum {max_overhang}"
|
||||
)
|
||||
|
||||
def _check_triangle_count(self, file_name: str | Path) -> None:
|
||||
"""Validate mesh triangle count against configured limit."""
|
||||
|
||||
limit = self.validator.rules.get("rules", {}).get("maximum_file_triangle_count")
|
||||
if limit is None:
|
||||
return
|
||||
|
||||
mesh = trimesh.load_mesh(file_name)
|
||||
tri_count = int(len(mesh.faces))
|
||||
logger.debug("Triangle count for %s: %s", file_name, tri_count)
|
||||
if tri_count > limit:
|
||||
logger.debug("Triangle count %s exceeds maximum %s", tri_count, limit)
|
||||
raise ValidationError(f"Triangle count {tri_count} exceeds maximum {limit}")
|
||||
|
||||
def _validate_file_format(self, file_name: str | Path | None) -> None:
|
||||
"""Ensure ``file_name`` uses an allowed extension."""
|
||||
|
||||
if file_name is None:
|
||||
return
|
||||
rules = self.validator.rules.get("rules", {})
|
||||
preferred = rules.get("preferred_file_format")
|
||||
alternates = rules.get("alternate_file_formats", []) or []
|
||||
allowed = []
|
||||
if preferred:
|
||||
allowed.append(str(preferred).lower().lstrip("."))
|
||||
for alt in alternates:
|
||||
if alt:
|
||||
allowed.append(str(alt).lower().lstrip("."))
|
||||
if not allowed:
|
||||
return
|
||||
ext = Path(file_name).suffix.lower().lstrip(".")
|
||||
if ext and ext not in allowed:
|
||||
logger.debug("File format %s not allowed; allowed: %s", ext, allowed)
|
||||
raise ValidationError(f"File format {ext.upper()} is not supported")
|
||||
|
||||
def export(self, obj: Any, *args: Any, **kwargs: Any) -> Any:
|
||||
"""Validate ``obj`` and delegate to :func:`cadquery.exporters.export`."""
|
||||
logger.debug("export called with %s", obj)
|
||||
self._validate_obj(obj)
|
||||
file_name = args[0] if args else None
|
||||
file_name = kwargs.get("fileName", kwargs.get("fname", file_name))
|
||||
self._validate_file_format(file_name)
|
||||
logger.debug("Saving with exporters.export to %s", file_name)
|
||||
return cq.exporters.export(obj, *args, **kwargs)
|
||||
|
||||
def cq_export(self, obj: Any, *args: Any, **kwargs: Any) -> Any:
|
||||
"""Validate ``obj`` and delegate to :func:`cadquery.export`."""
|
||||
logger.debug("cq_export called with %s", obj)
|
||||
self._validate_obj(obj)
|
||||
file_name = args[0] if args else None
|
||||
file_name = kwargs.get("fileName", kwargs.get("fname", file_name))
|
||||
self._validate_file_format(file_name)
|
||||
logger.debug("Saving with cq.export to %s", file_name)
|
||||
return cq.export(obj, *args, **kwargs) # type: ignore[attr-defined]
|
||||
|
||||
def export_stl(self, shape: cq.Shape, *args: Any, **kwargs: Any) -> None:
|
||||
"""Validate ``shape`` and call ``exportStl``."""
|
||||
logger.debug("export_stl called with %s", shape)
|
||||
self._validate_obj(shape)
|
||||
file_name = args[0] if args else None
|
||||
file_name = kwargs.get("fileName", file_name)
|
||||
self._validate_file_format(file_name)
|
||||
shape.exportStl(*args, **kwargs)
|
||||
if file_name is not None:
|
||||
try:
|
||||
self._check_triangle_count(file_name)
|
||||
except ValidationError:
|
||||
Path(file_name).unlink(missing_ok=True)
|
||||
raise
|
||||
|
||||
def export_step(self, shape: cq.Shape, *args: Any, **kwargs: Any) -> None:
|
||||
"""Validate ``shape`` and call ``exportStep``."""
|
||||
logger.debug("export_step called with %s", shape)
|
||||
self._validate_obj(shape)
|
||||
file_name = args[0] if args else None
|
||||
file_name = kwargs.get("fileName", file_name)
|
||||
self._validate_file_format(file_name)
|
||||
shape.exportStep(*args, **kwargs)
|
||||
|
||||
def export_bin(self, shape: cq.Shape, *args: Any, **kwargs: Any) -> None:
|
||||
"""Validate ``shape`` and call ``exportBin``."""
|
||||
logger.debug("export_bin called with %s", shape)
|
||||
self._validate_obj(shape)
|
||||
file_name = args[0] if args else None
|
||||
file_name = kwargs.get("fileName", file_name)
|
||||
self._validate_file_format(file_name)
|
||||
shape.exportBin(*args, **kwargs)
|
||||
|
||||
def export_brep(self, shape: cq.Shape, *args: Any, **kwargs: Any) -> None:
|
||||
"""Validate ``shape`` and call ``exportBrep``."""
|
||||
logger.debug("export_brep called with %s", shape)
|
||||
self._validate_obj(shape)
|
||||
file_name = args[0] if args else None
|
||||
file_name = kwargs.get("fileName", file_name)
|
||||
self._validate_file_format(file_name)
|
||||
shape.exportBrep(*args, **kwargs)
|
||||
|
||||
def assembly_export(self, assembly: cq.Assembly, *args: Any, **kwargs: Any) -> None:
|
||||
"""Validate ``assembly`` and call ``Assembly.export``."""
|
||||
logger.debug("assembly_export called with %s", assembly)
|
||||
self._validate_obj(assembly)
|
||||
file_name = args[0] if args else None
|
||||
file_name = kwargs.get("fileName", file_name)
|
||||
self._validate_file_format(file_name)
|
||||
assembly.export(*args, **kwargs)
|
||||
|
||||
def assembly_save(self, assembly: cq.Assembly, *args: Any, **kwargs: Any) -> None:
|
||||
"""Validate ``assembly`` and call ``Assembly.save``."""
|
||||
logger.debug("assembly_save called with %s", assembly)
|
||||
self._validate_obj(assembly)
|
||||
file_name = args[0] if args else None
|
||||
file_name = kwargs.get("fileName", file_name)
|
||||
self._validate_file_format(file_name)
|
||||
assembly.save(*args, **kwargs)
|
||||
|
||||
|
||||
__all__ = ["SaveValidator"]
|
||||
288
cadquerywrapper/cadquerywrapper/validator.py
Normal file
288
cadquerywrapper/cadquerywrapper/validator.py
Normal file
@@ -0,0 +1,288 @@
|
||||
"""Printability rules validation helpers."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from .logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ValidationError(Exception):
|
||||
"""Raised when an object fails printability validation."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def load_rules(rules_path: str | Path) -> dict:
|
||||
path = Path(rules_path)
|
||||
logger.debug("Loading rules from %s", path)
|
||||
with path.open() as f:
|
||||
data = json.load(f)
|
||||
logger.debug("Loaded rules: %s", data.keys())
|
||||
return data
|
||||
|
||||
|
||||
def validate(model: dict, rules: dict) -> list[str]:
|
||||
"""Validate model parameters against printability rules.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
model: dict
|
||||
A dictionary containing model parameters to validate. Keys should
|
||||
correspond to the rules names in the JSON file.
|
||||
rules: dict
|
||||
Dictionary loaded from a rules JSON file.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[str]
|
||||
List of human readable error messages. Empty if model is valid.
|
||||
"""
|
||||
logger.debug("Validating model: %s", model)
|
||||
errors = []
|
||||
rule_values = rules.get("rules", {})
|
||||
|
||||
for key, value in rule_values.items():
|
||||
model_value = model.get(key)
|
||||
if model_value is None:
|
||||
continue
|
||||
if isinstance(value, dict):
|
||||
if isinstance(model_value, dict) and key == "max_model_size_mm":
|
||||
for axis, limit in value.items():
|
||||
axis_value = model_value.get(axis)
|
||||
if axis_value is None:
|
||||
continue
|
||||
if axis_value > limit:
|
||||
msg = f"Model size {axis} {axis_value} exceeds maximum {limit}"
|
||||
errors.append(msg)
|
||||
logger.debug(msg)
|
||||
continue
|
||||
if model_value < value:
|
||||
msg = (
|
||||
f"{key.replace('_', ' ').capitalize()} {model_value} "
|
||||
f"is below minimum {value}"
|
||||
)
|
||||
errors.append(msg)
|
||||
logger.debug(msg)
|
||||
logger.debug("Validation errors: %s", errors)
|
||||
return errors
|
||||
|
||||
|
||||
class Validator:
|
||||
"""Object oriented wrapper around :func:`validate`.
|
||||
|
||||
The ``validate`` method will raise :class:`ValidationError` if the provided
|
||||
model data does not satisfy the stored rules.
|
||||
"""
|
||||
|
||||
def __init__(self, rules: dict | str | Path):
|
||||
if isinstance(rules, (str, Path)):
|
||||
logger.debug("Initializing Validator with rules file %s", rules)
|
||||
self.rules = load_rules(rules)
|
||||
else:
|
||||
logger.debug("Initializing Validator with rules dict")
|
||||
self.rules = rules
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, path: str | Path) -> "Validator":
|
||||
"""Create a :class:`Validator` from a rules JSON file."""
|
||||
logger.debug("Creating Validator from file %s", path)
|
||||
return cls(load_rules(path))
|
||||
|
||||
def validate(self, model: dict) -> None:
|
||||
"""Validate ``model`` against the stored ``rules``.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValidationError
|
||||
If any of the model values are below the configured limits.
|
||||
"""
|
||||
|
||||
errors = validate(model, self.rules)
|
||||
if errors:
|
||||
logger.debug("Validation failed with errors: %s", errors)
|
||||
raise ValidationError("; ".join(errors))
|
||||
logger.debug("Model valid")
|
||||
|
||||
|
||||
__all__ = ["ValidationError", "load_rules", "validate", "Validator"]
|
||||
|
||||
|
||||
def is_manifold(shape: object) -> bool:
|
||||
"""Return ``True`` if ``shape`` appears to be manifold."""
|
||||
logger.debug("Checking if shape is manifold")
|
||||
try:
|
||||
if hasattr(shape, "isValid") and not shape.isValid():
|
||||
logger.debug("Shape invalid")
|
||||
return False
|
||||
except Exception:
|
||||
logger.debug("isValid check failed")
|
||||
return False
|
||||
try:
|
||||
if hasattr(shape, "isClosed") and not shape.isClosed():
|
||||
logger.debug("Shape not closed")
|
||||
return False
|
||||
except Exception:
|
||||
logger.debug("isClosed check failed")
|
||||
return False
|
||||
logger.debug("Shape is manifold")
|
||||
return True
|
||||
|
||||
|
||||
def shape_has_open_edges(shape: object) -> bool:
|
||||
"""Return ``True`` if ``shape`` seems to have open edges."""
|
||||
logger.debug("Checking for open edges")
|
||||
if hasattr(shape, "hasOpenEdges"):
|
||||
try:
|
||||
result = bool(shape.hasOpenEdges())
|
||||
logger.debug("hasOpenEdges: %s", result)
|
||||
return result
|
||||
except Exception:
|
||||
logger.debug("hasOpenEdges check failed")
|
||||
return True
|
||||
if hasattr(shape, "open_edges"):
|
||||
result = bool(getattr(shape, "open_edges"))
|
||||
logger.debug("open_edges attribute: %s", result)
|
||||
return result
|
||||
logger.debug("No open edges detected")
|
||||
return False
|
||||
|
||||
|
||||
def assembly_has_intersections(assembly: object) -> bool:
|
||||
"""Return ``True`` if any solids in ``assembly`` intersect."""
|
||||
logger.debug("Checking assembly for intersections")
|
||||
solids = []
|
||||
if hasattr(assembly, "solids"):
|
||||
try:
|
||||
solids = list(assembly.solids())
|
||||
except Exception:
|
||||
solids = []
|
||||
if not solids and hasattr(assembly, "children"):
|
||||
solids = [c for c in assembly.children if hasattr(c, "intersect")]
|
||||
for i, shape1 in enumerate(solids):
|
||||
for shape2 in solids[i + 1 :]:
|
||||
try:
|
||||
result = shape1.intersect(shape2)
|
||||
except Exception:
|
||||
continue
|
||||
if result is None:
|
||||
continue
|
||||
is_null = False
|
||||
if hasattr(result, "isNull"):
|
||||
try:
|
||||
is_null = result.isNull()
|
||||
except Exception:
|
||||
is_null = False
|
||||
elif hasattr(result, "Volume"):
|
||||
try:
|
||||
is_null = result.Volume() == 0
|
||||
except Exception:
|
||||
is_null = False
|
||||
if not is_null:
|
||||
logger.debug("Intersection found between solids")
|
||||
return True
|
||||
logger.debug("No intersections found")
|
||||
return False
|
||||
|
||||
|
||||
def assembly_minimum_clearance(assembly: object) -> float | None:
|
||||
"""Return the minimum distance between solids in ``assembly``."""
|
||||
logger.debug("Computing minimum clearance in assembly")
|
||||
solids = []
|
||||
if hasattr(assembly, "solids"):
|
||||
try:
|
||||
solids = list(assembly.solids())
|
||||
except Exception: # pragma: no cover - solids retrieval failure
|
||||
solids = []
|
||||
if not solids and hasattr(assembly, "children"):
|
||||
solids = [c for c in assembly.children if hasattr(c, "distTo")]
|
||||
|
||||
min_dist: float | None = None
|
||||
for i, shape1 in enumerate(solids):
|
||||
for shape2 in solids[i + 1 :]:
|
||||
dists = []
|
||||
for shape_a, shape_b in ((shape1, shape2), (shape2, shape1)):
|
||||
method = (
|
||||
getattr(shape_a, "distTo", None)
|
||||
or getattr(shape_a, "distance", None)
|
||||
or getattr(shape_a, "Distance", None)
|
||||
)
|
||||
if callable(method):
|
||||
try:
|
||||
d = float(method(shape_b))
|
||||
except Exception: # pragma: no cover - distance failure
|
||||
continue
|
||||
dists.append(d)
|
||||
if not dists:
|
||||
continue
|
||||
pair_dist = min(dists)
|
||||
if min_dist is None or pair_dist < min_dist:
|
||||
min_dist = pair_dist
|
||||
logger.debug("Minimum clearance: %s", min_dist)
|
||||
return min_dist
|
||||
|
||||
|
||||
def shape_max_overhang_angle(
|
||||
shape: object, z_dir: tuple[float, float, float] = (0.0, 0.0, 1.0)
|
||||
) -> float | None:
|
||||
"""Return the maximum overhang angle of ``shape`` in degrees."""
|
||||
logger.debug("Calculating max overhang angle")
|
||||
faces = []
|
||||
for attr in ("faces", "Faces", "all_faces"):
|
||||
getter = getattr(shape, attr, None)
|
||||
if callable(getter):
|
||||
try:
|
||||
faces = list(getter())
|
||||
except Exception: # pragma: no cover - faces failure
|
||||
faces = []
|
||||
if faces:
|
||||
break
|
||||
elif isinstance(getter, (list, tuple)):
|
||||
faces = list(getter)
|
||||
break
|
||||
if not faces:
|
||||
logger.debug("No faces found")
|
||||
return None
|
||||
|
||||
import math
|
||||
|
||||
z_len = math.sqrt(sum(c * c for c in z_dir)) or 1.0
|
||||
z_axis = tuple(c / z_len for c in z_dir)
|
||||
max_angle = 0.0
|
||||
|
||||
for face in faces:
|
||||
normal = None
|
||||
if hasattr(face, "normalAt"):
|
||||
try:
|
||||
normal = face.normalAt()
|
||||
except Exception: # pragma: no cover - normal failure
|
||||
normal = None
|
||||
if normal is None and hasattr(face, "normal"):
|
||||
normal = face.normal
|
||||
if normal is None:
|
||||
continue
|
||||
if hasattr(normal, "toTuple"):
|
||||
normal = normal.toTuple()
|
||||
if not isinstance(normal, (list, tuple)) or len(normal) != 3:
|
||||
continue
|
||||
n_len = math.sqrt(sum(c * c for c in normal)) or 1.0
|
||||
norm = tuple(c / n_len for c in normal)
|
||||
dot = abs(sum(a * b for a, b in zip(norm, z_axis)))
|
||||
dot = max(-1.0, min(1.0, dot))
|
||||
angle = math.degrees(math.acos(dot))
|
||||
if angle > max_angle:
|
||||
max_angle = angle
|
||||
|
||||
logger.debug("Max overhang angle: %s", max_angle)
|
||||
return max_angle
|
||||
|
||||
|
||||
__all__ += [
|
||||
"is_manifold",
|
||||
"shape_has_open_edges",
|
||||
"assembly_has_intersections",
|
||||
"assembly_minimum_clearance",
|
||||
"shape_max_overhang_angle",
|
||||
]
|
||||
154547
cadquerywrapper/examples/CQ examples.ipynb
Normal file
154547
cadquerywrapper/examples/CQ examples.ipynb
Normal file
File diff suppressed because it is too large
Load Diff
33
cadquerywrapper/examples/Ex001_Simple_Block.py
Normal file
33
cadquerywrapper/examples/Ex001_Simple_Block.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import cadquery as cq
|
||||
from cadquerywrapper import CadQueryWrapper, ValidationError
|
||||
|
||||
# These can be modified rather than hardcoding values for each dimension.
|
||||
length = 80.0 # Length of the block
|
||||
height = 60.0 # Height of the block
|
||||
thickness = 10.0 # Thickness of the block
|
||||
|
||||
# Create a 3D block based on the dimension variables above.
|
||||
# 1. Establishes a workplane that an object can be built on.
|
||||
# 1a. Uses the X and Y origins to define the workplane, meaning that the
|
||||
# positive Z direction is "up", and the negative Z direction is "down".
|
||||
result = cq.Workplane("XY").box(length, height, thickness)
|
||||
|
||||
# Attach a simple model so validation has data to work with
|
||||
CadQueryWrapper.attach_model(result, {"minimum_wall_thickness_mm": thickness})
|
||||
|
||||
# Wrap the workplane with validation and saving helpers
|
||||
wrapper = CadQueryWrapper("cadquerywrapper/rules/bambu_printability_rules.json", result)
|
||||
|
||||
try:
|
||||
wrapper.validate()
|
||||
except ValidationError as exc:
|
||||
print("Model invalid:", exc)
|
||||
|
||||
wrapper.export_stl("simple_block.stl")
|
||||
|
||||
# The following method is now outdated, but can still be used to display the
|
||||
# results of the script if you want
|
||||
# from Helpers import show
|
||||
# show(result) # Render the result of this script
|
||||
|
||||
show_object(result)
|
||||
@@ -0,0 +1,25 @@
|
||||
import cadquery as cq
|
||||
|
||||
# These can be modified rather than hardcoding values for each dimension.
|
||||
length = 80.0 # Length of the block
|
||||
height = 60.0 # Height of the block
|
||||
thickness = 10.0 # Thickness of the block
|
||||
center_hole_dia = 22.0 # Diameter of center hole in block
|
||||
|
||||
# Create a block based on the dimensions above and add a 22mm center hole.
|
||||
# 1. Establishes a workplane that an object can be built on.
|
||||
# 1a. Uses the X and Y origins to define the workplane, meaning that the
|
||||
# positive Z direction is "up", and the negative Z direction is "down".
|
||||
# 2. The highest (max) Z face is selected and a new workplane is created on it.
|
||||
# 3. The new workplane is used to drill a hole through the block.
|
||||
# 3a. The hole is automatically centered in the workplane.
|
||||
result = (
|
||||
cq.Workplane("XY")
|
||||
.box(length, height, thickness)
|
||||
.faces(">Z")
|
||||
.workplane()
|
||||
.hole(center_hole_dia)
|
||||
)
|
||||
|
||||
# Displays the result of this script
|
||||
show_object(result)
|
||||
@@ -0,0 +1,44 @@
|
||||
import cadquery as cq
|
||||
|
||||
# These can be modified rather than hardcoding values for each dimension.
|
||||
length = 80.0 # Length of the block
|
||||
width = 100.0 # Width of the block
|
||||
thickness = 10.0 # Thickness of the block
|
||||
center_hole_dia = 22.0 # Diameter of center hole in block
|
||||
cbore_hole_diameter = 2.4 # Bolt shank/threads clearance hole diameter
|
||||
cbore_inset = 12.0 # How far from the edge the cbored holes are set
|
||||
cbore_diameter = 4.4 # Bolt head pocket hole diameter
|
||||
cbore_depth = 2.1 # Bolt head pocket hole depth
|
||||
|
||||
# Create a 3D block based on the dimensions above and add a 22mm center hold
|
||||
# and 4 counterbored holes for bolts
|
||||
# 1. Establishes a workplane that an object can be built on.
|
||||
# 1a. Uses the X and Y origins to define the workplane, meaning that the
|
||||
# positive Z direction is "up", and the negative Z direction is "down".
|
||||
# 2. The highest(max) Z face is selected and a new workplane is created on it.
|
||||
# 3. The new workplane is used to drill a hole through the block.
|
||||
# 3a. The hole is automatically centered in the workplane.
|
||||
# 4. The highest(max) Z face is selected and a new workplane is created on it.
|
||||
# 5. A for-construction rectangle is created on the workplane based on the
|
||||
# block's overall dimensions.
|
||||
# 5a. For-construction objects are used only to place other geometry, they
|
||||
# do not show up in the final displayed geometry.
|
||||
# 6. The vertices of the rectangle (corners) are selected, and a counter-bored
|
||||
# hole is placed at each of the vertices (all 4 of them at once).
|
||||
result = (
|
||||
cq.Workplane("XY")
|
||||
.box(length, width, thickness)
|
||||
.faces(">Z")
|
||||
.workplane()
|
||||
.hole(center_hole_dia)
|
||||
.faces(">Z")
|
||||
.workplane()
|
||||
.rect(length - cbore_inset, width - cbore_inset, forConstruction=True)
|
||||
.vertices()
|
||||
.cboreHole(cbore_hole_diameter, cbore_diameter, cbore_depth)
|
||||
.edges("|Z")
|
||||
.fillet(2.0)
|
||||
)
|
||||
|
||||
# Displays the result of this script
|
||||
show_object(result)
|
||||
32
cadquerywrapper/examples/Ex004_Extruded_Cylindrical_Plate.py
Normal file
32
cadquerywrapper/examples/Ex004_Extruded_Cylindrical_Plate.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import cadquery as cq
|
||||
|
||||
# These can be modified rather than hardcoding values for each dimension.
|
||||
circle_radius = 50.0 # Radius of the plate
|
||||
thickness = 13.0 # Thickness of the plate
|
||||
rectangle_width = 13.0 # Width of rectangular hole in cylindrical plate
|
||||
rectangle_length = 19.0 # Length of rectangular hole in cylindrical plate
|
||||
|
||||
# Extrude a cylindrical plate with a rectangular hole in the middle of it.
|
||||
# 1. Establishes a workplane that an object can be built on.
|
||||
# 1a. Uses the named plane orientation "front" to define the workplane, meaning
|
||||
# that the positive Z direction is "up", and the negative Z direction
|
||||
# is "down".
|
||||
# 2. The 2D geometry for the outer circle is created at the same time as the
|
||||
# rectangle that will create the hole in the center.
|
||||
# 2a. The circle and the rectangle will be automatically centered on the
|
||||
# workplane.
|
||||
# 2b. Unlike some other functions like the hole(), circle() takes
|
||||
# a radius and not a diameter.
|
||||
# 3. The circle and rectangle are extruded together, creating a cylindrical
|
||||
# plate with a rectangular hole in the center.
|
||||
# 3a. circle() and rect() could be changed to any other shape to completely
|
||||
# change the resulting plate and/or the hole in it.
|
||||
result = (
|
||||
cq.Workplane("front")
|
||||
.circle(circle_radius)
|
||||
.rect(rectangle_width, rectangle_length)
|
||||
.extrude(thickness)
|
||||
)
|
||||
|
||||
# Displays the result of this script
|
||||
show_object(result)
|
||||
49
cadquerywrapper/examples/Ex005_Extruded_Lines_and_Arcs.py
Normal file
49
cadquerywrapper/examples/Ex005_Extruded_Lines_and_Arcs.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import cadquery as cq
|
||||
|
||||
# These can be modified rather than hardcoding values for each dimension.
|
||||
width = 2.0 # Overall width of the plate
|
||||
thickness = 0.25 # Thickness of the plate
|
||||
|
||||
# Extrude a plate outline made of lines and an arc
|
||||
# 1. Establishes a workplane that an object can be built on.
|
||||
# 1a. Uses the named plane orientation "front" to define the workplane, meaning
|
||||
# that the positive Z direction is "up", and the negative Z direction
|
||||
# is "down".
|
||||
# 2. Draws a line from the origin to an X position of the plate's width.
|
||||
# 2a. The starting point of a 2D drawing like this will be at the center of the
|
||||
# workplane (0, 0) unless the moveTo() function moves the starting point.
|
||||
# 3. A line is drawn from the last position straight up in the Y direction
|
||||
# 1.0 millimeters.
|
||||
# 4. An arc is drawn from the last point, through point (1.0, 1.5) which is
|
||||
# half-way back to the origin in the X direction and 0.5 mm above where
|
||||
# the last line ended at. The arc then ends at (0.0, 1.0), which is 1.0 mm
|
||||
# above (in the Y direction) where our first line started from.
|
||||
# 5. An arc is drawn from the last point that ends on (-0.5, 1.0), the sag of
|
||||
# the curve 0.2 determines that the curve is concave with the midpoint 0.1 mm
|
||||
# from the arc baseline. If the sag was -0.2 the arc would be convex.
|
||||
# This convention is valid when the profile is drawn counterclockwise.
|
||||
# The reverse is true if the profile is drawn clockwise.
|
||||
# Clockwise: +sag => convex, -sag => concave
|
||||
# Counterclockwise: +sag => concave, -sag => convex
|
||||
# 6. An arc is drawn from the last point that ends on (-0.7, -0.2), the arc is
|
||||
# determined by the radius of -1.5 mm.
|
||||
# Clockwise: +radius => convex, -radius => concave
|
||||
# Counterclockwise: +radius => concave, -radius => convex
|
||||
# 7. close() is called to automatically draw the last line for us and close
|
||||
# the sketch so that it can be extruded.
|
||||
# 7a. Without the close(), the 2D sketch will be left open and the extrude
|
||||
# operation will provide unpredictable results.
|
||||
# 8. The 2D sketch is extruded into a solid object of the specified thickness.
|
||||
result = (
|
||||
cq.Workplane("front")
|
||||
.lineTo(width, 0)
|
||||
.lineTo(width, 1.0)
|
||||
.threePointArc((1.0, 1.5), (0.0, 1.0))
|
||||
.sagittaArc((-0.5, 1.0), 0.2)
|
||||
.radiusArc((-0.7, -0.2), -1.5)
|
||||
.close()
|
||||
.extrude(thickness)
|
||||
)
|
||||
|
||||
# Displays the result of this script
|
||||
show_object(result)
|
||||
@@ -0,0 +1,35 @@
|
||||
import cadquery as cq
|
||||
|
||||
# These can be modified rather than hardcoding values for each dimension.
|
||||
circle_radius = 3.0 # The outside radius of the plate
|
||||
thickness = 0.25 # The thickness of the plate
|
||||
|
||||
# Make a plate with two cutouts in it by moving the workplane center point
|
||||
# 1. Establishes a workplane that an object can be built on.
|
||||
# 1a. Uses the named plane orientation "front" to define the workplane, meaning
|
||||
# that the positive Z direction is "up", and the negative Z direction
|
||||
# is "down".
|
||||
# 1b. The initial workplane center point is the center of the circle, at (0,0).
|
||||
# 2. A circle is created at the center of the workplane
|
||||
# 2a. Notice that circle() takes a radius and not a diameter
|
||||
result = cq.Workplane("front").circle(circle_radius)
|
||||
|
||||
# 3. The work center is movide to (1.5, 0.0) by calling center().
|
||||
# 3a. The new center is specified relative to the previous center,not
|
||||
# relative to global coordinates.
|
||||
# 4. A 0.5mm x 0.5mm 2D square is drawn inside the circle.
|
||||
# 4a. The plate has not been extruded yet, only 2D geometry is being created.
|
||||
result = result.center(1.5, 0.0).rect(0.5, 0.5)
|
||||
|
||||
# 5. The work center is moved again, this time to (-1.5, 1.5).
|
||||
# 6. A 2D circle is created at that new center with a radius of 0.25mm.
|
||||
result = result.center(-1.5, 1.5).circle(0.25)
|
||||
|
||||
# 7. All 2D geometry is extruded to the specified thickness of the plate.
|
||||
# 7a. The small circle and the square are enclosed in the outer circle of the
|
||||
# plate and so it is assumed that we want them to be cut out of the plate.
|
||||
# A separate cut operation is not needed.
|
||||
result = result.extrude(thickness)
|
||||
|
||||
# Displays the result of this script
|
||||
show_object(result)
|
||||
32
cadquerywrapper/examples/Ex007_Using_Point_Lists.py
Normal file
32
cadquerywrapper/examples/Ex007_Using_Point_Lists.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import cadquery as cq
|
||||
|
||||
# These can be modified rather than hardcoding values for each dimension.
|
||||
plate_radius = 2.0 # The radius of the plate that will be extruded
|
||||
hole_pattern_radius = 0.25 # Radius of circle where the holes will be placed
|
||||
thickness = 0.125 # The thickness of the plate that will be extruded
|
||||
|
||||
# Make a plate with 4 holes in it at various points in a polar arrangement from
|
||||
# the center of the workplane.
|
||||
# 1. Establishes a workplane that an object can be built on.
|
||||
# 1a. Uses the named plane orientation "front" to define the workplane, meaning
|
||||
# that the positive Z direction is "up", and the negative Z direction
|
||||
# is "down".
|
||||
# 2. A 2D circle is drawn that will become though outer profile of the plate.
|
||||
r = cq.Workplane("front").circle(plate_radius)
|
||||
|
||||
# 3. Push 4 points on the stack that will be used as the center points of the
|
||||
# holes.
|
||||
r = r.pushPoints([(1.5, 0), (0, 1.5), (-1.5, 0), (0, -1.5)])
|
||||
|
||||
# 4. This circle() call will operate on all four points, putting a circle at
|
||||
# each one.
|
||||
r = r.circle(hole_pattern_radius)
|
||||
|
||||
# 5. All 2D geometry is extruded to the specified thickness of the plate.
|
||||
# 5a. The small hole circles are enclosed in the outer circle of the plate and
|
||||
# so it is assumed that we want them to be cut out of the plate. A
|
||||
# separate cut operation is not needed.
|
||||
result = r.extrude(thickness)
|
||||
|
||||
# Displays the result of this script
|
||||
show_object(result)
|
||||
42
cadquerywrapper/examples/Ex008_Polygon_Creation.py
Normal file
42
cadquerywrapper/examples/Ex008_Polygon_Creation.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import cadquery as cq
|
||||
|
||||
# These can be modified rather than hardcoding values for each dimension.
|
||||
width = 3.0 # The width of the plate
|
||||
height = 4.0 # The height of the plate
|
||||
thickness = 0.25 # The thickness of the plate
|
||||
polygon_sides = 6 # The number of sides that the polygonal holes should have
|
||||
polygon_dia = 1.0 # The diameter of the circle enclosing the polygon points
|
||||
|
||||
# Create a plate with two polygons cut through it
|
||||
# 1. Establishes a workplane that an object can be built on.
|
||||
# 1a. Uses the named plane orientation "front" to define the workplane, meaning
|
||||
# that the positive Z direction is "up", and the negative Z direction
|
||||
# is "down".
|
||||
# 2. A 3D box is created in one box() operation to represent the plate.
|
||||
# 2a. The box is centered around the origin, which creates a result that may
|
||||
# be unituitive when the polygon cuts are made.
|
||||
# 3. 2 points are pushed onto the stack and will be used as centers for the
|
||||
# polygonal holes.
|
||||
# 4. The two polygons are created, on for each point, with one call to
|
||||
# polygon() using the number of sides and the circle that bounds the
|
||||
# polygon.
|
||||
# 5. The polygons are cut thru all objects that are in the line of extrusion.
|
||||
# 5a. A face was not selected, and so the polygons are created on the
|
||||
# workplane. Since the box was centered around the origin, the polygons end
|
||||
# up being in the center of the box. This makes them cut from the center to
|
||||
# the outside along the normal (positive direction).
|
||||
# 6. The polygons are cut through all objects, starting at the center of the
|
||||
# box/plate and going "downward" (opposite of normal) direction. Functions
|
||||
# like cutBlind() assume a positive cut direction, but cutThruAll() assumes
|
||||
# instead that the cut is made from a max direction and cuts downward from
|
||||
# that max through all objects.
|
||||
result = (
|
||||
cq.Workplane("front")
|
||||
.box(width, height, thickness)
|
||||
.pushPoints([(0, 0.75), (0, -0.75)])
|
||||
.polygon(polygon_sides, polygon_dia)
|
||||
.cutThruAll()
|
||||
)
|
||||
|
||||
# Displays the result of this script
|
||||
show_object(result)
|
||||
37
cadquerywrapper/examples/Ex009_Polylines.py
Normal file
37
cadquerywrapper/examples/Ex009_Polylines.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import cadquery as cq
|
||||
|
||||
# These can be modified rather than hardcoding values for each dimension.
|
||||
# Define up our Length, Height, Width, and thickness of the beam
|
||||
(L, H, W, t) = (100.0, 20.0, 20.0, 1.0)
|
||||
|
||||
# Define the points that the polyline will be drawn to/thru
|
||||
pts = [
|
||||
(0, H / 2.0),
|
||||
(W / 2.0, H / 2.0),
|
||||
(W / 2.0, (H / 2.0 - t)),
|
||||
(t / 2.0, (H / 2.0 - t)),
|
||||
(t / 2.0, (t - H / 2.0)),
|
||||
(W / 2.0, (t - H / 2.0)),
|
||||
(W / 2.0, H / -2.0),
|
||||
(0, H / -2.0),
|
||||
]
|
||||
|
||||
# We generate half of the I-beam outline and then mirror it to create the full
|
||||
# I-beam.
|
||||
# 1. Establishes a workplane that an object can be built on.
|
||||
# 1a. Uses the named plane orientation "front" to define the workplane, meaning
|
||||
# that the positive Z direction is "up", and the negative Z direction
|
||||
# is "down".
|
||||
# 2. moveTo() is used to move the first point from the origin (0, 0) to
|
||||
# (0, 10.0), with 10.0 being half the height (H/2.0). If this is not done
|
||||
# the first line will start from the origin, creating an extra segment that
|
||||
# will cause the extrude to have an invalid shape.
|
||||
# 3. The polyline function takes a list of points and generates the lines
|
||||
# through all the points at once.
|
||||
# 3. Only half of the I-beam profile has been drawn so far. That half is
|
||||
# mirrored around the Y-axis to create the complete I-beam profile.
|
||||
# 4. The I-beam profile is extruded to the final length of the beam.
|
||||
result = cq.Workplane("front").polyline(pts).mirrorY().extrude(L)
|
||||
|
||||
# Displays the result of this script
|
||||
show_object(result)
|
||||
@@ -0,0 +1,27 @@
|
||||
import cadquery as cq
|
||||
|
||||
# 1. Establishes a workplane to create the spline on to extrude.
|
||||
# 1a. Uses the X and Y origins to define the workplane, meaning that the
|
||||
# positive Z direction is "up", and the negative Z direction is "down".
|
||||
s = cq.Workplane("XY")
|
||||
|
||||
# The points that the spline will pass through
|
||||
sPnts = [
|
||||
(2.75, 1.5),
|
||||
(2.5, 1.75),
|
||||
(2.0, 1.5),
|
||||
(1.5, 1.0),
|
||||
(1.0, 1.25),
|
||||
(0.5, 1.0),
|
||||
(0, 1.0),
|
||||
]
|
||||
|
||||
# 2. Generate our plate with the spline feature and make sure it is a
|
||||
# closed entity
|
||||
r = s.lineTo(3.0, 0).lineTo(3.0, 1.0).spline(sPnts, includeCurrent=True).close()
|
||||
|
||||
# 3. Extrude to turn the wire into a plate
|
||||
result = r.extrude(0.5)
|
||||
|
||||
# Displays the result of this script
|
||||
show_object(result)
|
||||
@@ -0,0 +1,20 @@
|
||||
import cadquery as cq
|
||||
|
||||
# 1. Establishes a workplane that an object can be built on.
|
||||
# 1a. Uses the named plane orientation "front" to define the workplane, meaning
|
||||
# that the positive Z direction is "up", and the negative Z direction
|
||||
# is "down".
|
||||
# 2. A horizontal line is drawn on the workplane with the hLine function.
|
||||
# 2a. 1.0 is the distance, not coordinate. hLineTo allows using xCoordinate
|
||||
# not distance.
|
||||
r = cq.Workplane("front").hLine(1.0)
|
||||
|
||||
# 3. Draw a series of vertical and horizontal lines with the vLine and hLine
|
||||
# functions.
|
||||
r = r.vLine(0.5).hLine(-0.25).vLine(-0.25).hLineTo(0.0)
|
||||
|
||||
# 4. Mirror the geometry about the Y axis and extrude it into a 3D object.
|
||||
result = r.mirrorY().extrude(0.25)
|
||||
|
||||
# Displays the result of this script
|
||||
show_object(result)
|
||||
@@ -0,0 +1,16 @@
|
||||
import cadquery as cq
|
||||
|
||||
# 1. Establishes a workplane that an object can be built on.
|
||||
# 1a. Uses the named plane orientation "front" to define the workplane, meaning
|
||||
# that the positive Z direction is "up", and the negative Z direction
|
||||
# is "down".
|
||||
# 2. Creates a 3D box that will have a hole placed in it later.
|
||||
result = cq.Workplane("front").box(2, 3, 0.5)
|
||||
|
||||
# 3. Find the top-most face with the >Z max selector.
|
||||
# 3a. Establish a new workplane to build geometry on.
|
||||
# 3b. Create a hole down into the box.
|
||||
result = result.faces(">Z").workplane().hole(0.5)
|
||||
|
||||
# Displays the result of this script
|
||||
show_object(result)
|
||||
@@ -0,0 +1,21 @@
|
||||
import cadquery as cq
|
||||
|
||||
# 1. Establishes a workplane that an object can be built on.
|
||||
# 1a. Uses the named plane orientation "front" to define the workplane, meaning
|
||||
# that the positive Z direction is "up", and the negative Z direction
|
||||
# is "down".
|
||||
# 2. Creates a 3D box that will have a hole placed in it later.
|
||||
result = cq.Workplane("front").box(3, 2, 0.5)
|
||||
|
||||
# 3. Select the lower left vertex and make a workplane.
|
||||
# 3a. The top-most Z face is selected using the >Z selector.
|
||||
# 3b. The lower-left vertex of the faces is selected with the <XY selector.
|
||||
# 3c. A new workplane is created on the vertex to build future geometry on.
|
||||
result = result.faces(">Z").vertices("<XY").workplane(centerOption="CenterOfMass")
|
||||
|
||||
# 4. A circle is drawn with the selected vertex as its center.
|
||||
# 4a. The circle is cut down through the box to cut the corner out.
|
||||
result = result.circle(1.0).cutThruAll()
|
||||
|
||||
# Displays the result of this script
|
||||
show_object(result)
|
||||
20
cadquerywrapper/examples/Ex014_Offset_Workplanes.py
Normal file
20
cadquerywrapper/examples/Ex014_Offset_Workplanes.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import cadquery as cq
|
||||
|
||||
# 1. Establishes a workplane that an object can be built on.
|
||||
# 1a. Uses the named plane orientation "front" to define the workplane, meaning
|
||||
# that the positive Z direction is "up", and the negative Z direction
|
||||
# is "down".
|
||||
# 2. Creates a 3D box that will have geometry based off it later.
|
||||
result = cq.Workplane("front").box(3, 2, 0.5)
|
||||
|
||||
# 3. The lowest face in the X direction is selected with the <X selector.
|
||||
# 4. A new workplane is created
|
||||
# 4a.The workplane is offset from the object surface so that it is not touching
|
||||
# the original box.
|
||||
result = result.faces("<X").workplane(offset=0.75)
|
||||
|
||||
# 5. Creates a thin disc on the offset workplane that is floating near the box.
|
||||
result = result.circle(1.0).extrude(0.5)
|
||||
|
||||
# Displays the result of this script
|
||||
show_object(result)
|
||||
28
cadquerywrapper/examples/Ex015_Rotated_Workplanes.py
Normal file
28
cadquerywrapper/examples/Ex015_Rotated_Workplanes.py
Normal file
@@ -0,0 +1,28 @@
|
||||
import cadquery as cq
|
||||
|
||||
# 1. Establishes a workplane that an object can be built on.
|
||||
# 1a. Uses the named plane orientation "front" to define the workplane, meaning
|
||||
# that the positive Z direction is "up", and the negative Z direction
|
||||
# is "down".
|
||||
# 2. Creates a plain box to base future geometry on with the box() function.
|
||||
# 3. Selects the top-most Z face of the box.
|
||||
# 4. Creates a new workplane and then moves and rotates it with the
|
||||
# transformed function.
|
||||
# 5. Creates a for-construction rectangle that only exists to use for placing
|
||||
# other geometry.
|
||||
# 6. Selects the vertices of the for-construction rectangle.
|
||||
# 7. Places holes at the center of each selected vertex.
|
||||
# 7a. Since the workplane is rotated, this results in angled holes in the face.
|
||||
result = (
|
||||
cq.Workplane("front")
|
||||
.box(4.0, 4.0, 0.25)
|
||||
.faces(">Z")
|
||||
.workplane()
|
||||
.transformed(offset=(0, -1.5, 1.0), rotate=(60, 0, 0))
|
||||
.rect(1.5, 1.5, forConstruction=True)
|
||||
.vertices()
|
||||
.hole(0.25)
|
||||
)
|
||||
|
||||
# Displays the result of this script
|
||||
show_object(result)
|
||||
@@ -0,0 +1,26 @@
|
||||
import cadquery as cq
|
||||
|
||||
# Create a block with holes in each corner of a rectangle on that workplane.
|
||||
# 1. Establishes a workplane that an object can be built on.
|
||||
# 1a. Uses the named plane orientation "front" to define the workplane, meaning
|
||||
# that the positive Z direction is "up", and the negative Z direction
|
||||
# is "down".
|
||||
# 2. Creates a plain box to base future geometry on with the box() function.
|
||||
# 3. Selects the top-most Z face of the box.
|
||||
# 4. Creates a new workplane to build new geometry on.
|
||||
# 5. Creates a for-construction rectangle that only exists to use for placing
|
||||
# other geometry.
|
||||
# 6. Selects the vertices of the for-construction rectangle.
|
||||
# 7. Places holes at the center of each selected vertex.
|
||||
result = (
|
||||
cq.Workplane("front")
|
||||
.box(2, 2, 0.5)
|
||||
.faces(">Z")
|
||||
.workplane()
|
||||
.rect(1.5, 1.5, forConstruction=True)
|
||||
.vertices()
|
||||
.hole(0.125)
|
||||
)
|
||||
|
||||
# Displays the result of this script
|
||||
show_object(result)
|
||||
@@ -0,0 +1,14 @@
|
||||
import cadquery as cq
|
||||
|
||||
# Create a hollow box that's open on both ends with a thin wall.
|
||||
# 1. Establishes a workplane that an object can be built on.
|
||||
# 1a. Uses the named plane orientation "front" to define the workplane, meaning
|
||||
# that the positive Z direction is "up", and the negative Z direction
|
||||
# is "down".
|
||||
# 2. Creates a plain box to base future geometry on with the box() function.
|
||||
# 3. Selects faces with normal in +z direction.
|
||||
# 4. Create a shell by cutting out the top-most Z face.
|
||||
result = cq.Workplane("front").box(2, 2, 2).faces("+Z").shell(0.05)
|
||||
|
||||
# Displays the result of this script
|
||||
show_object(result)
|
||||
25
cadquerywrapper/examples/Ex018_Making_Lofts.py
Normal file
25
cadquerywrapper/examples/Ex018_Making_Lofts.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import cadquery as cq
|
||||
|
||||
# Create a lofted section between a rectangle and a circular section.
|
||||
# 1. Establishes a workplane that an object can be built on.
|
||||
# 1a. Uses the named plane orientation "front" to define the workplane, meaning
|
||||
# that the positive Z direction is "up", and the negative Z direction
|
||||
# is "down".
|
||||
# 2. Creates a plain box to base future geometry on with the box() function.
|
||||
# 3. Selects the top-most Z face of the box.
|
||||
# 4. Draws a 2D circle at the center of the the top-most face of the box.
|
||||
# 5. Creates a workplane 3 mm above the face the circle was drawn on.
|
||||
# 6. Draws a 2D circle on the new, offset workplane.
|
||||
# 7. Creates a loft between the circle and the rectangle.
|
||||
result = (
|
||||
cq.Workplane("front")
|
||||
.box(4.0, 4.0, 0.25)
|
||||
.faces(">Z")
|
||||
.circle(1.5)
|
||||
.workplane(offset=3.0)
|
||||
.rect(0.75, 0.5)
|
||||
.loft(combine=True)
|
||||
)
|
||||
|
||||
# Displays the result of this script
|
||||
show_object(result)
|
||||
25
cadquerywrapper/examples/Ex019_Counter_Sunk_Holes.py
Normal file
25
cadquerywrapper/examples/Ex019_Counter_Sunk_Holes.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import cadquery as cq
|
||||
|
||||
# Create a plate with 4 counter-sunk holes in it.
|
||||
# 1. Establishes a workplane using an XY object instead of a named plane.
|
||||
# 2. Creates a plain box to base future geometry on with the box() function.
|
||||
# 3. Selects the top-most face of the box and established a workplane on that.
|
||||
# 4. Draws a for-construction rectangle on the workplane which only exists for
|
||||
# placing other geometry.
|
||||
# 5. Selects the corner vertices of the rectangle and places a counter-sink
|
||||
# hole, using each vertex as the center of a hole using the cskHole()
|
||||
# function.
|
||||
# 5a. When the depth of the counter-sink hole is set to None, the hole will be
|
||||
# cut through.
|
||||
result = (
|
||||
cq.Workplane(cq.Plane.XY())
|
||||
.box(4, 2, 0.5)
|
||||
.faces(">Z")
|
||||
.workplane()
|
||||
.rect(3.5, 1.5, forConstruction=True)
|
||||
.vertices()
|
||||
.cskHole(0.125, 0.25, 82.0, depth=None)
|
||||
)
|
||||
|
||||
# Displays the result of this script
|
||||
show_object(result)
|
||||
@@ -0,0 +1,13 @@
|
||||
import cadquery as cq
|
||||
|
||||
# Create a plate with 4 rounded corners in the Z-axis.
|
||||
# 1. Establishes a workplane that an object can be built on.
|
||||
# 1a. Uses the X and Y origins to define the workplane, meaning that the
|
||||
# positive Z direction is "up", and the negative Z direction is "down".
|
||||
# 2. Creates a plain box to base future geometry on with the box() function.
|
||||
# 3. Selects all edges that are parallel to the Z axis.
|
||||
# 4. Creates fillets on each of the selected edges with the specified radius.
|
||||
result = cq.Workplane("XY").box(3, 3, 0.5).edges("|Z").fillet(0.125)
|
||||
|
||||
# Displays the result of this script
|
||||
show_object(result)
|
||||
24
cadquerywrapper/examples/Ex021_Splitting_an_Object.py
Normal file
24
cadquerywrapper/examples/Ex021_Splitting_an_Object.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import cadquery as cq
|
||||
|
||||
# Create a simple block with a hole through it that we can split.
|
||||
# 1. Establishes a workplane that an object can be built on.
|
||||
# 1a. Uses the X and Y origins to define the workplane, meaning that the
|
||||
# positive Z direction is "up", and the negative Z direction is "down".
|
||||
# 2. Creates a plain box to base future geometry on with the box() function.
|
||||
# 3. Selects the top-most face of the box and establishes a workplane on it
|
||||
# that new geometry can be built on.
|
||||
# 4. Draws a 2D circle on the new workplane and then uses it to cut a hole
|
||||
# all the way through the box.
|
||||
c = cq.Workplane("XY").box(1, 1, 1).faces(">Z").workplane().circle(0.25).cutThruAll()
|
||||
|
||||
# 5. Selects the face furthest away from the origin in the +Y axis direction.
|
||||
# 6. Creates an offset workplane that is set in the center of the object.
|
||||
# 6a. One possible improvement to this script would be to make the dimensions
|
||||
# of the box variables, and then divide the Y-axis dimension by 2.0 and
|
||||
# use that to create the offset workplane.
|
||||
# 7. Uses the embedded workplane to split the object, keeping only the "top"
|
||||
# portion.
|
||||
result = c.faces(">Y").workplane(-0.5).split(keepTop=True)
|
||||
|
||||
# Displays the result of this script
|
||||
show_object(result)
|
||||
21
cadquerywrapper/examples/Ex022_Revolution.py
Normal file
21
cadquerywrapper/examples/Ex022_Revolution.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import cadquery as cq
|
||||
|
||||
# The dimensions of the model. These can be modified rather than changing the
|
||||
# shape's code directly.
|
||||
rectangle_width = 10.0
|
||||
rectangle_length = 10.0
|
||||
angle_degrees = 360.0
|
||||
|
||||
# Revolve a cylinder from a rectangle
|
||||
# Switch comments around in this section to try the revolve operation with different parameters
|
||||
result = cq.Workplane("XY").rect(rectangle_width, rectangle_length, False).revolve()
|
||||
# result = cq.Workplane("XY").rect(rectangle_width, rectangle_length, False).revolve(angle_degrees)
|
||||
# result = cq.Workplane("XY").rect(rectangle_width, rectangle_length).revolve(angle_degrees,(-5,-5))
|
||||
# result = cq.Workplane("XY").rect(rectangle_width, rectangle_length).revolve(angle_degrees,(-5, -5),(-5, 5))
|
||||
# result = cq.Workplane("XY").rect(rectangle_width, rectangle_length).revolve(angle_degrees,(-5,-5),(-5,5), False)
|
||||
|
||||
# Revolve a donut with square walls
|
||||
# result = cq.Workplane("XY").rect(rectangle_width, rectangle_length, True).revolve(angle_degrees, (20, 0), (20, 10))
|
||||
|
||||
# Displays the result of this script
|
||||
show_object(result)
|
||||
36
cadquerywrapper/examples/Ex023_Sweep.py
Normal file
36
cadquerywrapper/examples/Ex023_Sweep.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import cadquery as cq
|
||||
|
||||
# Points we will use to create spline and polyline paths to sweep over
|
||||
pts = [(0, 1), (1, 2), (2, 4)]
|
||||
|
||||
# Spline path generated from our list of points (tuples)
|
||||
path = cq.Workplane("XZ").spline(pts)
|
||||
|
||||
# Sweep a circle with a diameter of 1.0 units along the spline path we just created
|
||||
defaultSweep = cq.Workplane("XY").circle(1.0).sweep(path)
|
||||
|
||||
# Sweep defaults to making a solid and not generating a Frenet solid. Setting Frenet to True helps prevent creep in
|
||||
# the orientation of the profile as it is being swept
|
||||
frenetShell = cq.Workplane("XY").circle(1.0).sweep(path, makeSolid=True, isFrenet=True)
|
||||
|
||||
# We can sweep shapes other than circles
|
||||
defaultRect = cq.Workplane("XY").rect(1.0, 1.0).sweep(path)
|
||||
|
||||
# Switch to a polyline path, but have it use the same points as the spline
|
||||
path = cq.Workplane("XZ").polyline(pts, includeCurrent=True)
|
||||
|
||||
# Using a polyline path leads to the resulting solid having segments rather than a single swept outer face
|
||||
plineSweep = cq.Workplane("XY").circle(1.0).sweep(path)
|
||||
|
||||
# Switch to an arc for the path
|
||||
path = cq.Workplane("XZ").threePointArc((1.0, 1.5), (0.0, 1.0))
|
||||
|
||||
# Use a smaller circle section so that the resulting solid looks a little nicer
|
||||
arcSweep = cq.Workplane("XY").circle(0.5).sweep(path)
|
||||
|
||||
# Translate the resulting solids so that they do not overlap and display them left to right
|
||||
show_object(defaultSweep)
|
||||
show_object(frenetShell.translate((5, 0, 0)))
|
||||
show_object(defaultRect.translate((10, 0, 0)))
|
||||
show_object(plineSweep)
|
||||
show_object(arcSweep.translate((20, 0, 0)))
|
||||
@@ -0,0 +1,88 @@
|
||||
import cadquery as cq
|
||||
|
||||
# X axis line length 20.0
|
||||
path = cq.Workplane("XZ").moveTo(-10, 0).lineTo(10, 0)
|
||||
|
||||
# Sweep a circle from diameter 2.0 to diameter 1.0 to diameter 2.0 along X axis length 10.0 + 10.0
|
||||
defaultSweep = (
|
||||
cq.Workplane("YZ")
|
||||
.workplane(offset=-10.0)
|
||||
.circle(2.0)
|
||||
.workplane(offset=10.0)
|
||||
.circle(1.0)
|
||||
.workplane(offset=10.0)
|
||||
.circle(2.0)
|
||||
.sweep(path, multisection=True)
|
||||
)
|
||||
|
||||
# We can sweep through different shapes
|
||||
recttocircleSweep = (
|
||||
cq.Workplane("YZ")
|
||||
.workplane(offset=-10.0)
|
||||
.rect(2.0, 2.0)
|
||||
.workplane(offset=8.0)
|
||||
.circle(1.0)
|
||||
.workplane(offset=4.0)
|
||||
.circle(1.0)
|
||||
.workplane(offset=8.0)
|
||||
.rect(2.0, 2.0)
|
||||
.sweep(path, multisection=True)
|
||||
)
|
||||
|
||||
circletorectSweep = (
|
||||
cq.Workplane("YZ")
|
||||
.workplane(offset=-10.0)
|
||||
.circle(1.0)
|
||||
.workplane(offset=7.0)
|
||||
.rect(2.0, 2.0)
|
||||
.workplane(offset=6.0)
|
||||
.rect(2.0, 2.0)
|
||||
.workplane(offset=7.0)
|
||||
.circle(1.0)
|
||||
.sweep(path, multisection=True)
|
||||
)
|
||||
|
||||
|
||||
# Placement of the Shape is important otherwise could produce unexpected shape
|
||||
specialSweep = (
|
||||
cq.Workplane("YZ")
|
||||
.circle(1.0)
|
||||
.workplane(offset=10.0)
|
||||
.rect(2.0, 2.0)
|
||||
.sweep(path, multisection=True)
|
||||
)
|
||||
|
||||
# Switch to an arc for the path : line l=5.0 then half circle r=4.0 then line l=5.0
|
||||
path = (
|
||||
cq.Workplane("XZ")
|
||||
.moveTo(-5, 4)
|
||||
.lineTo(0, 4)
|
||||
.threePointArc((4, 0), (0, -4))
|
||||
.lineTo(-5, -4)
|
||||
)
|
||||
|
||||
# Placement of different shapes should follow the path
|
||||
# cylinder r=1.5 along first line
|
||||
# then sweep along arc from r=1.5 to r=1.0
|
||||
# then cylinder r=1.0 along last line
|
||||
arcSweep = (
|
||||
cq.Workplane("YZ")
|
||||
.workplane(offset=-5)
|
||||
.moveTo(0, 4)
|
||||
.circle(1.5)
|
||||
.workplane(offset=5, centerOption="CenterOfMass")
|
||||
.circle(1.5)
|
||||
.moveTo(0, -8)
|
||||
.circle(1.0)
|
||||
.workplane(offset=-5, centerOption="CenterOfMass")
|
||||
.circle(1.0)
|
||||
.sweep(path, multisection=True)
|
||||
)
|
||||
|
||||
|
||||
# Translate the resulting solids so that they do not overlap and display them left to right
|
||||
show_object(defaultSweep)
|
||||
show_object(circletorectSweep.translate((0, 5, 0)))
|
||||
show_object(recttocircleSweep.translate((0, 10, 0)))
|
||||
show_object(specialSweep.translate((0, 15, 0)))
|
||||
show_object(arcSweep.translate((0, -5, 0)))
|
||||
20
cadquerywrapper/examples/Ex025_Swept_Helix.py
Normal file
20
cadquerywrapper/examples/Ex025_Swept_Helix.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import cadquery as cq
|
||||
|
||||
r = 0.5 # Radius of the helix
|
||||
p = 0.4 # Pitch of the helix - vertical distance between loops
|
||||
h = 2.4 # Height of the helix - total height
|
||||
|
||||
# Helix
|
||||
wire = cq.Wire.makeHelix(pitch=p, height=h, radius=r)
|
||||
helix = cq.Workplane(obj=wire)
|
||||
|
||||
# Final result: A 2D shape swept along a helix.
|
||||
result = (
|
||||
cq.Workplane("XZ") # helix is moving up the Z axis
|
||||
.center(r, 0) # offset isosceles trapezoid
|
||||
.polyline(((-0.15, 0.1), (0.0, 0.05), (0, 0.35), (-0.15, 0.3)))
|
||||
.close() # make edges a wire
|
||||
.sweep(helix, isFrenet=True) # Frenet keeps orientation as expected
|
||||
)
|
||||
|
||||
show_object(result)
|
||||
47
cadquerywrapper/examples/Ex026_Case_Seam_Lip.py
Normal file
47
cadquerywrapper/examples/Ex026_Case_Seam_Lip.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import cadquery as cq
|
||||
from cadquery.selectors import AreaNthSelector
|
||||
|
||||
case_bottom = (
|
||||
cq.Workplane("XY")
|
||||
.rect(20, 20)
|
||||
.extrude(10) # solid 20x20x10 box
|
||||
.edges("|Z or <Z")
|
||||
.fillet(2) # rounding all edges except 4 edges of the top face
|
||||
.faces(">Z")
|
||||
.shell(2) # shell of thickness 2 with top face open
|
||||
.faces(">Z")
|
||||
.wires(AreaNthSelector(-1)) # selecting top outer wire
|
||||
.toPending()
|
||||
.workplane()
|
||||
.offset2D(-1) # creating centerline wire of case seam face
|
||||
.extrude(1) # covering the sell with temporary "lid"
|
||||
.faces(">Z[-2]")
|
||||
.wires(AreaNthSelector(0)) # selecting case crossection wire
|
||||
.toPending()
|
||||
.workplane()
|
||||
.cutBlind(2) # cutting through the "lid" leaving a lip on case seam surface
|
||||
)
|
||||
|
||||
# similar process repeated for the top part
|
||||
# but instead of "growing" an inner lip
|
||||
# material is removed inside case seam centerline
|
||||
# to create an outer lip
|
||||
case_top = (
|
||||
cq.Workplane("XY")
|
||||
.move(25)
|
||||
.rect(20, 20)
|
||||
.extrude(5)
|
||||
.edges("|Z or >Z")
|
||||
.fillet(2)
|
||||
.faces("<Z")
|
||||
.shell(2)
|
||||
.faces("<Z")
|
||||
.wires(AreaNthSelector(-1))
|
||||
.toPending()
|
||||
.workplane()
|
||||
.offset2D(-1)
|
||||
.cutBlind(-1)
|
||||
)
|
||||
|
||||
show_object(case_bottom)
|
||||
show_object(case_top, options={"alpha": 0.5})
|
||||
70
cadquerywrapper/examples/Ex100_Lego_Brick.py
Normal file
70
cadquerywrapper/examples/Ex100_Lego_Brick.py
Normal file
@@ -0,0 +1,70 @@
|
||||
# This script can create any regular rectangular Lego(TM) Brick
|
||||
import cadquery as cq
|
||||
|
||||
#####
|
||||
# Inputs
|
||||
######
|
||||
lbumps = 1 # number of bumps long
|
||||
wbumps = 1 # number of bumps wide
|
||||
thin = True # True for thin, False for thick
|
||||
|
||||
#
|
||||
# Lego Brick Constants-- these make a lego brick a lego :)
|
||||
#
|
||||
pitch = 8.0
|
||||
clearance = 0.1
|
||||
bumpDiam = 4.8
|
||||
bumpHeight = 1.8
|
||||
if thin:
|
||||
height = 3.2
|
||||
else:
|
||||
height = 9.6
|
||||
|
||||
t = (pitch - (2 * clearance) - bumpDiam) / 2.0
|
||||
postDiam = pitch - t # works out to 6.5
|
||||
total_length = lbumps * pitch - 2.0 * clearance
|
||||
total_width = wbumps * pitch - 2.0 * clearance
|
||||
|
||||
# make the base
|
||||
s = cq.Workplane("XY").box(total_length, total_width, height)
|
||||
|
||||
# shell inwards not outwards
|
||||
s = s.faces("<Z").shell(-1.0 * t)
|
||||
|
||||
# make the bumps on the top
|
||||
s = (
|
||||
s.faces(">Z")
|
||||
.workplane()
|
||||
.rarray(pitch, pitch, lbumps, wbumps, True)
|
||||
.circle(bumpDiam / 2.0)
|
||||
.extrude(bumpHeight)
|
||||
)
|
||||
|
||||
# add posts on the bottom. posts are different diameter depending on geometry
|
||||
# solid studs for 1 bump, tubes for multiple, none for 1x1
|
||||
tmp = s.faces("<Z").workplane(invert=True)
|
||||
|
||||
if lbumps > 1 and wbumps > 1:
|
||||
tmp = (
|
||||
tmp.rarray(pitch, pitch, lbumps - 1, wbumps - 1, center=True)
|
||||
.circle(postDiam / 2.0)
|
||||
.circle(bumpDiam / 2.0)
|
||||
.extrude(height - t)
|
||||
)
|
||||
elif lbumps > 1:
|
||||
tmp = (
|
||||
tmp.rarray(pitch, pitch, lbumps - 1, 1, center=True)
|
||||
.circle(t)
|
||||
.extrude(height - t)
|
||||
)
|
||||
elif wbumps > 1:
|
||||
tmp = (
|
||||
tmp.rarray(pitch, pitch, 1, wbumps - 1, center=True)
|
||||
.circle(t)
|
||||
.extrude(height - t)
|
||||
)
|
||||
else:
|
||||
tmp = s
|
||||
|
||||
# Render the solid
|
||||
show_object(tmp)
|
||||
177
cadquerywrapper/examples/Ex101_InterpPlate.py
Normal file
177
cadquerywrapper/examples/Ex101_InterpPlate.py
Normal file
@@ -0,0 +1,177 @@
|
||||
from math import sin, cos, pi, sqrt
|
||||
import cadquery as cq
|
||||
|
||||
# TEST_1
|
||||
# example from PythonOCC core_geometry_geomplate.py, use of thickness = 0 returns 2D surface.
|
||||
thickness = 0
|
||||
edge_points = [(0.0, 0.0, 0.0), (0.0, 10.0, 0.0), (0.0, 10.0, 10.0), (0.0, 0.0, 10.0)]
|
||||
surface_points = [(5.0, 5.0, 5.0)]
|
||||
plate_0 = cq.Workplane("XY").interpPlate(edge_points, surface_points, thickness)
|
||||
print("plate_0.val().Volume() = ", plate_0.val().Volume())
|
||||
plate_0 = plate_0.translate((0, 6 * 12, 0))
|
||||
show_object(plate_0)
|
||||
|
||||
# EXAMPLE 1
|
||||
# Plate with 5 sides and 2 bumps, one side is not co-planar with the other sides
|
||||
thickness = 0.1
|
||||
edge_points = [
|
||||
(-7.0, -7.0, 0.0),
|
||||
(-3.0, -10.0, 3.0),
|
||||
(7.0, -7.0, 0.0),
|
||||
(7.0, 7.0, 0.0),
|
||||
(-7.0, 7.0, 0.0),
|
||||
]
|
||||
edge_wire = cq.Workplane("XY").polyline(
|
||||
[(-7.0, -7.0), (7.0, -7.0), (7.0, 7.0), (-7.0, 7.0)]
|
||||
)
|
||||
# edge_wire = edge_wire.add(cq.Workplane("YZ").workplane().transformed(offset=cq.Vector(0, 0, -7), rotate=cq.Vector(45, 0, 0)).polyline([(-7.,0.), (3,-3), (7.,0.)]))
|
||||
# In CadQuery Sept-2019 it worked with rotate=cq.Vector(0, 45, 0). In CadQuery Dec-2019 rotate=cq.Vector(45, 0, 0) only closes the wire.
|
||||
edge_wire = edge_wire.add(
|
||||
cq.Workplane("YZ")
|
||||
.workplane()
|
||||
.transformed(offset=cq.Vector(0, 0, -7), rotate=cq.Vector(45, 0, 0))
|
||||
.spline([(-7.0, 0.0), (3, -3), (7.0, 0.0)])
|
||||
)
|
||||
surface_points = [(-3.0, -3.0, -3.0), (3.0, 3.0, 3.0)]
|
||||
plate_1 = cq.Workplane("XY").interpPlate(edge_wire, surface_points, thickness)
|
||||
# plate_1 = cq.Workplane("XY").interpPlate(edge_points, surface_points, thickness) # list of (x,y,z) points instead of wires for edges
|
||||
print("plate_1.val().Volume() = ", plate_1.val().Volume())
|
||||
show_object(plate_1)
|
||||
|
||||
# EXAMPLE 2
|
||||
# Embossed star, need to change optional parameters to obtain nice looking result.
|
||||
r1 = 3.0
|
||||
r2 = 10.0
|
||||
fn = 6
|
||||
thickness = 0.1
|
||||
edge_points = [
|
||||
(
|
||||
(r1 * cos(i * pi / fn), r1 * sin(i * pi / fn))
|
||||
if i % 2 == 0
|
||||
else (r2 * cos(i * pi / fn), r2 * sin(i * pi / fn))
|
||||
)
|
||||
for i in range(2 * fn + 1)
|
||||
]
|
||||
edge_wire = cq.Workplane("XY").polyline(edge_points)
|
||||
r2 = 4.5
|
||||
surface_points = [
|
||||
(r2 * cos(i * pi / fn), r2 * sin(i * pi / fn), 1.0) for i in range(2 * fn)
|
||||
] + [(0.0, 0.0, -2.0)]
|
||||
plate_2 = cq.Workplane("XY").interpPlate(
|
||||
edge_wire,
|
||||
surface_points,
|
||||
thickness,
|
||||
combine=True,
|
||||
clean=True,
|
||||
degree=3,
|
||||
nbPtsOnCur=15,
|
||||
nbIter=2,
|
||||
anisotropy=False,
|
||||
tol2d=0.00001,
|
||||
tol3d=0.0001,
|
||||
tolAng=0.01,
|
||||
tolCurv=0.1,
|
||||
maxDeg=8,
|
||||
maxSegments=49,
|
||||
)
|
||||
# plate_2 = cq.Workplane("XY").interpPlate(edge_points, surface_points, thickness, combine=True, clean=True, Degree=3, NbPtsOnCur=15, NbIter=2, Anisotropie=False, Tol2d=0.00001, Tol3d=0.0001, TolAng=0.01, TolCurv=0.1, MaxDeg=8, MaxSegments=49) # list of (x,y,z) points instead of wires for edges
|
||||
print("plate_2.val().Volume() = ", plate_2.val().Volume())
|
||||
plate_2 = plate_2.translate((0, 2 * 12, 0))
|
||||
show_object(plate_2)
|
||||
|
||||
# EXAMPLE 3
|
||||
# Points on hexagonal pattern coordinates, use of pushpoints.
|
||||
r1 = 1.0
|
||||
N = 3
|
||||
ca = cos(30.0 * pi / 180.0)
|
||||
sa = sin(30.0 * pi / 180.0)
|
||||
# EVEN ROWS
|
||||
pts = [
|
||||
(-3.0, -3.0),
|
||||
(-1.267949, -3.0),
|
||||
(0.464102, -3.0),
|
||||
(2.196152, -3.0),
|
||||
(-3.0, 0.0),
|
||||
(-1.267949, 0.0),
|
||||
(0.464102, 0.0),
|
||||
(2.196152, 0.0),
|
||||
(-2.133974, -1.5),
|
||||
(-0.401923, -1.5),
|
||||
(1.330127, -1.5),
|
||||
(3.062178, -1.5),
|
||||
(-2.133975, 1.5),
|
||||
(-0.401924, 1.5),
|
||||
(1.330127, 1.5),
|
||||
(3.062178, 1.5),
|
||||
]
|
||||
# Spike surface
|
||||
thickness = 0.1
|
||||
fn = 6
|
||||
edge_points = [
|
||||
(
|
||||
r1 * cos(i * 2 * pi / fn + 30 * pi / 180),
|
||||
r1 * sin(i * 2 * pi / fn + 30 * pi / 180),
|
||||
)
|
||||
for i in range(fn + 1)
|
||||
]
|
||||
surface_points = [
|
||||
(
|
||||
r1 / 4 * cos(i * 2 * pi / fn + 30 * pi / 180),
|
||||
r1 / 4 * sin(i * 2 * pi / fn + 30 * pi / 180),
|
||||
0.75,
|
||||
)
|
||||
for i in range(fn + 1)
|
||||
] + [(0, 0, 2)]
|
||||
edge_wire = cq.Workplane("XY").polyline(edge_points)
|
||||
plate_3 = (
|
||||
cq.Workplane("XY")
|
||||
.pushPoints(pts)
|
||||
.interpPlate(
|
||||
edge_wire,
|
||||
surface_points,
|
||||
thickness,
|
||||
combine=False,
|
||||
clean=False,
|
||||
degree=2,
|
||||
nbPtsOnCur=20,
|
||||
nbIter=2,
|
||||
anisotropy=False,
|
||||
tol2d=0.00001,
|
||||
tol3d=0.0001,
|
||||
tolAng=0.01,
|
||||
tolCurv=0.1,
|
||||
maxDeg=8,
|
||||
maxSegments=9,
|
||||
)
|
||||
)
|
||||
print("plate_3.val().Volume() = ", plate_3.val().Volume())
|
||||
plate_3 = plate_3.translate((0, 4 * 11, 0))
|
||||
show_object(plate_3)
|
||||
|
||||
# EXAMPLE 4
|
||||
# Gyroïd, all edges are splines on different workplanes.
|
||||
thickness = 0.1
|
||||
edge_points = [
|
||||
[[3.54, 3.54], [1.77, 0.0], [3.54, -3.54]],
|
||||
[[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]],
|
||||
[[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]],
|
||||
[[-3.54, -3.54], [-1.77, 0.0], [-3.54, 3.54]],
|
||||
[[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]],
|
||||
[[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]],
|
||||
]
|
||||
plane_list = ["XZ", "XY", "YZ", "XZ", "YZ", "XY"]
|
||||
offset_list = [-3.54, 3.54, 3.54, 3.54, -3.54, -3.54]
|
||||
edge_wire = (
|
||||
cq.Workplane(plane_list[0]).workplane(offset=-offset_list[0]).spline(edge_points[0])
|
||||
)
|
||||
for i in range(len(edge_points) - 1):
|
||||
edge_wire = edge_wire.add(
|
||||
cq.Workplane(plane_list[i + 1])
|
||||
.workplane(offset=-offset_list[i + 1])
|
||||
.spline(edge_points[i + 1])
|
||||
)
|
||||
surface_points = [(0, 0, 0)]
|
||||
plate_4 = cq.Workplane("XY").interpPlate(edge_wire, surface_points, thickness)
|
||||
print("plate_4.val().Volume() = ", plate_4.val().Volume())
|
||||
plate_4 = plate_4.translate((0, 5 * 12, 0))
|
||||
show_object(plate_4)
|
||||
29
cadquerywrapper/install_python.bat
Normal file
29
cadquerywrapper/install_python.bat
Normal file
@@ -0,0 +1,29 @@
|
||||
@echo off
|
||||
where python >NUL 2>NUL
|
||||
if %ERRORLEVEL%==0 (
|
||||
echo Python is already installed.
|
||||
exit /B 0
|
||||
)
|
||||
|
||||
echo Python not found. Attempting installation...
|
||||
where choco >NUL 2>NUL
|
||||
if %ERRORLEVEL%==0 (
|
||||
choco install -y python
|
||||
) else (
|
||||
where winget >NUL 2>NUL
|
||||
if %ERRORLEVEL%==0 (
|
||||
winget install -e --id Python.Python.3
|
||||
) else (
|
||||
echo No supported package manager found. Please install Python manually.
|
||||
exit /B 1
|
||||
)
|
||||
)
|
||||
|
||||
where python >NUL 2>NUL
|
||||
if %ERRORLEVEL%==0 (
|
||||
echo Python installed successfully.
|
||||
exit /B 0
|
||||
) else (
|
||||
echo Python installation failed. Please install manually.
|
||||
exit /B 1
|
||||
)
|
||||
24
cadquerywrapper/install_python.sh
Executable file
24
cadquerywrapper/install_python.sh
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
echo "Python is already installed." && exit 0
|
||||
fi
|
||||
|
||||
echo "Python not found. Attempting installation..."
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
sudo apt-get update && sudo apt-get install -y python3 python3-pip
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
sudo yum install -y python3 python3-pip
|
||||
elif command -v brew >/dev/null 2>&1; then
|
||||
brew install python
|
||||
else
|
||||
echo "Unsupported package manager. Please install Python manually." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v python >/dev/null 2>&1 && command -v python3 >/dev/null 2>&1; then
|
||||
sudo ln -s "$(command -v python3)" /usr/local/bin/python || true
|
||||
fi
|
||||
|
||||
echo "Python installed successfully."
|
||||
2
cadquerywrapper/requirements-dev.txt
Normal file
2
cadquerywrapper/requirements-dev.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
-r requirements.txt
|
||||
pytest>=8
|
||||
2
cadquerywrapper/requirements.txt
Normal file
2
cadquerywrapper/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
cadquery>=2.5
|
||||
trimesh>=4
|
||||
20
cadquerywrapper/setup.sh
Executable file
20
cadquerywrapper/setup.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
case "$(uname)" in
|
||||
Linux*) script="setup_linux.sh" ;;
|
||||
Darwin*) script="setup_mac.sh" ;;
|
||||
MINGW*|MSYS*|CYGWIN*) script="setup_windows.ps1" ;;
|
||||
*) echo "Unsupported OS: $(uname)" && exit 1 ;;
|
||||
esac
|
||||
|
||||
if [ "$script" = "setup_windows.ps1" ]; then
|
||||
if command -v pwsh >/dev/null 2>&1; then
|
||||
pwsh "$script"
|
||||
else
|
||||
powershell.exe -ExecutionPolicy Bypass -File "$script"
|
||||
fi
|
||||
else
|
||||
bash "$script"
|
||||
fi
|
||||
|
||||
35
cadquerywrapper/setup_linux.sh
Executable file
35
cadquerywrapper/setup_linux.sh
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "🔍 Checking Python 3.11+..."
|
||||
if ! python3 --version | grep -q "3.11"; then
|
||||
echo "Installing Python 3.11..."
|
||||
if command -v apt &>/dev/null; then
|
||||
sudo apt update
|
||||
sudo apt install -y python3.11 python3.11-venv python3.11-dev
|
||||
sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1
|
||||
elif command -v dnf &>/dev/null; then
|
||||
sudo dnf install -y python3.11
|
||||
elif command -v pacman &>/dev/null; then
|
||||
sudo pacman -S --noconfirm python
|
||||
else
|
||||
echo "❌ Unsupported Linux package manager."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "✅ Python version: $(python3 --version)"
|
||||
|
||||
echo "🔍 Checking for Poetry..."
|
||||
if ! command -v poetry &>/dev/null; then
|
||||
echo "📦 Installing Poetry..."
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
else
|
||||
echo "✅ Poetry is installed: $(poetry --version)"
|
||||
fi
|
||||
|
||||
echo "📦 Installing dependencies..."
|
||||
poetry install
|
||||
|
||||
echo "✅ Linux setup complete. Use 'poetry shell' to activate environment."
|
||||
30
cadquerywrapper/setup_mac.sh
Executable file
30
cadquerywrapper/setup_mac.sh
Executable file
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "🔍 Checking Python 3.11+..."
|
||||
if ! python3 --version | grep -q "3.11"; then
|
||||
echo "Installing Python 3.11 using Homebrew..."
|
||||
if ! command -v brew &>/dev/null; then
|
||||
echo "📦 Installing Homebrew..."
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
export PATH="/opt/homebrew/bin:$PATH"
|
||||
fi
|
||||
brew install python@3.11
|
||||
brew link python@3.11 --force
|
||||
fi
|
||||
|
||||
echo "✅ Python version: $(python3 --version)"
|
||||
|
||||
echo "🔍 Checking for Poetry..."
|
||||
if ! command -v poetry &>/dev/null; then
|
||||
echo "📦 Installing Poetry..."
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
else
|
||||
echo "✅ Poetry is installed: $(poetry --version)"
|
||||
fi
|
||||
|
||||
echo "📦 Installing dependencies..."
|
||||
poetry install
|
||||
|
||||
echo "✅ macOS setup complete. Use 'poetry shell' to activate environment."
|
||||
35
cadquerywrapper/setup_windows.ps1
Executable file
35
cadquerywrapper/setup_windows.ps1
Executable file
@@ -0,0 +1,35 @@
|
||||
# PowerShell script
|
||||
Write-Host "🔍 Checking for Chocolatey..."
|
||||
if (-not (Get-Command choco -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "📦 Installing Chocolatey..."
|
||||
Set-ExecutionPolicy Bypass -Scope Process -Force
|
||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
|
||||
Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
|
||||
}
|
||||
|
||||
Write-Host "📦 Updating Chocolatey..."
|
||||
choco upgrade chocolatey -y
|
||||
|
||||
Write-Host "📦 Installing build tools..."
|
||||
choco install python --version=3.11.5 -y
|
||||
choco install visualstudio2022buildtools --package-parameters '--add Microsoft.VisualStudio.Workload.VCTools --includeRecommended' -y
|
||||
choco install windows-sdk-10.0 -y
|
||||
choco install cmake --installargs 'ADD_CMAKE_TO_PATH=System' -y
|
||||
choco install ninja -y
|
||||
choco install make -y
|
||||
choco install git -y
|
||||
choco install openssl.light -y
|
||||
|
||||
Write-Host "✅ Python version: $(python --version)"
|
||||
|
||||
Write-Host "🔍 Checking for Poetry..."
|
||||
if (-not (Get-Command poetry -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "📦 Installing Poetry..."
|
||||
(Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | python -
|
||||
$env:Path += ';' + $env:USERPROFILE + '\.poetry\bin'
|
||||
}
|
||||
|
||||
Write-Host "📦 Installing dependencies..."
|
||||
poetry install
|
||||
|
||||
Write-Host "✅ Windows setup complete. Run 'poetry shell' to activate environment."
|
||||
331
cadquerywrapper/tests/test_validator.py
Normal file
331
cadquerywrapper/tests/test_validator.py
Normal file
@@ -0,0 +1,331 @@
|
||||
import importlib
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path
|
||||
|
||||
import trimesh
|
||||
|
||||
import pytest
|
||||
import math
|
||||
|
||||
# Provide stub cadquery module before importing package modules
|
||||
_dummy_cq = types.ModuleType("cadquery")
|
||||
|
||||
_dummy_cq.export_calls = []
|
||||
|
||||
|
||||
def _export(obj, *args, **kwargs):
|
||||
_dummy_cq.export_calls.append((obj, args, kwargs))
|
||||
|
||||
|
||||
_dummy_cq.export = _export
|
||||
_dummy_cq.exporters = types.SimpleNamespace()
|
||||
_dummy_cq.exporters.calls = []
|
||||
|
||||
|
||||
def _exporter(obj, *args, **kwargs):
|
||||
_dummy_cq.exporters.calls.append((obj, args, kwargs))
|
||||
|
||||
|
||||
_dummy_cq.exporters.export = _exporter
|
||||
|
||||
|
||||
class DummyShape:
|
||||
def __init__(self):
|
||||
self.called = []
|
||||
self.valid = True
|
||||
self.closed = True
|
||||
self._open_edges = False
|
||||
self.will_intersect = False
|
||||
|
||||
def exportStl(self, *args, **kwargs):
|
||||
self.called.append(("exportStl", args, kwargs))
|
||||
|
||||
def exportStep(self, *args, **kwargs):
|
||||
self.called.append(("exportStep", args, kwargs))
|
||||
|
||||
def exportBin(self, *args, **kwargs):
|
||||
self.called.append(("exportBin", args, kwargs))
|
||||
|
||||
def exportBrep(self, *args, **kwargs):
|
||||
self.called.append(("exportBrep", args, kwargs))
|
||||
|
||||
def isValid(self):
|
||||
return self.valid
|
||||
|
||||
def isClosed(self):
|
||||
return self.closed
|
||||
|
||||
def hasOpenEdges(self):
|
||||
return self._open_edges
|
||||
|
||||
class _IntersectResult:
|
||||
def __init__(self, has_volume: bool):
|
||||
self._has_volume = has_volume
|
||||
|
||||
def isNull(self):
|
||||
return not self._has_volume
|
||||
|
||||
def Volume(self):
|
||||
return 1 if self._has_volume else 0
|
||||
|
||||
def intersect(self, other):
|
||||
has_vol = self.will_intersect and getattr(other, "will_intersect", False)
|
||||
return self._IntersectResult(has_vol)
|
||||
|
||||
|
||||
class DummyAssembly:
|
||||
def __init__(self, solids=None):
|
||||
self.called = []
|
||||
self._solids = solids or []
|
||||
|
||||
def export(self, *args, **kwargs):
|
||||
self.called.append(("export", args, kwargs))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.called.append(("save", args, kwargs))
|
||||
|
||||
def solids(self):
|
||||
return self._solids
|
||||
|
||||
|
||||
_dummy_cq.Shape = DummyShape
|
||||
_dummy_cq.Assembly = DummyAssembly
|
||||
|
||||
|
||||
class DummyBBoxShape(DummyShape):
|
||||
def __init__(self, x: float, y: float, z: float):
|
||||
super().__init__()
|
||||
self._bbox = types.SimpleNamespace(xlen=x, ylen=y, zlen=z)
|
||||
|
||||
def val(self):
|
||||
return self
|
||||
|
||||
def BoundingBox(self):
|
||||
return self._bbox
|
||||
|
||||
|
||||
class SphereShape(DummyShape):
|
||||
def __init__(self, subdivisions: int):
|
||||
super().__init__()
|
||||
self.subdivisions = subdivisions
|
||||
|
||||
def exportStl(self, file_name: str, *args, **kwargs):
|
||||
super().exportStl(file_name, *args, **kwargs)
|
||||
mesh = trimesh.creation.icosphere(subdivisions=self.subdivisions)
|
||||
mesh.export(file_name)
|
||||
|
||||
|
||||
class NonManifoldShape(DummyShape):
|
||||
def __init__(self, valid: bool = True, closed: bool = True):
|
||||
super().__init__()
|
||||
self.valid = valid
|
||||
self.closed = closed
|
||||
|
||||
|
||||
class OpenEdgeShape(DummyShape):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._open_edges = True
|
||||
|
||||
|
||||
class IntersectSolid(DummyShape):
|
||||
def __init__(self, will_intersect: bool = True):
|
||||
super().__init__()
|
||||
self.will_intersect = will_intersect
|
||||
|
||||
|
||||
class ClearanceSolid(DummyShape):
|
||||
def __init__(self, distance: float):
|
||||
super().__init__()
|
||||
self.distance = distance
|
||||
|
||||
def distTo(self, other): # noqa: D401 - simple distance stub
|
||||
return self.distance
|
||||
|
||||
|
||||
class _OverhangFace:
|
||||
def __init__(self, angle: float):
|
||||
self._angle = angle
|
||||
|
||||
def normalAt(self):
|
||||
rad = math.radians(self._angle)
|
||||
return (math.sin(rad), 0.0, math.cos(rad))
|
||||
|
||||
|
||||
class OverhangShape(DummyShape):
|
||||
def __init__(self, angles: list[float]):
|
||||
super().__init__()
|
||||
self._angles = angles
|
||||
|
||||
def faces(self): # noqa: D401 - returns dummy faces
|
||||
return [_OverhangFace(a) for a in self._angles]
|
||||
|
||||
|
||||
sys.modules.setdefault("cadquery", _dummy_cq)
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from cadquerywrapper.validator import Validator, load_rules, validate
|
||||
from cadquerywrapper.save_validator import SaveValidator, ValidationError
|
||||
from cadquerywrapper.project import CadQueryWrapper
|
||||
|
||||
|
||||
RULES_PATH = Path("cadquerywrapper/rules/bambu_printability_rules.json")
|
||||
|
||||
|
||||
def test_load_rules():
|
||||
rules = load_rules(RULES_PATH)
|
||||
assert rules["printer"] == "Bambu Labs"
|
||||
|
||||
|
||||
def test_validate_errors():
|
||||
rules = {"rules": {"minimum_wall_thickness_mm": 0.8}}
|
||||
model = {"minimum_wall_thickness_mm": 0.5}
|
||||
errors = validate(model, rules)
|
||||
assert errors == ["Minimum wall thickness mm 0.5 is below minimum 0.8"]
|
||||
|
||||
|
||||
def test_validator_from_file():
|
||||
validator = Validator(RULES_PATH)
|
||||
assert "minimum_wall_thickness_mm" in validator.rules["rules"]
|
||||
|
||||
|
||||
def test_validator_validate_raises():
|
||||
validator = Validator({"rules": {"minimum_wall_thickness_mm": 0.8}})
|
||||
with pytest.raises(ValidationError):
|
||||
validator.validate({"minimum_wall_thickness_mm": 0.5})
|
||||
|
||||
|
||||
def test_save_validator_delegates_and_validates():
|
||||
obj = DummyShape()
|
||||
sv = SaveValidator(RULES_PATH, obj)
|
||||
sv.export(obj)
|
||||
assert _dummy_cq.exporters.calls[-1][0] is obj
|
||||
|
||||
|
||||
def test_save_validator_invalid_raises():
|
||||
sv = SaveValidator(RULES_PATH)
|
||||
obj = DummyShape()
|
||||
SaveValidator.attach_model(obj, {"minimum_wall_thickness_mm": 0.1})
|
||||
with pytest.raises(ValidationError):
|
||||
sv.export_stl(obj, "out.stl")
|
||||
|
||||
|
||||
def test_wrapper_delegates_and_validates():
|
||||
workplane = DummyShape()
|
||||
wrapper = CadQueryWrapper(RULES_PATH, workplane)
|
||||
# workplane already has an attached empty model
|
||||
wrapper.export_stl()
|
||||
assert workplane.called[-1][0] == "exportStl"
|
||||
|
||||
|
||||
def test_wrapper_invalid_raises():
|
||||
workplane = DummyShape()
|
||||
wrapper = CadQueryWrapper(RULES_PATH, workplane)
|
||||
CadQueryWrapper.attach_model(workplane, {"minimum_wall_thickness_mm": 0.1})
|
||||
with pytest.raises(ValidationError):
|
||||
wrapper.export_stl()
|
||||
|
||||
|
||||
def test_wrapper_validate_no_args():
|
||||
workplane = DummyShape()
|
||||
wrapper = CadQueryWrapper(RULES_PATH, workplane)
|
||||
CadQueryWrapper.attach_model(workplane, {"minimum_wall_thickness_mm": 0.5})
|
||||
with pytest.raises(ValidationError):
|
||||
wrapper.validate()
|
||||
CadQueryWrapper.attach_model(workplane, {"minimum_wall_thickness_mm": 0.9})
|
||||
wrapper.validate()
|
||||
|
||||
|
||||
def test_validate_max_model_size_dict():
|
||||
rules = {"rules": {"max_model_size_mm": {"X": 1, "Y": 1, "Z": 1}}}
|
||||
model = {"max_model_size_mm": {"X": 2, "Y": 0.5, "Z": 0.5}}
|
||||
errors = validate(model, rules)
|
||||
assert errors == ["Model size X 2 exceeds maximum 1"]
|
||||
|
||||
|
||||
def test_save_validator_model_too_large():
|
||||
rules = {"rules": {"max_model_size_mm": {"X": 1, "Y": 1, "Z": 1}}}
|
||||
obj = DummyBBoxShape(2, 0.5, 0.5)
|
||||
sv = SaveValidator(rules, obj)
|
||||
with pytest.raises(ValidationError):
|
||||
sv.export_stl(obj, "out.stl")
|
||||
|
||||
|
||||
def test_save_validator_triangle_count(tmp_path):
|
||||
rules = {"rules": {"maximum_file_triangle_count": 100}}
|
||||
shape = SphereShape(subdivisions=3)
|
||||
sv = SaveValidator(rules, shape)
|
||||
file_name = tmp_path / "sphere.stl"
|
||||
with pytest.raises(ValidationError):
|
||||
sv.export_stl(shape, file_name)
|
||||
|
||||
|
||||
def test_save_validator_manifold_required():
|
||||
rules = {"rules": {"manifold_geometry_required": True}}
|
||||
shape = NonManifoldShape(valid=False)
|
||||
sv = SaveValidator(rules, shape)
|
||||
with pytest.raises(ValidationError):
|
||||
sv.export_stl(shape, "out.stl")
|
||||
|
||||
|
||||
def test_save_validator_open_edges():
|
||||
rules = {"rules": {"no_open_edges": True}}
|
||||
shape = OpenEdgeShape()
|
||||
sv = SaveValidator(rules, shape)
|
||||
with pytest.raises(ValidationError):
|
||||
sv.export_stl(shape, "out.stl")
|
||||
|
||||
|
||||
def test_save_validator_intersections():
|
||||
rules = {"rules": {"no_intersecting_geometry": True}}
|
||||
solid1 = IntersectSolid()
|
||||
solid2 = IntersectSolid()
|
||||
assembly = DummyAssembly([solid1, solid2])
|
||||
sv = SaveValidator(rules, assembly)
|
||||
with pytest.raises(ValidationError):
|
||||
sv.assembly_save(assembly)
|
||||
|
||||
|
||||
def test_save_validator_disallowed_format():
|
||||
rules = {
|
||||
"rules": {
|
||||
"preferred_file_format": "STL",
|
||||
"alternate_file_formats": ["3MF", "OBJ"],
|
||||
}
|
||||
}
|
||||
sv = SaveValidator(rules)
|
||||
shape = DummyShape()
|
||||
with pytest.raises(ValidationError):
|
||||
sv.export_stl(shape, "out.step")
|
||||
|
||||
|
||||
def test_save_validator_disallowed_format_export():
|
||||
rules = {
|
||||
"rules": {
|
||||
"preferred_file_format": "STL",
|
||||
"alternate_file_formats": ["OBJ"],
|
||||
}
|
||||
}
|
||||
sv = SaveValidator(rules)
|
||||
obj = DummyShape()
|
||||
with pytest.raises(ValidationError):
|
||||
sv.export(obj, "model.step")
|
||||
|
||||
|
||||
def test_save_validator_minimum_clearance():
|
||||
rules = {"rules": {"minimum_clearance_between_parts_mm": 0.3}}
|
||||
s1 = ClearanceSolid(0.2)
|
||||
s2 = ClearanceSolid(0.5)
|
||||
assembly = DummyAssembly([s1, s2])
|
||||
sv = SaveValidator(rules, assembly)
|
||||
with pytest.raises(ValidationError):
|
||||
sv.assembly_save(assembly)
|
||||
|
||||
|
||||
def test_save_validator_overhang_angle():
|
||||
rules = {"rules": {"overhang_max_angle_deg": 45}}
|
||||
shape = OverhangShape([30, 50])
|
||||
sv = SaveValidator(rules, shape)
|
||||
with pytest.raises(ValidationError):
|
||||
sv.export_stl(shape, "out.stl")
|
||||
Reference in New Issue
Block a user