Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- Dockerfile +91 -0
- README.md +207 -5
- __init__.py +16 -0
- client.py +50 -0
- inference.py +734 -0
- models.py +59 -0
- openenv.yaml +7 -0
- pyproject.toml +42 -0
- scripts/generate_ground_truth.py +92 -0
- scripts/generate_tasks_thomasmaker.py +329 -0
- scripts/verify_all_tasks.py +261 -0
- scripts/verify_reference_codes.py +106 -0
- scripts/verify_reward_scenarios.py +370 -0
- scripts/verify_rewards.py +205 -0
- server/__init__.py +11 -0
- server/app.py +36 -0
- server/cadforge_environment.py +363 -0
- server/docs/concepts/brep-mindset.md +121 -0
- server/docs/concepts/free-function-api.md +306 -0
- server/docs/concepts/selectors.md +200 -0
- server/docs/concepts/workplanes.md +274 -0
- server/docs/patterns/anti-patterns.md +353 -0
- server/docs/patterns/common-patterns.md +278 -0
- server/docs/reference/Untitled +1 -0
- server/docs/reference/examples.rst +1705 -0
- server/docs/reference/extending.rst +242 -0
- server/docs/reference/free-func.rst +461 -0
- server/docs/reference/primer.rst +370 -0
- server/docs/reference/quickstart.rst +294 -0
- server/docs/reference/selectors.rst +232 -0
- server/docs/reference/sketch.rst +378 -0
- server/docs/reference/workplane.rst +557 -0
- server/docs/skill.md +264 -0
- server/docs_search.py +200 -0
- server/executor.py +183 -0
- server/geometry.py +206 -0
- server/preprocessor.py +339 -0
- server/requirements.txt +7 -0
- server/reward.py +325 -0
- server/tasks/task_001_flat_plate/ground_truth.json +39 -0
- server/tasks/task_001_flat_plate/ground_truth.step +416 -0
- server/tasks/task_001_flat_plate/ground_truth_normalized.step +416 -0
- server/tasks/task_001_flat_plate/reference_code.py +2 -0
- server/tasks/task_001_flat_plate/surface_points.npy +3 -0
- server/tasks/task_001_flat_plate/task.json +10 -0
- server/tasks/task_001_flat_plate/voxels_64.npy +3 -0
- server/tasks/task_002_box_with_hole/ground_truth.json +42 -0
- server/tasks/task_002_box_with_hole/ground_truth.step +533 -0
- server/tasks/task_002_box_with_hole/ground_truth_normalized.step +533 -0
- server/tasks/task_002_box_with_hole/reference_code.py +2 -0
Dockerfile
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
# Multi-stage build using openenv-base
|
| 8 |
+
# This Dockerfile is flexible and works for both:
|
| 9 |
+
# - In-repo environments (with local OpenEnv sources)
|
| 10 |
+
# - Standalone environments (with openenv from PyPI/Git)
|
| 11 |
+
# The build script (openenv build) handles context detection and sets appropriate build args.
|
| 12 |
+
|
| 13 |
+
ARG BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest
|
| 14 |
+
FROM --platform=linux/amd64 ${BASE_IMAGE} AS builder
|
| 15 |
+
|
| 16 |
+
WORKDIR /app
|
| 17 |
+
|
| 18 |
+
# Ensure git is available (required for installing dependencies from VCS)
|
| 19 |
+
RUN apt-get update && \
|
| 20 |
+
apt-get install -y --no-install-recommends git && \
|
| 21 |
+
rm -rf /var/lib/apt/lists/*
|
| 22 |
+
|
| 23 |
+
# Build argument to control whether we're building standalone or in-repo
|
| 24 |
+
ARG BUILD_MODE=in-repo
|
| 25 |
+
ARG ENV_NAME=cadforge
|
| 26 |
+
|
| 27 |
+
# Copy environment code (always at root of build context)
|
| 28 |
+
COPY . /app/env
|
| 29 |
+
|
| 30 |
+
# For in-repo builds, openenv is already vendored in the build context
|
| 31 |
+
# For standalone builds, openenv will be installed via pyproject.toml
|
| 32 |
+
WORKDIR /app/env
|
| 33 |
+
|
| 34 |
+
# Ensure uv is available (for local builds where base image lacks it)
|
| 35 |
+
RUN if ! command -v uv >/dev/null 2>&1; then \
|
| 36 |
+
curl -LsSf https://astral.sh/uv/install.sh | sh && \
|
| 37 |
+
mv /root/.local/bin/uv /usr/local/bin/uv && \
|
| 38 |
+
mv /root/.local/bin/uvx /usr/local/bin/uvx; \
|
| 39 |
+
fi
|
| 40 |
+
|
| 41 |
+
# Install dependencies using uv sync
|
| 42 |
+
# If uv.lock exists, use it; otherwise resolve on the fly
|
| 43 |
+
RUN --mount=type=cache,target=/root/.cache/uv \
|
| 44 |
+
if [ -f uv.lock ]; then \
|
| 45 |
+
uv sync --frozen --no-install-project --no-editable; \
|
| 46 |
+
else \
|
| 47 |
+
uv sync --no-install-project --no-editable; \
|
| 48 |
+
fi
|
| 49 |
+
|
| 50 |
+
RUN --mount=type=cache,target=/root/.cache/uv \
|
| 51 |
+
if [ -f uv.lock ]; then \
|
| 52 |
+
uv sync --frozen --no-editable; \
|
| 53 |
+
else \
|
| 54 |
+
uv sync --no-editable; \
|
| 55 |
+
fi
|
| 56 |
+
|
| 57 |
+
# # Explicitly install scipy if it's missing (workaround for dependency resolution issues)
|
| 58 |
+
# RUN /app/env/.venv/bin/pip install scipy>=1.10.0
|
| 59 |
+
|
| 60 |
+
# Final runtime stage
|
| 61 |
+
FROM --platform=linux/amd64 ${BASE_IMAGE}
|
| 62 |
+
|
| 63 |
+
WORKDIR /app
|
| 64 |
+
|
| 65 |
+
RUN apt-get update && \
|
| 66 |
+
apt-get install -y --no-install-recommends \
|
| 67 |
+
libgl1 libglib2.0-0 libxrender1 libxext6 libx11-6 \
|
| 68 |
+
libsm6 libice6 libxmu6 libxi6 libgomp1 && \
|
| 69 |
+
rm -rf /var/lib/apt/lists/*
|
| 70 |
+
|
| 71 |
+
# Copy the virtual environment from builder
|
| 72 |
+
COPY --from=builder /app/env/.venv /app/.venv
|
| 73 |
+
|
| 74 |
+
# Copy the environment code
|
| 75 |
+
COPY --from=builder /app/env /app/env
|
| 76 |
+
|
| 77 |
+
# Set PATH to use the virtual environment
|
| 78 |
+
ENV PATH="/app/.venv/bin:$PATH"
|
| 79 |
+
|
| 80 |
+
# Set PYTHONPATH so imports work correctly
|
| 81 |
+
ENV PYTHONPATH="/app/env:$PYTHONPATH"
|
| 82 |
+
|
| 83 |
+
ENV ENABLE_WEB_INTERFACE=true
|
| 84 |
+
|
| 85 |
+
# Health check
|
| 86 |
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
| 87 |
+
CMD curl -f http://localhost:8000/health || exit 1
|
| 88 |
+
|
| 89 |
+
# Run the FastAPI server
|
| 90 |
+
# The module path is constructed to work with the /app/env structure
|
| 91 |
+
CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
|
README.md
CHANGED
|
@@ -1,10 +1,212 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: CAD Forge Environment Server
|
| 3 |
+
emoji: 🔩
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
+
app_port: 8000
|
| 9 |
+
base_path: /web
|
| 10 |
+
tags:
|
| 11 |
+
- openenv
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# CadForge
|
| 15 |
+
|
| 16 |
+
A multi-step RL environment built on the [OpenEnv](https://github.com/facebookresearch/openenv) framework where an LLM agent writes CadQuery (Python CAD) code to produce 3D geometry matching a ground truth target.
|
| 17 |
+
|
| 18 |
+
## Overview
|
| 19 |
+
|
| 20 |
+
CadForge presents the agent with a natural-language description of a 3D mechanical part (e.g. "Create a hollow tube with outer diameter 40mm, inner diameter 30mm, height 60mm"). The agent iteratively writes CadQuery code, receives geometric feedback, and refines its design. The reward measures how closely the agent's shape matches the reference geometry using volumetric (IoU), surface (Chamfer Distance), and property-based metrics.
|
| 21 |
+
|
| 22 |
+
## Architecture
|
| 23 |
+
|
| 24 |
+
```
|
| 25 |
+
cadforge/
|
| 26 |
+
├── client.py # CadforgeEnv client wrapper
|
| 27 |
+
├── models.py # CadforgeAction / CadforgeObservation (Pydantic)
|
| 28 |
+
├── openenv.yaml # OpenEnv manifest
|
| 29 |
+
├── pyproject.toml # Dependencies and build config
|
| 30 |
+
├── requirements.txt # Pip-installable dependencies
|
| 31 |
+
├── verify_all_tasks.py # End-to-end verification script
|
| 32 |
+
├── scripts/
|
| 33 |
+
│ ├── generate_ground_truth.py # Generate GT data for all tasks
|
| 34 |
+
│ ├── verify_reference_codes.py # Phase 1: verify all reference codes execute
|
| 35 |
+
│ └── verify_rewards.py # Phase 2: verify reward pipeline (ref code = high score)
|
| 36 |
+
└── server/
|
| 37 |
+
├── app.py # FastAPI application (HTTP + WebSocket)
|
| 38 |
+
├── cadforge_environment.py# Core environment: reset(), step(), GEOMETRY_RUNNER
|
| 39 |
+
├── docs_search.py # CadQuery documentation search for agent
|
| 40 |
+
├── executor.py # Sandboxed CadQuery code execution (subprocess)
|
| 41 |
+
├── geometry.py # Property extraction (faces, volume, euler, holes)
|
| 42 |
+
├── preprocessor.py # Ground truth generation, voxelization, surface sampling
|
| 43 |
+
├── reward.py # Reward rubrics: Rexec, Rgeom (IoU+CD), Reval
|
| 44 |
+
├── docs/ # CadQuery documentation for agent read_docs action
|
| 45 |
+
└── tasks/
|
| 46 |
+
├── task_001_flat_plate/ # Each task has: task.json, reference_code.py, GT data
|
| 47 |
+
├── task_002_box_with_hole/
|
| 48 |
+
├── ...
|
| 49 |
+
└── task_020_dovetail_block/
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
## Tasks (20 total)
|
| 53 |
+
|
| 54 |
+
| # | Task | Key Features |
|
| 55 |
+
|---|------|-------------|
|
| 56 |
+
| 001 | Flat Plate | Simple box, 6 planar faces |
|
| 57 |
+
| 002 | Box with Hole | Through-hole, boolean cut |
|
| 58 |
+
| 003 | Cylinder Shaft | Revolve, cylindrical faces |
|
| 59 |
+
| 004 | L-Bracket | Extrude + union, 8 faces |
|
| 60 |
+
| 005 | Stepped Shaft | Multi-diameter revolve |
|
| 61 |
+
| 006 | Hollow Tube | Shell / boolean subtract |
|
| 62 |
+
| 007 | Plate with Four Holes | Array of cuts |
|
| 63 |
+
| 008 | Flange Disc | Concentric features |
|
| 64 |
+
| 009 | Hexagonal Prism | Polygon extrude |
|
| 65 |
+
| 010 | Counterbore Box | Multi-depth cuts |
|
| 66 |
+
| 011 | T-Bracket | Multi-body union |
|
| 67 |
+
| 012 | Chamfered Block | Edge chamfers |
|
| 68 |
+
| 013 | Filleted Box | Edge fillets, through-hole |
|
| 69 |
+
| 014 | Bushing | Thick-wall tube |
|
| 70 |
+
| 015 | U-Channel | Extrude profile |
|
| 71 |
+
| 016 | Cone | Loft / revolve |
|
| 72 |
+
| 017 | Plate with Slot | Slot cut |
|
| 73 |
+
| 018 | Sphere | Full revolve |
|
| 74 |
+
| 019 | Bolt Flange | Circular bolt pattern |
|
| 75 |
+
| 020 | Dovetail Block | Angled extrude |
|
| 76 |
+
|
| 77 |
+
## Reward Structure
|
| 78 |
+
|
| 79 |
+
```
|
| 80 |
+
R = Rexec_gate x (0.70 x Rgeom + 0.30 x Reval)
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
### Rexec (Gate)
|
| 84 |
+
Binary gate: code must execute, produce a valid shape with positive volume.
|
| 85 |
+
|
| 86 |
+
### Rgeom (Geometric Similarity) — weight 0.70
|
| 87 |
+
- **IoU (best-of-6 rotations)**: 60% — voxel grid overlap at 64^3
|
| 88 |
+
- **Mean Chamfer Distance**: 20% — average nearest-point distance (2048 surface points)
|
| 89 |
+
- **Median Chamfer Distance**: 20% — robust to outliers
|
| 90 |
+
|
| 91 |
+
Chamfer Distance can be toggled via `ENABLE_CHAMFER_DISTANCE` flag.
|
| 92 |
+
|
| 93 |
+
### Reval (Property Evaluation) — weight 0.30
|
| 94 |
+
- **Volume similarity**: 35% — min/max ratio
|
| 95 |
+
- **Bounding box aspect ratio**: 30% — sorted dimension comparison
|
| 96 |
+
- **Dominant face type match**: 15% — PLANE vs CYLINDER vs CONE etc.
|
| 97 |
+
- **Euler characteristic match**: 20% — topological invariant
|
| 98 |
+
|
| 99 |
+
## Voxelization Pipeline
|
| 100 |
+
|
| 101 |
+
Shapes are voxelized at 64^3 resolution using:
|
| 102 |
+
1. OCC tessellation (`BRepMesh_IncrementalMesh`) to extract triangle mesh
|
| 103 |
+
2. `trimesh.Trimesh` construction with `process=True` + `fix_normals()`
|
| 104 |
+
3. `trimesh.contains()` for vectorized ray-cast point-in-solid test
|
| 105 |
+
4. Backed by **Embree** (Intel's ray tracing kernel) for ~130ms per shape
|
| 106 |
+
|
| 107 |
+
### Normalization
|
| 108 |
+
Both ground truth and agent shapes are normalized before comparison:
|
| 109 |
+
1. Center at origin
|
| 110 |
+
2. Align longest axis to X, second to Y, shortest to Z
|
| 111 |
+
|
| 112 |
+
Original (unnormalized) STEP files are preserved as `ground_truth.step`.
|
| 113 |
+
|
| 114 |
+
## Ground Truth Data (per task)
|
| 115 |
+
|
| 116 |
+
Each task directory contains:
|
| 117 |
+
- `task.json` — prompt, metadata, difficulty
|
| 118 |
+
- `reference_code.py` — canonical CadQuery solution
|
| 119 |
+
- `ground_truth.json` — properties (volume, bbox, face counts, euler, etc.)
|
| 120 |
+
- `ground_truth.step` — original STEP geometry
|
| 121 |
+
- `ground_truth_normalized.step` — normalized STEP geometry
|
| 122 |
+
- `surface_points.npy` — 2048 surface sample points (normalized frame)
|
| 123 |
+
- `voxels_64.npy` — 64^3 boolean voxel grid (normalized frame)
|
| 124 |
+
|
| 125 |
+
## Verification Pipeline
|
| 126 |
+
|
| 127 |
+
Three-phase verification ensures correctness:
|
| 128 |
+
|
| 129 |
+
```bash
|
| 130 |
+
# Phase 1: All reference codes execute and produce valid shapes
|
| 131 |
+
python scripts/verify_reference_codes.py
|
| 132 |
+
|
| 133 |
+
# Ground truth generation
|
| 134 |
+
python scripts/generate_ground_truth.py
|
| 135 |
+
|
| 136 |
+
# Phase 2: Reference code achieves high reward against its own GT
|
| 137 |
+
python scripts/verify_rewards.py
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
**Latest results**: 20/20 PASS on all phases (rewards 0.954–0.976).
|
| 141 |
+
|
| 142 |
+
## Installation
|
| 143 |
+
|
| 144 |
+
```bash
|
| 145 |
+
pip install -r requirements.txt
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
Key dependencies:
|
| 149 |
+
- **cadquery** >= 2.4.0 — parametric 3D CAD (wraps OpenCASCADE)
|
| 150 |
+
- **trimesh** >= 4.0.0 — mesh operations
|
| 151 |
+
- **embreex** >= 4.0.0 — Embree ray tracing (makes trimesh.contains() fast)
|
| 152 |
+
- **scipy** >= 1.10.0 — cKDTree for Chamfer Distance
|
| 153 |
+
- **numpy** >= 1.24.0
|
| 154 |
+
- **networkx** >= 3.0.0 — required by trimesh for mesh repair
|
| 155 |
+
- **pydantic** >= 2.0.0 — action/observation models
|
| 156 |
+
- **openenv-core** >= 0.2.2 — OpenEnv framework
|
| 157 |
+
|
| 158 |
+
## Development Notes: What We Tried
|
| 159 |
+
|
| 160 |
+
### Voxelization Approaches (chronological)
|
| 161 |
+
|
| 162 |
+
1. **OCC `BRepClass3d_SolidClassifier` at 64^3** (first implementation)
|
| 163 |
+
- Pure Python triple loop, 262K OCC calls per shape
|
| 164 |
+
- Accurate but catastrophically slow: 2–191s per shape (cone worst case)
|
| 165 |
+
- **Abandoned** due to speed
|
| 166 |
+
|
| 167 |
+
2. **trimesh `.voxelized(pitch).fill()`** (second attempt)
|
| 168 |
+
- Fast for simple solids (0.05–0.3s) but slow for hollow/complex shapes (5–25s)
|
| 169 |
+
- `.fill()` uses flood-fill which breaks on non-watertight OCC tessellations
|
| 170 |
+
- **Abandoned** due to inconsistent performance on hollow shapes
|
| 171 |
+
|
| 172 |
+
3. **STL export -> trimesh load -> `.voxelized().fill()`**
|
| 173 |
+
- CQ's STL export produces watertight meshes, solving the fill problem
|
| 174 |
+
- Still slow for some shapes due to trimesh's Python flood-fill internals
|
| 175 |
+
- **Abandoned** — marginal improvement
|
| 176 |
+
|
| 177 |
+
4. **OCC SolidClassifier at 32^3 + scipy zoom to 64^3** (attempted optimization)
|
| 178 |
+
- 8x fewer points, ~0.5s uniform across shapes
|
| 179 |
+
- Explored but superseded by embree solution
|
| 180 |
+
- **Not used** — embree was faster
|
| 181 |
+
|
| 182 |
+
5. **trimesh `.contains()` without embree**
|
| 183 |
+
- Vectorized ray-cast, should have been fast
|
| 184 |
+
- Without embree: falls back to pure-Python ray-triangle test
|
| 185 |
+
- 4–70s per shape — **worse than OCC loop**
|
| 186 |
+
|
| 187 |
+
6. **trimesh `.contains()` + embreex** (current, final)
|
| 188 |
+
- Embree provides C++ SIMD ray tracing kernel
|
| 189 |
+
- ~15–20ms at 32^3, ~100–140ms at 64^3, uniform across all shapes
|
| 190 |
+
- Handles non-watertight meshes via winding-number ray parity
|
| 191 |
+
- **Winner** — 1000x faster than original OCC loop
|
| 192 |
+
|
| 193 |
+
### Hole Detection
|
| 194 |
+
|
| 195 |
+
- Attempted automated through-hole detection via cylindrical face analysis
|
| 196 |
+
- Used `BRepAdaptor_Surface` to get cylinder radius, compared height to bounding box
|
| 197 |
+
- 10/20 tasks had incorrect counts (outer cylindrical surfaces falsely detected as holes)
|
| 198 |
+
- **Removed from reward** — not reliable enough. Kept in `geometry.py` for informational purposes but not scored.
|
| 199 |
+
|
| 200 |
+
### Reward Metrics Removed
|
| 201 |
+
|
| 202 |
+
- **frame_score**: Compared orientation alignment — redundant after normalization (both sides aligned)
|
| 203 |
+
- **hole_count**: Removed due to unreliable detection (see above)
|
| 204 |
+
- **param_score**: Dimension-matching metric — redundant with bbox aspect ratio comparison
|
| 205 |
+
- **face_count_ratio**: Risky — different valid CAD approaches can produce different face counts
|
| 206 |
+
- **IoU canonical** in Reval: Redundant with Rgeom's best-of-6 IoU
|
| 207 |
+
|
| 208 |
+
### OCP API Gotchas
|
| 209 |
+
|
| 210 |
+
- `TopExp_Explorer.Current()` returns `TopoDS_Shape`, not `TopoDS_Face` — must downcast with `TopoDS.Face_s()`
|
| 211 |
+
- CadQuery v2.7.0 `Face` objects don't have `.Surface()` — use `BRepAdaptor_Surface(TopoDS.Face_s(f.wrapped))` instead
|
| 212 |
+
- `BRep_Tool.Triangulation_s()` requires `TopoDS_Face`, not generic `TopoDS_Shape`
|
__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""Cadforge Environment."""
|
| 8 |
+
|
| 9 |
+
from .client import CadforgeEnv
|
| 10 |
+
from .models import CadforgeAction, CadforgeObservation
|
| 11 |
+
|
| 12 |
+
__all__ = [
|
| 13 |
+
"CadforgeAction",
|
| 14 |
+
"CadforgeObservation",
|
| 15 |
+
"CadforgeEnv",
|
| 16 |
+
]
|
client.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict
|
| 2 |
+
|
| 3 |
+
from openenv.core import EnvClient
|
| 4 |
+
from openenv.core.client_types import StepResult
|
| 5 |
+
from openenv.core.env_server.types import State
|
| 6 |
+
|
| 7 |
+
try:
|
| 8 |
+
from .models import CadforgeAction, CadforgeObservation
|
| 9 |
+
except ImportError:
|
| 10 |
+
from models import CadforgeAction, CadforgeObservation
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class CadforgeEnv(
|
| 14 |
+
EnvClient[CadforgeAction, CadforgeObservation, State]
|
| 15 |
+
):
|
| 16 |
+
def _step_payload(self, action: CadforgeAction) -> Dict:
|
| 17 |
+
return {
|
| 18 |
+
"action_type": action.action_type,
|
| 19 |
+
"params": action.params,
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
def _parse_result(self, payload: Dict) -> StepResult[CadforgeObservation]:
|
| 23 |
+
obs_data = payload.get("observation", {})
|
| 24 |
+
observation = CadforgeObservation(
|
| 25 |
+
task=obs_data.get("task"),
|
| 26 |
+
step_count=obs_data.get("step_count", 0),
|
| 27 |
+
done=payload.get("done", False),
|
| 28 |
+
reward=payload.get("reward"),
|
| 29 |
+
docs_results=obs_data.get("docs_results"),
|
| 30 |
+
code_executed=obs_data.get("code_executed"),
|
| 31 |
+
code_error=obs_data.get("code_error"),
|
| 32 |
+
object_id=obs_data.get("object_id"),
|
| 33 |
+
object_properties=obs_data.get("object_properties"),
|
| 34 |
+
artifacts=obs_data.get("artifacts"),
|
| 35 |
+
last_executed=obs_data.get("last_executed"),
|
| 36 |
+
image_path=obs_data.get("image_path"),
|
| 37 |
+
metadata=obs_data.get("metadata", {}),
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
return StepResult(
|
| 41 |
+
observation=observation,
|
| 42 |
+
reward=payload.get("reward"),
|
| 43 |
+
done=payload.get("done", False),
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
def _parse_state(self, payload: Dict) -> State:
|
| 47 |
+
return State(
|
| 48 |
+
episode_id=payload.get("episode_id"),
|
| 49 |
+
step_count=payload.get("step_count", 0),
|
| 50 |
+
)
|
inference.py
ADDED
|
@@ -0,0 +1,734 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
import re
|
| 5 |
+
import textwrap
|
| 6 |
+
import time
|
| 7 |
+
from typing import Dict, List, Optional
|
| 8 |
+
|
| 9 |
+
from openai import OpenAI
|
| 10 |
+
|
| 11 |
+
from client import CadforgeEnv
|
| 12 |
+
from models import CadforgeAction
|
| 13 |
+
|
| 14 |
+
API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
|
| 15 |
+
MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-7B-Instruct") # Qwen/Qwen2.5-72B-Instruct
|
| 16 |
+
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 17 |
+
LOCAL_IMAGE_NAME = os.getenv("LOCAL_IMAGE_NAME")
|
| 18 |
+
|
| 19 |
+
BENCHMARK = "cadforge"
|
| 20 |
+
TASKS = [
|
| 21 |
+
"task_001_flat_plate",
|
| 22 |
+
"task_002_box_with_hole",
|
| 23 |
+
"task_003_cylinder_shaft",
|
| 24 |
+
"task_004_l_bracket",
|
| 25 |
+
"task_005_stepped_shaft",
|
| 26 |
+
"task_006_hollow_tube",
|
| 27 |
+
"task_007_plate_four_holes",
|
| 28 |
+
"task_008_flange_disc",
|
| 29 |
+
"task_009_hexagonal_prism",
|
| 30 |
+
"task_010_counterbore_box",
|
| 31 |
+
"task_011_t_bracket",
|
| 32 |
+
"task_012_chamfered_block",
|
| 33 |
+
"task_013_filleted_box",
|
| 34 |
+
"task_014_bushing",
|
| 35 |
+
"task_015_u_channel",
|
| 36 |
+
"task_016_cone",
|
| 37 |
+
"task_017_plate_with_slot",
|
| 38 |
+
"task_018_sphere",
|
| 39 |
+
"task_019_bolt_flange",
|
| 40 |
+
"task_020_dovetail_block",
|
| 41 |
+
"task_021_hf_317",
|
| 42 |
+
"task_022_hf_711",
|
| 43 |
+
"task_023_hf_2915",
|
| 44 |
+
"task_024_hf_20869",
|
| 45 |
+
"task_025_hf_24513",
|
| 46 |
+
"task_026_hf_42634",
|
| 47 |
+
"task_027_hf_45747",
|
| 48 |
+
"task_028_hf_53812",
|
| 49 |
+
"task_029_hf_54584",
|
| 50 |
+
"task_030_hf_54975",
|
| 51 |
+
"task_031_hf_80392",
|
| 52 |
+
"task_032_hf_82865",
|
| 53 |
+
"task_033_hf_107429",
|
| 54 |
+
"task_034_hf_107462",
|
| 55 |
+
"task_035_hf_134249",
|
| 56 |
+
"task_036_hf_138605",
|
| 57 |
+
"task_037_hf_147235",
|
| 58 |
+
"task_038_hf_989",
|
| 59 |
+
"task_039_hf_17430",
|
| 60 |
+
"task_040_hf_22992",
|
| 61 |
+
"task_041_hf_30431",
|
| 62 |
+
"task_042_hf_31743",
|
| 63 |
+
"task_043_hf_40909",
|
| 64 |
+
"task_044_hf_51152",
|
| 65 |
+
"task_045_hf_71839",
|
| 66 |
+
"task_046_hf_76438",
|
| 67 |
+
"task_047_hf_90788",
|
| 68 |
+
"task_048_hf_103736",
|
| 69 |
+
"task_049_hf_113406",
|
| 70 |
+
"task_050_hf_113626",
|
| 71 |
+
"task_051_hf_128576",
|
| 72 |
+
"task_052_hf_131511",
|
| 73 |
+
"task_053_hf_133973",
|
| 74 |
+
"task_054_hf_141332",
|
| 75 |
+
"task_055_hf_7795",
|
| 76 |
+
"task_056_hf_9711",
|
| 77 |
+
"task_057_hf_12096",
|
| 78 |
+
"task_058_hf_12781",
|
| 79 |
+
"task_059_hf_17967",
|
| 80 |
+
"task_060_hf_38041",
|
| 81 |
+
"task_061_hf_40479",
|
| 82 |
+
"task_062_hf_50296",
|
| 83 |
+
"task_063_hf_57703",
|
| 84 |
+
"task_064_hf_59913",
|
| 85 |
+
"task_065_hf_75184",
|
| 86 |
+
"task_066_hf_89325",
|
| 87 |
+
"task_067_hf_106267",
|
| 88 |
+
"task_068_hf_106604",
|
| 89 |
+
"task_069_hf_119258",
|
| 90 |
+
"task_070_hf_121236",
|
| 91 |
+
]
|
| 92 |
+
MAX_STEPS_DEFAULT = 10
|
| 93 |
+
TEMPERATURE = 0.3
|
| 94 |
+
MAX_TOKENS = 8192
|
| 95 |
+
|
| 96 |
+
# ---------------------------------------------------------------------------
|
| 97 |
+
# TOOLS — Hermes-style <tools> block for Qwen2.5-Instruct
|
| 98 |
+
# These definitions match the server handlers in cadforge_environment.py:
|
| 99 |
+
# _handle_read_docs(params) -> params: {topic?, query?}
|
| 100 |
+
# _handle_execute_cadquery(params) -> params: {code}
|
| 101 |
+
# _handle_submit(params) -> params: {object_id?} (defaults to best object if omitted)
|
| 102 |
+
# ---------------------------------------------------------------------------
|
| 103 |
+
|
| 104 |
+
TOOLS_BLOCK = textwrap.dedent("""\
|
| 105 |
+
# Tools
|
| 106 |
+
|
| 107 |
+
You may call one or more functions to assist with the user query.
|
| 108 |
+
|
| 109 |
+
You are provided with function signatures within <tools></tools> XML tags:
|
| 110 |
+
<tools>
|
| 111 |
+
{
|
| 112 |
+
"type": "function",
|
| 113 |
+
"function": {
|
| 114 |
+
"name": "read_docs",
|
| 115 |
+
"description": "Search CadQuery documentation. Pass just a query to grep across ALL docs, or narrow with a topic. Available docs cover: basics (overview, quickstart, primer), selectors (face/edge selector syntax like >Z |X %Circle), booleans (BRep mindset, cut/union/intersect), transforms (workplane placement, .transformed(), offset), features (holes, fillets, chamfers, shell, polar/rect arrays, anti-patterns), sketch (Sketch API: rect, circle, slot, arc, constraints), advanced (free-function API, loft, sweep, surfaces), examples (full worked examples: plates, brackets, enclosures, gears), anti-patterns (common mistakes & fixes), workplanes (creation, offset, centerOption).",
|
| 116 |
+
"parameters": {
|
| 117 |
+
"type": "object",
|
| 118 |
+
"properties": {
|
| 119 |
+
"topic": {
|
| 120 |
+
"type": "string",
|
| 121 |
+
"enum": ["basics", "selectors", "booleans", "transforms", "features", "sketch", "advanced", "examples", "anti-patterns", "workplanes"],
|
| 122 |
+
"description": "Optional. Narrow search to a topic. Omit to search all docs."
|
| 123 |
+
},
|
| 124 |
+
"query": {
|
| 125 |
+
"type": "string",
|
| 126 |
+
"description": "Keyword to grep for, e.g. 'polarArray', 'cboreHole', 'shell', 'loft'. Omit to browse the full topic."
|
| 127 |
+
}
|
| 128 |
+
},
|
| 129 |
+
"required": []
|
| 130 |
+
}
|
| 131 |
+
}
|
| 132 |
+
}
|
| 133 |
+
{
|
| 134 |
+
"type": "function",
|
| 135 |
+
"function": {
|
| 136 |
+
"name": "execute_cadquery",
|
| 137 |
+
"description": "Execute a CadQuery Python script. The script MUST define a variable called 'result' containing the final CadQuery Workplane or Shape object. On success, returns the object_id and geometric properties of the shape (volume, bounding box, face counts, euler characteristic, etc.). On failure, returns the error message.",
|
| 138 |
+
"parameters": {
|
| 139 |
+
"type": "object",
|
| 140 |
+
"properties": {
|
| 141 |
+
"code": {
|
| 142 |
+
"type": "string",
|
| 143 |
+
"description": "Complete Python script using CadQuery. Must 'import cadquery as cq' and assign the final shape to a variable named 'result'."
|
| 144 |
+
}
|
| 145 |
+
},
|
| 146 |
+
"required": ["code"]
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
}
|
| 150 |
+
{
|
| 151 |
+
"type": "function",
|
| 152 |
+
"function": {
|
| 153 |
+
"name": "submit",
|
| 154 |
+
"description": "Submit and end the episode. Specify which object_id to submit (from a previous execute_cadquery executions). Without submit, even if you do your best work, it will not be considered.",
|
| 155 |
+
"parameters": {
|
| 156 |
+
"type": "object",
|
| 157 |
+
"properties": {
|
| 158 |
+
"object_id": {
|
| 159 |
+
"type": "string",
|
| 160 |
+
"description": "The object_id from a previous execute_cadquery result to submit. If omitted, the best object is submitted."
|
| 161 |
+
}
|
| 162 |
+
},
|
| 163 |
+
"required": ["object_id"]
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
</tools>
|
| 168 |
+
|
| 169 |
+
IMPORTANT: Every response MUST contain exactly one <tool_call></tool_call> block. You may include analysis or reasoning text before it, but you MUST end with a tool call. Responses without a <tool_call> block are invalid and waste a step.
|
| 170 |
+
|
| 171 |
+
Format:
|
| 172 |
+
<tool_call>
|
| 173 |
+
{"name": <function-name>, "arguments": <args-json-object>}
|
| 174 |
+
</tool_call>
|
| 175 |
+
|
| 176 |
+
IMPORTANT: You MUST call `submit` before your steps run out. If you never submit, your score is 0 regardless of how good your shape is.""")
|
| 177 |
+
|
| 178 |
+
# ---------------------------------------------------------------------------
|
| 179 |
+
# SYSTEM_PROMPT — CadQuery domain knowledge (no workflow/strategy section)
|
| 180 |
+
# Separated from tools block for clarity; combined at runtime.
|
| 181 |
+
# ---------------------------------------------------------------------------
|
| 182 |
+
|
| 183 |
+
SYSTEM_PROMPT_KNOWLEDGE = textwrap.dedent("""\
|
| 184 |
+
You are a CadQuery CAD agent. You write CadQuery Python scripts that create 3D solid geometry matching a task description. You produce correct, idiomatic CadQuery code on the first try whenever possible, and iterate using execution feedback and library docs when needed to reach to the correct query that generates the expected 3D object.
|
| 185 |
+
|
| 186 |
+
Your shape is compared against a ground truth 3D object. You will receive the geometric properties of your shape after each execution — use these to verify your shape matches the task description (correct volume, bounding box dimensions, face types, euler characteristic, etc.).
|
| 187 |
+
|
| 188 |
+
You MUST call `submit` before your steps run out. Without submit, your score is 0.
|
| 189 |
+
|
| 190 |
+
## CadQuery Knowledge
|
| 191 |
+
|
| 192 |
+
### Fluent (Workplane) API — Primary API
|
| 193 |
+
```python
|
| 194 |
+
import cadquery as cq
|
| 195 |
+
result = cq.Workplane("XY").box(10, 10, 10).faces(">Z").hole(3)
|
| 196 |
+
```
|
| 197 |
+
- Chain operations on a `Workplane` with hidden state (current plane, stack)
|
| 198 |
+
- Best for: parts built from sketches and feature operations (extrude, hole, fillet, shell)
|
| 199 |
+
- `result` must be the final Workplane object
|
| 200 |
+
|
| 201 |
+
### Core Mindset: BRep, NOT CSG
|
| 202 |
+
CadQuery uses Boundary Representation. Avoid the CSG reflex of union/cut for everything.
|
| 203 |
+
|
| 204 |
+
| Instead of... | Use... |
|
| 205 |
+
|---------------------------|---------------------------------------------|
|
| 206 |
+
| `.cut(cylinder)` for hole | `.faces(...).hole(d)` |
|
| 207 |
+
| `.cut(shell_solid)` | `.shell(thickness)` |
|
| 208 |
+
| `.union(chamfered_edge)` | `.edges(...).chamfer(d)` |
|
| 209 |
+
| `.cut(box)` for pocket | `.faces(...).workplane().rect(w,h).cutBlind(depth)` |
|
| 210 |
+
|
| 211 |
+
Only use `.cut()`, `.union()`, `.intersect()` when genuinely combining separate solids.
|
| 212 |
+
|
| 213 |
+
### Workplanes
|
| 214 |
+
```python
|
| 215 |
+
wp = cq.Workplane("XY") # named plane at origin
|
| 216 |
+
wp = solid.faces(">Z").workplane() # on a face
|
| 217 |
+
wp = solid.faces(">Z").workplane(offset=5) # offset from face
|
| 218 |
+
```
|
| 219 |
+
- `"XY"` = horizontal, `"XZ"` = front, `"YZ"` = side
|
| 220 |
+
- `.workplane()` resets 2D origin to center of selected face by default
|
| 221 |
+
- After `.workplane()`, coordinates are LOCAL to that plane
|
| 222 |
+
|
| 223 |
+
### Selectors — Cheat Sheet
|
| 224 |
+
| Selector | Meaning |
|
| 225 |
+
|-------------|-------------------------------------------|
|
| 226 |
+
| `">Z"` | Highest Z centroid (top face) |
|
| 227 |
+
| `"<Z"` | Lowest Z centroid (bottom face) |
|
| 228 |
+
| `"|X"` | Normal parallel to X axis (vertical faces)|
|
| 229 |
+
| `"#Z"` | Orthogonal to Z |
|
| 230 |
+
| `">Z and |X"` | Combine with AND |
|
| 231 |
+
| `"%Plane"` | Faces of type Plane |
|
| 232 |
+
| `"%Circle"` | Circular edges |
|
| 233 |
+
|
| 234 |
+
### 2D Sketch Operations (on Workplane)
|
| 235 |
+
`center`, `lineTo`, `line`, `vLine`, `hLine`, `moveTo`, `spline`, `threePointArc`, `sagittaArc`, `radiusArc`, `tangentArcPoint`, `mirrorY`, `mirrorX`, `rect`, `circle`, `ellipse`, `polyline`, `close`, `polygon`, `slot2D`, `offset2D`
|
| 236 |
+
|
| 237 |
+
### 3D Operations
|
| 238 |
+
**Require 2D wire on workplane:** `extrude`, `cutBlind`, `cutThruAll`, `hole`, `cboreHole`, `cskHole`, `loft`, `sweep`, `revolve`, `twistExtrude`
|
| 239 |
+
**No workplane needed:** `shell`, `fillet`, `chamfer`, `split`, `translate`, `rotate`, `mirror`
|
| 240 |
+
|
| 241 |
+
### Common Patterns
|
| 242 |
+
|
| 243 |
+
**Box:** `cq.Workplane("XY").box(L, W, H)`
|
| 244 |
+
- `.box()` centers the box at the workplane origin by default
|
| 245 |
+
|
| 246 |
+
**Cylinder:** `cq.Workplane("XY").cylinder(height, radius)`
|
| 247 |
+
|
| 248 |
+
**Sphere:** `cq.Workplane("XY").sphere(radius)`
|
| 249 |
+
|
| 250 |
+
**Cone:** `cq.Workplane("XY").union(cq.Solid.makeCone(r1, r2, h))`
|
| 251 |
+
|
| 252 |
+
**Through-hole:** `.faces(">Z").hole(diameter)`
|
| 253 |
+
|
| 254 |
+
**Blind hole:** `.faces(">Z").hole(diameter, depth)`
|
| 255 |
+
|
| 256 |
+
**Fillet edges:** `.edges("|Z").fillet(radius)` or `.edges(">Z").fillet(radius)`
|
| 257 |
+
|
| 258 |
+
**Chamfer:** `.edges(">Z").chamfer(distance)`
|
| 259 |
+
|
| 260 |
+
**Shell (hollow):** `.faces(">Z").shell(-wallThickness)` (negative = inward)
|
| 261 |
+
|
| 262 |
+
**Polar array of holes:**
|
| 263 |
+
```python
|
| 264 |
+
result = (
|
| 265 |
+
cq.Workplane("XY")
|
| 266 |
+
.cylinder(height, outer_r)
|
| 267 |
+
.faces(">Z").workplane()
|
| 268 |
+
.polarArray(bolt_circle_r, startAngle, 360, count)
|
| 269 |
+
.hole(hole_d)
|
| 270 |
+
)
|
| 271 |
+
```
|
| 272 |
+
|
| 273 |
+
**Rectangular array of holes:**
|
| 274 |
+
```python
|
| 275 |
+
result = (
|
| 276 |
+
cq.Workplane("XY")
|
| 277 |
+
.box(L, W, H)
|
| 278 |
+
.faces(">Z").workplane()
|
| 279 |
+
.rarray(xSpacing, ySpacing, xCount, yCount)
|
| 280 |
+
.hole(hole_d)
|
| 281 |
+
)
|
| 282 |
+
```
|
| 283 |
+
|
| 284 |
+
**Revolve profile:**
|
| 285 |
+
```python
|
| 286 |
+
result = (
|
| 287 |
+
cq.Workplane("XZ")
|
| 288 |
+
.moveTo(r_inner, 0)
|
| 289 |
+
.lineTo(r_outer, 0).lineTo(r_outer, h).lineTo(r_inner, h)
|
| 290 |
+
.close()
|
| 291 |
+
.revolve(360, (0, 0, 0), (0, 1, 0))
|
| 292 |
+
)
|
| 293 |
+
```
|
| 294 |
+
|
| 295 |
+
**Sweep:**
|
| 296 |
+
```python
|
| 297 |
+
path = cq.Workplane("XZ").spline([(0,0), (10,5), (20,0)])
|
| 298 |
+
result = cq.Workplane("XY").circle(r).sweep(path)
|
| 299 |
+
```
|
| 300 |
+
|
| 301 |
+
**Loft:**
|
| 302 |
+
```python
|
| 303 |
+
result = (
|
| 304 |
+
cq.Workplane("XY")
|
| 305 |
+
.rect(w1, h1)
|
| 306 |
+
.workplane(offset=height)
|
| 307 |
+
.circle(r2)
|
| 308 |
+
.loft()
|
| 309 |
+
)
|
| 310 |
+
```
|
| 311 |
+
|
| 312 |
+
**Sketch API:**
|
| 313 |
+
```python
|
| 314 |
+
s = cq.Sketch().rect(10, 10).vertices().fillet(1)
|
| 315 |
+
result = cq.Workplane("XY").placeSketch(s).extrude(5)
|
| 316 |
+
```
|
| 317 |
+
|
| 318 |
+
**Hexagonal prism:**
|
| 319 |
+
```python
|
| 320 |
+
result = cq.Workplane("XY").polygon(6, diameter).extrude(height)
|
| 321 |
+
```
|
| 322 |
+
Note: `.polygon(nSides, diameter)` where diameter is the circumscribed circle diameter (distance across corners).
|
| 323 |
+
|
| 324 |
+
**L-bracket (subtractive approach):**
|
| 325 |
+
```python
|
| 326 |
+
result = (
|
| 327 |
+
cq.Workplane("XY")
|
| 328 |
+
.box(L, W, H)
|
| 329 |
+
.faces(">Z").workplane()
|
| 330 |
+
.center(offset_x, offset_y)
|
| 331 |
+
.rect(cut_w, cut_h)
|
| 332 |
+
.cutBlind(-cut_depth)
|
| 333 |
+
)
|
| 334 |
+
```
|
| 335 |
+
|
| 336 |
+
**Dovetail / trapezoidal groove (profile-based cut):**
|
| 337 |
+
```python
|
| 338 |
+
result = (
|
| 339 |
+
cq.Workplane("XY")
|
| 340 |
+
.box(L, W, H)
|
| 341 |
+
.faces(">Y").workplane()
|
| 342 |
+
.moveTo(-top_w/2, H)
|
| 343 |
+
.lineTo(-bot_w/2, H - depth)
|
| 344 |
+
.lineTo(bot_w/2, H - depth)
|
| 345 |
+
.lineTo(top_w/2, H)
|
| 346 |
+
.close()
|
| 347 |
+
.cutThruAll()
|
| 348 |
+
)
|
| 349 |
+
```
|
| 350 |
+
|
| 351 |
+
### Critical Anti-Patterns — AVOID
|
| 352 |
+
1. **Boolean when a feature suffices** — use `.hole()`, `.shell()`, `.fillet()`, `.chamfer()`, `.cutBlind()`
|
| 353 |
+
2. **Unclosed wires** — always call `.close()` before `.extrude()` or `.revolve()` with profiles
|
| 354 |
+
3. **`.cutBlind()` direction** — negative value cuts INTO the solid from the current face
|
| 355 |
+
4. **Fillet/chamfer too large** — radius must be less than shortest adjacent edge length
|
| 356 |
+
5. **Boolean in a loop** — combine into compound first, then subtract once
|
| 357 |
+
6. **Forgetting `import cadquery as cq`**
|
| 358 |
+
7. **Forgetting to assign to `result`**
|
| 359 |
+
|
| 360 |
+
All dimensions are in millimeters. Always use `import cadquery as cq`.""")
|
| 361 |
+
|
| 362 |
+
SYSTEM_PROMPT = SYSTEM_PROMPT_KNOWLEDGE + "\n\n" + TOOLS_BLOCK
|
| 363 |
+
|
| 364 |
+
# ---------------------------------------------------------------------------
|
| 365 |
+
# USER_PROMPT_TEMPLATE — First turn: task description
|
| 366 |
+
# ---------------------------------------------------------------------------
|
| 367 |
+
|
| 368 |
+
USER_PROMPT_TEMPLATE = textwrap.dedent("""\
|
| 369 |
+
## Task
|
| 370 |
+
{task_description}
|
| 371 |
+
|
| 372 |
+
You have {max_steps} steps to complete this task.
|
| 373 |
+
Write CadQuery code that creates this exact geometry. Assign the final shape to `result`.
|
| 374 |
+
Pay careful attention to every dimension (in mm), orientation, and feature described above.""")
|
| 375 |
+
|
| 376 |
+
# ---------------------------------------------------------------------------
|
| 377 |
+
# Per-tool observation templates
|
| 378 |
+
# ---------------------------------------------------------------------------
|
| 379 |
+
|
| 380 |
+
OBS_EXECUTE_SUCCESS = textwrap.dedent("""\
|
| 381 |
+
## execute_cadquery — Success
|
| 382 |
+
- Code Executed Successfully: True
|
| 383 |
+
- object_id: {object_id}
|
| 384 |
+
- Object properties:
|
| 385 |
+
```json
|
| 386 |
+
{properties_json}
|
| 387 |
+
```
|
| 388 |
+
- Objects so far: {artifacts}
|
| 389 |
+
Step {step_count}/{max_steps} used.""")
|
| 390 |
+
|
| 391 |
+
OBS_EXECUTE_ERROR = textwrap.dedent("""\
|
| 392 |
+
## execute_cadquery — Error
|
| 393 |
+
- Code Executed Successfully: False
|
| 394 |
+
- error: {code_error}
|
| 395 |
+
Step {step_count}/{max_steps} used.""")
|
| 396 |
+
|
| 397 |
+
OBS_READ_DOCS = textwrap.dedent("""\
|
| 398 |
+
## read_docs — Results
|
| 399 |
+
{docs_text}
|
| 400 |
+
Step {step_count}/{max_steps} used.""")
|
| 401 |
+
|
| 402 |
+
OBS_SUBMIT = textwrap.dedent("""\
|
| 403 |
+
## submit — Episode Complete
|
| 404 |
+
- done: True""")
|
| 405 |
+
|
| 406 |
+
OBS_UNKNOWN_ACTION = textwrap.dedent("""\
|
| 407 |
+
## Error — Unknown Action
|
| 408 |
+
{error_message}
|
| 409 |
+
Step {step_count}/{max_steps} used.
|
| 410 |
+
Valid actions: read_docs, execute_cadquery, submit.""")
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
def log_start(task: str, env: str, model: str) -> None:
|
| 414 |
+
print(f"[START] task={task} env={env} model={model}", flush=True)
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
|
| 418 |
+
error_val = error if error else "null"
|
| 419 |
+
done_val = str(done).lower()
|
| 420 |
+
print(f"[STEP] step={step} action={action} reward={reward:.4f} done={done_val} error={error_val}", flush=True)
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:
|
| 424 |
+
rewards_str = ",".join(f"{r:.4f}" for r in rewards)
|
| 425 |
+
print(f"[END] success={str(success).lower()} steps={steps} score={score:.4f} rewards={rewards_str}", flush=True)
|
| 426 |
+
|
| 427 |
+
|
| 428 |
+
def format_user_prompt(obs, max_steps: int) -> str:
|
| 429 |
+
return USER_PROMPT_TEMPLATE.format(
|
| 430 |
+
task_description=obs.task or "N/A",
|
| 431 |
+
max_steps=max_steps,
|
| 432 |
+
)
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
def format_observation(obs, max_steps: int) -> str:
|
| 436 |
+
if obs.done and obs.metadata and obs.metadata.get("message") == "Episode submitted":
|
| 437 |
+
return OBS_SUBMIT.format()
|
| 438 |
+
|
| 439 |
+
if obs.docs_results:
|
| 440 |
+
docs_text = "\n".join(doc[:800] for doc in obs.docs_results[:5])
|
| 441 |
+
return OBS_READ_DOCS.format(
|
| 442 |
+
docs_text=docs_text,
|
| 443 |
+
step_count=obs.step_count,
|
| 444 |
+
max_steps=max_steps,
|
| 445 |
+
)
|
| 446 |
+
|
| 447 |
+
if obs.code_error:
|
| 448 |
+
return OBS_EXECUTE_ERROR.format(
|
| 449 |
+
code_error=obs.code_error,
|
| 450 |
+
step_count=obs.step_count,
|
| 451 |
+
max_steps=max_steps,
|
| 452 |
+
)
|
| 453 |
+
|
| 454 |
+
if obs.code_executed is True:
|
| 455 |
+
safe_props = {}
|
| 456 |
+
if obs.object_properties:
|
| 457 |
+
safe_props = dict(obs.object_properties)
|
| 458 |
+
artifacts_list = []
|
| 459 |
+
if obs.artifacts:
|
| 460 |
+
artifacts_list = [a.get("object_id", "?") for a in obs.artifacts]
|
| 461 |
+
return OBS_EXECUTE_SUCCESS.format(
|
| 462 |
+
object_id=obs.object_id or "N/A",
|
| 463 |
+
properties_json=json.dumps(safe_props, indent=2),
|
| 464 |
+
artifacts=json.dumps(artifacts_list),
|
| 465 |
+
step_count=obs.step_count,
|
| 466 |
+
max_steps=max_steps,
|
| 467 |
+
)
|
| 468 |
+
|
| 469 |
+
if obs.code_error or (obs.metadata and "Unknown action_type" in str(obs.metadata)):
|
| 470 |
+
return OBS_UNKNOWN_ACTION.format(
|
| 471 |
+
error_message=obs.code_error or json.dumps(obs.metadata),
|
| 472 |
+
step_count=obs.step_count,
|
| 473 |
+
max_steps=max_steps,
|
| 474 |
+
)
|
| 475 |
+
|
| 476 |
+
parts = []
|
| 477 |
+
if obs.task:
|
| 478 |
+
parts.append(f"Task: {obs.task}")
|
| 479 |
+
parts.append(f"Step: {obs.step_count}/{max_steps}")
|
| 480 |
+
if obs.metadata:
|
| 481 |
+
parts.append(f"Info: {json.dumps(obs.metadata)}")
|
| 482 |
+
return "\n".join(parts)
|
| 483 |
+
|
| 484 |
+
|
| 485 |
+
def fallback_action(obs) -> Optional[Dict]:
|
| 486 |
+
if obs.done:
|
| 487 |
+
return None
|
| 488 |
+
return {"action_type": "read_docs", "params": {"topic": "basics"}}
|
| 489 |
+
|
| 490 |
+
|
| 491 |
+
def _extract_tool_call(text: str) -> Optional[Dict]:
|
| 492 |
+
tc_match = re.search(r'<tool_call>\s*(\{.*?\})\s*</tool_call>', text, re.DOTALL)
|
| 493 |
+
if tc_match:
|
| 494 |
+
try:
|
| 495 |
+
call = json.loads(tc_match.group(1))
|
| 496 |
+
name = call.get("name", "")
|
| 497 |
+
arguments = call.get("arguments", {})
|
| 498 |
+
return {"action_type": name, "params": arguments}
|
| 499 |
+
except json.JSONDecodeError:
|
| 500 |
+
pass
|
| 501 |
+
|
| 502 |
+
tc_open = re.search(r'<tool_call>\s*(\{.*)', text, re.DOTALL)
|
| 503 |
+
if tc_open:
|
| 504 |
+
raw = tc_open.group(1).strip()
|
| 505 |
+
raw = re.sub(r'</tool_call>.*', '', raw, flags=re.DOTALL).strip()
|
| 506 |
+
try:
|
| 507 |
+
call = json.loads(raw)
|
| 508 |
+
name = call.get("name", "")
|
| 509 |
+
arguments = call.get("arguments", {})
|
| 510 |
+
if name:
|
| 511 |
+
return {"action_type": name, "params": arguments}
|
| 512 |
+
except json.JSONDecodeError:
|
| 513 |
+
brace_depth = 0
|
| 514 |
+
end_idx = -1
|
| 515 |
+
for i, ch in enumerate(raw):
|
| 516 |
+
if ch == '{':
|
| 517 |
+
brace_depth += 1
|
| 518 |
+
elif ch == '}':
|
| 519 |
+
brace_depth -= 1
|
| 520 |
+
if brace_depth == 0:
|
| 521 |
+
end_idx = i
|
| 522 |
+
break
|
| 523 |
+
if end_idx > 0:
|
| 524 |
+
try:
|
| 525 |
+
call = json.loads(raw[:end_idx + 1])
|
| 526 |
+
name = call.get("name", "")
|
| 527 |
+
arguments = call.get("arguments", {})
|
| 528 |
+
if name:
|
| 529 |
+
return {"action_type": name, "params": arguments}
|
| 530 |
+
except json.JSONDecodeError:
|
| 531 |
+
pass
|
| 532 |
+
|
| 533 |
+
text_clean = text.strip().strip("`")
|
| 534 |
+
if text_clean.startswith("json"):
|
| 535 |
+
text_clean = text_clean[4:].strip()
|
| 536 |
+
try:
|
| 537 |
+
parsed = json.loads(text_clean)
|
| 538 |
+
if "action_type" in parsed:
|
| 539 |
+
return parsed
|
| 540 |
+
if "name" in parsed:
|
| 541 |
+
return {"action_type": parsed["name"], "params": parsed.get("arguments", {})}
|
| 542 |
+
except json.JSONDecodeError:
|
| 543 |
+
pass
|
| 544 |
+
|
| 545 |
+
match = re.search(r'\{[^{}]*"action_type"[^{}]*\}', text, re.DOTALL)
|
| 546 |
+
if match:
|
| 547 |
+
try:
|
| 548 |
+
return json.loads(match.group())
|
| 549 |
+
except json.JSONDecodeError:
|
| 550 |
+
pass
|
| 551 |
+
|
| 552 |
+
match = re.search(r'\{[^{}]*"name"\s*:\s*"(read_docs|execute_cadquery|submit)".*?\}', text, re.DOTALL)
|
| 553 |
+
if match:
|
| 554 |
+
try:
|
| 555 |
+
call = json.loads(match.group())
|
| 556 |
+
return {"action_type": call["name"], "params": call.get("arguments", {})}
|
| 557 |
+
except json.JSONDecodeError:
|
| 558 |
+
pass
|
| 559 |
+
|
| 560 |
+
code_match = re.search(r'```python\s*(.*?)```', text, re.DOTALL)
|
| 561 |
+
if code_match:
|
| 562 |
+
code = code_match.group(1).strip()
|
| 563 |
+
if "import cadquery" in code or "cq." in code:
|
| 564 |
+
return {"action_type": "execute_cadquery", "params": {"code": code}}
|
| 565 |
+
|
| 566 |
+
return None
|
| 567 |
+
|
| 568 |
+
|
| 569 |
+
RETRY_NUDGE = "Your previous response did not contain a valid <tool_call> block and was discarded. You MUST respond with exactly one <tool_call></tool_call> block. Respond now with a tool call."
|
| 570 |
+
|
| 571 |
+
|
| 572 |
+
def _build_messages(history: List[Dict], obs, is_first_turn: bool, max_steps: int, nudge: Optional[str] = None) -> List[Dict]:
|
| 573 |
+
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
|
| 574 |
+
|
| 575 |
+
for h in history[-6:]:
|
| 576 |
+
action = h["action"]
|
| 577 |
+
if action.get("action_type") == "execute_cadquery":
|
| 578 |
+
tc_json = json.dumps({"name": "execute_cadquery", "arguments": {"code": action["params"].get("code", "")}})
|
| 579 |
+
elif action.get("action_type") == "submit":
|
| 580 |
+
tc_json = json.dumps({"name": "submit", "arguments": action.get("params", {})})
|
| 581 |
+
else:
|
| 582 |
+
tc_json = json.dumps({"name": action.get("action_type", "read_docs"), "arguments": action.get("params", {})})
|
| 583 |
+
messages.append({"role": "assistant", "content": f"<tool_call>\n{tc_json}\n</tool_call>"})
|
| 584 |
+
messages.append({"role": "user", "content": h["observation"]})
|
| 585 |
+
|
| 586 |
+
if is_first_turn:
|
| 587 |
+
messages.append({"role": "user", "content": format_user_prompt(obs, max_steps)})
|
| 588 |
+
else:
|
| 589 |
+
messages.append({"role": "user", "content": format_observation(obs, max_steps)})
|
| 590 |
+
|
| 591 |
+
if nudge:
|
| 592 |
+
messages.append({"role": "user", "content": nudge})
|
| 593 |
+
|
| 594 |
+
return messages
|
| 595 |
+
|
| 596 |
+
|
| 597 |
+
def get_model_action(client: OpenAI, obs, history: List[Dict], is_first_turn: bool = False, max_steps: int = MAX_STEPS_DEFAULT, current_step: int = 0) -> Optional[Dict]:
|
| 598 |
+
for attempt in range(2):
|
| 599 |
+
nudge = RETRY_NUDGE if attempt > 0 else None
|
| 600 |
+
messages = _build_messages(history, obs, is_first_turn, max_steps, nudge=nudge)
|
| 601 |
+
|
| 602 |
+
try:
|
| 603 |
+
t0 = time.time()
|
| 604 |
+
completion = client.chat.completions.create(
|
| 605 |
+
model=MODEL_NAME,
|
| 606 |
+
messages=messages,
|
| 607 |
+
temperature=TEMPERATURE,
|
| 608 |
+
max_tokens=MAX_TOKENS,
|
| 609 |
+
stream=False,
|
| 610 |
+
)
|
| 611 |
+
elapsed = time.time() - t0
|
| 612 |
+
text = (completion.choices[0].message.content or "").strip()
|
| 613 |
+
print(f"[LLM] attempt={attempt} response in {elapsed:.1f}s: {text[:300]}", flush=True)
|
| 614 |
+
|
| 615 |
+
parsed = _extract_tool_call(text)
|
| 616 |
+
if parsed is not None:
|
| 617 |
+
return parsed
|
| 618 |
+
print(f"[DEBUG] Could not parse tool call (attempt {attempt}): {text[:500]}", flush=True)
|
| 619 |
+
except Exception as exc:
|
| 620 |
+
print(f"[DEBUG] Model request failed (attempt {attempt}): {exc}", flush=True)
|
| 621 |
+
|
| 622 |
+
print(f"[DEBUG] All parse attempts failed, using fallback (step {current_step}/{max_steps})", flush=True)
|
| 623 |
+
if current_step >= max_steps - 1 and obs.object_id:
|
| 624 |
+
return {"action_type": "submit", "params": {"object_id": obs.object_id}}
|
| 625 |
+
return fallback_action(obs)
|
| 626 |
+
|
| 627 |
+
|
| 628 |
+
async def run_task(task_id: str, llm: OpenAI) -> float:
|
| 629 |
+
if LOCAL_IMAGE_NAME:
|
| 630 |
+
env = await CadforgeEnv.from_docker_image(LOCAL_IMAGE_NAME)
|
| 631 |
+
else:
|
| 632 |
+
base_url = os.getenv("ENV_BASE_URL", "http://localhost:8000")
|
| 633 |
+
env = CadforgeEnv(base_url=base_url)
|
| 634 |
+
|
| 635 |
+
rewards: List[float] = []
|
| 636 |
+
steps_taken = 0
|
| 637 |
+
score = 0.0
|
| 638 |
+
success = False
|
| 639 |
+
max_steps = MAX_STEPS_DEFAULT
|
| 640 |
+
|
| 641 |
+
log_start(task=task_id, env=BENCHMARK, model=MODEL_NAME)
|
| 642 |
+
|
| 643 |
+
try:
|
| 644 |
+
await env.connect()
|
| 645 |
+
result = await env.reset(task_id=task_id)
|
| 646 |
+
obs = result.observation
|
| 647 |
+
|
| 648 |
+
if obs.metadata and "max_steps" in obs.metadata:
|
| 649 |
+
max_steps = obs.metadata["max_steps"]
|
| 650 |
+
print(f"[INFO] max_steps loaded from task: {max_steps}", flush=True)
|
| 651 |
+
|
| 652 |
+
history: List[Dict] = []
|
| 653 |
+
consecutive_failures = 0
|
| 654 |
+
|
| 655 |
+
for step in range(1, max_steps + 1):
|
| 656 |
+
if result.done:
|
| 657 |
+
break
|
| 658 |
+
|
| 659 |
+
is_first_turn = step == 1
|
| 660 |
+
action_dict = get_model_action(llm, obs, history, is_first_turn=is_first_turn, max_steps=max_steps, current_step=step)
|
| 661 |
+
|
| 662 |
+
if action_dict is None:
|
| 663 |
+
consecutive_failures += 1
|
| 664 |
+
if consecutive_failures >= 3:
|
| 665 |
+
print("[DEBUG] LLM unavailable and no valid fallback. Ending task.", flush=True)
|
| 666 |
+
break
|
| 667 |
+
continue
|
| 668 |
+
else:
|
| 669 |
+
consecutive_failures = 0
|
| 670 |
+
|
| 671 |
+
action_type = action_dict.get("action_type", "read_docs")
|
| 672 |
+
params = action_dict.get("params", {})
|
| 673 |
+
|
| 674 |
+
try:
|
| 675 |
+
action = CadforgeAction(action_type=action_type, params=params)
|
| 676 |
+
except Exception:
|
| 677 |
+
action = CadforgeAction(action_type="read_docs", params={"topic": "basics"})
|
| 678 |
+
action_dict = {"action_type": "read_docs", "params": {"topic": "basics"}}
|
| 679 |
+
|
| 680 |
+
result = await env.step(action)
|
| 681 |
+
obs = result.observation
|
| 682 |
+
|
| 683 |
+
reward = result.reward or 0.0
|
| 684 |
+
done = result.done
|
| 685 |
+
error = obs.code_error if obs.code_error else None
|
| 686 |
+
|
| 687 |
+
rewards.append(reward)
|
| 688 |
+
steps_taken = step
|
| 689 |
+
|
| 690 |
+
action_str = f"{action_type}({json.dumps(params)[:100]})"
|
| 691 |
+
log_step(step=step, action=action_str, reward=reward, done=done, error=error)
|
| 692 |
+
|
| 693 |
+
history.append({
|
| 694 |
+
"action": action_dict,
|
| 695 |
+
"observation": format_observation(obs, max_steps),
|
| 696 |
+
})
|
| 697 |
+
|
| 698 |
+
if done:
|
| 699 |
+
break
|
| 700 |
+
|
| 701 |
+
score = rewards[-1] if rewards else 0.0
|
| 702 |
+
score = min(max(score, 0.0), 1.0)
|
| 703 |
+
success = score > 0.5
|
| 704 |
+
|
| 705 |
+
finally:
|
| 706 |
+
try:
|
| 707 |
+
await env.close()
|
| 708 |
+
except Exception as e:
|
| 709 |
+
print(f"[DEBUG] env.close() error: {e}", flush=True)
|
| 710 |
+
log_end(success=success, steps=steps_taken, score=score, rewards=rewards)
|
| 711 |
+
|
| 712 |
+
return score
|
| 713 |
+
|
| 714 |
+
|
| 715 |
+
async def main() -> None:
|
| 716 |
+
llm = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN)
|
| 717 |
+
|
| 718 |
+
task_filter = os.getenv("TASKS")
|
| 719 |
+
if task_filter:
|
| 720 |
+
tasks = [t.strip() for t in task_filter.split(",")]
|
| 721 |
+
else:
|
| 722 |
+
tasks = TASKS
|
| 723 |
+
|
| 724 |
+
scores = []
|
| 725 |
+
for task_id in tasks:
|
| 726 |
+
score = await run_task(task_id, llm)
|
| 727 |
+
scores.append(score)
|
| 728 |
+
|
| 729 |
+
avg = sum(scores) / len(scores) if scores else 0.0
|
| 730 |
+
print(f"\n[SUMMARY] tasks={len(tasks)} avg_score={avg:.4f} scores={','.join(f'{s:.4f}' for s in scores)}", flush=True)
|
| 731 |
+
|
| 732 |
+
|
| 733 |
+
if __name__ == "__main__":
|
| 734 |
+
asyncio.run(main())
|
models.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any, Dict, List, Optional
|
| 2 |
+
|
| 3 |
+
from openenv.core.env_server.types import Action, Observation
|
| 4 |
+
from pydantic import Field, PrivateAttr
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class CadforgeAction(Action):
|
| 8 |
+
action_type: str = Field(
|
| 9 |
+
...,
|
| 10 |
+
description="One of: read_docs | execute_cadquery | submit | render_object",
|
| 11 |
+
)
|
| 12 |
+
params: Dict[str, Any] = Field(
|
| 13 |
+
default_factory=dict,
|
| 14 |
+
description="Parameters for the action",
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class CadforgeObservation(Observation):
|
| 19 |
+
task: Optional[str] = Field(
|
| 20 |
+
default=None,
|
| 21 |
+
description="Task prompt provided on reset",
|
| 22 |
+
)
|
| 23 |
+
step_count: int = Field(
|
| 24 |
+
default=0,
|
| 25 |
+
description="Number of steps taken so far",
|
| 26 |
+
)
|
| 27 |
+
docs_results: Optional[List[str]] = Field(
|
| 28 |
+
default=None,
|
| 29 |
+
description="Documentation paragraphs returned by read_docs",
|
| 30 |
+
)
|
| 31 |
+
code_executed: Optional[bool] = Field(
|
| 32 |
+
default=None,
|
| 33 |
+
description="True if code ran successfully, False if errored, None if no code run",
|
| 34 |
+
)
|
| 35 |
+
code_error: Optional[str] = Field(
|
| 36 |
+
default=None,
|
| 37 |
+
description="Exception message if code_executed is False",
|
| 38 |
+
)
|
| 39 |
+
object_id: Optional[str] = Field(
|
| 40 |
+
default=None,
|
| 41 |
+
description="Unique identifier for the shape produced by execute_cadquery",
|
| 42 |
+
)
|
| 43 |
+
object_properties: Optional[Dict[str, Any]] = Field(
|
| 44 |
+
default=None,
|
| 45 |
+
description="Geometric properties of the shape produced by execute_cadquery",
|
| 46 |
+
)
|
| 47 |
+
artifacts: Optional[List[Dict[str, Any]]] = Field(
|
| 48 |
+
default=None,
|
| 49 |
+
description="Registry of all shapes created so far: [{object_id, step_path}]",
|
| 50 |
+
)
|
| 51 |
+
last_executed: Optional[str] = Field(
|
| 52 |
+
default=None,
|
| 53 |
+
description="Object ID of the most recently executed shape",
|
| 54 |
+
)
|
| 55 |
+
image_path: Optional[str] = Field(
|
| 56 |
+
default=None,
|
| 57 |
+
description="Path to rendered PNG (Phase 2 only)",
|
| 58 |
+
)
|
| 59 |
+
_raw_data: Optional[Dict[str, Any]] = PrivateAttr(default=None)
|
openenv.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
spec_version: 1
|
| 2 |
+
name: cadforge
|
| 3 |
+
type: space
|
| 4 |
+
runtime: fastapi
|
| 5 |
+
app: server.app:app
|
| 6 |
+
port: 8000
|
| 7 |
+
|
pyproject.toml
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
[build-system]
|
| 8 |
+
requires = ["setuptools>=45", "wheel"]
|
| 9 |
+
build-backend = "setuptools.build_meta"
|
| 10 |
+
|
| 11 |
+
[project]
|
| 12 |
+
name = "openenv-cadforge"
|
| 13 |
+
version = "0.1.0"
|
| 14 |
+
description = "Cadforge environment for OpenEnv"
|
| 15 |
+
requires-python = ">=3.10"
|
| 16 |
+
dependencies = [
|
| 17 |
+
"openenv-core[core]>=0.2.2",
|
| 18 |
+
"cadquery>=2.4.0",
|
| 19 |
+
"numpy>=1.24.0",
|
| 20 |
+
"scipy>=1.10.0",
|
| 21 |
+
"trimesh>=4.0.0",
|
| 22 |
+
"embreex>=4.0.0",
|
| 23 |
+
"networkx>=3.0.0",
|
| 24 |
+
"pydantic>=2.0.0",
|
| 25 |
+
]
|
| 26 |
+
|
| 27 |
+
[project.optional-dependencies]
|
| 28 |
+
dev = [
|
| 29 |
+
"pytest>=8.0.0",
|
| 30 |
+
"pytest-cov>=4.0.0",
|
| 31 |
+
]
|
| 32 |
+
|
| 33 |
+
[project.scripts]
|
| 34 |
+
# Server entry point - enables running via: uv run --project . server
|
| 35 |
+
# or: python -m cadforge.server.app
|
| 36 |
+
server = "cadforge.server.app:main"
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
[tool.setuptools]
|
| 40 |
+
include-package-data = true
|
| 41 |
+
packages = ["cadforge", "cadforge.server"]
|
| 42 |
+
package-dir = { "cadforge" = ".", "cadforge.server" = "server" }
|
scripts/generate_ground_truth.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Generate ground truth files (ground_truth.json, .npy) for all tasks.
|
| 3 |
+
|
| 4 |
+
Usage:
|
| 5 |
+
python scripts/generate_ground_truth.py
|
| 6 |
+
python scripts/generate_ground_truth.py --task task_001_flat_plate
|
| 7 |
+
"""
|
| 8 |
+
import argparse
|
| 9 |
+
import json
|
| 10 |
+
import sys
|
| 11 |
+
import time
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
|
| 14 |
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 15 |
+
|
| 16 |
+
TASKS_ROOT = Path(__file__).parent.parent / "server" / "tasks"
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def generate_one(task_dir: Path) -> dict:
|
| 20 |
+
task_id = task_dir.name
|
| 21 |
+
t0 = time.time()
|
| 22 |
+
|
| 23 |
+
result = {
|
| 24 |
+
"task_id": task_id,
|
| 25 |
+
"success": False,
|
| 26 |
+
"error": None,
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
try:
|
| 30 |
+
from server.preprocessor import preprocess_from_code
|
| 31 |
+
|
| 32 |
+
ref_code = (task_dir / "reference_code.py").read_text()
|
| 33 |
+
gt = preprocess_from_code(ref_code, str(task_dir), task_id=task_id)
|
| 34 |
+
|
| 35 |
+
result["success"] = True
|
| 36 |
+
result["volume"] = gt.get("volume_mm3")
|
| 37 |
+
result["bbox"] = gt.get("bbox_mm")
|
| 38 |
+
result["euler"] = gt.get("euler_characteristic")
|
| 39 |
+
result["dominant_face_type"] = gt.get("dominant_face_type")
|
| 40 |
+
result["face_count"] = gt.get("face_count")
|
| 41 |
+
|
| 42 |
+
except Exception as e:
|
| 43 |
+
import traceback
|
| 44 |
+
result["error"] = f"{type(e).__name__}: {e}"
|
| 45 |
+
result["traceback"] = traceback.format_exc()
|
| 46 |
+
|
| 47 |
+
result["elapsed_s"] = round(time.time() - t0, 2)
|
| 48 |
+
return result
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def main():
|
| 52 |
+
parser = argparse.ArgumentParser(description="Generate ground truth for CadForge tasks")
|
| 53 |
+
parser.add_argument("--task", type=str, default=None, help="Specific task id")
|
| 54 |
+
args = parser.parse_args()
|
| 55 |
+
|
| 56 |
+
if args.task:
|
| 57 |
+
task_dirs = [TASKS_ROOT / args.task]
|
| 58 |
+
else:
|
| 59 |
+
task_dirs = sorted(TASKS_ROOT.glob("task_*"))
|
| 60 |
+
|
| 61 |
+
print(f"Generating ground truth for {len(task_dirs)} tasks...")
|
| 62 |
+
print("-" * 80)
|
| 63 |
+
|
| 64 |
+
results = []
|
| 65 |
+
for task_dir in task_dirs:
|
| 66 |
+
r = generate_one(task_dir)
|
| 67 |
+
status = "OK" if r["success"] else "FAIL"
|
| 68 |
+
print(
|
| 69 |
+
f" {r['task_id']:35s} {status:5s} "
|
| 70 |
+
f"vol={r.get('volume', 'N/A')} "
|
| 71 |
+
f"euler={r.get('euler', 'N/A')} "
|
| 72 |
+
f"faces={r.get('face_count', 'N/A')} "
|
| 73 |
+
f"({r['elapsed_s']:.1f}s)"
|
| 74 |
+
)
|
| 75 |
+
if r["error"]:
|
| 76 |
+
print(f" ERROR: {r['error']}")
|
| 77 |
+
results.append(r)
|
| 78 |
+
|
| 79 |
+
passed = sum(1 for r in results if r["success"])
|
| 80 |
+
print("-" * 80)
|
| 81 |
+
print(f"Result: {passed}/{len(results)} generated successfully")
|
| 82 |
+
|
| 83 |
+
out_path = TASKS_ROOT.parent / "verification_ground_truth.json"
|
| 84 |
+
with open(out_path, "w") as f:
|
| 85 |
+
json.dump(results, f, indent=2)
|
| 86 |
+
print(f"Saved to {out_path}")
|
| 87 |
+
|
| 88 |
+
return 0 if passed == len(results) else 1
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
if __name__ == "__main__":
|
| 92 |
+
sys.exit(main())
|
scripts/generate_tasks_thomasmaker.py
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Generate task files from the ThomasTheMaker/cadquery HuggingFace dataset.
|
| 3 |
+
|
| 4 |
+
Pipeline per example:
|
| 5 |
+
1. Execute CadQuery code -> get shape
|
| 6 |
+
2. Run preprocess_from_code -> ground_truth.step, ground_truth.json, .npy
|
| 7 |
+
3. Send HF image + code + ground_truth.json to Claude Sonnet -> get NL prompt
|
| 8 |
+
4. Write task.json, reference_code.py
|
| 9 |
+
|
| 10 |
+
Usage:
|
| 11 |
+
python scripts/generate_tasks_thomasmaker.py --limit 3 # test on 3
|
| 12 |
+
python scripts/generate_tasks_thomasmaker.py # all 50
|
| 13 |
+
python scripts/generate_tasks_thomasmaker.py --dry-run # just show prompts
|
| 14 |
+
"""
|
| 15 |
+
import argparse
|
| 16 |
+
import base64
|
| 17 |
+
import io
|
| 18 |
+
import json
|
| 19 |
+
import logging
|
| 20 |
+
import math
|
| 21 |
+
import os
|
| 22 |
+
import re
|
| 23 |
+
import sys
|
| 24 |
+
import time
|
| 25 |
+
import traceback
|
| 26 |
+
from pathlib import Path
|
| 27 |
+
|
| 28 |
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 29 |
+
|
| 30 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
| 31 |
+
logger = logging.getLogger(__name__)
|
| 32 |
+
|
| 33 |
+
TASKS_ROOT = Path(__file__).parent.parent / "server" / "tasks"
|
| 34 |
+
SELECTED_PATH = Path(__file__).parent.parent / "selected_50.json"
|
| 35 |
+
START_TASK_NUM = 21
|
| 36 |
+
|
| 37 |
+
def _load_api_key():
|
| 38 |
+
key = os.environ.get("ANTHROPIC_API_KEY") or os.environ.get("ANTHROPIC_KEY")
|
| 39 |
+
if key:
|
| 40 |
+
return key
|
| 41 |
+
env_path = Path(__file__).parent.parent.parent / ".env"
|
| 42 |
+
if env_path.exists():
|
| 43 |
+
for line in env_path.read_text().splitlines():
|
| 44 |
+
line = line.strip()
|
| 45 |
+
if line.startswith("ANTHROPIC_KEY="):
|
| 46 |
+
return line.split("=", 1)[1].strip().strip("'\"")
|
| 47 |
+
if line.startswith("ANTHROPIC_API_KEY="):
|
| 48 |
+
return line.split("=", 1)[1].strip().strip("'\"")
|
| 49 |
+
return ""
|
| 50 |
+
|
| 51 |
+
ANTHROPIC_API_KEY = _load_api_key()
|
| 52 |
+
|
| 53 |
+
PROMPT_SYSTEM = """You are a CAD engineering assistant. You will be shown an image of a 3D CAD part along with its geometric properties. Your job is to write a clear, detailed natural language description that a CAD engineer could use to recreate this exact part in CadQuery.
|
| 54 |
+
|
| 55 |
+
Rules:
|
| 56 |
+
- Write in natural language, NOT code. Do not use any code syntax.
|
| 57 |
+
- Be specific about dimensions in millimeters (use the bbox and geometry data provided).
|
| 58 |
+
- Describe the construction steps: what base shape, what features are added/removed, what boolean operations.
|
| 59 |
+
- Mention the number and types of faces if it helps clarify the geometry.
|
| 60 |
+
- Describe holes, bosses, pockets, curved surfaces, arcs, fillets if visible.
|
| 61 |
+
- Mention symmetry if present.
|
| 62 |
+
- End with "Orient the longest axis along X."
|
| 63 |
+
- Keep it to one clear paragraph, 3-8 sentences.
|
| 64 |
+
- Do NOT start with "Create a" - vary your openings."""
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def image_to_base64(pil_image):
|
| 68 |
+
buf = io.BytesIO()
|
| 69 |
+
pil_image.save(buf, format="PNG")
|
| 70 |
+
return base64.standard_b64encode(buf.getvalue()).decode("utf-8")
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def call_claude_for_prompt(pil_image, gt_json, code, code_info):
|
| 74 |
+
import anthropic
|
| 75 |
+
client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)
|
| 76 |
+
|
| 77 |
+
img_b64 = image_to_base64(pil_image)
|
| 78 |
+
|
| 79 |
+
user_text = f"""Here is the ground truth geometry data for this 3D part:
|
| 80 |
+
|
| 81 |
+
{json.dumps(gt_json, indent=2)}
|
| 82 |
+
|
| 83 |
+
Here is the CadQuery code that generates this part:
|
| 84 |
+
|
| 85 |
+
```python
|
| 86 |
+
{code}
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
Additional construction info:
|
| 90 |
+
- Number of extrusions: {code_info.get('extrudes', 0)}
|
| 91 |
+
- Number of union operations: {code_info.get('unions', 0)}
|
| 92 |
+
- Number of cut operations: {code_info.get('cuts', 0)}
|
| 93 |
+
- Number of arc segments: {code_info.get('arcs', 0)}
|
| 94 |
+
- Number of circular features: {code_info.get('circles', 0)}
|
| 95 |
+
|
| 96 |
+
Look at the image carefully along with the ground truth data and the code above. Write a detailed natural language prompt describing this part so someone could recreate it in CadQuery."""
|
| 97 |
+
|
| 98 |
+
t0 = time.time()
|
| 99 |
+
response = client.messages.create(
|
| 100 |
+
model="claude-sonnet-4-20250514",
|
| 101 |
+
max_tokens=700,
|
| 102 |
+
system=PROMPT_SYSTEM,
|
| 103 |
+
messages=[
|
| 104 |
+
{
|
| 105 |
+
"role": "user",
|
| 106 |
+
"content": [
|
| 107 |
+
{
|
| 108 |
+
"type": "image",
|
| 109 |
+
"source": {
|
| 110 |
+
"type": "base64",
|
| 111 |
+
"media_type": "image/png",
|
| 112 |
+
"data": img_b64,
|
| 113 |
+
},
|
| 114 |
+
},
|
| 115 |
+
{
|
| 116 |
+
"type": "text",
|
| 117 |
+
"text": user_text,
|
| 118 |
+
},
|
| 119 |
+
],
|
| 120 |
+
}
|
| 121 |
+
],
|
| 122 |
+
)
|
| 123 |
+
elapsed = time.time() - t0
|
| 124 |
+
prompt_text = response.content[0].text.strip()
|
| 125 |
+
logger.info(f" Claude API call took {elapsed:.1f}s, prompt length={len(prompt_text)}")
|
| 126 |
+
return prompt_text
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def execute_code(code):
|
| 130 |
+
import cadquery as cq
|
| 131 |
+
adapted = code.rstrip()
|
| 132 |
+
if "\nresult" not in adapted and "\nresult " not in adapted:
|
| 133 |
+
last_solid = None
|
| 134 |
+
for m in re.finditer(r"^(solid\w*)\s*=", adapted, re.MULTILINE):
|
| 135 |
+
last_solid = m.group(1)
|
| 136 |
+
if last_solid:
|
| 137 |
+
adapted += f"\nresult = {last_solid}"
|
| 138 |
+
else:
|
| 139 |
+
adapted += "\nresult = solid"
|
| 140 |
+
|
| 141 |
+
local_ns = {"cq": cq, "cadquery": cq, "math": math}
|
| 142 |
+
exec(adapted, local_ns)
|
| 143 |
+
|
| 144 |
+
result = local_ns.get("result")
|
| 145 |
+
if result is None:
|
| 146 |
+
raise ValueError("No 'result' variable after execution")
|
| 147 |
+
|
| 148 |
+
if hasattr(result, "val"):
|
| 149 |
+
shape = result.val()
|
| 150 |
+
else:
|
| 151 |
+
shape = result
|
| 152 |
+
|
| 153 |
+
bb = shape.BoundingBox()
|
| 154 |
+
if bb.xlen < 1e-6 and bb.ylen < 1e-6 and bb.zlen < 1e-6:
|
| 155 |
+
raise ValueError("Degenerate shape (zero bbox)")
|
| 156 |
+
|
| 157 |
+
return shape, adapted
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def analyze_geometry(shape, code_info):
|
| 161 |
+
from server.geometry import extract_properties
|
| 162 |
+
props = extract_properties(shape)
|
| 163 |
+
|
| 164 |
+
bb = shape.BoundingBox()
|
| 165 |
+
bbox = [round(bb.xlen, 4), round(bb.ylen, 4), round(bb.zlen, 4)]
|
| 166 |
+
|
| 167 |
+
return {
|
| 168 |
+
"bbox": bbox,
|
| 169 |
+
"volume": props.get("volume_mm3", 0),
|
| 170 |
+
"surface_area": props.get("surface_area_mm2", 0),
|
| 171 |
+
"face_count": props.get("face_count", 0),
|
| 172 |
+
"face_type_counts": props.get("face_type_counts", {}),
|
| 173 |
+
"dominant_face_type": props.get("dominant_face_type", ""),
|
| 174 |
+
"euler": props.get("euler_characteristic", 2),
|
| 175 |
+
"shape_class": props.get("shape_class", "COMPLEX_SOLID"),
|
| 176 |
+
"edge_count": props.get("edge_count", 0),
|
| 177 |
+
"vertex_count": props.get("vertex_count", 0),
|
| 178 |
+
"has_xy_symmetry": props.get("has_xy_symmetry", False),
|
| 179 |
+
"has_xz_symmetry": props.get("has_xz_symmetry", False),
|
| 180 |
+
"has_yz_symmetry": props.get("has_yz_symmetry", False),
|
| 181 |
+
"extrudes": code_info.get("extrudes", 0),
|
| 182 |
+
"unions": code_info.get("unions", 0),
|
| 183 |
+
"cuts": code_info.get("cuts", 0),
|
| 184 |
+
"arcs": code_info.get("arcs", 0),
|
| 185 |
+
"circles": code_info.get("circles", 0),
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def difficulty_bin(label, score):
|
| 190 |
+
if label == "medium":
|
| 191 |
+
return min(5, max(3, 3 + (score - 10) // 3))
|
| 192 |
+
elif label == "hard":
|
| 193 |
+
return min(7, max(5, 5 + (score - 19) // 6))
|
| 194 |
+
else:
|
| 195 |
+
return min(9, max(7, 7 + (score - 35) // 15))
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def generate_one_task(ds, info, task_num, dry_run=False):
|
| 199 |
+
t0 = time.time()
|
| 200 |
+
idx = info["idx"]
|
| 201 |
+
row = ds[idx]
|
| 202 |
+
code = row["texts"][0]["assistant"]
|
| 203 |
+
pil_image = row["images"][0]
|
| 204 |
+
label = info["difficulty_label"]
|
| 205 |
+
|
| 206 |
+
task_id = f"task_{task_num:03d}_hf_{idx}"
|
| 207 |
+
task_dir = TASKS_ROOT / task_id
|
| 208 |
+
|
| 209 |
+
logger.info(f"[{task_num}] Processing {task_id} (hf_idx={idx}, score={info['score']}, {label})")
|
| 210 |
+
|
| 211 |
+
try:
|
| 212 |
+
shape, adapted_code = execute_code(code)
|
| 213 |
+
logger.info(f" Code executed OK")
|
| 214 |
+
except Exception as e:
|
| 215 |
+
logger.error(f" EXEC FAIL: {e}")
|
| 216 |
+
return {"task_id": task_id, "success": False, "error": f"exec: {e}",
|
| 217 |
+
"elapsed_s": round(time.time() - t0, 2)}
|
| 218 |
+
|
| 219 |
+
task_dir.mkdir(parents=True, exist_ok=True)
|
| 220 |
+
|
| 221 |
+
with open(task_dir / "reference_code.py", "w") as f:
|
| 222 |
+
f.write(adapted_code)
|
| 223 |
+
|
| 224 |
+
try:
|
| 225 |
+
from server.preprocessor import preprocess_from_code
|
| 226 |
+
gt = preprocess_from_code(adapted_code, str(task_dir), task_id=task_id)
|
| 227 |
+
logger.info(f" GT OK: vol={gt.get('volume_mm3')}, bbox={gt.get('bbox_mm')}")
|
| 228 |
+
except Exception as e:
|
| 229 |
+
logger.error(f" GT FAIL: {e}")
|
| 230 |
+
logger.error(traceback.format_exc())
|
| 231 |
+
return {"task_id": task_id, "success": False, "error": f"gt: {e}",
|
| 232 |
+
"elapsed_s": round(time.time() - t0, 2)}
|
| 233 |
+
|
| 234 |
+
gt_json_path = task_dir / "ground_truth.json"
|
| 235 |
+
with open(gt_json_path) as f:
|
| 236 |
+
gt_json = json.load(f)
|
| 237 |
+
|
| 238 |
+
try:
|
| 239 |
+
nl_prompt = call_claude_for_prompt(pil_image, gt_json, code, info)
|
| 240 |
+
except Exception as e:
|
| 241 |
+
logger.error(f" PROMPT FAIL: {e}")
|
| 242 |
+
return {"task_id": task_id, "success": False, "error": f"prompt: {e}",
|
| 243 |
+
"elapsed_s": round(time.time() - t0, 2)}
|
| 244 |
+
|
| 245 |
+
d_bin = difficulty_bin(label, info["score"])
|
| 246 |
+
max_steps = 20 if label == "medium" else (25 if label == "hard" else 30)
|
| 247 |
+
|
| 248 |
+
task_json = {
|
| 249 |
+
"id": task_id,
|
| 250 |
+
"part_class": gt_json.get("dominant_face_type", "complex").lower(),
|
| 251 |
+
"difficulty_bin": d_bin,
|
| 252 |
+
"max_steps": max_steps,
|
| 253 |
+
"prompt": nl_prompt,
|
| 254 |
+
"ground_truth_step": f"tasks/{task_id}/ground_truth.step",
|
| 255 |
+
"ground_truth_json": f"tasks/{task_id}/ground_truth.json",
|
| 256 |
+
"reference_code": f"tasks/{task_id}/reference_code.py",
|
| 257 |
+
"source": "ThomasTheMaker/cadquery",
|
| 258 |
+
"hf_index": idx,
|
| 259 |
+
"complexity_score": info["score"],
|
| 260 |
+
"difficulty_label": label,
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
with open(task_dir / "task.json", "w") as f:
|
| 264 |
+
json.dump(task_json, f, indent=2)
|
| 265 |
+
|
| 266 |
+
elapsed = round(time.time() - t0, 2)
|
| 267 |
+
logger.info(f" DONE {task_id} ({elapsed}s)")
|
| 268 |
+
return {"task_id": task_id, "success": True, "elapsed_s": elapsed,
|
| 269 |
+
"prompt_preview": nl_prompt[:150],
|
| 270 |
+
"volume": gt.get("volume_mm3"), "face_count": gt.get("face_count")}
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
def main():
|
| 274 |
+
parser = argparse.ArgumentParser()
|
| 275 |
+
parser.add_argument("--dry-run", action="store_true")
|
| 276 |
+
parser.add_argument("--limit", type=int, default=None)
|
| 277 |
+
parser.add_argument("--start-num", type=int, default=START_TASK_NUM)
|
| 278 |
+
args = parser.parse_args()
|
| 279 |
+
|
| 280 |
+
t0_total = time.time()
|
| 281 |
+
|
| 282 |
+
if not ANTHROPIC_API_KEY:
|
| 283 |
+
logger.error("ANTHROPIC_API_KEY not set")
|
| 284 |
+
return 1
|
| 285 |
+
|
| 286 |
+
with open(SELECTED_PATH) as f:
|
| 287 |
+
selected = json.load(f)
|
| 288 |
+
logger.info(f"Loaded {len(selected)} selected examples")
|
| 289 |
+
|
| 290 |
+
if args.limit:
|
| 291 |
+
selected = selected[:args.limit]
|
| 292 |
+
|
| 293 |
+
from datasets import load_dataset
|
| 294 |
+
logger.info("Loading HF dataset...")
|
| 295 |
+
ds = load_dataset("ThomasTheMaker/cadquery", split="train")
|
| 296 |
+
logger.info(f"Loaded {len(ds)} rows")
|
| 297 |
+
|
| 298 |
+
results = []
|
| 299 |
+
task_num = args.start_num
|
| 300 |
+
success_count = 0
|
| 301 |
+
fail_count = 0
|
| 302 |
+
|
| 303 |
+
for i, info in enumerate(selected):
|
| 304 |
+
r = generate_one_task(ds, info, task_num, dry_run=args.dry_run)
|
| 305 |
+
results.append(r)
|
| 306 |
+
if r.get("success"):
|
| 307 |
+
success_count += 1
|
| 308 |
+
else:
|
| 309 |
+
fail_count += 1
|
| 310 |
+
task_num += 1
|
| 311 |
+
|
| 312 |
+
if (i + 1) % 5 == 0:
|
| 313 |
+
logger.info(f"=== Progress: {i+1}/{len(selected)} (ok={success_count}, fail={fail_count}) ===")
|
| 314 |
+
|
| 315 |
+
elapsed_total = time.time() - t0_total
|
| 316 |
+
print("\n" + "=" * 80)
|
| 317 |
+
print(f"DONE: {success_count}/{len(selected)} succeeded, {fail_count} failed")
|
| 318 |
+
print(f"Total time: {elapsed_total:.1f}s ({elapsed_total/60:.1f}m)")
|
| 319 |
+
|
| 320 |
+
report_path = TASKS_ROOT.parent / "thomasmaker_generation_report.json"
|
| 321 |
+
with open(report_path, "w") as f:
|
| 322 |
+
json.dump(results, f, indent=2)
|
| 323 |
+
print(f"Report: {report_path}")
|
| 324 |
+
|
| 325 |
+
return 0 if fail_count == 0 else 1
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
if __name__ == "__main__":
|
| 329 |
+
sys.exit(main())
|
scripts/verify_all_tasks.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
import json
|
| 3 |
+
import sys
|
| 4 |
+
import time
|
| 5 |
+
import traceback
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
TASKS_ROOT = Path(__file__).parent.parent / "server" / "tasks"
|
| 9 |
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def verify_reference_code(task_dir: Path) -> dict:
|
| 13 |
+
task_json_path = task_dir / "task.json"
|
| 14 |
+
ref_code_path = task_dir / "reference_code.py"
|
| 15 |
+
|
| 16 |
+
with open(task_json_path) as f:
|
| 17 |
+
task_data = json.load(f)
|
| 18 |
+
|
| 19 |
+
code = ref_code_path.read_text()
|
| 20 |
+
|
| 21 |
+
result_dict = {
|
| 22 |
+
"task_id": task_data["id"],
|
| 23 |
+
"code_executes": False,
|
| 24 |
+
"shape_valid": False,
|
| 25 |
+
"volume_gt_zero": False,
|
| 26 |
+
"ground_truth_generated": False,
|
| 27 |
+
"error": None,
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
try:
|
| 31 |
+
import cadquery as cq
|
| 32 |
+
import math
|
| 33 |
+
local_ns = {"cq": cq, "cadquery": cq, "math": math}
|
| 34 |
+
exec(code, local_ns)
|
| 35 |
+
|
| 36 |
+
if "result" not in local_ns:
|
| 37 |
+
result_dict["error"] = "No 'result' variable defined"
|
| 38 |
+
return result_dict
|
| 39 |
+
|
| 40 |
+
result_dict["code_executes"] = True
|
| 41 |
+
result_obj = local_ns["result"]
|
| 42 |
+
|
| 43 |
+
if hasattr(result_obj, "val"):
|
| 44 |
+
shape = result_obj.val()
|
| 45 |
+
else:
|
| 46 |
+
shape = result_obj
|
| 47 |
+
|
| 48 |
+
result_dict["shape_valid"] = shape.isValid()
|
| 49 |
+
vol = shape.Volume()
|
| 50 |
+
result_dict["volume"] = round(vol, 2)
|
| 51 |
+
result_dict["volume_gt_zero"] = vol > 0
|
| 52 |
+
|
| 53 |
+
bb = shape.BoundingBox()
|
| 54 |
+
result_dict["bbox"] = [round(bb.xlen, 2), round(bb.ylen, 2), round(bb.zlen, 2)]
|
| 55 |
+
|
| 56 |
+
faces = shape.Faces()
|
| 57 |
+
result_dict["face_count"] = len(faces)
|
| 58 |
+
|
| 59 |
+
except Exception as e:
|
| 60 |
+
result_dict["error"] = f"{type(e).__name__}: {e}"
|
| 61 |
+
return result_dict
|
| 62 |
+
|
| 63 |
+
try:
|
| 64 |
+
from server.preprocessor import preprocess_from_code
|
| 65 |
+
gt = preprocess_from_code(code, str(task_dir), task_id=task_data["id"])
|
| 66 |
+
result_dict["ground_truth_generated"] = True
|
| 67 |
+
result_dict["gt_volume"] = gt.get("volume_mm3")
|
| 68 |
+
result_dict["gt_euler"] = gt.get("euler_characteristic")
|
| 69 |
+
result_dict["gt_dominant_face"] = gt.get("dominant_face_type")
|
| 70 |
+
except Exception as e:
|
| 71 |
+
result_dict["error"] = f"Preprocessing failed: {type(e).__name__}: {e}"
|
| 72 |
+
|
| 73 |
+
return result_dict
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def run_reward_verification(task_dir: Path) -> dict:
|
| 77 |
+
result_dict = {
|
| 78 |
+
"reward_computed": False,
|
| 79 |
+
"reward_value": 0.0,
|
| 80 |
+
"error": None,
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
gt_json = task_dir / "ground_truth.json"
|
| 84 |
+
if not gt_json.exists():
|
| 85 |
+
result_dict["error"] = "No ground_truth.json"
|
| 86 |
+
return result_dict
|
| 87 |
+
|
| 88 |
+
ref_code = (task_dir / "reference_code.py").read_text()
|
| 89 |
+
|
| 90 |
+
try:
|
| 91 |
+
from server.executor import execute_cadquery_code
|
| 92 |
+
exec_result = execute_cadquery_code(ref_code, timeout=15.0)
|
| 93 |
+
|
| 94 |
+
if not exec_result["success"]:
|
| 95 |
+
result_dict["error"] = f"Execution failed: {exec_result['error']}"
|
| 96 |
+
return result_dict
|
| 97 |
+
|
| 98 |
+
props = exec_result["properties"]
|
| 99 |
+
|
| 100 |
+
import numpy as np
|
| 101 |
+
from server.preprocessor import sample_surface_points, voxelize, normalize_shape
|
| 102 |
+
import cadquery as cq
|
| 103 |
+
import math
|
| 104 |
+
|
| 105 |
+
local_ns = {"cq": cq, "cadquery": cq, "math": math}
|
| 106 |
+
exec(ref_code, local_ns)
|
| 107 |
+
result_obj = local_ns["result"]
|
| 108 |
+
shape = result_obj.val() if hasattr(result_obj, "val") else result_obj
|
| 109 |
+
|
| 110 |
+
normalized_shape, _ = normalize_shape(shape)
|
| 111 |
+
agent_points = sample_surface_points(normalized_shape, 2048)
|
| 112 |
+
agent_voxels = voxelize(normalized_shape, 64)
|
| 113 |
+
|
| 114 |
+
gt_points = np.load(str(task_dir / "surface_points.npy"))
|
| 115 |
+
gt_voxels = np.load(str(task_dir / "voxels_64.npy"))
|
| 116 |
+
|
| 117 |
+
from server.reward import compute_iou, best_of_6_iou, compute_mean_chamfer, compute_median_chamfer
|
| 118 |
+
|
| 119 |
+
iou = compute_iou(agent_voxels, gt_voxels)
|
| 120 |
+
iou_best = best_of_6_iou(agent_voxels, gt_voxels)
|
| 121 |
+
mean_cd = compute_mean_chamfer(agent_points, gt_points)
|
| 122 |
+
median_cd = compute_median_chamfer(agent_points, gt_points)
|
| 123 |
+
|
| 124 |
+
with open(gt_json) as f:
|
| 125 |
+
gt_data = json.load(f)
|
| 126 |
+
|
| 127 |
+
bbox_mm = gt_data.get("bbox_mm", [1, 1, 1])
|
| 128 |
+
bbox_diag = (sum(d**2 for d in bbox_mm)) ** 0.5
|
| 129 |
+
threshold = bbox_diag * 0.1
|
| 130 |
+
|
| 131 |
+
mean_cd_r = max(0, 1 - mean_cd / threshold) if threshold > 0 else 0
|
| 132 |
+
median_cd_r = max(0, 1 - median_cd / threshold) if threshold > 0 else 0
|
| 133 |
+
|
| 134 |
+
rgeom = 0.60 * iou_best + 0.20 * mean_cd_r + 0.20 * median_cd_r
|
| 135 |
+
|
| 136 |
+
frame_gap = iou_best - iou
|
| 137 |
+
frame_score = 0.1 if frame_gap > 0.15 else 1.0
|
| 138 |
+
|
| 139 |
+
norm_bb = normalized_shape.BoundingBox()
|
| 140 |
+
agent_bbox = [round(norm_bb.xlen, 4), round(norm_bb.ylen, 4), round(norm_bb.zlen, 4)]
|
| 141 |
+
gt_bbox = gt_data["bbox_mm"]
|
| 142 |
+
sorted_a = sorted(agent_bbox, reverse=True)
|
| 143 |
+
sorted_t = sorted(gt_bbox, reverse=True)
|
| 144 |
+
|
| 145 |
+
def match(a, b, tol=0.05):
|
| 146 |
+
for ai, bi in zip(a, b):
|
| 147 |
+
if bi == 0:
|
| 148 |
+
continue
|
| 149 |
+
if abs(ai - bi) / max(abs(bi), 1e-6) > tol:
|
| 150 |
+
return False
|
| 151 |
+
return True
|
| 152 |
+
|
| 153 |
+
s_match = match(sorted_a, sorted_t)
|
| 154 |
+
u_match = match(agent_bbox, gt_bbox)
|
| 155 |
+
param_score = 0.1 if (s_match and not u_match) else 1.0
|
| 156 |
+
|
| 157 |
+
face_score = 1.0 if props["dominant_face_type"] == gt_data["dominant_face_type"] else 0.0
|
| 158 |
+
|
| 159 |
+
reval = 0.40 * frame_score + 0.40 * param_score + 0.20 * face_score
|
| 160 |
+
total = 1.0 * (0.70 * rgeom + 0.30 * reval)
|
| 161 |
+
|
| 162 |
+
result_dict["reward_computed"] = True
|
| 163 |
+
result_dict["reward_value"] = round(total, 4)
|
| 164 |
+
result_dict["detail"] = {
|
| 165 |
+
"iou": round(iou, 4),
|
| 166 |
+
"iou_best": round(iou_best, 4),
|
| 167 |
+
"mean_cd": round(mean_cd, 4),
|
| 168 |
+
"mean_cd_reward": round(mean_cd_r, 4),
|
| 169 |
+
"median_cd": round(median_cd, 4),
|
| 170 |
+
"median_cd_reward": round(median_cd_r, 4),
|
| 171 |
+
"rgeom": round(rgeom, 4),
|
| 172 |
+
"frame_score": frame_score,
|
| 173 |
+
"param_score": param_score,
|
| 174 |
+
"face_score": face_score,
|
| 175 |
+
"reval": round(reval, 4),
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
except Exception as e:
|
| 179 |
+
result_dict["error"] = f"{type(e).__name__}: {e}\n{traceback.format_exc()}"
|
| 180 |
+
|
| 181 |
+
return result_dict
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def main():
|
| 185 |
+
print("=" * 80)
|
| 186 |
+
print("CadForge Task Verification Pipeline")
|
| 187 |
+
print("=" * 80)
|
| 188 |
+
|
| 189 |
+
task_dirs = sorted(TASKS_ROOT.glob("task_*"))
|
| 190 |
+
print(f"\nFound {len(task_dirs)} tasks\n")
|
| 191 |
+
|
| 192 |
+
phase1_results = []
|
| 193 |
+
print("PHASE 1: Verify reference codes execute and generate ground truth")
|
| 194 |
+
print("-" * 60)
|
| 195 |
+
|
| 196 |
+
for task_dir in task_dirs:
|
| 197 |
+
t0 = time.time()
|
| 198 |
+
result = verify_reference_code(task_dir)
|
| 199 |
+
elapsed = time.time() - t0
|
| 200 |
+
|
| 201 |
+
status = "PASS" if all([
|
| 202 |
+
result["code_executes"],
|
| 203 |
+
result["shape_valid"],
|
| 204 |
+
result["volume_gt_zero"],
|
| 205 |
+
result["ground_truth_generated"],
|
| 206 |
+
]) else "FAIL"
|
| 207 |
+
|
| 208 |
+
print(f" {result['task_id']:35s} {status:5s} ({elapsed:.1f}s) "
|
| 209 |
+
f"vol={result.get('volume', 'N/A')} bbox={result.get('bbox', 'N/A')}")
|
| 210 |
+
if result.get("error"):
|
| 211 |
+
print(f" ERROR: {result['error']}")
|
| 212 |
+
phase1_results.append(result)
|
| 213 |
+
|
| 214 |
+
phase1_pass = sum(1 for r in phase1_results if r["ground_truth_generated"])
|
| 215 |
+
print(f"\nPhase 1: {phase1_pass}/{len(phase1_results)} tasks passed\n")
|
| 216 |
+
|
| 217 |
+
print("PHASE 2: Verify reward scores for reference code (should be ~1.0)")
|
| 218 |
+
print("-" * 60)
|
| 219 |
+
|
| 220 |
+
phase2_results = []
|
| 221 |
+
for task_dir in task_dirs:
|
| 222 |
+
t0 = time.time()
|
| 223 |
+
result = run_reward_verification(task_dir)
|
| 224 |
+
elapsed = time.time() - t0
|
| 225 |
+
|
| 226 |
+
task_id = task_dir.name
|
| 227 |
+
reward = result.get("reward_value", 0)
|
| 228 |
+
status = "PASS" if reward > 0.85 else "WARN" if reward > 0.5 else "FAIL"
|
| 229 |
+
|
| 230 |
+
print(f" {task_id:35s} {status:5s} reward={reward:.4f} ({elapsed:.1f}s)")
|
| 231 |
+
if result.get("error"):
|
| 232 |
+
print(f" ERROR: {result['error'][:200]}")
|
| 233 |
+
if result.get("detail"):
|
| 234 |
+
d = result["detail"]
|
| 235 |
+
print(f" IoU={d['iou_best']:.3f} MeanCD_r={d['mean_cd_reward']:.3f} "
|
| 236 |
+
f"MedianCD_r={d['median_cd_reward']:.3f} Rgeom={d['rgeom']:.3f} Reval={d['reval']:.3f}")
|
| 237 |
+
phase2_results.append(result)
|
| 238 |
+
|
| 239 |
+
phase2_pass = sum(1 for r in phase2_results if r.get("reward_value", 0) > 0.85)
|
| 240 |
+
print(f"\nPhase 2: {phase2_pass}/{len(phase2_results)} tasks score > 0.85")
|
| 241 |
+
|
| 242 |
+
print("\n" + "=" * 80)
|
| 243 |
+
print("SUMMARY")
|
| 244 |
+
print(f" Phase 1 (code + ground truth): {phase1_pass}/{len(phase1_results)}")
|
| 245 |
+
print(f" Phase 2 (reward > 0.85): {phase2_pass}/{len(phase2_results)}")
|
| 246 |
+
print("=" * 80)
|
| 247 |
+
|
| 248 |
+
with open(TASKS_ROOT.parent / "verification_results.json", "w") as f:
|
| 249 |
+
json.dump({
|
| 250 |
+
"phase1": phase1_results,
|
| 251 |
+
"phase2": phase2_results,
|
| 252 |
+
}, f, indent=2, default=str)
|
| 253 |
+
|
| 254 |
+
print(f"\nDetailed results saved to verification_results.json")
|
| 255 |
+
|
| 256 |
+
if phase1_pass < len(phase1_results) or phase2_pass < len(phase2_results):
|
| 257 |
+
sys.exit(1)
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
if __name__ == "__main__":
|
| 261 |
+
main()
|
scripts/verify_reference_codes.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Verify all 20 reference codes execute and produce valid geometry.
|
| 3 |
+
|
| 4 |
+
Usage:
|
| 5 |
+
python scripts/verify_reference_codes.py
|
| 6 |
+
python scripts/verify_reference_codes.py --task task_001_flat_plate
|
| 7 |
+
"""
|
| 8 |
+
import argparse
|
| 9 |
+
import json
|
| 10 |
+
import sys
|
| 11 |
+
import time
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
|
| 14 |
+
TASKS_ROOT = Path(__file__).parent.parent / "server" / "tasks"
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def verify_one(task_dir: Path) -> dict:
|
| 18 |
+
task_id = task_dir.name
|
| 19 |
+
ref_code = (task_dir / "reference_code.py").read_text()
|
| 20 |
+
t0 = time.time()
|
| 21 |
+
|
| 22 |
+
result = {
|
| 23 |
+
"task_id": task_id,
|
| 24 |
+
"code_executes": False,
|
| 25 |
+
"shape_valid": False,
|
| 26 |
+
"volume_gt_zero": False,
|
| 27 |
+
"volume": None,
|
| 28 |
+
"bbox": None,
|
| 29 |
+
"face_count": None,
|
| 30 |
+
"error": None,
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
try:
|
| 34 |
+
import cadquery as cq
|
| 35 |
+
import math
|
| 36 |
+
|
| 37 |
+
local_ns = {"cq": cq, "cadquery": cq, "math": math}
|
| 38 |
+
exec(ref_code, local_ns)
|
| 39 |
+
|
| 40 |
+
if "result" not in local_ns:
|
| 41 |
+
result["error"] = "No 'result' variable defined"
|
| 42 |
+
return result
|
| 43 |
+
|
| 44 |
+
result["code_executes"] = True
|
| 45 |
+
obj = local_ns["result"]
|
| 46 |
+
shape = obj.val() if hasattr(obj, "val") else obj
|
| 47 |
+
|
| 48 |
+
result["shape_valid"] = shape.isValid()
|
| 49 |
+
vol = shape.Volume()
|
| 50 |
+
result["volume"] = round(vol, 2)
|
| 51 |
+
result["volume_gt_zero"] = vol > 0
|
| 52 |
+
|
| 53 |
+
bb = shape.BoundingBox()
|
| 54 |
+
result["bbox"] = [round(bb.xlen, 2), round(bb.ylen, 2), round(bb.zlen, 2)]
|
| 55 |
+
result["face_count"] = len(shape.Faces())
|
| 56 |
+
|
| 57 |
+
except Exception as e:
|
| 58 |
+
result["error"] = f"{type(e).__name__}: {e}"
|
| 59 |
+
|
| 60 |
+
result["elapsed_s"] = round(time.time() - t0, 2)
|
| 61 |
+
return result
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def main():
|
| 65 |
+
parser = argparse.ArgumentParser(description="Verify CadForge reference codes")
|
| 66 |
+
parser.add_argument("--task", type=str, default=None, help="Specific task id to verify")
|
| 67 |
+
args = parser.parse_args()
|
| 68 |
+
|
| 69 |
+
if args.task:
|
| 70 |
+
task_dirs = [TASKS_ROOT / args.task]
|
| 71 |
+
else:
|
| 72 |
+
task_dirs = sorted(TASKS_ROOT.glob("task_*"))
|
| 73 |
+
|
| 74 |
+
print(f"Verifying {len(task_dirs)} reference codes...")
|
| 75 |
+
print("-" * 80)
|
| 76 |
+
|
| 77 |
+
results = []
|
| 78 |
+
for task_dir in task_dirs:
|
| 79 |
+
r = verify_one(task_dir)
|
| 80 |
+
passed = r["code_executes"] and r["shape_valid"] and r["volume_gt_zero"]
|
| 81 |
+
status = "PASS" if passed else "FAIL"
|
| 82 |
+
print(
|
| 83 |
+
f" {r['task_id']:35s} {status:5s} "
|
| 84 |
+
f"vol={r['volume'] or 'N/A':>10} "
|
| 85 |
+
f"bbox={r['bbox'] or 'N/A'} "
|
| 86 |
+
f"faces={r['face_count'] or 'N/A'} "
|
| 87 |
+
f"({r['elapsed_s']:.1f}s)"
|
| 88 |
+
)
|
| 89 |
+
if r["error"]:
|
| 90 |
+
print(f" ERROR: {r['error']}")
|
| 91 |
+
results.append(r)
|
| 92 |
+
|
| 93 |
+
passed = sum(1 for r in results if r["code_executes"] and r["shape_valid"] and r["volume_gt_zero"])
|
| 94 |
+
print("-" * 80)
|
| 95 |
+
print(f"Result: {passed}/{len(results)} passed")
|
| 96 |
+
|
| 97 |
+
out_path = TASKS_ROOT.parent / "verification_phase1.json"
|
| 98 |
+
with open(out_path, "w") as f:
|
| 99 |
+
json.dump(results, f, indent=2)
|
| 100 |
+
print(f"Saved to {out_path}")
|
| 101 |
+
|
| 102 |
+
return 0 if passed == len(results) else 1
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
if __name__ == "__main__":
|
| 106 |
+
sys.exit(main())
|
scripts/verify_reward_scenarios.py
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Verify reward component breakdown for adversarial / wrong-shape scenarios.
|
| 3 |
+
|
| 4 |
+
Computes all reward components locally (no server needed) for various
|
| 5 |
+
agent code vs ground truth combinations. Useful for tuning weights.
|
| 6 |
+
|
| 7 |
+
Prerequisites: run generate_ground_truth.py first.
|
| 8 |
+
|
| 9 |
+
Usage:
|
| 10 |
+
python scripts/verify_reward_scenarios.py
|
| 11 |
+
python scripts/verify_reward_scenarios.py --task task_001_flat_plate
|
| 12 |
+
python scripts/verify_reward_scenarios.py --json
|
| 13 |
+
"""
|
| 14 |
+
import argparse
|
| 15 |
+
import json
|
| 16 |
+
import sys
|
| 17 |
+
import time
|
| 18 |
+
import traceback
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
|
| 21 |
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 22 |
+
|
| 23 |
+
TASKS_ROOT = Path(__file__).parent.parent / "server" / "tasks"
|
| 24 |
+
|
| 25 |
+
W_RGEOM = 0.80
|
| 26 |
+
W_REVAL = 0.20
|
| 27 |
+
W_IOU = 0.60
|
| 28 |
+
W_MEAN_CD = 0.20
|
| 29 |
+
W_MEDIAN_CD = 0.20
|
| 30 |
+
W_VOLUME = 0.35
|
| 31 |
+
W_BBOX = 0.30
|
| 32 |
+
W_FACE_TYPE = 0.15
|
| 33 |
+
W_EULER = 0.20
|
| 34 |
+
|
| 35 |
+
SCENARIOS = [
|
| 36 |
+
{
|
| 37 |
+
"task_id": "task_001_flat_plate",
|
| 38 |
+
"label": "perfect_match",
|
| 39 |
+
"code": "import cadquery as cq\nresult = cq.Workplane('XY').box(80, 40, 5)",
|
| 40 |
+
"expect_reward_gt": 0.85,
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"task_id": "task_001_flat_plate",
|
| 44 |
+
"label": "tiny_cube",
|
| 45 |
+
"code": "import cadquery as cq\nresult = cq.Workplane('XY').box(1, 1, 1)",
|
| 46 |
+
"expect_reward_lt": 0.15,
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
"task_id": "task_001_flat_plate",
|
| 50 |
+
"label": "cylinder_instead",
|
| 51 |
+
"code": "import cadquery as cq\nresult = cq.Workplane('XY').cylinder(50, 20)",
|
| 52 |
+
"expect_reward_lt": 0.5,
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
"task_id": "task_001_flat_plate",
|
| 56 |
+
"label": "sphere_instead",
|
| 57 |
+
"code": "import cadquery as cq\nresult = cq.Workplane('XY').sphere(25)",
|
| 58 |
+
"expect_reward_lt": 0.5,
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
"task_id": "task_001_flat_plate",
|
| 62 |
+
"label": "cylinder_similar_dims",
|
| 63 |
+
"code": "import cadquery as cq\nresult = cq.Workplane('XY').cylinder(40, 40)",
|
| 64 |
+
"expect_reward_lt": 0.7,
|
| 65 |
+
},
|
| 66 |
+
{
|
| 67 |
+
"task_id": "task_001_flat_plate",
|
| 68 |
+
"label": "10x_smaller_plate",
|
| 69 |
+
"code": "import cadquery as cq\nresult = cq.Workplane('XY').box(8, 4, 0.5)",
|
| 70 |
+
"expect_reward_lt": 0.15,
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
"task_id": "task_001_flat_plate",
|
| 74 |
+
"label": "10x_bigger_plate",
|
| 75 |
+
"code": "import cadquery as cq\nresult = cq.Workplane('XY').box(800, 400, 50)",
|
| 76 |
+
"expect_reward_lt": 0.7,
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
"task_id": "task_001_flat_plate",
|
| 80 |
+
"label": "wrong_proportions",
|
| 81 |
+
"code": "import cadquery as cq\nresult = cq.Workplane('XY').box(80, 80, 80)",
|
| 82 |
+
"expect_reward_lt": 0.6,
|
| 83 |
+
},
|
| 84 |
+
{
|
| 85 |
+
"task_id": "task_002_box_with_hole",
|
| 86 |
+
"label": "perfect_match",
|
| 87 |
+
"code": "import cadquery as cq\nresult = cq.Workplane('XY').box(50, 30, 20).faces('>Z').hole(10)",
|
| 88 |
+
"expect_reward_gt": 0.85,
|
| 89 |
+
},
|
| 90 |
+
{
|
| 91 |
+
"task_id": "task_002_box_with_hole",
|
| 92 |
+
"label": "box_no_hole",
|
| 93 |
+
"code": "import cadquery as cq\nresult = cq.Workplane('XY').box(50, 30, 20)",
|
| 94 |
+
"expect_reward_gt": 0.0,
|
| 95 |
+
},
|
| 96 |
+
{
|
| 97 |
+
"task_id": "task_002_box_with_hole",
|
| 98 |
+
"label": "wrong_hole_diameter",
|
| 99 |
+
"code": "import cadquery as cq\nresult = cq.Workplane('XY').box(50, 30, 20).faces('>Z').hole(25)",
|
| 100 |
+
"expect_reward_gt": 0.0,
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
"task_id": "task_002_box_with_hole",
|
| 104 |
+
"label": "cylinder_instead",
|
| 105 |
+
"code": "import cadquery as cq\nresult = cq.Workplane('XY').cylinder(20, 15)",
|
| 106 |
+
"expect_reward_lt": 0.7,
|
| 107 |
+
},
|
| 108 |
+
{
|
| 109 |
+
"task_id": "task_003_cylinder_shaft",
|
| 110 |
+
"label": "box_instead",
|
| 111 |
+
"code": "import cadquery as cq\nresult = cq.Workplane('XY').box(30, 30, 60)",
|
| 112 |
+
"expect_reward_lt": 0.8,
|
| 113 |
+
},
|
| 114 |
+
{
|
| 115 |
+
"task_id": "task_003_cylinder_shaft",
|
| 116 |
+
"label": "wrong_radius",
|
| 117 |
+
"code": "import cadquery as cq\nresult = cq.Workplane('XY').cylinder(60, 5)",
|
| 118 |
+
"expect_reward_lt": 0.7,
|
| 119 |
+
},
|
| 120 |
+
{
|
| 121 |
+
"task_id": "task_006_hollow_tube",
|
| 122 |
+
"label": "solid_cylinder_no_hole",
|
| 123 |
+
"code": "import cadquery as cq\nresult = cq.Workplane('XY').cylinder(40, 15)",
|
| 124 |
+
"expect_reward_lt": 0.85,
|
| 125 |
+
},
|
| 126 |
+
{
|
| 127 |
+
"task_id": "task_009_hexagonal_prism",
|
| 128 |
+
"label": "regular_box",
|
| 129 |
+
"code": "import cadquery as cq\nresult = cq.Workplane('XY').box(30, 26, 25)",
|
| 130 |
+
"expect_reward_lt": 0.8,
|
| 131 |
+
},
|
| 132 |
+
{
|
| 133 |
+
"task_id": "task_016_cone",
|
| 134 |
+
"label": "cylinder_instead",
|
| 135 |
+
"code": "import cadquery as cq\nresult = cq.Workplane('XY').cylinder(30, 20)",
|
| 136 |
+
"expect_reward_lt": 0.8,
|
| 137 |
+
},
|
| 138 |
+
{
|
| 139 |
+
"task_id": "task_018_sphere",
|
| 140 |
+
"label": "cube_instead",
|
| 141 |
+
"code": "import cadquery as cq\nresult = cq.Workplane('XY').box(40, 40, 40)",
|
| 142 |
+
"expect_reward_lt": 0.8,
|
| 143 |
+
},
|
| 144 |
+
{
|
| 145 |
+
"task_id": "task_018_sphere",
|
| 146 |
+
"label": "tiny_sphere",
|
| 147 |
+
"code": "import cadquery as cq\nresult = cq.Workplane('XY').sphere(1)",
|
| 148 |
+
"expect_reward_lt": 0.15,
|
| 149 |
+
},
|
| 150 |
+
{
|
| 151 |
+
"task_id": "task_001_flat_plate",
|
| 152 |
+
"label": "syntax_error",
|
| 153 |
+
"code": "this is not valid python!!!",
|
| 154 |
+
"expect_reward_lt": 0.01,
|
| 155 |
+
},
|
| 156 |
+
{
|
| 157 |
+
"task_id": "task_001_flat_plate",
|
| 158 |
+
"label": "no_result_var",
|
| 159 |
+
"code": "import cadquery as cq\nx = cq.Workplane('XY').box(80, 40, 5)",
|
| 160 |
+
"expect_reward_lt": 0.01,
|
| 161 |
+
},
|
| 162 |
+
]
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def compute_scenario_reward(task_dir: Path, code: str) -> dict:
|
| 166 |
+
t0 = time.time()
|
| 167 |
+
|
| 168 |
+
result = {
|
| 169 |
+
"reward_computed": False,
|
| 170 |
+
"total": 0.0,
|
| 171 |
+
"rexec": 0.0,
|
| 172 |
+
"rgeom": 0.0,
|
| 173 |
+
"reval": 0.0,
|
| 174 |
+
"iou_best6": 0.0,
|
| 175 |
+
"mean_cd_r": 0.0,
|
| 176 |
+
"median_cd_r": 0.0,
|
| 177 |
+
"vol_score": 0.0,
|
| 178 |
+
"bbox_score": 0.0,
|
| 179 |
+
"face_score": 0.0,
|
| 180 |
+
"euler_score": 0.0,
|
| 181 |
+
"error": None,
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
try:
|
| 185 |
+
from server.executor import execute_cadquery_code
|
| 186 |
+
exec_result = execute_cadquery_code(code, timeout=15.0)
|
| 187 |
+
|
| 188 |
+
if not exec_result["success"]:
|
| 189 |
+
result["error"] = f"exec_fail: {exec_result.get('error', '')[:100]}"
|
| 190 |
+
return result
|
| 191 |
+
|
| 192 |
+
props = exec_result["properties"]
|
| 193 |
+
if not props.get("is_valid", False) or props.get("volume_mm3", 0) <= 0:
|
| 194 |
+
result["error"] = "invalid_shape_or_zero_volume"
|
| 195 |
+
return result
|
| 196 |
+
|
| 197 |
+
result["rexec"] = 1.0
|
| 198 |
+
|
| 199 |
+
import cadquery as cq
|
| 200 |
+
import math
|
| 201 |
+
import numpy as np
|
| 202 |
+
|
| 203 |
+
local_ns = {"cq": cq, "cadquery": cq, "math": math}
|
| 204 |
+
exec(code, local_ns)
|
| 205 |
+
obj = local_ns["result"]
|
| 206 |
+
shape = obj.val() if hasattr(obj, "val") else obj
|
| 207 |
+
|
| 208 |
+
from server.preprocessor import normalize_shape, sample_surface_points, voxelize_in_bbox
|
| 209 |
+
normalized_shape, _ = normalize_shape(shape)
|
| 210 |
+
agent_points = sample_surface_points(normalized_shape, 2048)
|
| 211 |
+
|
| 212 |
+
gt_step_path = task_dir / "ground_truth_normalized.step"
|
| 213 |
+
if not gt_step_path.exists():
|
| 214 |
+
gt_step_path = task_dir / "ground_truth.step"
|
| 215 |
+
gt_wp = cq.importers.importStep(str(gt_step_path))
|
| 216 |
+
gt_shape = gt_wp.val()
|
| 217 |
+
gt_bb = gt_shape.BoundingBox()
|
| 218 |
+
bbox_min = [gt_bb.xmin, gt_bb.ymin, gt_bb.zmin]
|
| 219 |
+
bbox_max = [gt_bb.xmax, gt_bb.ymax, gt_bb.zmax]
|
| 220 |
+
|
| 221 |
+
agent_voxels = voxelize_in_bbox(normalized_shape, bbox_min, bbox_max, 64)
|
| 222 |
+
gt_voxels = voxelize_in_bbox(gt_shape, bbox_min, bbox_max, 64)
|
| 223 |
+
gt_points = np.load(str(task_dir / "surface_points.npy"))
|
| 224 |
+
|
| 225 |
+
import json as _json
|
| 226 |
+
with open(task_dir / "ground_truth.json") as f:
|
| 227 |
+
gt_data = _json.load(f)
|
| 228 |
+
|
| 229 |
+
from server.reward import best_of_6_iou, compute_mean_chamfer, compute_median_chamfer
|
| 230 |
+
|
| 231 |
+
iou_best = best_of_6_iou(agent_voxels, gt_voxels)
|
| 232 |
+
result["iou_best6"] = round(iou_best, 4)
|
| 233 |
+
|
| 234 |
+
bbox_mm = gt_data.get("bbox_mm", [1, 1, 1])
|
| 235 |
+
bbox_diag = sum(d**2 for d in bbox_mm) ** 0.5
|
| 236 |
+
threshold = bbox_diag * 0.1
|
| 237 |
+
|
| 238 |
+
mean_cd = compute_mean_chamfer(agent_points, gt_points)
|
| 239 |
+
median_cd = compute_median_chamfer(agent_points, gt_points)
|
| 240 |
+
mean_cd_r = max(0.0, 1.0 - mean_cd / threshold) if threshold > 0 else 0.0
|
| 241 |
+
median_cd_r = max(0.0, 1.0 - median_cd / threshold) if threshold > 0 else 0.0
|
| 242 |
+
result["mean_cd_r"] = round(mean_cd_r, 4)
|
| 243 |
+
result["median_cd_r"] = round(median_cd_r, 4)
|
| 244 |
+
|
| 245 |
+
rgeom = W_IOU * iou_best + W_MEAN_CD * mean_cd_r + W_MEDIAN_CD * median_cd_r
|
| 246 |
+
result["rgeom"] = round(rgeom, 4)
|
| 247 |
+
|
| 248 |
+
gt_vol = gt_data.get("volume_mm3", 0)
|
| 249 |
+
agent_vol = props.get("volume_mm3", 0)
|
| 250 |
+
if gt_vol > 0:
|
| 251 |
+
vol_score = max(0.0, min(agent_vol, gt_vol) / max(agent_vol, gt_vol))
|
| 252 |
+
else:
|
| 253 |
+
vol_score = 1.0 if agent_vol == 0 else 0.0
|
| 254 |
+
result["vol_score"] = round(vol_score, 4)
|
| 255 |
+
|
| 256 |
+
gt_bbox_sorted = sorted(gt_data.get("bbox_mm", [0, 0, 0]), reverse=True)
|
| 257 |
+
agent_bbox_sorted = sorted([
|
| 258 |
+
props.get("bbox_x_mm", 0),
|
| 259 |
+
props.get("bbox_y_mm", 0),
|
| 260 |
+
props.get("bbox_z_mm", 0),
|
| 261 |
+
], reverse=True)
|
| 262 |
+
bbox_scores = []
|
| 263 |
+
for a, g in zip(agent_bbox_sorted, gt_bbox_sorted):
|
| 264 |
+
if g > 0:
|
| 265 |
+
bbox_scores.append(max(0.0, 1.0 - abs(a - g) / g))
|
| 266 |
+
else:
|
| 267 |
+
bbox_scores.append(1.0 if a == 0 else 0.0)
|
| 268 |
+
bbox_score = sum(bbox_scores) / 3.0 if bbox_scores else 0.0
|
| 269 |
+
result["bbox_score"] = round(bbox_score, 4)
|
| 270 |
+
|
| 271 |
+
face_score = 1.0 if props.get("dominant_face_type", "") == gt_data.get("dominant_face_type", "") else 0.0
|
| 272 |
+
result["face_score"] = face_score
|
| 273 |
+
|
| 274 |
+
euler_score = 1.0 if props.get("euler_characteristic", 2) == gt_data.get("euler_characteristic", 2) else 0.0
|
| 275 |
+
result["euler_score"] = euler_score
|
| 276 |
+
|
| 277 |
+
reval = W_VOLUME * vol_score + W_BBOX * bbox_score + W_FACE_TYPE * face_score + W_EULER * euler_score
|
| 278 |
+
result["reval"] = round(reval, 4)
|
| 279 |
+
|
| 280 |
+
total = result["rexec"] * (W_RGEOM * rgeom + W_REVAL * reval)
|
| 281 |
+
result["total"] = round(total, 4)
|
| 282 |
+
result["reward_computed"] = True
|
| 283 |
+
|
| 284 |
+
except Exception as e:
|
| 285 |
+
result["error"] = f"{type(e).__name__}: {str(e)[:100]}"
|
| 286 |
+
result["traceback"] = traceback.format_exc()
|
| 287 |
+
|
| 288 |
+
result["elapsed_s"] = round(time.time() - t0, 2)
|
| 289 |
+
return result
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
def main():
|
| 293 |
+
parser = argparse.ArgumentParser(description="Verify reward scenarios with component breakdown")
|
| 294 |
+
parser.add_argument("--task", type=str, default=None, help="Filter to specific task_id")
|
| 295 |
+
parser.add_argument("--json", action="store_true", help="Output JSON instead of table")
|
| 296 |
+
args = parser.parse_args()
|
| 297 |
+
|
| 298 |
+
scenarios = SCENARIOS
|
| 299 |
+
if args.task:
|
| 300 |
+
scenarios = [s for s in scenarios if s["task_id"] == args.task]
|
| 301 |
+
|
| 302 |
+
if not scenarios:
|
| 303 |
+
print(f"No scenarios found for task={args.task}")
|
| 304 |
+
return 1
|
| 305 |
+
|
| 306 |
+
header = (
|
| 307 |
+
f" {'TASK':30s} {'SCENARIO':25s} {'TOTAL':>6s} "
|
| 308 |
+
f"{'Rexec':>5s} {'IoU':>5s} {'MnCD':>5s} {'MdCD':>5s} {'Rgeom':>5s} "
|
| 309 |
+
f"{'Vol':>5s} {'Bbox':>5s} {'Face':>4s} {'Eulr':>4s} {'Reval':>5s} "
|
| 310 |
+
f"{'TIME':>5s} {'STATUS'}"
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
print(f"Reward scenario analysis ({len(scenarios)} scenarios)")
|
| 314 |
+
print(f"Weights: Rgeom={W_RGEOM} (IoU={W_IOU}, MnCD={W_MEAN_CD}, MdCD={W_MEDIAN_CD}) | Reval={W_REVAL} (Vol={W_VOLUME}, Bbox={W_BBOX}, Face={W_FACE_TYPE}, Euler={W_EULER})")
|
| 315 |
+
print("-" * 160)
|
| 316 |
+
print(header)
|
| 317 |
+
print("-" * 160)
|
| 318 |
+
|
| 319 |
+
all_results = []
|
| 320 |
+
pass_count, fail_count = 0, 0
|
| 321 |
+
|
| 322 |
+
for s in scenarios:
|
| 323 |
+
task_dir = TASKS_ROOT / s["task_id"]
|
| 324 |
+
r = compute_scenario_reward(task_dir, s["code"])
|
| 325 |
+
r["task_id"] = s["task_id"]
|
| 326 |
+
r["label"] = s["label"]
|
| 327 |
+
|
| 328 |
+
expectation_met = True
|
| 329 |
+
if "expect_reward_gt" in s and r["total"] <= s["expect_reward_gt"]:
|
| 330 |
+
expectation_met = False
|
| 331 |
+
if "expect_reward_lt" in s and r["total"] >= s["expect_reward_lt"]:
|
| 332 |
+
expectation_met = False
|
| 333 |
+
|
| 334 |
+
status = "PASS" if expectation_met else "FAIL"
|
| 335 |
+
if expectation_met:
|
| 336 |
+
pass_count += 1
|
| 337 |
+
else:
|
| 338 |
+
fail_count += 1
|
| 339 |
+
|
| 340 |
+
expect_str = ""
|
| 341 |
+
if "expect_reward_gt" in s:
|
| 342 |
+
expect_str = f" (exp >{s['expect_reward_gt']})"
|
| 343 |
+
if "expect_reward_lt" in s:
|
| 344 |
+
expect_str = f" (exp <{s['expect_reward_lt']})"
|
| 345 |
+
|
| 346 |
+
print(
|
| 347 |
+
f" {s['task_id']:30s} {s['label']:25s} {r['total']:6.4f} "
|
| 348 |
+
f"{r['rexec']:5.1f} {r['iou_best6']:5.3f} {r['mean_cd_r']:5.3f} {r['median_cd_r']:5.3f} {r['rgeom']:5.3f} "
|
| 349 |
+
f"{r['vol_score']:5.3f} {r['bbox_score']:5.3f} {r['face_score']:4.1f} {r['euler_score']:4.1f} {r['reval']:5.3f} "
|
| 350 |
+
f"{r.get('elapsed_s', 0):5.1f}s {status}{expect_str}"
|
| 351 |
+
)
|
| 352 |
+
if r.get("error"):
|
| 353 |
+
print(f" -> {r['error']}")
|
| 354 |
+
|
| 355 |
+
all_results.append(r)
|
| 356 |
+
|
| 357 |
+
print("-" * 160)
|
| 358 |
+
print(f"PASS: {pass_count} | FAIL: {fail_count} | Total: {len(scenarios)}")
|
| 359 |
+
|
| 360 |
+
if args.json:
|
| 361 |
+
out_path = TASKS_ROOT.parent / "reward_scenarios.json"
|
| 362 |
+
with open(out_path, "w") as f:
|
| 363 |
+
json.dump(all_results, f, indent=2, default=str)
|
| 364 |
+
print(f"\nSaved JSON to {out_path}")
|
| 365 |
+
|
| 366 |
+
return 0 if fail_count == 0 else 1
|
| 367 |
+
|
| 368 |
+
|
| 369 |
+
if __name__ == "__main__":
|
| 370 |
+
sys.exit(main())
|
scripts/verify_rewards.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Verify that reference codes score ~1.0 when evaluated against their own ground truth.
|
| 3 |
+
|
| 4 |
+
Prerequisites: run generate_ground_truth.py first.
|
| 5 |
+
|
| 6 |
+
Usage:
|
| 7 |
+
python scripts/verify_rewards.py
|
| 8 |
+
python scripts/verify_rewards.py --task task_001_flat_plate
|
| 9 |
+
"""
|
| 10 |
+
import argparse
|
| 11 |
+
import json
|
| 12 |
+
import sys
|
| 13 |
+
import time
|
| 14 |
+
import traceback
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
|
| 17 |
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 18 |
+
|
| 19 |
+
TASKS_ROOT = Path(__file__).parent.parent / "server" / "tasks"
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def verify_reward_one(task_dir: Path) -> dict:
|
| 23 |
+
task_id = task_dir.name
|
| 24 |
+
t0 = time.time()
|
| 25 |
+
|
| 26 |
+
result = {
|
| 27 |
+
"task_id": task_id,
|
| 28 |
+
"reward_computed": False,
|
| 29 |
+
"reward_value": 0.0,
|
| 30 |
+
"detail": None,
|
| 31 |
+
"error": None,
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
gt_json = task_dir / "ground_truth.json"
|
| 35 |
+
if not gt_json.exists():
|
| 36 |
+
result["error"] = "No ground_truth.json — run generate_ground_truth.py first"
|
| 37 |
+
return result
|
| 38 |
+
|
| 39 |
+
ref_code = (task_dir / "reference_code.py").read_text()
|
| 40 |
+
|
| 41 |
+
try:
|
| 42 |
+
import cadquery as cq
|
| 43 |
+
import math
|
| 44 |
+
import numpy as np
|
| 45 |
+
|
| 46 |
+
local_ns = {"cq": cq, "cadquery": cq, "math": math}
|
| 47 |
+
exec(ref_code, local_ns)
|
| 48 |
+
obj = local_ns["result"]
|
| 49 |
+
shape = obj.val() if hasattr(obj, "val") else obj
|
| 50 |
+
|
| 51 |
+
from server.executor import execute_cadquery_code
|
| 52 |
+
exec_result = execute_cadquery_code(ref_code, timeout=15.0)
|
| 53 |
+
if not exec_result["success"]:
|
| 54 |
+
result["error"] = f"Execution failed: {exec_result['error']}"
|
| 55 |
+
return result
|
| 56 |
+
props = exec_result["properties"]
|
| 57 |
+
|
| 58 |
+
from server.preprocessor import sample_surface_points, voxelize_in_bbox, normalize_shape
|
| 59 |
+
normalized_shape, _ = normalize_shape(shape)
|
| 60 |
+
agent_points = sample_surface_points(normalized_shape, 2048)
|
| 61 |
+
|
| 62 |
+
gt_step_path = task_dir / "ground_truth_normalized.step"
|
| 63 |
+
if not gt_step_path.exists():
|
| 64 |
+
gt_step_path = task_dir / "ground_truth.step"
|
| 65 |
+
gt_wp = cq.importers.importStep(str(gt_step_path))
|
| 66 |
+
gt_shape = gt_wp.val()
|
| 67 |
+
gt_bb = gt_shape.BoundingBox()
|
| 68 |
+
bbox_min = [gt_bb.xmin, gt_bb.ymin, gt_bb.zmin]
|
| 69 |
+
bbox_max = [gt_bb.xmax, gt_bb.ymax, gt_bb.zmax]
|
| 70 |
+
|
| 71 |
+
agent_voxels = voxelize_in_bbox(normalized_shape, bbox_min, bbox_max, 64)
|
| 72 |
+
gt_voxels = voxelize_in_bbox(gt_shape, bbox_min, bbox_max, 64)
|
| 73 |
+
|
| 74 |
+
gt_points = np.load(str(task_dir / "surface_points.npy"))
|
| 75 |
+
|
| 76 |
+
from server.reward import (
|
| 77 |
+
compute_iou, best_of_6_iou,
|
| 78 |
+
compute_mean_chamfer, compute_median_chamfer,
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
iou_best = best_of_6_iou(agent_voxels, gt_voxels)
|
| 82 |
+
|
| 83 |
+
with open(gt_json) as f:
|
| 84 |
+
gt_data = json.load(f)
|
| 85 |
+
|
| 86 |
+
bbox_mm = gt_data.get("bbox_mm", [1, 1, 1])
|
| 87 |
+
bbox_diag = sum(d**2 for d in bbox_mm) ** 0.5
|
| 88 |
+
threshold = bbox_diag * 0.1
|
| 89 |
+
|
| 90 |
+
mean_cd = compute_mean_chamfer(agent_points, gt_points)
|
| 91 |
+
median_cd = compute_median_chamfer(agent_points, gt_points)
|
| 92 |
+
mean_cd_r = max(0, 1 - mean_cd / threshold) if threshold > 0 else 0
|
| 93 |
+
median_cd_r = max(0, 1 - median_cd / threshold) if threshold > 0 else 0
|
| 94 |
+
|
| 95 |
+
rgeom = 0.60 * iou_best + 0.20 * mean_cd_r + 0.20 * median_cd_r
|
| 96 |
+
|
| 97 |
+
gt_vol = gt_data.get("volume_mm3", 0)
|
| 98 |
+
agent_vol = props.get("volume_mm3", 0)
|
| 99 |
+
if gt_vol > 0:
|
| 100 |
+
volume_score = max(0.0, min(agent_vol, gt_vol) / max(agent_vol, gt_vol))
|
| 101 |
+
else:
|
| 102 |
+
volume_score = 1.0 if agent_vol == 0 else 0.0
|
| 103 |
+
|
| 104 |
+
norm_bb = normalized_shape.BoundingBox()
|
| 105 |
+
agent_bbox = sorted([round(norm_bb.xlen, 4), round(norm_bb.ylen, 4), round(norm_bb.zlen, 4)], reverse=True)
|
| 106 |
+
gt_bbox = sorted(gt_data["bbox_mm"], reverse=True)
|
| 107 |
+
bbox_scores = []
|
| 108 |
+
for a, g in zip(agent_bbox, gt_bbox):
|
| 109 |
+
if g > 0:
|
| 110 |
+
bbox_scores.append(max(0.0, 1.0 - abs(a - g) / g))
|
| 111 |
+
else:
|
| 112 |
+
bbox_scores.append(1.0 if a == 0 else 0.0)
|
| 113 |
+
bbox_score = sum(bbox_scores) / 3.0
|
| 114 |
+
|
| 115 |
+
target_dom = gt_data.get("dominant_face_type", "")
|
| 116 |
+
agent_dom = props.get("dominant_face_type", "")
|
| 117 |
+
face_type_score = 1.0 if agent_dom == target_dom else 0.0
|
| 118 |
+
|
| 119 |
+
gt_euler = gt_data.get("euler_characteristic", 2)
|
| 120 |
+
agent_euler = props.get("euler_characteristic", 2)
|
| 121 |
+
euler_score = 1.0 if agent_euler == gt_euler else 0.0
|
| 122 |
+
|
| 123 |
+
reval = 0.35 * volume_score + 0.30 * bbox_score + 0.15 * face_type_score + 0.20 * euler_score
|
| 124 |
+
|
| 125 |
+
rexec = 1.0
|
| 126 |
+
total = rexec * (0.80 * rgeom + 0.20 * reval)
|
| 127 |
+
|
| 128 |
+
result["reward_computed"] = True
|
| 129 |
+
result["reward_value"] = round(total, 4)
|
| 130 |
+
result["detail"] = {
|
| 131 |
+
"rexec": rexec,
|
| 132 |
+
"iou_best_of_6": round(iou_best, 4),
|
| 133 |
+
"mean_cd_reward": round(mean_cd_r, 4),
|
| 134 |
+
"median_cd_reward": round(median_cd_r, 4),
|
| 135 |
+
"rgeom": round(rgeom, 4),
|
| 136 |
+
"volume_score": round(volume_score, 4),
|
| 137 |
+
"bbox_score": round(bbox_score, 4),
|
| 138 |
+
"face_type_score": face_type_score,
|
| 139 |
+
"euler_score": euler_score,
|
| 140 |
+
"reval": round(reval, 4),
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
except Exception as e:
|
| 144 |
+
result["error"] = f"{type(e).__name__}: {e}"
|
| 145 |
+
result["traceback"] = traceback.format_exc()
|
| 146 |
+
|
| 147 |
+
result["elapsed_s"] = round(time.time() - t0, 2)
|
| 148 |
+
return result
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def main():
|
| 152 |
+
parser = argparse.ArgumentParser(description="Verify reward scores for reference codes")
|
| 153 |
+
parser.add_argument("--task", type=str, default=None, help="Specific task id")
|
| 154 |
+
args = parser.parse_args()
|
| 155 |
+
|
| 156 |
+
if args.task:
|
| 157 |
+
task_dirs = [TASKS_ROOT / args.task]
|
| 158 |
+
else:
|
| 159 |
+
task_dirs = sorted(TASKS_ROOT.glob("task_*"))
|
| 160 |
+
|
| 161 |
+
print(f"Verifying rewards for {len(task_dirs)} tasks...")
|
| 162 |
+
print("-" * 100)
|
| 163 |
+
print(f" {'TASK':35s} {'STATUS':6s} {'REWARD':>7s} {'IoU':>5s} {'MnCD':>5s} {'MdCD':>5s} {'Rgeom':>5s} {'Vol':>5s} {'Bbox':>5s} {'Face':>4s} {'Eulr':>4s} {'Reval':>5s} {'TIME':>5s}")
|
| 164 |
+
print("-" * 100)
|
| 165 |
+
|
| 166 |
+
results = []
|
| 167 |
+
for task_dir in task_dirs:
|
| 168 |
+
r = verify_reward_one(task_dir)
|
| 169 |
+
reward = r.get("reward_value", 0)
|
| 170 |
+
status = "PASS" if reward > 0.85 else "WARN" if reward > 0.5 else "FAIL"
|
| 171 |
+
d = r.get("detail", {})
|
| 172 |
+
|
| 173 |
+
print(
|
| 174 |
+
f" {r['task_id']:35s} {status:6s} {reward:7.4f} "
|
| 175 |
+
f"{d.get('iou_best_of_6', 0):5.3f} "
|
| 176 |
+
f"{d.get('mean_cd_reward', 0):5.3f} "
|
| 177 |
+
f"{d.get('median_cd_reward', 0):5.3f} "
|
| 178 |
+
f"{d.get('rgeom', 0):5.3f} "
|
| 179 |
+
f"{d.get('volume_score', 0):5.3f} "
|
| 180 |
+
f"{d.get('bbox_score', 0):5.3f} "
|
| 181 |
+
f"{d.get('face_type_score', 0):4.1f} "
|
| 182 |
+
f"{d.get('euler_score', 0):4.1f} "
|
| 183 |
+
f"{d.get('reval', 0):5.3f} "
|
| 184 |
+
f"{r.get('elapsed_s', 0):5.1f}s"
|
| 185 |
+
)
|
| 186 |
+
if r["error"]:
|
| 187 |
+
print(f" ERROR: {r['error'][:200]}")
|
| 188 |
+
results.append(r)
|
| 189 |
+
|
| 190 |
+
print("-" * 100)
|
| 191 |
+
passed = sum(1 for r in results if r.get("reward_value", 0) > 0.85)
|
| 192 |
+
warned = sum(1 for r in results if 0.5 < r.get("reward_value", 0) <= 0.85)
|
| 193 |
+
failed = sum(1 for r in results if r.get("reward_value", 0) <= 0.5)
|
| 194 |
+
print(f"PASS (>0.85): {passed} | WARN (0.5-0.85): {warned} | FAIL (<0.5): {failed}")
|
| 195 |
+
|
| 196 |
+
out_path = TASKS_ROOT.parent / "verification_rewards.json"
|
| 197 |
+
with open(out_path, "w") as f:
|
| 198 |
+
json.dump(results, f, indent=2, default=str)
|
| 199 |
+
print(f"\nSaved to {out_path}")
|
| 200 |
+
|
| 201 |
+
return 0 if passed == len(results) else 1
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
if __name__ == "__main__":
|
| 205 |
+
sys.exit(main())
|
server/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""Cadforge environment server components."""
|
| 8 |
+
|
| 9 |
+
from .cadforge_environment import CadforgeEnvironment
|
| 10 |
+
|
| 11 |
+
__all__ = ["CadforgeEnvironment"]
|
server/app.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
try:
|
| 8 |
+
from openenv.core.env_server.http_server import create_app
|
| 9 |
+
except Exception as e:
|
| 10 |
+
raise ImportError(
|
| 11 |
+
"openenv is required for the web interface. Install dependencies with '\n uv sync\n'"
|
| 12 |
+
) from e
|
| 13 |
+
|
| 14 |
+
try:
|
| 15 |
+
from ..models import CadforgeAction, CadforgeObservation
|
| 16 |
+
from .cadforge_environment import CadforgeEnvironment
|
| 17 |
+
except (ImportError, ValueError):
|
| 18 |
+
from models import CadforgeAction, CadforgeObservation
|
| 19 |
+
from server.cadforge_environment import CadforgeEnvironment
|
| 20 |
+
|
| 21 |
+
app = create_app(
|
| 22 |
+
CadforgeEnvironment,
|
| 23 |
+
CadforgeAction,
|
| 24 |
+
CadforgeObservation,
|
| 25 |
+
env_name="cadforge",
|
| 26 |
+
max_concurrent_envs=4,
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def main(host: str = "0.0.0.0", port: int = 8000):
|
| 31 |
+
import uvicorn
|
| 32 |
+
uvicorn.run(app, host=host, port=port)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
if __name__ == "__main__":
|
| 36 |
+
main()
|
server/cadforge_environment.py
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import logging
|
| 3 |
+
import pickle
|
| 4 |
+
import subprocess
|
| 5 |
+
import sys
|
| 6 |
+
import time
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Any, Dict, List, Optional
|
| 9 |
+
from uuid import uuid4
|
| 10 |
+
|
| 11 |
+
import numpy as np
|
| 12 |
+
|
| 13 |
+
from openenv.core.env_server.interfaces import Environment
|
| 14 |
+
from openenv.core.env_server.types import State
|
| 15 |
+
|
| 16 |
+
from .docs_search import search_docs
|
| 17 |
+
from .executor import execute_cadquery_code
|
| 18 |
+
from .reward import build_cadforge_rubric
|
| 19 |
+
|
| 20 |
+
try:
|
| 21 |
+
from ..models import CadforgeAction, CadforgeObservation
|
| 22 |
+
except ImportError:
|
| 23 |
+
from models import CadforgeAction, CadforgeObservation
|
| 24 |
+
|
| 25 |
+
logger = logging.getLogger(__name__)
|
| 26 |
+
|
| 27 |
+
TASKS_ROOT = Path(__file__).parent / "tasks"
|
| 28 |
+
STEP_OUTPUT_DIR = Path("/tmp/cadforge_steps")
|
| 29 |
+
|
| 30 |
+
STEP_EXPORT_RUNNER = r'''
|
| 31 |
+
import sys
|
| 32 |
+
import pickle
|
| 33 |
+
import os
|
| 34 |
+
|
| 35 |
+
try:
|
| 36 |
+
import cadquery as cq
|
| 37 |
+
import math
|
| 38 |
+
|
| 39 |
+
code = sys.stdin.buffer.read().decode("utf-8")
|
| 40 |
+
parts = code.split("\n---STEP_PATH---\n", 1)
|
| 41 |
+
user_code = parts[0]
|
| 42 |
+
step_path = parts[1].strip() if len(parts) > 1 else "/tmp/agent_shape.step"
|
| 43 |
+
|
| 44 |
+
local_ns = {"cq": cq, "cadquery": cq, "math": math}
|
| 45 |
+
exec(user_code, local_ns)
|
| 46 |
+
|
| 47 |
+
result = local_ns.get("result")
|
| 48 |
+
if result is None:
|
| 49 |
+
pickle.dump({"success": False, "error": "No variable named 'result' was defined"}, sys.stdout.buffer)
|
| 50 |
+
sys.exit(0)
|
| 51 |
+
|
| 52 |
+
if hasattr(result, "val"):
|
| 53 |
+
shape = result.val()
|
| 54 |
+
else:
|
| 55 |
+
shape = result
|
| 56 |
+
|
| 57 |
+
os.makedirs(os.path.dirname(step_path), exist_ok=True)
|
| 58 |
+
cq.exporters.export(cq.Workplane().add(shape), step_path, exportType="STEP")
|
| 59 |
+
|
| 60 |
+
pickle.dump({"success": True, "step_path": step_path}, sys.stdout.buffer)
|
| 61 |
+
|
| 62 |
+
except Exception as e:
|
| 63 |
+
import traceback
|
| 64 |
+
pickle.dump({"success": False, "error": str(e), "traceback": traceback.format_exc()}, sys.stdout.buffer)
|
| 65 |
+
'''
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
class CadforgeEnvironment(Environment):
|
| 69 |
+
SUPPORTS_CONCURRENT_SESSIONS: bool = True
|
| 70 |
+
|
| 71 |
+
def __init__(self):
|
| 72 |
+
super().__init__()
|
| 73 |
+
self._state = State(episode_id=str(uuid4()), step_count=0)
|
| 74 |
+
self._task_data = None
|
| 75 |
+
self._task_dir = None
|
| 76 |
+
self._ground_truth = None
|
| 77 |
+
self._max_steps = 10
|
| 78 |
+
self._best_reward = 0.0
|
| 79 |
+
self._done = False
|
| 80 |
+
self._task_prompt = None
|
| 81 |
+
self._artifact_registry: List[Dict[str, Any]] = []
|
| 82 |
+
self._last_executed: Optional[str] = None
|
| 83 |
+
self._best_object_id: Optional[str] = None
|
| 84 |
+
self._reward_cache: Dict[str, float] = {}
|
| 85 |
+
|
| 86 |
+
def reset(
|
| 87 |
+
self,
|
| 88 |
+
seed: Optional[int] = None,
|
| 89 |
+
episode_id: Optional[str] = None,
|
| 90 |
+
**kwargs: Any,
|
| 91 |
+
) -> CadforgeObservation:
|
| 92 |
+
t0 = time.time()
|
| 93 |
+
|
| 94 |
+
self._state = State(
|
| 95 |
+
episode_id=episode_id or str(uuid4()),
|
| 96 |
+
step_count=0,
|
| 97 |
+
)
|
| 98 |
+
self._best_reward = 0.0
|
| 99 |
+
self._done = False
|
| 100 |
+
self._artifact_registry = []
|
| 101 |
+
self._last_executed = None
|
| 102 |
+
self._best_object_id = None
|
| 103 |
+
self._reward_cache = {}
|
| 104 |
+
|
| 105 |
+
task_id = kwargs.get("task_id", None)
|
| 106 |
+
if task_id is None:
|
| 107 |
+
task_dirs = sorted(TASKS_ROOT.glob("task_*"))
|
| 108 |
+
if not task_dirs:
|
| 109 |
+
raise RuntimeError(f"No tasks found in {TASKS_ROOT}")
|
| 110 |
+
if seed is not None:
|
| 111 |
+
rng = np.random.RandomState(seed)
|
| 112 |
+
idx = rng.randint(0, len(task_dirs))
|
| 113 |
+
else:
|
| 114 |
+
idx = np.random.randint(0, len(task_dirs))
|
| 115 |
+
self._task_dir = task_dirs[idx]
|
| 116 |
+
task_id = self._task_dir.name
|
| 117 |
+
else:
|
| 118 |
+
self._task_dir = TASKS_ROOT / task_id
|
| 119 |
+
|
| 120 |
+
task_json_path = self._task_dir / "task.json"
|
| 121 |
+
if not task_json_path.exists():
|
| 122 |
+
raise RuntimeError(f"task.json not found at {task_json_path}")
|
| 123 |
+
|
| 124 |
+
with open(task_json_path) as f:
|
| 125 |
+
self._task_data = json.load(f)
|
| 126 |
+
|
| 127 |
+
self._task_prompt = self._task_data.get("prompt", "")
|
| 128 |
+
self._max_steps = self._task_data.get("max_steps", 10)
|
| 129 |
+
|
| 130 |
+
gt_json_path = self._task_dir / "ground_truth.json"
|
| 131 |
+
if gt_json_path.exists():
|
| 132 |
+
with open(gt_json_path) as f:
|
| 133 |
+
self._ground_truth = json.load(f)
|
| 134 |
+
self.rubric = build_cadforge_rubric(str(self._task_dir))
|
| 135 |
+
else:
|
| 136 |
+
self._ground_truth = None
|
| 137 |
+
self.rubric = None
|
| 138 |
+
logger.warning(f"No ground_truth.json for task {task_id}")
|
| 139 |
+
|
| 140 |
+
self._reset_rubric()
|
| 141 |
+
|
| 142 |
+
elapsed = time.time() - t0
|
| 143 |
+
logger.info(f"reset(task={task_id}) took {elapsed:.3f}s")
|
| 144 |
+
|
| 145 |
+
return CadforgeObservation(
|
| 146 |
+
task=self._task_prompt,
|
| 147 |
+
step_count=0,
|
| 148 |
+
done=False,
|
| 149 |
+
reward=0.0,
|
| 150 |
+
metadata={"task_id": task_id, "max_steps": self._max_steps},
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
def step(
|
| 154 |
+
self,
|
| 155 |
+
action: CadforgeAction,
|
| 156 |
+
timeout_s: Optional[float] = None,
|
| 157 |
+
**kwargs: Any,
|
| 158 |
+
) -> CadforgeObservation:
|
| 159 |
+
t0 = time.time()
|
| 160 |
+
|
| 161 |
+
if self._done:
|
| 162 |
+
return CadforgeObservation(
|
| 163 |
+
task=self._task_prompt,
|
| 164 |
+
step_count=self._state.step_count,
|
| 165 |
+
done=True,
|
| 166 |
+
reward=self._best_reward,
|
| 167 |
+
metadata={"message": "Episode already done"},
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
self._state.step_count += 1
|
| 171 |
+
action_type = action.action_type
|
| 172 |
+
params = action.params or {}
|
| 173 |
+
|
| 174 |
+
logger.info(f"step {self._state.step_count}: action_type={action_type}")
|
| 175 |
+
|
| 176 |
+
if action_type == "read_docs":
|
| 177 |
+
obs = self._handle_read_docs(params)
|
| 178 |
+
elif action_type == "execute_cadquery":
|
| 179 |
+
obs = self._handle_execute_cadquery(action, params)
|
| 180 |
+
elif action_type == "submit":
|
| 181 |
+
obs = self._handle_submit(params)
|
| 182 |
+
elif action_type == "render_object":
|
| 183 |
+
obs = self._handle_render_object(params)
|
| 184 |
+
else:
|
| 185 |
+
obs = CadforgeObservation(
|
| 186 |
+
task=self._task_prompt,
|
| 187 |
+
step_count=self._state.step_count,
|
| 188 |
+
done=False,
|
| 189 |
+
reward=0.0,
|
| 190 |
+
code_error=f"Unknown action_type: {action_type}",
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
if self._state.step_count >= self._max_steps and not self._done:
|
| 194 |
+
self._done = True
|
| 195 |
+
obs.done = True
|
| 196 |
+
obs.reward = 0.0
|
| 197 |
+
logger.info(f"Max steps reached without submit. Episode done. reward=0.")
|
| 198 |
+
|
| 199 |
+
elapsed = time.time() - t0
|
| 200 |
+
logger.info(f"step took {elapsed:.3f}s total")
|
| 201 |
+
return obs
|
| 202 |
+
|
| 203 |
+
def _handle_read_docs(self, params: dict) -> CadforgeObservation:
|
| 204 |
+
topic = params.get("topic")
|
| 205 |
+
query = params.get("query")
|
| 206 |
+
results = search_docs(topic=topic, query=query)
|
| 207 |
+
|
| 208 |
+
return CadforgeObservation(
|
| 209 |
+
task=self._task_prompt,
|
| 210 |
+
step_count=self._state.step_count,
|
| 211 |
+
done=False,
|
| 212 |
+
reward=0.0,
|
| 213 |
+
docs_results=results,
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
def _handle_execute_cadquery(self, action: CadforgeAction, params: dict) -> CadforgeObservation:
|
| 217 |
+
code = params.get("code", "")
|
| 218 |
+
if not code:
|
| 219 |
+
return CadforgeObservation(
|
| 220 |
+
task=self._task_prompt,
|
| 221 |
+
step_count=self._state.step_count,
|
| 222 |
+
done=False,
|
| 223 |
+
reward=0.0,
|
| 224 |
+
code_executed=False,
|
| 225 |
+
code_error="No code provided in params.code",
|
| 226 |
+
artifacts=self._artifact_registry[:] or None,
|
| 227 |
+
last_executed=self._last_executed,
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
exec_result = execute_cadquery_code(code, timeout=10.0)
|
| 231 |
+
|
| 232 |
+
if not exec_result["success"]:
|
| 233 |
+
return CadforgeObservation(
|
| 234 |
+
task=self._task_prompt,
|
| 235 |
+
step_count=self._state.step_count,
|
| 236 |
+
done=False,
|
| 237 |
+
reward=0.0,
|
| 238 |
+
code_executed=False,
|
| 239 |
+
code_error=exec_result.get("error", "Unknown execution error"),
|
| 240 |
+
artifacts=self._artifact_registry[:] or None,
|
| 241 |
+
last_executed=self._last_executed,
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
props = exec_result["properties"]
|
| 245 |
+
agent_visible_props = dict(props)
|
| 246 |
+
|
| 247 |
+
object_id = f"{self._state.episode_id}_step{self._state.step_count}"
|
| 248 |
+
step_path = str(STEP_OUTPUT_DIR / f"{object_id}.step")
|
| 249 |
+
|
| 250 |
+
export_result = self._export_agent_step(code, step_path)
|
| 251 |
+
|
| 252 |
+
raw_data = None
|
| 253 |
+
if export_result and export_result.get("success"):
|
| 254 |
+
raw_data = {"step_path": export_result["step_path"]}
|
| 255 |
+
artifact = {"object_id": object_id, "step_path": export_result["step_path"]}
|
| 256 |
+
self._artifact_registry.append(artifact)
|
| 257 |
+
self._last_executed = object_id
|
| 258 |
+
else:
|
| 259 |
+
error_msg = export_result.get("error", "unknown") if export_result else "export failed"
|
| 260 |
+
logger.warning(f"STEP export failed for {object_id}: {error_msg}")
|
| 261 |
+
self._last_executed = object_id
|
| 262 |
+
|
| 263 |
+
obs = CadforgeObservation(
|
| 264 |
+
task=self._task_prompt,
|
| 265 |
+
step_count=self._state.step_count,
|
| 266 |
+
done=False,
|
| 267 |
+
reward=0.0,
|
| 268 |
+
code_executed=True,
|
| 269 |
+
object_id=object_id,
|
| 270 |
+
object_properties=agent_visible_props,
|
| 271 |
+
artifacts=self._artifact_registry[:] or None,
|
| 272 |
+
last_executed=self._last_executed,
|
| 273 |
+
)
|
| 274 |
+
obs._raw_data = raw_data
|
| 275 |
+
|
| 276 |
+
reward = self._apply_rubric(action, obs)
|
| 277 |
+
self._reward_cache[object_id] = reward
|
| 278 |
+
logger.info(f"Reward for {object_id}: {reward:.4f} (cached)")
|
| 279 |
+
|
| 280 |
+
if reward > self._best_reward:
|
| 281 |
+
self._best_reward = reward
|
| 282 |
+
self._best_object_id = object_id
|
| 283 |
+
logger.info(f"New best reward: {self._best_reward:.4f} (object_id={object_id})")
|
| 284 |
+
|
| 285 |
+
obs.reward = reward
|
| 286 |
+
return obs
|
| 287 |
+
|
| 288 |
+
def _export_agent_step(self, code: str, step_path: str) -> Optional[Dict[str, Any]]:
|
| 289 |
+
t0 = time.time()
|
| 290 |
+
try:
|
| 291 |
+
payload = code + "\n---STEP_PATH---\n" + step_path
|
| 292 |
+
proc = subprocess.run(
|
| 293 |
+
[sys.executable, "-c", STEP_EXPORT_RUNNER],
|
| 294 |
+
input=payload.encode(),
|
| 295 |
+
capture_output=True,
|
| 296 |
+
timeout=30,
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
if proc.returncode != 0:
|
| 300 |
+
logger.warning(f"STEP export subprocess failed (rc={proc.returncode}): {proc.stderr[:500]}")
|
| 301 |
+
return {"success": False, "error": proc.stderr[:500].decode() if isinstance(proc.stderr, bytes) else proc.stderr[:500]}
|
| 302 |
+
|
| 303 |
+
data = pickle.loads(proc.stdout)
|
| 304 |
+
elapsed = time.time() - t0
|
| 305 |
+
logger.info(f"_export_agent_step took {elapsed:.3f}s, success={data.get('success')}")
|
| 306 |
+
return data
|
| 307 |
+
|
| 308 |
+
except Exception as e:
|
| 309 |
+
elapsed = time.time() - t0
|
| 310 |
+
logger.error(f"_export_agent_step failed after {elapsed:.3f}s: {e}")
|
| 311 |
+
return {"success": False, "error": str(e)}
|
| 312 |
+
|
| 313 |
+
def _handle_submit(self, params: dict) -> CadforgeObservation:
|
| 314 |
+
self._done = True
|
| 315 |
+
|
| 316 |
+
submit_object_id = params.get("object_id", None)
|
| 317 |
+
if submit_object_id is None:
|
| 318 |
+
submit_object_id = self._best_object_id
|
| 319 |
+
logger.info(f"Submit: no object_id provided, using best: {submit_object_id}")
|
| 320 |
+
|
| 321 |
+
if submit_object_id and submit_object_id in self._reward_cache:
|
| 322 |
+
submit_reward = self._reward_cache[submit_object_id]
|
| 323 |
+
else:
|
| 324 |
+
submit_reward = self._best_reward
|
| 325 |
+
logger.warning(
|
| 326 |
+
f"Submit: object_id={submit_object_id} not in reward cache, "
|
| 327 |
+
f"falling back to best_reward={self._best_reward:.4f}"
|
| 328 |
+
)
|
| 329 |
+
|
| 330 |
+
logger.info(
|
| 331 |
+
f"Submit: object_id={submit_object_id}, reward={submit_reward:.4f}, "
|
| 332 |
+
f"best_reward={self._best_reward:.4f}, best_object={self._best_object_id}"
|
| 333 |
+
)
|
| 334 |
+
|
| 335 |
+
return CadforgeObservation(
|
| 336 |
+
task=self._task_prompt,
|
| 337 |
+
step_count=self._state.step_count,
|
| 338 |
+
done=True,
|
| 339 |
+
reward=submit_reward,
|
| 340 |
+
artifacts=self._artifact_registry[:] or None,
|
| 341 |
+
last_executed=self._last_executed,
|
| 342 |
+
metadata={
|
| 343 |
+
"message": "Episode submitted",
|
| 344 |
+
"submitted_object_id": submit_object_id,
|
| 345 |
+
"submitted_reward": submit_reward,
|
| 346 |
+
"best_reward": self._best_reward,
|
| 347 |
+
"best_object_id": self._best_object_id,
|
| 348 |
+
"reward_cache": {k: round(v, 4) for k, v in self._reward_cache.items()},
|
| 349 |
+
},
|
| 350 |
+
)
|
| 351 |
+
|
| 352 |
+
def _handle_render_object(self, params: dict) -> CadforgeObservation:
|
| 353 |
+
return CadforgeObservation(
|
| 354 |
+
task=self._task_prompt,
|
| 355 |
+
step_count=self._state.step_count,
|
| 356 |
+
done=False,
|
| 357 |
+
reward=0.0,
|
| 358 |
+
metadata={"message": "render_object is not available in Phase 1"},
|
| 359 |
+
)
|
| 360 |
+
|
| 361 |
+
@property
|
| 362 |
+
def state(self) -> State:
|
| 363 |
+
return self._state
|
server/docs/concepts/brep-mindset.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# BRep vs CSG — The Core Mindset
|
| 2 |
+
|
| 3 |
+
## What BRep Is
|
| 4 |
+
|
| 5 |
+
**Boundary Representation (BRep)** defines a solid by its boundary: the faces, edges, and vertices
|
| 6 |
+
that enclose it. The interior is implied - it is whatever is inside those boundaries.
|
| 7 |
+
|
| 8 |
+
A BRep model has explicit **topology** (how faces connect to edges, edges connect to vertices)
|
| 9 |
+
and **geometry** (the actual surfaces and curves those topological entities lie on).
|
| 10 |
+
|
| 11 |
+
```
|
| 12 |
+
Solid
|
| 13 |
+
├── Shell (closed set of faces)
|
| 14 |
+
│ ├── Face (bounded region of a surface)
|
| 15 |
+
│ │ ├── Wire (closed loop of edges bounding the face)
|
| 16 |
+
│ │ │ └── Edge (bounded curve)
|
| 17 |
+
│ │ │ └── Vertex (point)
|
| 18 |
+
```
|
| 19 |
+
|
| 20 |
+
This hierarchy is what CadQuery exposes through selectors. When you write `.faces(">Z")`,
|
| 21 |
+
you are querying the topology of a BRep solid.
|
| 22 |
+
|
| 23 |
+
## What CSG Is
|
| 24 |
+
|
| 25 |
+
**Constructive Solid Geometry (CSG)** defines a solid as a tree of Boolean operations on primitives:
|
| 26 |
+
|
| 27 |
+
```
|
| 28 |
+
union
|
| 29 |
+
├── box(10, 10, 10)
|
| 30 |
+
└── cut
|
| 31 |
+
├── cylinder(r=3, h=15)
|
| 32 |
+
└── box(5, 5, 5)
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
CSG is intuitive and maps well to how humans think about "adding" and "removing" material.
|
| 36 |
+
OpenSCAD is pure CSG. Many LLMs default to this mental model because most training examples use it.
|
| 37 |
+
|
| 38 |
+
## Why CadQuery Is Different
|
| 39 |
+
|
| 40 |
+
CadQuery's kernel (OpenCASCADE) is BRep-native. The model is always stored as BRep.
|
| 41 |
+
Boolean operations exist but are expensive — they recompute the entire boundary from scratch.
|
| 42 |
+
|
| 43 |
+
CadQuery's fluent API is designed around **working with existing topology**:
|
| 44 |
+
- Select a face → define a new workplane on it → sketch → extrude
|
| 45 |
+
- Select edges → apply fillet or chamfer directly
|
| 46 |
+
- Select a face → shell inward
|
| 47 |
+
|
| 48 |
+
These operations modify or extend the BRep directly, without rebuilding it from scratch.
|
| 49 |
+
|
| 50 |
+
## The Practical Difference
|
| 51 |
+
|
| 52 |
+
**CSG thinking** asks: *what shapes do I combine?*
|
| 53 |
+
|
| 54 |
+
**BRep thinking** asks: *what topology already exists, and what can I build from it?*
|
| 55 |
+
|
| 56 |
+
### Example: adding a boss to a plate
|
| 57 |
+
|
| 58 |
+
CSG approach:
|
| 59 |
+
```python
|
| 60 |
+
plate = cq.Workplane("XY").box(50, 50, 5)
|
| 61 |
+
boss = cq.Workplane("XY").cylinder(10, 8).translate((10, 10, 7.5))
|
| 62 |
+
result = plate.union(boss)
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
BRep approach:
|
| 66 |
+
```python
|
| 67 |
+
result = (
|
| 68 |
+
cq.Workplane("XY")
|
| 69 |
+
.box(50, 50, 5)
|
| 70 |
+
.faces(">Z").workplane()
|
| 71 |
+
.center(10, 10)
|
| 72 |
+
.circle(8).extrude(10)
|
| 73 |
+
)
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
The BRep approach:
|
| 77 |
+
- Produces cleaner topology (no Boolean seam)
|
| 78 |
+
- Is faster to compute
|
| 79 |
+
- Keeps the chain readable
|
| 80 |
+
- Leaves faces available for further selection
|
| 81 |
+
|
| 82 |
+
### Example: hollowing a box
|
| 83 |
+
|
| 84 |
+
CSG approach:
|
| 85 |
+
```python
|
| 86 |
+
outer = cq.Workplane("XY").box(20, 20, 20)
|
| 87 |
+
inner = cq.Workplane("XY").box(16, 16, 20).translate((0, 0, 2))
|
| 88 |
+
result = outer.cut(inner)
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
BRep approach:
|
| 92 |
+
```python
|
| 93 |
+
result = cq.Workplane("XY").box(20, 20, 20).faces(">Z").shell(-2)
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
## When Booleans Are Appropriate
|
| 97 |
+
|
| 98 |
+
Not all Booleans are wrong. Use them when:
|
| 99 |
+
|
| 100 |
+
- Combining **separately constructed solids** that have no shared topology
|
| 101 |
+
- The geometry cannot be described as a profile operation (extrude/revolve/sweep/loft)
|
| 102 |
+
- Working with imported STEP/IGES geometry that you need to subtract from
|
| 103 |
+
- Using the Free Function API where operator syntax (`+`, `-`, `*`) is natural
|
| 104 |
+
|
| 105 |
+
Even then, in the Free Function API, prefer `addHole()` and `replace()` over cut() when
|
| 106 |
+
modifying individual faces — it avoids recomputing the full solid boundary.
|
| 107 |
+
|
| 108 |
+
## BRep Vocabulary in CadQuery
|
| 109 |
+
|
| 110 |
+
| Term | Meaning | CadQuery access |
|
| 111 |
+
|------|---------|-----------------|
|
| 112 |
+
| `Solid` | A closed volume | `.solids()` |
|
| 113 |
+
| `Shell` | A set of connected faces (may be open) | `.shells()` |
|
| 114 |
+
| `Face` | A bounded surface region | `.faces()` |
|
| 115 |
+
| `Wire` | A closed or open loop of edges | `.wires()` |
|
| 116 |
+
| `Edge` | A bounded curve | `.edges()` |
|
| 117 |
+
| `Vertex` | A point | `.vertices()` |
|
| 118 |
+
| `Compound` | A collection of any shapes | `.compounds()` |
|
| 119 |
+
|
| 120 |
+
Understanding this hierarchy is essential for writing correct selectors and knowing
|
| 121 |
+
which `.val()` / `.vals()` calls will return what type.
|
server/docs/concepts/free-function-api.md
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Free Function API
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
The Free Function API provides an alternative to the fluent Workplane API.
|
| 6 |
+
It has no hidden state - every operation takes explicit inputs and returns explicit outputs.
|
| 7 |
+
Selectors still work as methods on Shape objects, but all other operations are free functions.
|
| 8 |
+
|
| 9 |
+
```python
|
| 10 |
+
from cadquery.func import *
|
| 11 |
+
```
|
| 12 |
+
|
| 13 |
+
Use this API when:
|
| 14 |
+
- Building shapes face-by-face or edge-by-edge
|
| 15 |
+
- Lofting between non-planar edges or constructing organic surfaces
|
| 16 |
+
- Avoiding booleans via `addHole()` / `replace()` for performance
|
| 17 |
+
- Working in parametric surface space (`trim()`, `edgeOn()`, `wireOn()`)
|
| 18 |
+
- The explicit, stateless style is clearer for the task at hand
|
| 19 |
+
|
| 20 |
+
---
|
| 21 |
+
|
| 22 |
+
## Primitives
|
| 23 |
+
|
| 24 |
+
All primitives return Shape objects that can be immediately operated on.
|
| 25 |
+
|
| 26 |
+
```python
|
| 27 |
+
from cadquery.func import *
|
| 28 |
+
|
| 29 |
+
e = segment((0, 0), (0, 1)) # line edge
|
| 30 |
+
c = circle(1) # circular edge
|
| 31 |
+
r = rect(2, 1) # rectangular wire
|
| 32 |
+
f = plane(1, 1.5) # flat face
|
| 33 |
+
b = box(1, 1, 1) # solid box
|
| 34 |
+
cy = cylinder(1, 2) # solid cylinder (radius, height)
|
| 35 |
+
sp = sphere(1) # solid sphere
|
| 36 |
+
co = cone(1, 1.5) # solid cone (r1, r2)
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
Combine unrelated shapes into a compound for display or export:
|
| 40 |
+
|
| 41 |
+
```python
|
| 42 |
+
result = compound(e, c.moved(x=2), f.moved(x=4), b.moved(x=6))
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
---
|
| 46 |
+
|
| 47 |
+
## Shape Construction
|
| 48 |
+
|
| 49 |
+
Build higher-level shapes from lower-level ones.
|
| 50 |
+
|
| 51 |
+
```python
|
| 52 |
+
e1 = segment((0, 0), (1, 0))
|
| 53 |
+
e2 = segment((1, 0), (1, 1))
|
| 54 |
+
|
| 55 |
+
w = wire(e1, e2) # edges → wire
|
| 56 |
+
f = face(circle(1)) # closed wire → face
|
| 57 |
+
s = solid(f1, f2, f3) # faces → solid
|
| 58 |
+
sh = shell(f1, f2) # faces → shell (open solid — use before solid() when sewing)
|
| 59 |
+
cp = compound(s1, s2) # shapes → compound
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
**`solid()` vs `shell()`:** Use `solid()` when all faces are already properly connected.
|
| 63 |
+
Use `shell()` first to sew faces together when the topology needs explicit stitching
|
| 64 |
+
(e.g., when adding protrusions or working with trimmed surfaces).
|
| 65 |
+
|
| 66 |
+
---
|
| 67 |
+
|
| 68 |
+
## Operations
|
| 69 |
+
|
| 70 |
+
### Extrude
|
| 71 |
+
|
| 72 |
+
Accepts a wire or a face. Direction is a 3-tuple vector.
|
| 73 |
+
|
| 74 |
+
```python
|
| 75 |
+
r = rect(1, 0.5)
|
| 76 |
+
f = face(r)
|
| 77 |
+
|
| 78 |
+
s1 = extrude(r, (0, 0, 2)) # wire → solid with open ends
|
| 79 |
+
s2 = extrude(f, (0, 0, 1)) # face → closed solid
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
### Loft
|
| 83 |
+
|
| 84 |
+
Lofts through a sequence of edges or wires. `cap=True` closes the ends automatically.
|
| 85 |
+
|
| 86 |
+
```python
|
| 87 |
+
s = loft(circle(1), circle(1.5).moved(z=5), circle(1).moved(z=10))
|
| 88 |
+
s_capped = loft(rect(2, 1), circle(1).moved(z=5), cap=True)
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
For curvature-continuous end caps, use `cap()` instead of `fill()` after lofting:
|
| 92 |
+
|
| 93 |
+
```python
|
| 94 |
+
side = loft(circle(1), circle(1.3).moved(z=5), circle(1).moved(z=10))
|
| 95 |
+
base = fill(side.edges("<Z")) # flat cap — no curvature continuity
|
| 96 |
+
top = cap(side.edges(">Z"), side) # smooth cap — continuous with side
|
| 97 |
+
result = solid(side, base, top)
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
### Sweep
|
| 101 |
+
|
| 102 |
+
```python
|
| 103 |
+
profile = rect(0.5, 0.3)
|
| 104 |
+
path = segment((0, 0, 0), (0, 0, 10)) # straight path
|
| 105 |
+
|
| 106 |
+
result = sweep(profile, path)
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
Spline paths are supported - see the CadQuery docs and tests for the correct
|
| 110 |
+
`spline()` argument form, as dispatch is sensitive to argument types.
|
| 111 |
+
|
| 112 |
+
### Revolve
|
| 113 |
+
|
| 114 |
+
```python
|
| 115 |
+
# revolve(face, axis_point, axis_direction, angle_degrees)
|
| 116 |
+
f = face(rect(1, 0.5)).moved(x=2)
|
| 117 |
+
result = revolve(f, (0, 0, 0), (0, 1, 0), 90)
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+
---
|
| 121 |
+
|
| 122 |
+
## Placement
|
| 123 |
+
|
| 124 |
+
The Free Function API has no workplane. Position shapes with `.moved()` and `.move()`.
|
| 125 |
+
|
| 126 |
+
```python
|
| 127 |
+
b = box(1, 1, 1)
|
| 128 |
+
|
| 129 |
+
b.moved(x=2) # translate
|
| 130 |
+
b.moved(rx=90) # rotate 90° around X (degrees)
|
| 131 |
+
b.moved(x=2, rz=45) # translate and rotate
|
| 132 |
+
b.move(z=5) # in-place variant (modifies and returns self)
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
### Patterns - multiple locations in one call
|
| 136 |
+
|
| 137 |
+
Passing multiple position tuples to `.moved()` creates a **compound** of copies:
|
| 138 |
+
|
| 139 |
+
```python
|
| 140 |
+
peg = cylinder(1, 5)
|
| 141 |
+
|
| 142 |
+
result = peg.moved(
|
| 143 |
+
(-5, -5, 0),
|
| 144 |
+
( 5, -5, 0),
|
| 145 |
+
(-5, 5, 0),
|
| 146 |
+
( 5, 5, 0),
|
| 147 |
+
) # compound of 4 pegs
|
| 148 |
+
```
|
| 149 |
+
|
| 150 |
+
### Face-relative placement
|
| 151 |
+
|
| 152 |
+
Select the face and use its geometry to derive position:
|
| 153 |
+
|
| 154 |
+
```python
|
| 155 |
+
b = box(20, 20, 10)
|
| 156 |
+
top = b.faces(">Z")
|
| 157 |
+
z = top.Center().z
|
| 158 |
+
|
| 159 |
+
boss = cylinder(3, 8).moved(z=z)
|
| 160 |
+
result = b + boss
|
| 161 |
+
```
|
| 162 |
+
|
| 163 |
+
---
|
| 164 |
+
|
| 165 |
+
## Boolean Operations
|
| 166 |
+
|
| 167 |
+
Boolean ops are available as both operators and free functions.
|
| 168 |
+
**Avoid booleans in loops** - they recompute the full boundary each time.
|
| 169 |
+
|
| 170 |
+
```python
|
| 171 |
+
c1 = cylinder(1, 2)
|
| 172 |
+
c2 = cylinder(0.5, 3)
|
| 173 |
+
|
| 174 |
+
r1 = c1 + c2 # union (also: fuse(c1, c2))
|
| 175 |
+
r2 = c1 - c2 # cut (also: cut(c1, c2))
|
| 176 |
+
r3 = c1 * c2 # intersect (also: intersect(c1, c2))
|
| 177 |
+
r4 = c1 / plane() # split (also: split(c1, plane()))
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
Boolean ops work on 2D shapes (faces, wires) as well as solids:
|
| 181 |
+
|
| 182 |
+
```python
|
| 183 |
+
outer = plane(20, 20)
|
| 184 |
+
inner = plane(10, 10)
|
| 185 |
+
frame = outer - inner # face with a hole
|
| 186 |
+
result = extrude(frame, (0, 0, 5))
|
| 187 |
+
```
|
| 188 |
+
|
| 189 |
+
When unioning many shapes, combine into a compound first to reduce operation count:
|
| 190 |
+
|
| 191 |
+
```python
|
| 192 |
+
pins = [cylinder(0.5, 5).moved(x=i*1.5) for i in range(8)]
|
| 193 |
+
result = box(20, 5, 5) - compound(pins) # one boolean, not 8
|
| 194 |
+
```
|
| 195 |
+
|
| 196 |
+
---
|
| 197 |
+
|
| 198 |
+
## Adding Features Without Booleans
|
| 199 |
+
|
| 200 |
+
For complex shapes where boolean performance matters, use `addHole()` and `replace()`
|
| 201 |
+
to modify individual faces directly rather than recomputing the full solid boundary.
|
| 202 |
+
|
| 203 |
+
```python
|
| 204 |
+
from cadquery.func import *
|
| 205 |
+
|
| 206 |
+
w = 1
|
| 207 |
+
r = 0.9 * w / 2
|
| 208 |
+
|
| 209 |
+
b = box(w, w, w)
|
| 210 |
+
b_bot = b.faces("<Z")
|
| 211 |
+
b_top = b.faces(">Z")
|
| 212 |
+
|
| 213 |
+
inner = extrude(circle(r), (0, 0, w))
|
| 214 |
+
|
| 215 |
+
b_bot_hole = b_bot.addHole(inner.edges("<Z"))
|
| 216 |
+
b_top_hole = b_top.addHole(inner.edges(">Z"))
|
| 217 |
+
|
| 218 |
+
result = solid(
|
| 219 |
+
b.remove(b_top, b_bot).faces(),
|
| 220 |
+
b_bot_hole,
|
| 221 |
+
inner,
|
| 222 |
+
b_top_hole,
|
| 223 |
+
)
|
| 224 |
+
```
|
| 225 |
+
|
| 226 |
+
For protrusions (adding material rather than removing it), sew with `shell()` first
|
| 227 |
+
to give the kernel enough context to stitch the new faces correctly:
|
| 228 |
+
|
| 229 |
+
```python
|
| 230 |
+
b = box(1, 1, 1)
|
| 231 |
+
b_top = b.faces(">Z")
|
| 232 |
+
|
| 233 |
+
feat_side = extrude(circle(0.4).moved(b_top.Center()), (0, 0, 0.2))
|
| 234 |
+
feat_top = face(feat_side.edges(">Z"))
|
| 235 |
+
feat = shell(feat_side, feat_top) # sew into a shell first
|
| 236 |
+
|
| 237 |
+
b_top_hole = b_top.addHole(feat.edges("<Z"))
|
| 238 |
+
b = b.replace(b_top, b_top_hole)
|
| 239 |
+
|
| 240 |
+
sh = shell(b_top_hole, feat.faces("<Z"), ctx=(b, feat))
|
| 241 |
+
result = solid(sh)
|
| 242 |
+
```
|
| 243 |
+
|
| 244 |
+
---
|
| 245 |
+
|
| 246 |
+
## Text
|
| 247 |
+
|
| 248 |
+
`text()` uses multimethod dispatch — **all arguments must be positional**.
|
| 249 |
+
Keyword arguments will raise a `DispatchError`.
|
| 250 |
+
|
| 251 |
+
```python
|
| 252 |
+
from cadquery.func import *
|
| 253 |
+
|
| 254 |
+
# Planar text along a line
|
| 255 |
+
spine = segment((0, 0, 0), (30, 0, 0))
|
| 256 |
+
result = text("CadQuery", 3, spine, planar=True)
|
| 257 |
+
|
| 258 |
+
# Normal (projected) text along a spine on a surface
|
| 259 |
+
# See the CadQuery docs for the full surface projection example
|
| 260 |
+
```
|
| 261 |
+
|
| 262 |
+
---
|
| 263 |
+
|
| 264 |
+
## Parametric Surface Mapping
|
| 265 |
+
|
| 266 |
+
Advanced: trim faces and edges in parametric (u, v) space.
|
| 267 |
+
|
| 268 |
+
```python
|
| 269 |
+
from cadquery.func import cylinder, edgeOn, wire
|
| 270 |
+
|
| 271 |
+
base = cylinder(1.5, 3).faces("%CYLINDER")
|
| 272 |
+
|
| 273 |
+
# Rectangular trim
|
| 274 |
+
r = base.trim(-1.5, 0, 0, 1)
|
| 275 |
+
|
| 276 |
+
# Construct an edge in parametric space and trim with it
|
| 277 |
+
from math import pi
|
| 278 |
+
pcurve = edgeOn(base, [(0, 0.5), (pi, 0.5), (pi, 1.5), (0, 1.5)], periodic=True)
|
| 279 |
+
trimmed = base.trim(wire(pcurve))
|
| 280 |
+
```
|
| 281 |
+
|
| 282 |
+
Use `wireOn()` to map a 3D wire onto a surface, and `faceOn()` to map a face:
|
| 283 |
+
|
| 284 |
+
```python
|
| 285 |
+
from cadquery.func import sphere, text, faceOn
|
| 286 |
+
|
| 287 |
+
base = sphere(5).faces()
|
| 288 |
+
result = faceOn(base, text("CadQuery", 1))
|
| 289 |
+
```
|
| 290 |
+
|
| 291 |
+
---
|
| 292 |
+
|
| 293 |
+
## Selectors in the Free Function API
|
| 294 |
+
|
| 295 |
+
Selectors work identically to the fluent API — as string methods on any Shape object.
|
| 296 |
+
|
| 297 |
+
```python
|
| 298 |
+
b = box(20, 20, 10)
|
| 299 |
+
|
| 300 |
+
top = b.faces(">Z")
|
| 301 |
+
side_faces = b.faces("#Z")
|
| 302 |
+
circ_edges = b.faces(">Z").edges("%Circle")
|
| 303 |
+
```
|
| 304 |
+
|
| 305 |
+
No Workplane is needed. The same selector strings, combinators, and tag syntax all apply.
|
| 306 |
+
See `concepts/selectors.md` for the full reference.
|
server/docs/concepts/selectors.md
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Selectors
|
| 2 |
+
|
| 3 |
+
Selectors filter the topology of a shape - faces, edges, wires, or vertices - down to the
|
| 4 |
+
subset you want to operate on. They work the same way in both the fluent API and the
|
| 5 |
+
Free Function API (as methods on Shape objects).
|
| 6 |
+
|
| 7 |
+
## Selector Syntax
|
| 8 |
+
|
| 9 |
+
Selectors are strings passed to `.faces()`, `.edges()`, `.wires()`, or `.vertices()`.
|
| 10 |
+
|
| 11 |
+
### Axis-based selectors
|
| 12 |
+
|
| 13 |
+
| Selector | Applies to | Meaning |
|
| 14 |
+
|----------|-----------|---------|
|
| 15 |
+
| `">X"` | faces, edges | Highest centroid along X |
|
| 16 |
+
| `"<X"` | faces, edges | Lowest centroid along X |
|
| 17 |
+
| `"\|X"` | faces | Normal is parallel to X axis (faces the ±X direction) |
|
| 18 |
+
| `"#X"` | faces, edges | Face normal or edge direction is orthogonal to X axis |
|
| 19 |
+
| `"+X"` | faces | Normal points in the +X direction |
|
| 20 |
+
| `"-X"` | faces | Normal points in the -X direction |
|
| 21 |
+
|
| 22 |
+
Replace `X` with `Y` or `Z` as needed.
|
| 23 |
+
|
| 24 |
+
### Sorted index selectors
|
| 25 |
+
|
| 26 |
+
`">>X[n]"` and `"<<X[n]"` sort entities by their centroid along an axis and pick by index (0-based).
|
| 27 |
+
|
| 28 |
+
```python
|
| 29 |
+
# Face with the second-highest Z centroid
|
| 30 |
+
solid.faces(">>Z[1]")
|
| 31 |
+
|
| 32 |
+
# Edge with the lowest X centroid
|
| 33 |
+
solid.edges("<<X[0]")
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
**Warning:** index selectors are fragile. Adding fillets, chamfers, or other features
|
| 37 |
+
changes the entity count and shifts indices. Prefer geometric selectors or tags.
|
| 38 |
+
|
| 39 |
+
### Type selectors
|
| 40 |
+
|
| 41 |
+
Filters by the geometric type of the surface or curve.
|
| 42 |
+
|
| 43 |
+
| Selector | Meaning |
|
| 44 |
+
|----------|---------|
|
| 45 |
+
| `"%Plane"` | Planar faces |
|
| 46 |
+
| `"%Cylinder"` | Cylindrical faces |
|
| 47 |
+
| `"%Cone"` | Conical faces |
|
| 48 |
+
| `"%Sphere"` | Spherical faces |
|
| 49 |
+
| `"%Torus"` | Toroidal faces |
|
| 50 |
+
| `"%Line"` | Linear edges |
|
| 51 |
+
| `"%Circle"` | Circular edges |
|
| 52 |
+
|
| 53 |
+
```python
|
| 54 |
+
# All cylindrical faces
|
| 55 |
+
solid.faces("%Cylinder")
|
| 56 |
+
|
| 57 |
+
# All circular edges
|
| 58 |
+
solid.edges("%Circle")
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
### Boolean combinators
|
| 62 |
+
|
| 63 |
+
| Syntax | Meaning |
|
| 64 |
+
|--------|---------|
|
| 65 |
+
| `"not >Z"` | All faces except the highest Z face |
|
| 66 |
+
| `">Z and \|X"` | Faces that are both highest Z AND normal parallel to X |
|
| 67 |
+
| `">Z or <Z"` | Top and bottom faces |
|
| 68 |
+
|
| 69 |
+
```python
|
| 70 |
+
# All faces except the top
|
| 71 |
+
solid.faces("not >Z")
|
| 72 |
+
|
| 73 |
+
# Edges that are both circular and on the top face - use chaining instead
|
| 74 |
+
solid.faces(">Z").edges("%Circle")
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
For complex filtering, chaining selectors (`.faces(...).edges(...)`) is clearer than
|
| 78 |
+
combining them in a single string.
|
| 79 |
+
|
| 80 |
+
## Tag-Based Selection
|
| 81 |
+
|
| 82 |
+
Tags are the most stable selection mechanism. They survive feature additions that would
|
| 83 |
+
shift index-based selectors.
|
| 84 |
+
|
| 85 |
+
```python
|
| 86 |
+
result = (
|
| 87 |
+
cq.Workplane("XY")
|
| 88 |
+
.box(20, 20, 10)
|
| 89 |
+
.faces(">Z").tag("top")
|
| 90 |
+
.end()
|
| 91 |
+
.faces("<Z").tag("bottom")
|
| 92 |
+
.end()
|
| 93 |
+
.faces(tag="top").workplane().hole(4)
|
| 94 |
+
.faces(tag="bottom").workplane().hole(2)
|
| 95 |
+
)
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
Tag early - as soon as a face or edge is created that you will need later.
|
| 99 |
+
Tags are stored on the Workplane object, not on the shape itself.
|
| 100 |
+
|
| 101 |
+
## Selectors in the Free Function API
|
| 102 |
+
|
| 103 |
+
Selectors work as methods on any Shape object - no Workplane needed.
|
| 104 |
+
|
| 105 |
+
```python
|
| 106 |
+
from cadquery.func import *
|
| 107 |
+
|
| 108 |
+
b = box(20, 20, 10)
|
| 109 |
+
|
| 110 |
+
top = b.faces(">Z")
|
| 111 |
+
top_edges = b.faces(">Z").edges("%Circle")
|
| 112 |
+
side_faces = b.faces("|Z") # faces whose normal is parallel to Z i i.e. sides
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
The same selector strings work identically in both APIs.
|
| 116 |
+
|
| 117 |
+
## How Selectors Match
|
| 118 |
+
|
| 119 |
+
Understanding what a selector actually tests prevents subtle bugs.
|
| 120 |
+
|
| 121 |
+
### `">Z"` — highest centroid
|
| 122 |
+
|
| 123 |
+
Finds the entity whose **centroid** has the highest Z coordinate. On a simple box this
|
| 124 |
+
is the top face. On a complex solid it may not be the face you expect - the centroid
|
| 125 |
+
is the geometric center of the face area, not the highest point.
|
| 126 |
+
|
| 127 |
+
### `"|X"` — normal parallel to axis
|
| 128 |
+
|
| 129 |
+
Selects faces whose **outward normal** is parallel (in either direction) to the given axis.
|
| 130 |
+
On a box, `"|X"` gives both the left and right faces.
|
| 131 |
+
|
| 132 |
+
```python
|
| 133 |
+
# Both faces whose normal is parallel to X
|
| 134 |
+
solid.faces("|X") # returns 2 faces on a box
|
| 135 |
+
|
| 136 |
+
# Just the face pointing in +X
|
| 137 |
+
solid.faces("+X") # returns 1 face
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
### `"#X"` — orthogonal to axis
|
| 141 |
+
|
| 142 |
+
Selects faces whose **normal** is orthogonal to the given axis, or edges whose
|
| 143 |
+
**direction** is orthogonal to the given axis.
|
| 144 |
+
|
| 145 |
+
On a box, `#Z` gives the four side faces (normals point in X or Y, which are orthogonal to Z),
|
| 146 |
+
and the horizontal edges (direction lies in the XY plane, orthogonal to Z).
|
| 147 |
+
|
| 148 |
+
```python
|
| 149 |
+
# Side faces of a box (normals orthogonal to Z)
|
| 150 |
+
solid.faces("#Z") # 4 side faces
|
| 151 |
+
|
| 152 |
+
# Horizontal edges (direction orthogonal to Z)
|
| 153 |
+
solid.edges("#Z") # 8 horizontal edges on a box
|
| 154 |
+
```
|
| 155 |
+
|
| 156 |
+
Contrast with `|Z`, which selects entities whose normal or direction is **parallel** to Z
|
| 157 |
+
(top and bottom faces, vertical edges).
|
| 158 |
+
|
| 159 |
+
## Common Mistakes
|
| 160 |
+
|
| 161 |
+
### Expecting `">Z"` to return the topmost point
|
| 162 |
+
|
| 163 |
+
`">Z"` returns the face with the highest **centroid Z**, not the face containing
|
| 164 |
+
the highest Z vertex. On a slanted or irregular solid these can differ.
|
| 165 |
+
|
| 166 |
+
### Using `"|Z"` when you mean `">Z"`
|
| 167 |
+
|
| 168 |
+
`"|Z"` selects faces whose normal is parallel to Z - both top AND bottom faces on a box.
|
| 169 |
+
`">Z"` selects only the top face (highest centroid).
|
| 170 |
+
|
| 171 |
+
```python
|
| 172 |
+
solid.faces("|Z") # top and bottom - probably not what you want
|
| 173 |
+
solid.faces(">Z") # top face only
|
| 174 |
+
```
|
| 175 |
+
|
| 176 |
+
### Chaining selectors vs combining them
|
| 177 |
+
|
| 178 |
+
Two chained calls filter progressively; a combined string filters in one pass.
|
| 179 |
+
These are not always equivalent:
|
| 180 |
+
|
| 181 |
+
```python
|
| 182 |
+
# First selects all circular edges, then filters to those on >Z faces - may not work as expected
|
| 183 |
+
solid.edges("%Circle and >Z")
|
| 184 |
+
|
| 185 |
+
# Correct: select the top face first, then get its circular edges
|
| 186 |
+
solid.faces(">Z").edges("%Circle")
|
| 187 |
+
```
|
| 188 |
+
|
| 189 |
+
### Index selectors shifting after feature changes
|
| 190 |
+
|
| 191 |
+
```python
|
| 192 |
+
# Fragile: index 1 may shift after a fillet is added
|
| 193 |
+
solid.faces(">>Z[1]").workplane().hole(3)
|
| 194 |
+
|
| 195 |
+
# Stable: tag the face when you create it
|
| 196 |
+
solid.faces(">>Z[1]").tag("target").end()
|
| 197 |
+
# ... add fillets or other features ...
|
| 198 |
+
solid.faces(tag="target").workplane().hole(3)
|
| 199 |
+
```
|
| 200 |
+
|
server/docs/concepts/workplanes.md
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Workplanes
|
| 2 |
+
|
| 3 |
+
## What a Workplane Is
|
| 4 |
+
|
| 5 |
+
A workplane is a local 2D coordinate system attached to a point in 3D space.
|
| 6 |
+
All sketch operations (circles, rectangles, polygons, splines) are defined in this
|
| 7 |
+
local space, then extruded, revolved, swept, etc into 3D geometry.
|
| 8 |
+
|
| 9 |
+
The workplane has:
|
| 10 |
+
- An **origin** — the 2D (0, 0) point in 3D space
|
| 11 |
+
- An **x-direction** — defines the local X axis
|
| 12 |
+
- A **normal** — the Z axis of the local system (points "out" of the plane)
|
| 13 |
+
|
| 14 |
+
## Creating a Workplane
|
| 15 |
+
|
| 16 |
+
```python
|
| 17 |
+
import cadquery as cq
|
| 18 |
+
|
| 19 |
+
# Named planes — origin at (0,0,0)
|
| 20 |
+
wp1 = cq.Workplane("XY") # normal = +Z
|
| 21 |
+
wp2 = cq.Workplane("YZ") # normal = +X
|
| 22 |
+
wp3 = cq.Workplane("XZ") # normal = +Y
|
| 23 |
+
|
| 24 |
+
# From a cq.Plane object - explicit origin and normal
|
| 25 |
+
wp4 = cq.Workplane(cq.Plane(origin=(0, 0, 10), normal=(0, 0, 1)))
|
| 26 |
+
|
| 27 |
+
# From an existing solid's face
|
| 28 |
+
wp5 = solid.faces(">Z").workplane()
|
| 29 |
+
|
| 30 |
+
# Offset along the face normal
|
| 31 |
+
wp6 = solid.faces(">Z").workplane(offset=5)
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
## Moving the Workplane
|
| 35 |
+
|
| 36 |
+
### `.workplane()` — attaches to selected geometry
|
| 37 |
+
|
| 38 |
+
After selecting a face or edge, `.workplane()` creates a new plane on that entity.
|
| 39 |
+
The origin defaults to the center of the selected entity (controlled by `centerOption`).
|
| 40 |
+
|
| 41 |
+
```python
|
| 42 |
+
result = (
|
| 43 |
+
cq.Workplane("XY")
|
| 44 |
+
.box(20, 20, 10)
|
| 45 |
+
.faces(">Z").workplane() # plane is now on the top face
|
| 46 |
+
.circle(5).extrude(10)
|
| 47 |
+
)
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
### `.transformed()` — offsets and rotates the current plane
|
| 51 |
+
|
| 52 |
+
`.transformed()` moves the workplane relative to its current position.
|
| 53 |
+
Rotation is in degrees around the local X, Y, Z axes (applied in that order).
|
| 54 |
+
|
| 55 |
+
```python
|
| 56 |
+
result = (
|
| 57 |
+
cq.Workplane("XY")
|
| 58 |
+
.box(20, 20, 10)
|
| 59 |
+
.faces(">Z").workplane()
|
| 60 |
+
.transformed(offset=(5, 5, 0), rotate=(0, 0, 45))
|
| 61 |
+
.rect(4, 4).extrude(3)
|
| 62 |
+
)
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
`.transformed()` is cumulative — each call moves relative to the current plane, not the global origin.
|
| 66 |
+
|
| 67 |
+
### `.center()` — shifts the 2D origin within the current plane
|
| 68 |
+
|
| 69 |
+
```python
|
| 70 |
+
solid.faces(">Z").workplane().center(10, 5).circle(3).extrude(5)
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
This shifts where (0, 0) is in the current plane. Useful for placing features
|
| 74 |
+
without recalculating coordinates manually.
|
| 75 |
+
|
| 76 |
+
## Placing a Sketch on a Workplane
|
| 77 |
+
|
| 78 |
+
Instead of building a profile directly in the fluent chain, you can define a `cq.Sketch`
|
| 79 |
+
independently and place it onto a workplane with `.placeSketch()`. This separates
|
| 80 |
+
profile definition from placement and allows the same sketch to be reused in multiple locations.
|
| 81 |
+
|
| 82 |
+
```python
|
| 83 |
+
import cadquery as cq
|
| 84 |
+
|
| 85 |
+
# Define the profile separately
|
| 86 |
+
profile = (
|
| 87 |
+
cq.Sketch()
|
| 88 |
+
.rect(10, 10)
|
| 89 |
+
.circle(3, mode="s") # subtract a circular hole from the rect
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
# Place it on a workplane and extrude
|
| 93 |
+
result = (
|
| 94 |
+
cq.Workplane("XY")
|
| 95 |
+
.box(30, 30, 5)
|
| 96 |
+
.faces(">Z").workplane()
|
| 97 |
+
.placeSketch(profile)
|
| 98 |
+
.extrude(5)
|
| 99 |
+
)
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
Sketches can also be placed at multiple locations in a single call by passing
|
| 103 |
+
pre-moved sketch instances:
|
| 104 |
+
|
| 105 |
+
```python
|
| 106 |
+
s = cq.Sketch().circle(3)
|
| 107 |
+
|
| 108 |
+
result = (
|
| 109 |
+
cq.Workplane("XY")
|
| 110 |
+
.box(30, 30, 5)
|
| 111 |
+
.faces(">Z").workplane()
|
| 112 |
+
.placeSketch(
|
| 113 |
+
s.moved(cq.Location((8, 8, 0))),
|
| 114 |
+
s.moved(cq.Location((-8, -8, 0))),
|
| 115 |
+
)
|
| 116 |
+
.extrude(5)
|
| 117 |
+
)
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+
The Sketch API also supports constraints, arcs, splines, and hull construction -
|
| 121 |
+
operations that are cumbersome or impossible to express in the fluent chain alone.
|
| 122 |
+
When a profile is complex, build it as a `cq.Sketch` and place it, rather than
|
| 123 |
+
trying to encode it inline.
|
| 124 |
+
|
| 125 |
+
## `centerOption` — Where the Origin Lands
|
| 126 |
+
|
| 127 |
+
When calling `.workplane()` on a selected face, `centerOption` controls the origin placement.
|
| 128 |
+
|
| 129 |
+
| Value | Origin location |
|
| 130 |
+
|-------|----------------|
|
| 131 |
+
| `"CenterOfMass"` | Center of mass of the face |
|
| 132 |
+
| `"CenterOfBoundBox"` | Center of the bounding box |
|
| 133 |
+
| `"ProjectedOrigin"` | Projects the parent workplane's origin onto the new plane (default) |
|
| 134 |
+
|
| 135 |
+
```python
|
| 136 |
+
# Explicit is always safer
|
| 137 |
+
solid.faces(">Z").workplane(centerOption="CenterOfBoundBox")
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
**Never assume the default matches your intent.** For symmetric faces it usually
|
| 141 |
+
doesn't matter, but for irregular faces it can place the origin far from where you expect.
|
| 142 |
+
|
| 143 |
+
## The Workplane Stack
|
| 144 |
+
|
| 145 |
+
The Workplane object maintains a stack of shapes. Operations push results onto the stack;
|
| 146 |
+
selectors filter the stack. This is how the fluent chain works.
|
| 147 |
+
|
| 148 |
+
```python
|
| 149 |
+
result = (
|
| 150 |
+
cq.Workplane("XY")
|
| 151 |
+
.box(20, 20, 10) # stack: [box solid]
|
| 152 |
+
.faces(">Z") # stack: [top face]
|
| 153 |
+
.workplane() # stack: [top face], plane updated
|
| 154 |
+
.circle(5) # stack: [circle wire]
|
| 155 |
+
.extrude(10) # stack: [new solid]
|
| 156 |
+
)
|
| 157 |
+
```
|
| 158 |
+
|
| 159 |
+
Key rules:
|
| 160 |
+
- `.val()` returns the first item on the stack as a `Shape`
|
| 161 |
+
- `.vals()` returns all items on the stack as a list of `Shape`
|
| 162 |
+
- `.end(n)` pops `n` levels back up the chain (default 1)
|
| 163 |
+
- `.newObject(list)` replaces the stack with a new list
|
| 164 |
+
|
| 165 |
+
## Common Mistakes
|
| 166 |
+
|
| 167 |
+
### Forgetting that coordinates are local
|
| 168 |
+
|
| 169 |
+
After `.workplane()`, all coordinates are in the **local** frame, not global.
|
| 170 |
+
|
| 171 |
+
```python
|
| 172 |
+
# Wrong mental model: thinking (10, 10) is a global position
|
| 173 |
+
solid.faces(">Z").workplane().circle(3).extrude(5) # circle at local (0,0) = face center
|
| 174 |
+
|
| 175 |
+
# Correct: use .center() or .transformed() to shift within the local frame
|
| 176 |
+
solid.faces(">Z").workplane().center(10, 10).circle(3).extrude(5)
|
| 177 |
+
```
|
| 178 |
+
|
| 179 |
+
### Losing context after `.tag()`
|
| 180 |
+
|
| 181 |
+
`.tag()` records the current stack state but does not change it.
|
| 182 |
+
After tagging, chain `.end()` to return to the solid before continuing.
|
| 183 |
+
|
| 184 |
+
```python
|
| 185 |
+
result = (
|
| 186 |
+
cq.Workplane("XY")
|
| 187 |
+
.box(20, 20, 10)
|
| 188 |
+
.faces(">Z").tag("top")
|
| 189 |
+
.end() # back to the solid
|
| 190 |
+
.faces("<Z").workplane().hole(4)
|
| 191 |
+
)
|
| 192 |
+
```
|
| 193 |
+
|
| 194 |
+
### Cut going the wrong direction — forgetting `invert=True`
|
| 195 |
+
|
| 196 |
+
The workplane normal points **outward** from the selected face by default.
|
| 197 |
+
Cut operations (`.cutBlind()`, `.extrude(..., combine="cut")`) remove material
|
| 198 |
+
in the direction of the normal - so cutting from the top face will cut upward,
|
| 199 |
+
away from the solid, removing nothing.
|
| 200 |
+
|
| 201 |
+
Use `invert=True` to flip the normal inward when you need to cut into the solid
|
| 202 |
+
from an outward-facing face:
|
| 203 |
+
|
| 204 |
+
```python
|
| 205 |
+
# Wrong: cuts upward away from the solid — no material removed
|
| 206 |
+
solid.faces(">Z").workplane().circle(3).cutBlind(5)
|
| 207 |
+
|
| 208 |
+
# Correct: invert flips the normal to point into the solid
|
| 209 |
+
solid.faces(">Z").workplane(invert=True).circle(3).cutBlind(5)
|
| 210 |
+
```
|
| 211 |
+
|
| 212 |
+
This also applies to extrusions where you want to build downward (into the solid)
|
| 213 |
+
rather than upward. When in doubt, check which direction the face normal points
|
| 214 |
+
relative to the solid's interior.
|
| 215 |
+
|
| 216 |
+
### Operating on a face without creating a workplane
|
| 217 |
+
|
| 218 |
+
After selecting a face, some operations (`.hole()`, `.shell()`, `.fillet()`) work
|
| 219 |
+
directly on the selected geometry without needing a workplane. Others require one.
|
| 220 |
+
Skipping `.workplane()` when it is needed will silently use whatever plane was
|
| 221 |
+
active before the face selection, producing geometry in the wrong position or orientation.
|
| 222 |
+
|
| 223 |
+
```python
|
| 224 |
+
# Fine - .hole() works directly on the selected face
|
| 225 |
+
solid.faces(">Z").hole(3)
|
| 226 |
+
|
| 227 |
+
# Fine - .shell() operates on the selected face
|
| 228 |
+
solid.faces(">Z").shell(-2)
|
| 229 |
+
|
| 230 |
+
# Wrong - .circle().extrude() needs a workplane; this uses the previous plane context
|
| 231 |
+
solid.faces(">Z").circle(3).extrude(5)
|
| 232 |
+
|
| 233 |
+
# Correct
|
| 234 |
+
solid.faces(">Z").workplane().circle(3).extrude(5)
|
| 235 |
+
```
|
| 236 |
+
|
| 237 |
+
When in doubt, always call `.workplane()` after a face selection before sketching.
|
| 238 |
+
|
| 239 |
+
### Using `.workplane()` without a prior face selection
|
| 240 |
+
|
| 241 |
+
Calling `.workplane()` without first selecting a face creates a plane at the
|
| 242 |
+
center of whatever is on the stack - often not what you want.
|
| 243 |
+
Always select a face first.
|
| 244 |
+
|
| 245 |
+
```python
|
| 246 |
+
# Ambiguous
|
| 247 |
+
solid.workplane().circle(3).extrude(5)
|
| 248 |
+
|
| 249 |
+
# Explicit
|
| 250 |
+
solid.faces(">Z").workplane().circle(3).extrude(5)
|
| 251 |
+
```
|
| 252 |
+
|
| 253 |
+
## Workplanes in the Free Function API
|
| 254 |
+
|
| 255 |
+
The Free Function API has no workplane concept. Placement is done with `.moved()` and `.move()`.
|
| 256 |
+
|
| 257 |
+
```python
|
| 258 |
+
from cadquery.func import *
|
| 259 |
+
|
| 260 |
+
b = box(20, 20, 10)
|
| 261 |
+
|
| 262 |
+
# Place a feature by moving it to the right position, then fuse or addHole
|
| 263 |
+
boss = cylinder(5, 10).moved(z=10) # sits on top of the box
|
| 264 |
+
result = b + boss
|
| 265 |
+
```
|
| 266 |
+
|
| 267 |
+
For face-relative placement, select the face and use its properties:
|
| 268 |
+
|
| 269 |
+
```python
|
| 270 |
+
top = b.faces(">Z")
|
| 271 |
+
origin = top.Center() # returns a Vector
|
| 272 |
+
boss = cylinder(5, 10).moved(z=origin.z)
|
| 273 |
+
result = b + boss
|
| 274 |
+
```
|
server/docs/patterns/anti-patterns.md
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Anti-Patterns
|
| 2 |
+
|
| 3 |
+
Common mistakes LLMs make when generating CadQuery code, and how to fix them.
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## 1. The CSG Reflex — Booleans Instead of Feature Operations
|
| 8 |
+
|
| 9 |
+
The most pervasive anti-pattern: reaching for `.union()` / `.cut()` when a direct
|
| 10 |
+
feature operation would be cleaner, faster, and produce better topology.
|
| 11 |
+
|
| 12 |
+
| Instead of... | Use... |
|
| 13 |
+
|--------------|--------|
|
| 14 |
+
| `.union(cylinder(...))` for a boss | `.faces(...).workplane().circle(r).extrude(h)` |
|
| 15 |
+
| `.cut(cylinder(...))` for a hole | `.faces(...).hole(diameter)` |
|
| 16 |
+
| `.cut(box(...))` for a pocket | `.faces(...).workplane().rect(w, h).cutBlind(depth)` |
|
| 17 |
+
| `.union(...)` for a chamfered edge | `.edges(...).chamfer(d)` |
|
| 18 |
+
| `.union(...)` for a filleted edge | `.edges(...).fillet(r)` |
|
| 19 |
+
| `.cut(shell_solid)` to hollow | `.faces(...).shell(thickness)` |
|
| 20 |
+
|
| 21 |
+
Booleans rebuild the full boundary from scratch. Feature operations extend or modify
|
| 22 |
+
the existing BRep directly - they are faster and leave cleaner topology for subsequent
|
| 23 |
+
selections.
|
| 24 |
+
|
| 25 |
+
**Use booleans only when combining genuinely separate solids** or when the geometry
|
| 26 |
+
cannot be expressed as a profile operation.
|
| 27 |
+
|
| 28 |
+
---
|
| 29 |
+
|
| 30 |
+
## 2. Using `.translate()` on a Workplane Chain
|
| 31 |
+
|
| 32 |
+
`.translate()` is a method on `Shape` objects, not on `Workplane` objects. Calling it
|
| 33 |
+
in a fluent chain after building geometry moves the shape, but does so in global
|
| 34 |
+
coordinates — bypassing the workplane system entirely.
|
| 35 |
+
|
| 36 |
+
```python
|
| 37 |
+
# Wrong: translate is called on the Workplane, not the Shape
|
| 38 |
+
result = cq.Workplane("XY").box(10, 10, 10).translate((5, 0, 0))
|
| 39 |
+
|
| 40 |
+
# If you need to position geometry, use workplane placement instead
|
| 41 |
+
result = cq.Workplane("XY").center(5, 0).box(10, 10, 10)
|
| 42 |
+
|
| 43 |
+
# Or move the workplane origin explicitly
|
| 44 |
+
result = cq.Workplane(cq.Plane(origin=(5, 0, 0))).box(10, 10, 10)
|
| 45 |
+
|
| 46 |
+
# .translate() is valid when called on a Shape extracted from the chain
|
| 47 |
+
shape = cq.Workplane("XY").box(10, 10, 10).val()
|
| 48 |
+
moved = shape.translate(cq.Vector(5, 0, 0))
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
---
|
| 52 |
+
|
| 53 |
+
## 3. Building in Global Coordinates Instead of Using Workplanes
|
| 54 |
+
|
| 55 |
+
Hardcoding global coordinates creates scripts that are brittle and hard to modify.
|
| 56 |
+
Use workplanes and relative positioning instead.
|
| 57 |
+
|
| 58 |
+
```python
|
| 59 |
+
# Wrong: all positions hardcoded in global space
|
| 60 |
+
result = (
|
| 61 |
+
cq.Workplane("XY")
|
| 62 |
+
.box(30, 30, 10)
|
| 63 |
+
.union(cq.Workplane("XY").cylinder(5, 8).translate((0, 0, 9)))
|
| 64 |
+
.union(cq.Workplane("XY").cylinder(5, 8).translate((10, 10, 9)))
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
# Right: build relative to existing faces
|
| 68 |
+
result = (
|
| 69 |
+
cq.Workplane("XY")
|
| 70 |
+
.box(30, 30, 10)
|
| 71 |
+
.faces(">Z").workplane()
|
| 72 |
+
.circle(5).extrude(8) # center boss
|
| 73 |
+
.faces(">Z").workplane()
|
| 74 |
+
.center(10, 10).circle(5).extrude(8) # offset boss
|
| 75 |
+
)
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
---
|
| 79 |
+
|
| 80 |
+
## 4. Forgetting `combine=False` on `.extrude()`
|
| 81 |
+
|
| 82 |
+
By default, `.extrude()` unions the new solid with whatever is on the context stack.
|
| 83 |
+
If you want a separate solid (e.g., to combine later or export independently),
|
| 84 |
+
pass `combine=False`.
|
| 85 |
+
|
| 86 |
+
```python
|
| 87 |
+
# Default: extrusion is unioned into the existing solid
|
| 88 |
+
result = solid.faces(">Z").workplane().circle(3).extrude(5)
|
| 89 |
+
|
| 90 |
+
# Separate solid
|
| 91 |
+
new_part = cq.Workplane("XY").circle(3).extrude(5, combine=False)
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
The inverse mistake also occurs: forgetting that `combine=True` is the default,
|
| 95 |
+
and then trying to union the result again — producing a double union.
|
| 96 |
+
|
| 97 |
+
---
|
| 98 |
+
|
| 99 |
+
## 5. Misreading `.cutBlind()` Direction
|
| 100 |
+
|
| 101 |
+
`.cutBlind(depth)` cuts in the direction of the workplane normal. The normal points
|
| 102 |
+
**outward** from the selected face by default, so cutting from the top face without
|
| 103 |
+
inverting will cut away from the solid.
|
| 104 |
+
|
| 105 |
+
```python
|
| 106 |
+
# Wrong: cuts upward, away from the solid
|
| 107 |
+
solid.faces(">Z").workplane().rect(5, 5).cutBlind(3)
|
| 108 |
+
|
| 109 |
+
# Correct: invert=True flips the normal into the solid
|
| 110 |
+
solid.faces(">Z").workplane(invert=True).rect(5, 5).cutBlind(3)
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
Also applies to `.extrude(..., combine="cut")`.
|
| 114 |
+
|
| 115 |
+
---
|
| 116 |
+
|
| 117 |
+
## 6. `.val()` on a Multi-Item Stack
|
| 118 |
+
|
| 119 |
+
`.val()` returns the **first** item on the stack. It does not error if there are
|
| 120 |
+
multiple items - it silently discards the rest. Use `.vals()` when you need all items.
|
| 121 |
+
|
| 122 |
+
```python
|
| 123 |
+
solid = cq.Workplane("XY").box(10, 10, 10)
|
| 124 |
+
|
| 125 |
+
# Returns one Face - the first face in iteration order, not necessarily ">Z"
|
| 126 |
+
face = solid.faces().val()
|
| 127 |
+
|
| 128 |
+
# Returns all 6 faces as a list
|
| 129 |
+
faces = solid.faces().vals()
|
| 130 |
+
|
| 131 |
+
# To get a specific face as a Shape, select first
|
| 132 |
+
top_face = solid.faces(">Z").val()
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
---
|
| 136 |
+
|
| 137 |
+
## 7. Misusing `.each()`
|
| 138 |
+
|
| 139 |
+
`.each(callback)` calls `callback` on every item in the stack and **replaces the stack**
|
| 140 |
+
with the results. The callback must return a `Shape`. It is not for iteration with
|
| 141 |
+
side effects.
|
| 142 |
+
|
| 143 |
+
```python
|
| 144 |
+
# Wrong: callback doesn't return a Shape; result is undefined
|
| 145 |
+
solid.edges().each(lambda e: print(e.Length()))
|
| 146 |
+
|
| 147 |
+
# Wrong: trying to accumulate results
|
| 148 |
+
results = []
|
| 149 |
+
solid.faces().each(lambda f: results.append(f)) # each() ignores the return value None
|
| 150 |
+
|
| 151 |
+
# Right: use .each() to transform stack items
|
| 152 |
+
# e.g., get the center point of each face as a Vertex
|
| 153 |
+
centers = solid.faces().each(lambda f: f.Center())
|
| 154 |
+
|
| 155 |
+
# For iteration/inspection, use .vals() instead
|
| 156 |
+
for face in solid.faces().vals():
|
| 157 |
+
print(face.Area())
|
| 158 |
+
```
|
| 159 |
+
|
| 160 |
+
---
|
| 161 |
+
|
| 162 |
+
## 8. Losing the Solid Context After a Selector
|
| 163 |
+
|
| 164 |
+
After `.faces(...)` or `.edges(...)`, the stack contains only the selected entities —
|
| 165 |
+
not the full solid. Calling `.workplane()` on a face selection is correct;
|
| 166 |
+
calling feature operations directly on the selection without a workplane can
|
| 167 |
+
silently use the wrong context.
|
| 168 |
+
|
| 169 |
+
```python
|
| 170 |
+
# Wrong: after .faces(), the solid is not the active context
|
| 171 |
+
solid.faces(">Z").box(5, 5, 3) # adds a new box to the face selection context, not the solid
|
| 172 |
+
|
| 173 |
+
# Correct: use .workplane() to re-establish context for sketching
|
| 174 |
+
solid.faces(">Z").workplane().rect(5, 5).extrude(3)
|
| 175 |
+
|
| 176 |
+
# Correct: use .end() to return to the solid context
|
| 177 |
+
solid.faces(">Z").tag("top").end().faces("<Z").workplane().hole(3)
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
---
|
| 181 |
+
|
| 182 |
+
## 9. Assuming `centerOption` Default
|
| 183 |
+
|
| 184 |
+
The default `centerOption="ProjectedOrigin"` projects the parent workplane's origin
|
| 185 |
+
onto the new face - which is often not the face center and can place the origin
|
| 186 |
+
well outside the face bounds on irregular geometry.
|
| 187 |
+
|
| 188 |
+
```python
|
| 189 |
+
# Fragile: origin placement depends on face shape
|
| 190 |
+
solid.faces(">Z").workplane().circle(3).extrude(5)
|
| 191 |
+
|
| 192 |
+
# Explicit and predictable
|
| 193 |
+
solid.faces(">Z").workplane(centerOption="CenterOfBoundBox").circle(3).extrude(5)
|
| 194 |
+
```
|
| 195 |
+
|
| 196 |
+
---
|
| 197 |
+
|
| 198 |
+
## 10. Index Selector Fragility
|
| 199 |
+
|
| 200 |
+
Selectors like `">>Z[1]"` depend on a sorted count of all matching entities.
|
| 201 |
+
Adding fillets, chamfers, holes, or shells changes the entity count and can
|
| 202 |
+
silently shift which entity is selected.
|
| 203 |
+
|
| 204 |
+
```python
|
| 205 |
+
# Fragile: index may shift after any feature is added
|
| 206 |
+
solid.faces(">>Z[1]").workplane().hole(3)
|
| 207 |
+
|
| 208 |
+
# Robust: tag the face before adding features
|
| 209 |
+
solid = (
|
| 210 |
+
cq.Workplane("XY")
|
| 211 |
+
.box(20, 20, 20)
|
| 212 |
+
.faces(">>Z[1]").tag("target")
|
| 213 |
+
.end()
|
| 214 |
+
.edges(">Z").fillet(1) # this would shift the index above
|
| 215 |
+
.faces(tag="target").workplane().hole(3)
|
| 216 |
+
)
|
| 217 |
+
```
|
| 218 |
+
|
| 219 |
+
---
|
| 220 |
+
|
| 221 |
+
## 11. Mixing Fluent and Free Function APIs Unintentionally
|
| 222 |
+
|
| 223 |
+
The two APIs are not interchangeable mid-script. A `Workplane` object is not
|
| 224 |
+
a `Shape`, and Free Function operations do not accept `Workplane` objects.
|
| 225 |
+
|
| 226 |
+
```python
|
| 227 |
+
from cadquery.func import *
|
| 228 |
+
import cadquery as cq
|
| 229 |
+
|
| 230 |
+
# Wrong: passing a Workplane to a free function
|
| 231 |
+
b = box(10, 10, 10)
|
| 232 |
+
wp = cq.Workplane("XY").box(5, 5, 5)
|
| 233 |
+
result = b + wp # TypeError — wp is not a Shape
|
| 234 |
+
|
| 235 |
+
# Correct: extract the Shape from the Workplane first
|
| 236 |
+
wp_shape = cq.Workplane("XY").box(5, 5, 5).val()
|
| 237 |
+
result = b + wp_shape
|
| 238 |
+
```
|
| 239 |
+
|
| 240 |
+
When mixing is intentional, always extract with `.val()` or `.vals()` before
|
| 241 |
+
passing to Free Function API operations.
|
| 242 |
+
|
| 243 |
+
---
|
| 244 |
+
|
| 245 |
+
## 12. Unclosed Wires
|
| 246 |
+
|
| 247 |
+
When building a profile with `.lineTo()`, `.spline()`, `.threePointArc()`, etc.,
|
| 248 |
+
the wire must be closed before `.extrude()` or `.revolve()`. An unclosed wire
|
| 249 |
+
produces an error or unexpected open shell.
|
| 250 |
+
|
| 251 |
+
```python
|
| 252 |
+
# Wrong: missing .close()
|
| 253 |
+
result = (
|
| 254 |
+
cq.Workplane("XY")
|
| 255 |
+
.moveTo(0, 0)
|
| 256 |
+
.lineTo(10, 0)
|
| 257 |
+
.lineTo(10, 5)
|
| 258 |
+
.lineTo(0, 5)
|
| 259 |
+
.extrude(3) # error - wire is not closed
|
| 260 |
+
)
|
| 261 |
+
|
| 262 |
+
# Correct
|
| 263 |
+
result = (
|
| 264 |
+
cq.Workplane("XY")
|
| 265 |
+
.moveTo(0, 0)
|
| 266 |
+
.lineTo(10, 0)
|
| 267 |
+
.lineTo(10, 5)
|
| 268 |
+
.lineTo(0, 5)
|
| 269 |
+
.close()
|
| 270 |
+
.extrude(3)
|
| 271 |
+
)
|
| 272 |
+
```
|
| 273 |
+
|
| 274 |
+
---
|
| 275 |
+
|
| 276 |
+
## 13. Free Function API: Boolean in a Loop
|
| 277 |
+
|
| 278 |
+
Boolean operations in the Free Function API (and the fluent API) are expensive
|
| 279 |
+
because they recompute the full boundary. Never perform them in a loop.
|
| 280 |
+
|
| 281 |
+
```python
|
| 282 |
+
from cadquery.func import *
|
| 283 |
+
|
| 284 |
+
holes = [cylinder(0.5, 5).moved(x=i*2) for i in range(10)]
|
| 285 |
+
|
| 286 |
+
# Wrong: 10 sequential boolean operations
|
| 287 |
+
result = box(20, 5, 5)
|
| 288 |
+
for h in holes:
|
| 289 |
+
result = result - h
|
| 290 |
+
|
| 291 |
+
# Correct: combine into a compound first, then subtract once
|
| 292 |
+
result = box(20, 5, 5) - compound(holes)
|
| 293 |
+
```
|
| 294 |
+
|
| 295 |
+
---
|
| 296 |
+
|
| 297 |
+
## 14. `BREP_API command not done` Errors
|
| 298 |
+
|
| 299 |
+
This error comes from the OpenCASCADE kernel and means a BRep operation could not be
|
| 300 |
+
completed geometrically. It is not a CadQuery bug — it means the requested geometry
|
| 301 |
+
is invalid or degenerate. The error message does not identify which operation failed.
|
| 302 |
+
|
| 303 |
+
**Common causes:**
|
| 304 |
+
|
| 305 |
+
### Fillet or chamfer radius too large
|
| 306 |
+
|
| 307 |
+
A fillet fails when its radius is larger than the shortest adjacent edge, or when
|
| 308 |
+
adjacent fillets overlap each other.
|
| 309 |
+
|
| 310 |
+
```python
|
| 311 |
+
# Fails if the box edge is shorter than radius=5, or if adjacent fillets intersect
|
| 312 |
+
solid.edges("|Z").fillet(5) # BREP_API command not done
|
| 313 |
+
```
|
| 314 |
+
|
| 315 |
+
Fixes:
|
| 316 |
+
- Reduce the radius
|
| 317 |
+
- Fillet edges in separate calls, smallest features first
|
| 318 |
+
- Fillet different edge groups separately rather than all at once
|
| 319 |
+
|
| 320 |
+
```python
|
| 321 |
+
# Apply smaller fillets to shorter edges first
|
| 322 |
+
result = (
|
| 323 |
+
cq.Workplane("XY")
|
| 324 |
+
.box(20, 20, 5)
|
| 325 |
+
.edges("|Z").fillet(1) # vertical edges first
|
| 326 |
+
.edges(">Z").fillet(0.5) # then top perimeter with smaller radius
|
| 327 |
+
)
|
| 328 |
+
```
|
| 329 |
+
|
| 330 |
+
### Self-intersecting profiles
|
| 331 |
+
|
| 332 |
+
A profile (wire or sketch) that crosses itself cannot be extruded, revolved, or swept.
|
| 333 |
+
This includes profiles where an arc or spline curves back through the outline.
|
| 334 |
+
|
| 335 |
+
Check that:
|
| 336 |
+
- All `lineTo()` / `spline()` sequences form a simple (non-self-intersecting) closed loop
|
| 337 |
+
- Revolve profiles do not cross the axis of revolution
|
| 338 |
+
- Sweep profiles are small enough to navigate tight path curvature without self-intersection
|
| 339 |
+
|
| 340 |
+
### Self-intersecting sweep
|
| 341 |
+
|
| 342 |
+
A sweep fails when the profile is too large relative to the path curvature — the
|
| 343 |
+
extruded solid folds back on itself at tight bends.
|
| 344 |
+
|
| 345 |
+
Reduce the profile size or increase the path radius.
|
| 346 |
+
|
| 347 |
+
### Shell thickness too large
|
| 348 |
+
|
| 349 |
+
`.shell()` fails when the thickness is larger than the smallest local radius of
|
| 350 |
+
curvature — the offset surface self-intersects.
|
| 351 |
+
|
| 352 |
+
Reduce the shell thickness or simplify the geometry before shelling.
|
| 353 |
+
|
server/docs/patterns/common-patterns.md
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Common Patterns
|
| 2 |
+
|
| 3 |
+
Idiomatic CadQuery recipes for tasks that come up frequently.
|
| 4 |
+
Each pattern shows the preferred approach and explains why.
|
| 5 |
+
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
## Fluent API Patterns
|
| 9 |
+
|
| 10 |
+
### Base solid with features on multiple faces
|
| 11 |
+
|
| 12 |
+
Build the base first, then add features face by face. Tag faces before adding
|
| 13 |
+
features that might change topology.
|
| 14 |
+
|
| 15 |
+
```python
|
| 16 |
+
import cadquery as cq
|
| 17 |
+
|
| 18 |
+
result = (
|
| 19 |
+
cq.Workplane("XY")
|
| 20 |
+
.box(40, 30, 15)
|
| 21 |
+
.faces(">Z").tag("top")
|
| 22 |
+
.faces("<Z").tag("bottom")
|
| 23 |
+
.end()
|
| 24 |
+
.faces(tag="top").workplane()
|
| 25 |
+
.rect(20, 15).cutBlind(-5) # pocket on top
|
| 26 |
+
.faces(tag="bottom").workplane()
|
| 27 |
+
.hole(6) # through hole from bottom
|
| 28 |
+
.edges("|Z").fillet(2) # fillet vertical edges last
|
| 29 |
+
)
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
### Polar pattern of features
|
| 33 |
+
|
| 34 |
+
Use `.polarArray()` to place features at equal angular intervals.
|
| 35 |
+
|
| 36 |
+
```python
|
| 37 |
+
result = (
|
| 38 |
+
cq.Workplane("XY")
|
| 39 |
+
.cylinder(5, 20)
|
| 40 |
+
.faces(">Z").workplane()
|
| 41 |
+
.polarArray(radius=8, startAngle=0, angle=360, count=6)
|
| 42 |
+
.hole(2, depth=8)
|
| 43 |
+
)
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
### Rectangular pattern of features
|
| 47 |
+
|
| 48 |
+
Use `.rarray()` to place features in a grid.
|
| 49 |
+
|
| 50 |
+
```python
|
| 51 |
+
result = (
|
| 52 |
+
cq.Workplane("XY")
|
| 53 |
+
.box(40, 40, 10)
|
| 54 |
+
.faces(">Z").workplane()
|
| 55 |
+
.rarray(xSpacing=10, ySpacing=10, xCount=3, yCount=3, center=True)
|
| 56 |
+
.hole(3, depth=8)
|
| 57 |
+
)
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
### Revolve a profile
|
| 61 |
+
|
| 62 |
+
Define the profile on a plane that includes the rotation axis, then revolve.
|
| 63 |
+
The axis is always on the X axis of the workplane by default.
|
| 64 |
+
|
| 65 |
+
```python
|
| 66 |
+
result = (
|
| 67 |
+
cq.Workplane("XZ")
|
| 68 |
+
.moveTo(5, 0)
|
| 69 |
+
.lineTo(5, 10)
|
| 70 |
+
.lineTo(8, 10)
|
| 71 |
+
.lineTo(8, 6)
|
| 72 |
+
.lineTo(5, 6)
|
| 73 |
+
.close()
|
| 74 |
+
.revolve(angleDegrees=360, axisStart=(0, 0, 0), axisEnd=(0, 1, 0))
|
| 75 |
+
)
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
### Sweep a profile along a path
|
| 79 |
+
|
| 80 |
+
Define the path, then the profile, then sweep. The profile is placed perpendicular
|
| 81 |
+
to the path at its start point.
|
| 82 |
+
|
| 83 |
+
```python
|
| 84 |
+
path = (
|
| 85 |
+
cq.Workplane("XZ")
|
| 86 |
+
.moveTo(0, 0)
|
| 87 |
+
.spline([(10, 5), (20, 0)], includeCurrent=True)
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
result = (
|
| 91 |
+
cq.Workplane("YZ")
|
| 92 |
+
.circle(2)
|
| 93 |
+
.sweep(path)
|
| 94 |
+
)
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
### Loft between profiles
|
| 98 |
+
|
| 99 |
+
Chain profiles on the same Workplane — each sketch pushed onto the stack before
|
| 100 |
+
`.loft()` becomes a section. Use `.workplane(offset=...)` to move between levels.
|
| 101 |
+
|
| 102 |
+
```python
|
| 103 |
+
result = (
|
| 104 |
+
cq.Workplane("XY")
|
| 105 |
+
.rect(20, 10)
|
| 106 |
+
.workplane(offset=15)
|
| 107 |
+
.circle(6)
|
| 108 |
+
.loft()
|
| 109 |
+
)
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
### Shell a solid
|
| 113 |
+
|
| 114 |
+
Select the face(s) to open before calling `.shell()`. Negative thickness = inward.
|
| 115 |
+
|
| 116 |
+
```python
|
| 117 |
+
# Open-top box
|
| 118 |
+
result = cq.Workplane("XY").box(30, 20, 15).faces(">Z").shell(-2)
|
| 119 |
+
|
| 120 |
+
# Open on two opposite faces
|
| 121 |
+
result = cq.Workplane("XY").box(30, 20, 15).faces(">Z").shell(-2)
|
| 122 |
+
```
|
| 123 |
+
|
| 124 |
+
### Reusable sketch profile with `.placeSketch()`
|
| 125 |
+
|
| 126 |
+
Define the profile once and place it at multiple locations.
|
| 127 |
+
|
| 128 |
+
```python
|
| 129 |
+
slot = (
|
| 130 |
+
cq.Sketch()
|
| 131 |
+
.slot(8, 3) # length, width
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
result = (
|
| 135 |
+
cq.Workplane("XY")
|
| 136 |
+
.box(40, 20, 8)
|
| 137 |
+
.faces(">Z").workplane()
|
| 138 |
+
.placeSketch(
|
| 139 |
+
slot.moved(cq.Location((-10, 0, 0))),
|
| 140 |
+
slot.moved(cq.Location((10, 0, 0))),
|
| 141 |
+
)
|
| 142 |
+
.cutBlind(-4)
|
| 143 |
+
)
|
| 144 |
+
```
|
| 145 |
+
|
| 146 |
+
### Selecting edges for fillet/chamfer
|
| 147 |
+
|
| 148 |
+
Fillet or chamfer specific edges by combining type and position selectors.
|
| 149 |
+
|
| 150 |
+
```python
|
| 151 |
+
result = (
|
| 152 |
+
cq.Workplane("XY")
|
| 153 |
+
.box(20, 20, 20)
|
| 154 |
+
.edges(">Z").chamfer(1) # top face perimeter edges
|
| 155 |
+
.edges("<Z").fillet(2) # bottom face perimeter edges
|
| 156 |
+
.edges("|Z").fillet(1) # vertical edges
|
| 157 |
+
)
|
| 158 |
+
```
|
| 159 |
+
|
| 160 |
+
---
|
| 161 |
+
|
| 162 |
+
## Free Function API Patterns
|
| 163 |
+
|
| 164 |
+
### Loft with curvature-continuous cap
|
| 165 |
+
|
| 166 |
+
Use `loft()` for the side and `cap()` (not `fill()`) for the top when you need
|
| 167 |
+
the top to maintain curvature continuity with the side surface.
|
| 168 |
+
|
| 169 |
+
```python
|
| 170 |
+
from cadquery.func import *
|
| 171 |
+
|
| 172 |
+
r = 5
|
| 173 |
+
h = 10
|
| 174 |
+
|
| 175 |
+
bottom = circle(r)
|
| 176 |
+
mid = circle(r * 1.3).moved(z=h * 0.5)
|
| 177 |
+
top_edge_guide = circle(r).moved(z=h)
|
| 178 |
+
|
| 179 |
+
side = loft(bottom, mid, top_edge_guide)
|
| 180 |
+
base = fill(side.edges("<Z"))
|
| 181 |
+
top = cap(side.edges(">Z"), side) # curvature-continuous with side
|
| 182 |
+
|
| 183 |
+
result = solid(side, base, top)
|
| 184 |
+
```
|
| 185 |
+
|
| 186 |
+
### Adding a hole without a boolean
|
| 187 |
+
|
| 188 |
+
For performance on complex shapes, use `addHole()` instead of subtracting a cylinder.
|
| 189 |
+
|
| 190 |
+
```python
|
| 191 |
+
from cadquery.func import *
|
| 192 |
+
|
| 193 |
+
w = 1
|
| 194 |
+
r = 0.9*w/2
|
| 195 |
+
|
| 196 |
+
# box
|
| 197 |
+
b = box(w, w, w)
|
| 198 |
+
# bottom face
|
| 199 |
+
b_bot = b.faces('<Z')
|
| 200 |
+
# top faces
|
| 201 |
+
b_top = b.faces('>Z')
|
| 202 |
+
|
| 203 |
+
# inner face
|
| 204 |
+
inner = extrude(circle(r), (0,0,w))
|
| 205 |
+
|
| 206 |
+
# add holes to the bottom and top face
|
| 207 |
+
b_bot_hole = b_bot.addHole(inner.edges('<Z'))
|
| 208 |
+
b_top_hole = b_top.addHole(inner.edges('>Z'))
|
| 209 |
+
|
| 210 |
+
# construct the final solid
|
| 211 |
+
result = solid(
|
| 212 |
+
b.remove(b_top, b_bot).faces(), #side faces
|
| 213 |
+
b_bot_hole, # bottom with a hole
|
| 214 |
+
inner, # inner cylinder face
|
| 215 |
+
b_top_hole, # top with a hole
|
| 216 |
+
)
|
| 217 |
+
```
|
| 218 |
+
|
| 219 |
+
### Pattern with `.moved()`
|
| 220 |
+
|
| 221 |
+
Pass multiple location tuples to `.moved()` to create a compound of copies.
|
| 222 |
+
|
| 223 |
+
```python
|
| 224 |
+
from cadquery.func import *
|
| 225 |
+
|
| 226 |
+
peg = cylinder(2, 8)
|
| 227 |
+
|
| 228 |
+
# 4 pegs in a 2x2 grid
|
| 229 |
+
result = peg.moved(
|
| 230 |
+
(-5, -5, 0),
|
| 231 |
+
( 5, -5, 0),
|
| 232 |
+
(-5, 5, 0),
|
| 233 |
+
( 5, 5, 0),
|
| 234 |
+
)
|
| 235 |
+
```
|
| 236 |
+
|
| 237 |
+
### Boolean on 2D shapes
|
| 238 |
+
|
| 239 |
+
Boolean operators work on faces and wires, not just solids.
|
| 240 |
+
Useful for constructing profiles before extruding.
|
| 241 |
+
|
| 242 |
+
```python
|
| 243 |
+
from cadquery.func import *
|
| 244 |
+
|
| 245 |
+
outer = plane(20, 20)
|
| 246 |
+
cutout = plane(10, 10)
|
| 247 |
+
|
| 248 |
+
profile = outer - cutout # frame-shaped face with hole
|
| 249 |
+
result = extrude(profile, (0, 0, 5)) # hollow frame solid
|
| 250 |
+
```
|
| 251 |
+
|
| 252 |
+
### Text along a spine
|
| 253 |
+
|
| 254 |
+
`text()` takes positional arguments — keyword args will fail multimethod dispatch.
|
| 255 |
+
For planar text, pass a line segment as the spine with `planar=True`.
|
| 256 |
+
For text projected onto a curved surface, see the full example in the CadQuery docs.
|
| 257 |
+
|
| 258 |
+
```python
|
| 259 |
+
from cadquery.func import *
|
| 260 |
+
|
| 261 |
+
from math import pi
|
| 262 |
+
|
| 263 |
+
# parameters
|
| 264 |
+
D = 5
|
| 265 |
+
H = 2*D
|
| 266 |
+
S = H/10
|
| 267 |
+
TH = S/10
|
| 268 |
+
TXT = "CadQuery"
|
| 269 |
+
|
| 270 |
+
c = cylinder(D, H).moved(rz=-135)
|
| 271 |
+
spine = (c*plane().moved(z=D)).edges().trim(pi/2, pi)
|
| 272 |
+
|
| 273 |
+
# planar
|
| 274 |
+
r1 = text(TXT, 1, spine, planar=True).moved(z=-S)
|
| 275 |
+
```
|
| 276 |
+
|
| 277 |
+
```
|
| 278 |
+
|
server/docs/reference/Untitled
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
I'm just going to do this one thing. I think in Cadforge, the original documentation that they have on the website has a list of examples. I think they are even present in our folder, in docs, in @examples.rst, where you have examples. I was just thinking if we can use those examples to get harder tasks and work with them. Let's add them; let's do that.
|
server/docs/reference/examples.rst
ADDED
|
@@ -0,0 +1,1705 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.. _examples:
|
| 2 |
+
|
| 3 |
+
.. currentmodule:: cadquery
|
| 4 |
+
|
| 5 |
+
*********************************
|
| 6 |
+
Examples
|
| 7 |
+
*********************************
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
The examples on this page can help you learn how to build objects with CadQuery.
|
| 12 |
+
|
| 13 |
+
They are organized from simple to complex, so working through them in order is the best way to absorb them.
|
| 14 |
+
|
| 15 |
+
Each example lists the API elements used in the example for easy reference.
|
| 16 |
+
Items introduced in the example are marked with a **!**
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
.. note::
|
| 21 |
+
|
| 22 |
+
We strongly recommend installing `CQ-editor <https://github.com/CadQuery/CQ-editor>`_,
|
| 23 |
+
so that you can work along with these examples interactively. See :ref:`installation` for more info.
|
| 24 |
+
|
| 25 |
+
If you do, make sure to take these steps so that they work:
|
| 26 |
+
|
| 27 |
+
1. import cadquery as cq
|
| 28 |
+
2. add the line ``show_object(result)`` at the end. The samples below are autogenerated, but they use a different
|
| 29 |
+
syntax than the models on the website need to be.
|
| 30 |
+
|
| 31 |
+
.. contents:: List of Examples
|
| 32 |
+
:backlinks: entry
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
Simple Rectangular Plate
|
| 36 |
+
------------------------
|
| 37 |
+
|
| 38 |
+
Just about the simplest possible example, a rectangular box
|
| 39 |
+
|
| 40 |
+
.. cadquery::
|
| 41 |
+
|
| 42 |
+
result = cadquery.Workplane("front").box(2.0, 2.0, 0.5)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
.. topic:: Api References
|
| 46 |
+
|
| 47 |
+
.. hlist::
|
| 48 |
+
:columns: 2
|
| 49 |
+
|
| 50 |
+
* :py:meth:`Workplane` **!**
|
| 51 |
+
* :py:meth:`Workplane.box` **!**
|
| 52 |
+
|
| 53 |
+
Plate with Hole
|
| 54 |
+
------------------------
|
| 55 |
+
|
| 56 |
+
A rectangular box, but with a hole added.
|
| 57 |
+
|
| 58 |
+
"\>Z" selects the top most face of the resulting box. The hole is located in the center because the default origin
|
| 59 |
+
of a working plane is the projected origin of the last Workplane, the last Workplane having origin at (0,0,0) the
|
| 60 |
+
projection is at the center of the face. The default hole depth is through the entire part.
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
.. cadquery::
|
| 64 |
+
|
| 65 |
+
# The dimensions of the box. These can be modified rather than changing the
|
| 66 |
+
# object's code directly.
|
| 67 |
+
length = 80.0
|
| 68 |
+
height = 60.0
|
| 69 |
+
thickness = 10.0
|
| 70 |
+
center_hole_dia = 22.0
|
| 71 |
+
|
| 72 |
+
# Create a box based on the dimensions above and add a 22mm center hole
|
| 73 |
+
result = (
|
| 74 |
+
cq.Workplane("XY")
|
| 75 |
+
.box(length, height, thickness)
|
| 76 |
+
.faces(">Z")
|
| 77 |
+
.workplane()
|
| 78 |
+
.hole(center_hole_dia)
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
.. topic:: Api References
|
| 82 |
+
|
| 83 |
+
.. hlist::
|
| 84 |
+
:columns: 2
|
| 85 |
+
|
| 86 |
+
* :py:meth:`Workplane.hole` **!**
|
| 87 |
+
* :py:meth:`Workplane.box`
|
| 88 |
+
* :py:meth:`Workplane.box`
|
| 89 |
+
|
| 90 |
+
An extruded prismatic solid
|
| 91 |
+
-------------------------------
|
| 92 |
+
|
| 93 |
+
Build a prismatic solid using extrusion. After a drawing operation, the center of the previous object
|
| 94 |
+
is placed on the stack, and is the reference for the next operation. So in this case, the rect() is drawn
|
| 95 |
+
centered on the previously draw circle.
|
| 96 |
+
|
| 97 |
+
By default, rectangles and circles are centered around the previous working point.
|
| 98 |
+
|
| 99 |
+
.. cadquery::
|
| 100 |
+
|
| 101 |
+
result = cq.Workplane("front").circle(2.0).rect(0.5, 0.75).extrude(0.5)
|
| 102 |
+
|
| 103 |
+
.. topic:: Api References
|
| 104 |
+
|
| 105 |
+
.. hlist::
|
| 106 |
+
:columns: 2
|
| 107 |
+
|
| 108 |
+
* :py:meth:`Workplane.circle` **!**
|
| 109 |
+
* :py:meth:`Workplane.rect` **!**
|
| 110 |
+
* :py:meth:`Workplane.extrude` **!**
|
| 111 |
+
* :py:meth:`Workplane`
|
| 112 |
+
|
| 113 |
+
Building Profiles using lines and arcs
|
| 114 |
+
--------------------------------------
|
| 115 |
+
|
| 116 |
+
Sometimes you need to build complex profiles using lines and arcs. This example builds a prismatic
|
| 117 |
+
solid from 2D operations.
|
| 118 |
+
|
| 119 |
+
2D operations maintain a current point, which is initially at the origin. Use close() to finish a
|
| 120 |
+
closed curve.
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
.. cadquery::
|
| 124 |
+
|
| 125 |
+
result = (
|
| 126 |
+
cq.Workplane("front")
|
| 127 |
+
.lineTo(2.0, 0)
|
| 128 |
+
.lineTo(2.0, 1.0)
|
| 129 |
+
.threePointArc((1.0, 1.5), (0.0, 1.0))
|
| 130 |
+
.close()
|
| 131 |
+
.extrude(0.25)
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
.. topic:: Api References
|
| 136 |
+
|
| 137 |
+
.. hlist::
|
| 138 |
+
:columns: 2
|
| 139 |
+
|
| 140 |
+
* :py:meth:`Workplane.threePointArc` **!**
|
| 141 |
+
* :py:meth:`Workplane.lineTo` **!**
|
| 142 |
+
* :py:meth:`Workplane.extrude`
|
| 143 |
+
* :py:meth:`Workplane`
|
| 144 |
+
|
| 145 |
+
Moving The Current working point
|
| 146 |
+
---------------------------------
|
| 147 |
+
|
| 148 |
+
In this example, a closed profile is required, with some interior features as well.
|
| 149 |
+
|
| 150 |
+
This example also demonstrates using multiple lines of code instead of longer chained commands,
|
| 151 |
+
though of course in this case it was possible to do it in one long line as well.
|
| 152 |
+
|
| 153 |
+
A new work plane center can be established at any point.
|
| 154 |
+
|
| 155 |
+
.. cadquery::
|
| 156 |
+
|
| 157 |
+
result = cq.Workplane("front").circle(
|
| 158 |
+
3.0
|
| 159 |
+
) # current point is the center of the circle, at (0, 0)
|
| 160 |
+
result = result.center(1.5, 0.0).rect(0.5, 0.5) # new work center is (1.5, 0.0)
|
| 161 |
+
|
| 162 |
+
result = result.center(-1.5, 1.5).circle(0.25) # new work center is (0.0, 1.5).
|
| 163 |
+
# The new center is specified relative to the previous center, not global coordinates!
|
| 164 |
+
|
| 165 |
+
result = result.extrude(0.25)
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
.. topic:: Api References
|
| 169 |
+
|
| 170 |
+
.. hlist::
|
| 171 |
+
:columns: 2
|
| 172 |
+
|
| 173 |
+
* :py:meth:`Workplane.center` **!**
|
| 174 |
+
* :py:meth:`Workplane`
|
| 175 |
+
* :py:meth:`Workplane.circle`
|
| 176 |
+
* :py:meth:`Workplane.rect`
|
| 177 |
+
* :py:meth:`Workplane.extrude`
|
| 178 |
+
|
| 179 |
+
Using Point Lists
|
| 180 |
+
---------------------------
|
| 181 |
+
|
| 182 |
+
Sometimes you need to create a number of features at various locations, and using :py:meth:`Workplane.center`
|
| 183 |
+
is too cumbersome.
|
| 184 |
+
|
| 185 |
+
You can use a list of points to construct multiple objects at once. Most construction methods,
|
| 186 |
+
like :py:meth:`Workplane.circle` and :py:meth:`Workplane.rect`, will operate on multiple points if they are on the stack
|
| 187 |
+
|
| 188 |
+
.. cadquery::
|
| 189 |
+
|
| 190 |
+
r = cq.Workplane("front").circle(2.0) # make base
|
| 191 |
+
r = r.pushPoints(
|
| 192 |
+
[(1.5, 0), (0, 1.5), (-1.5, 0), (0, -1.5)]
|
| 193 |
+
) # now four points are on the stack
|
| 194 |
+
r = r.circle(0.25) # circle will operate on all four points
|
| 195 |
+
result = r.extrude(0.125) # make prism
|
| 196 |
+
|
| 197 |
+
.. topic:: Api References
|
| 198 |
+
|
| 199 |
+
.. hlist::
|
| 200 |
+
:columns: 2
|
| 201 |
+
|
| 202 |
+
* :py:meth:`Workplane.pushPoints` **!**
|
| 203 |
+
* :py:meth:`Workplane`
|
| 204 |
+
* :py:meth:`Workplane.circle`
|
| 205 |
+
* :py:meth:`Workplane.extrude`
|
| 206 |
+
|
| 207 |
+
Polygons
|
| 208 |
+
-------------------------
|
| 209 |
+
|
| 210 |
+
You can create polygons for each stack point if you would like. Useful in 3d printers whose firmware does not
|
| 211 |
+
correct for small hole sizes.
|
| 212 |
+
|
| 213 |
+
.. cadquery::
|
| 214 |
+
|
| 215 |
+
result = (
|
| 216 |
+
cq.Workplane("front")
|
| 217 |
+
.box(3.0, 4.0, 0.25)
|
| 218 |
+
.pushPoints([(0, 0.75), (0, -0.75)])
|
| 219 |
+
.polygon(6, 1.0)
|
| 220 |
+
.cutThruAll()
|
| 221 |
+
)
|
| 222 |
+
|
| 223 |
+
.. topic:: Api References
|
| 224 |
+
|
| 225 |
+
.. hlist::
|
| 226 |
+
:columns: 2
|
| 227 |
+
|
| 228 |
+
* :py:meth:`Workplane.polygon` **!**
|
| 229 |
+
* :py:meth:`Workplane.pushPoints`
|
| 230 |
+
* :py:meth:`Workplane.box`
|
| 231 |
+
|
| 232 |
+
Polylines
|
| 233 |
+
-------------------------
|
| 234 |
+
|
| 235 |
+
:py:meth:`Workplane.polyline` allows creating a shape from a large number of chained points connected by lines.
|
| 236 |
+
|
| 237 |
+
This example uses a polyline to create one half of an i-beam shape, which is mirrored to create the final profile.
|
| 238 |
+
|
| 239 |
+
.. cadquery::
|
| 240 |
+
|
| 241 |
+
(L, H, W, t) = (100.0, 20.0, 20.0, 1.0)
|
| 242 |
+
pts = [
|
| 243 |
+
(0, H / 2.0),
|
| 244 |
+
(W / 2.0, H / 2.0),
|
| 245 |
+
(W / 2.0, (H / 2.0 - t)),
|
| 246 |
+
(t / 2.0, (H / 2.0 - t)),
|
| 247 |
+
(t / 2.0, (t - H / 2.0)),
|
| 248 |
+
(W / 2.0, (t - H / 2.0)),
|
| 249 |
+
(W / 2.0, H / -2.0),
|
| 250 |
+
(0, H / -2.0),
|
| 251 |
+
]
|
| 252 |
+
result = cq.Workplane("front").polyline(pts).mirrorY().extrude(L)
|
| 253 |
+
|
| 254 |
+
.. topic:: Api References
|
| 255 |
+
|
| 256 |
+
.. hlist::
|
| 257 |
+
:columns: 2
|
| 258 |
+
|
| 259 |
+
* :py:meth:`Workplane.polyline` **!**
|
| 260 |
+
* :py:meth:`Workplane`
|
| 261 |
+
* :py:meth:`Workplane.mirrorY`
|
| 262 |
+
* :py:meth:`Workplane.extrude`
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
Defining an Edge with a Spline
|
| 267 |
+
------------------------------
|
| 268 |
+
|
| 269 |
+
This example defines a side using a spline curve through a collection of points. Useful when you have an edge that
|
| 270 |
+
needs a complex profile
|
| 271 |
+
|
| 272 |
+
.. cadquery::
|
| 273 |
+
|
| 274 |
+
s = cq.Workplane("XY")
|
| 275 |
+
sPnts = [
|
| 276 |
+
(2.75, 1.5),
|
| 277 |
+
(2.5, 1.75),
|
| 278 |
+
(2.0, 1.5),
|
| 279 |
+
(1.5, 1.0),
|
| 280 |
+
(1.0, 1.25),
|
| 281 |
+
(0.5, 1.0),
|
| 282 |
+
(0, 1.0),
|
| 283 |
+
]
|
| 284 |
+
r = s.lineTo(3.0, 0).lineTo(3.0, 1.0).spline(sPnts, includeCurrent=True).close()
|
| 285 |
+
result = r.extrude(0.5)
|
| 286 |
+
|
| 287 |
+
.. topic:: Api References
|
| 288 |
+
|
| 289 |
+
.. hlist::
|
| 290 |
+
:columns: 2
|
| 291 |
+
|
| 292 |
+
* :py:meth:`Workplane.spline` **!**
|
| 293 |
+
* :py:meth:`Workplane`
|
| 294 |
+
* :py:meth:`Workplane.close`
|
| 295 |
+
* :py:meth:`Workplane.lineTo`
|
| 296 |
+
* :py:meth:`Workplane.extrude`
|
| 297 |
+
|
| 298 |
+
Mirroring Symmetric Geometry
|
| 299 |
+
-----------------------------
|
| 300 |
+
|
| 301 |
+
You can mirror 2D geometry when your shape is symmetric. In this example we also
|
| 302 |
+
introduce horizontal and vertical lines, which make for slightly easier coding.
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
.. cadquery::
|
| 306 |
+
|
| 307 |
+
r = cq.Workplane("front").hLine(1.0) # 1.0 is the distance, not coordinate
|
| 308 |
+
r = (
|
| 309 |
+
r.vLine(0.5).hLine(-0.25).vLine(-0.25).hLineTo(0.0)
|
| 310 |
+
) # hLineTo allows using xCoordinate not distance
|
| 311 |
+
result = r.mirrorY().extrude(0.25) # mirror the geometry and extrude
|
| 312 |
+
|
| 313 |
+
.. topic:: Api References
|
| 314 |
+
|
| 315 |
+
.. hlist::
|
| 316 |
+
:columns: 2
|
| 317 |
+
|
| 318 |
+
* :py:meth:`Workplane.hLine` **!**
|
| 319 |
+
* :py:meth:`Workplane.vLine` **!**
|
| 320 |
+
* :py:meth:`Workplane.hLineTo` **!**
|
| 321 |
+
* :py:meth:`Workplane.mirrorY` **!**
|
| 322 |
+
* :py:meth:`Workplane.mirrorX` **!**
|
| 323 |
+
* :py:meth:`Workplane`
|
| 324 |
+
* :py:meth:`Workplane.extrude`
|
| 325 |
+
|
| 326 |
+
Mirroring 3D Objects
|
| 327 |
+
-----------------------------
|
| 328 |
+
|
| 329 |
+
.. cadquery::
|
| 330 |
+
|
| 331 |
+
result0 = (
|
| 332 |
+
cadquery.Workplane("XY")
|
| 333 |
+
.moveTo(10, 0)
|
| 334 |
+
.lineTo(5, 0)
|
| 335 |
+
.threePointArc((3.9393, 0.4393), (3.5, 1.5))
|
| 336 |
+
.threePointArc((3.0607, 2.5607), (2, 3))
|
| 337 |
+
.lineTo(1.5, 3)
|
| 338 |
+
.threePointArc((0.4393, 3.4393), (0, 4.5))
|
| 339 |
+
.lineTo(0, 13.5)
|
| 340 |
+
.threePointArc((0.4393, 14.5607), (1.5, 15))
|
| 341 |
+
.lineTo(28, 15)
|
| 342 |
+
.lineTo(28, 13.5)
|
| 343 |
+
.lineTo(24, 13.5)
|
| 344 |
+
.lineTo(24, 11.5)
|
| 345 |
+
.lineTo(27, 11.5)
|
| 346 |
+
.lineTo(27, 10)
|
| 347 |
+
.lineTo(22, 10)
|
| 348 |
+
.lineTo(22, 13.2)
|
| 349 |
+
.lineTo(14.5, 13.2)
|
| 350 |
+
.lineTo(14.5, 10)
|
| 351 |
+
.lineTo(12.5, 10)
|
| 352 |
+
.lineTo(12.5, 13.2)
|
| 353 |
+
.lineTo(5.5, 13.2)
|
| 354 |
+
.lineTo(5.5, 2)
|
| 355 |
+
.threePointArc((5.793, 1.293), (6.5, 1))
|
| 356 |
+
.lineTo(10, 1)
|
| 357 |
+
.close()
|
| 358 |
+
)
|
| 359 |
+
result = result0.extrude(100)
|
| 360 |
+
|
| 361 |
+
result = result.rotate((0, 0, 0), (1, 0, 0), 90)
|
| 362 |
+
|
| 363 |
+
result = result.translate(result.val().BoundingBox().center.multiply(-1))
|
| 364 |
+
|
| 365 |
+
mirXY_neg = result.mirror(mirrorPlane="XY", basePointVector=(0, 0, -30))
|
| 366 |
+
mirXY_pos = result.mirror(mirrorPlane="XY", basePointVector=(0, 0, 30))
|
| 367 |
+
mirZY_neg = result.mirror(mirrorPlane="ZY", basePointVector=(-30, 0, 0))
|
| 368 |
+
mirZY_pos = result.mirror(mirrorPlane="ZY", basePointVector=(30, 0, 0))
|
| 369 |
+
|
| 370 |
+
result = result.union(mirXY_neg).union(mirXY_pos).union(mirZY_neg).union(mirZY_pos)
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
.. topic:: Api References
|
| 374 |
+
|
| 375 |
+
.. hlist::
|
| 376 |
+
:columns: 2
|
| 377 |
+
|
| 378 |
+
* :py:meth:`Workplane.moveTo`
|
| 379 |
+
* :py:meth:`Workplane.lineTo`
|
| 380 |
+
* :py:meth:`Workplane.threePointArc`
|
| 381 |
+
* :py:meth:`Workplane.extrude`
|
| 382 |
+
* :py:meth:`Workplane.mirror`
|
| 383 |
+
* :py:meth:`Workplane.union`
|
| 384 |
+
* :py:meth:`Workplane.rotate`
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
Mirroring From Faces
|
| 388 |
+
-----------------------------
|
| 389 |
+
|
| 390 |
+
This example shows how you can mirror about a selected face. It also shows how the resulting mirrored object can be unioned immediately with the referenced mirror geometry.
|
| 391 |
+
|
| 392 |
+
.. cadquery::
|
| 393 |
+
|
| 394 |
+
result = cq.Workplane("XY").line(0, 1).line(1, 0).line(0, -0.5).close().extrude(1)
|
| 395 |
+
|
| 396 |
+
result = result.mirror(result.faces(">X"), union=True)
|
| 397 |
+
|
| 398 |
+
|
| 399 |
+
.. topic:: Api References
|
| 400 |
+
|
| 401 |
+
.. hlist::
|
| 402 |
+
:columns: 2
|
| 403 |
+
|
| 404 |
+
* :py:meth:`Workplane.line`
|
| 405 |
+
* :py:meth:`Workplane.close`
|
| 406 |
+
* :py:meth:`Workplane.extrude`
|
| 407 |
+
* :py:meth:`Workplane.faces`
|
| 408 |
+
* :py:meth:`Workplane.mirror`
|
| 409 |
+
* :py:meth:`Workplane.union`
|
| 410 |
+
|
| 411 |
+
Creating Workplanes on Faces
|
| 412 |
+
-----------------------------
|
| 413 |
+
|
| 414 |
+
This example shows how to locate a new workplane on the face of a previously created feature.
|
| 415 |
+
|
| 416 |
+
.. note::
|
| 417 |
+
Using workplanes in this way are a key feature of CadQuery. Unlike a typical 3d scripting
|
| 418 |
+
language, using work planes frees you from tracking the position of various features in
|
| 419 |
+
variables, and allows the model to adjust itself with removing redundant dimensions
|
| 420 |
+
|
| 421 |
+
The :py:meth:`Workplane.faces()` method allows you to select the faces of a resulting solid. It
|
| 422 |
+
accepts a selector string or object, that allows you to target a single face, and make a workplane
|
| 423 |
+
oriented on that face.
|
| 424 |
+
|
| 425 |
+
Keep in mind that by default the origin of a new workplane is calculated by forming a plane from the
|
| 426 |
+
selected face and projecting the previous origin onto that plane. This behaviour can be changed
|
| 427 |
+
through the centerOption argument of :py:meth:`Workplane.workplane`.
|
| 428 |
+
|
| 429 |
+
.. cadquery::
|
| 430 |
+
|
| 431 |
+
result = cq.Workplane("front").box(2, 3, 0.5) # make a basic prism
|
| 432 |
+
result = (
|
| 433 |
+
result.faces(">Z").workplane().hole(0.5)
|
| 434 |
+
) # find the top-most face and make a hole
|
| 435 |
+
|
| 436 |
+
.. topic:: Api References
|
| 437 |
+
|
| 438 |
+
.. hlist::
|
| 439 |
+
:columns: 2
|
| 440 |
+
|
| 441 |
+
* :py:meth:`Workplane.faces` **!**
|
| 442 |
+
* :py:meth:`StringSyntaxSelector` **!**
|
| 443 |
+
* :ref:`selector_reference` **!**
|
| 444 |
+
* :py:meth:`Workplane.workplane`
|
| 445 |
+
* :py:meth:`Workplane.box`
|
| 446 |
+
* :py:meth:`Workplane`
|
| 447 |
+
|
| 448 |
+
Locating a Workplane on a vertex
|
| 449 |
+
---------------------------------
|
| 450 |
+
|
| 451 |
+
Normally, the :py:meth:`Workplane.workplane` method requires a face to be selected. But if a vertex
|
| 452 |
+
is selected **immediately after a face**, :py:meth:`Workplane.workplane` with the centerOption
|
| 453 |
+
argument set to CenterOfMass will locate the workplane on the face, with the origin at the vertex
|
| 454 |
+
instead of at the center of the face
|
| 455 |
+
|
| 456 |
+
The example also introduces :py:meth:`Workplane.cutThruAll`, which makes a cut through the entire
|
| 457 |
+
part, no matter how deep the part is.
|
| 458 |
+
|
| 459 |
+
.. cadquery::
|
| 460 |
+
|
| 461 |
+
result = cq.Workplane("front").box(3, 2, 0.5) # make a basic prism
|
| 462 |
+
result = (
|
| 463 |
+
result.faces(">Z").vertices("<XY").workplane(centerOption="CenterOfMass")
|
| 464 |
+
) # select the lower left vertex and make a workplane
|
| 465 |
+
result = result.circle(1.0).cutThruAll() # cut the corner out
|
| 466 |
+
|
| 467 |
+
.. topic:: Api References
|
| 468 |
+
|
| 469 |
+
.. hlist::
|
| 470 |
+
:columns: 2
|
| 471 |
+
|
| 472 |
+
* :py:meth:`Workplane.cutThruAll` **!**
|
| 473 |
+
|
| 474 |
+
* :ref:`selector_reference` **!**
|
| 475 |
+
* :py:meth:`Workplane.vertices` **!**
|
| 476 |
+
* :py:meth:`Workplane.box`
|
| 477 |
+
* :py:meth:`Workplane`
|
| 478 |
+
* :py:meth:`StringSyntaxSelector` **!**
|
| 479 |
+
|
| 480 |
+
Offset Workplanes
|
| 481 |
+
--------------------------
|
| 482 |
+
|
| 483 |
+
Workplanes do not have to lie exactly on a face. When you make a workplane, you can define it at an offset
|
| 484 |
+
from an existing face.
|
| 485 |
+
|
| 486 |
+
This example uses an offset workplane to make a compound object, which is perfectly valid!
|
| 487 |
+
|
| 488 |
+
.. cadquery::
|
| 489 |
+
|
| 490 |
+
result = cq.Workplane("front").box(3, 2, 0.5) # make a basic prism
|
| 491 |
+
result = result.faces("<X").workplane(
|
| 492 |
+
offset=0.75
|
| 493 |
+
) # workplane is offset from the object surface
|
| 494 |
+
result = result.circle(1.0).extrude(0.5) # disc
|
| 495 |
+
|
| 496 |
+
.. topic:: Api References
|
| 497 |
+
|
| 498 |
+
.. hlist::
|
| 499 |
+
:columns: 2
|
| 500 |
+
|
| 501 |
+
* :py:meth:`Workplane.extrude`
|
| 502 |
+
* :ref:`selector_reference` **!**
|
| 503 |
+
* :py:meth:`Workplane.box`
|
| 504 |
+
* :py:meth:`Workplane`
|
| 505 |
+
|
| 506 |
+
Copying Workplanes
|
| 507 |
+
--------------------------
|
| 508 |
+
|
| 509 |
+
An existing CQ object can copy a workplane from another CQ object.
|
| 510 |
+
|
| 511 |
+
.. cadquery::
|
| 512 |
+
|
| 513 |
+
result = (
|
| 514 |
+
cq.Workplane("front")
|
| 515 |
+
.circle(1)
|
| 516 |
+
.extrude(10) # make a cylinder
|
| 517 |
+
# We want to make a second cylinder perpendicular to the first,
|
| 518 |
+
# but we have no face to base the workplane off
|
| 519 |
+
.copyWorkplane(
|
| 520 |
+
# create a temporary object with the required workplane
|
| 521 |
+
cq.Workplane("right", origin=(-5, 0, 0))
|
| 522 |
+
)
|
| 523 |
+
.circle(1)
|
| 524 |
+
.extrude(10)
|
| 525 |
+
)
|
| 526 |
+
|
| 527 |
+
.. topic:: API References
|
| 528 |
+
|
| 529 |
+
.. hlist::
|
| 530 |
+
:columns: 2
|
| 531 |
+
|
| 532 |
+
* :py:meth:`Workplane.copyWorkplane` **!**
|
| 533 |
+
* :py:meth:`Workplane.circle`
|
| 534 |
+
* :py:meth:`Workplane.extrude`
|
| 535 |
+
* :py:meth:`Workplane`
|
| 536 |
+
|
| 537 |
+
Rotated Workplanes
|
| 538 |
+
--------------------------
|
| 539 |
+
|
| 540 |
+
You can create a rotated work plane by specifying angles of rotation relative to another workplane
|
| 541 |
+
|
| 542 |
+
.. cadquery::
|
| 543 |
+
|
| 544 |
+
result = (
|
| 545 |
+
cq.Workplane("front")
|
| 546 |
+
.box(4.0, 4.0, 0.25)
|
| 547 |
+
.faces(">Z")
|
| 548 |
+
.workplane()
|
| 549 |
+
.transformed(offset=cq.Vector(0, -1.5, 1.0), rotate=cq.Vector(60, 0, 0))
|
| 550 |
+
.rect(1.5, 1.5, forConstruction=True)
|
| 551 |
+
.vertices()
|
| 552 |
+
.hole(0.25)
|
| 553 |
+
)
|
| 554 |
+
|
| 555 |
+
.. topic:: Api References
|
| 556 |
+
|
| 557 |
+
.. hlist::
|
| 558 |
+
:columns: 2
|
| 559 |
+
|
| 560 |
+
* :py:meth:`Workplane.transformed` **!**
|
| 561 |
+
* :py:meth:`Workplane.box`
|
| 562 |
+
* :py:meth:`Workplane.rect`
|
| 563 |
+
* :py:meth:`Workplane.faces`
|
| 564 |
+
|
| 565 |
+
Using construction Geometry
|
| 566 |
+
---------------------------
|
| 567 |
+
|
| 568 |
+
You can draw shapes to use the vertices as points to locate other features. Features that are used to
|
| 569 |
+
locate other features, rather than to create them, are called ``Construction Geometry``
|
| 570 |
+
|
| 571 |
+
In the example below, a rectangle is drawn, and its vertices are used to locate a set of holes.
|
| 572 |
+
|
| 573 |
+
.. cadquery::
|
| 574 |
+
|
| 575 |
+
result = (
|
| 576 |
+
cq.Workplane("front")
|
| 577 |
+
.box(2, 2, 0.5)
|
| 578 |
+
.faces(">Z")
|
| 579 |
+
.workplane()
|
| 580 |
+
.rect(1.5, 1.5, forConstruction=True)
|
| 581 |
+
.vertices()
|
| 582 |
+
.hole(0.125)
|
| 583 |
+
)
|
| 584 |
+
|
| 585 |
+
.. topic:: Api References
|
| 586 |
+
|
| 587 |
+
.. hlist::
|
| 588 |
+
:columns: 2
|
| 589 |
+
|
| 590 |
+
* :py:meth:`Workplane.rect` (forConstruction=True)
|
| 591 |
+
* :ref:`selector_reference`
|
| 592 |
+
* :py:meth:`Workplane.workplane`
|
| 593 |
+
* :py:meth:`Workplane.box`
|
| 594 |
+
* :py:meth:`Workplane.hole`
|
| 595 |
+
* :py:meth:`Workplane`
|
| 596 |
+
|
| 597 |
+
Shelling To Create Thin features
|
| 598 |
+
--------------------------------
|
| 599 |
+
|
| 600 |
+
Shelling converts a solid object into a shell of uniform thickness.
|
| 601 |
+
|
| 602 |
+
To shell an object and 'hollow out' the inside pass a negative thickness parameter
|
| 603 |
+
to the :py:meth:`Workplane.shell()` method of a shape.
|
| 604 |
+
|
| 605 |
+
.. cadquery::
|
| 606 |
+
|
| 607 |
+
result = cq.Workplane("front").box(2, 2, 2).shell(-0.1)
|
| 608 |
+
|
| 609 |
+
A positive thickness parameter wraps an object with filleted outside edges
|
| 610 |
+
and the original object will be the 'hollowed out' portion.
|
| 611 |
+
|
| 612 |
+
.. cadquery::
|
| 613 |
+
|
| 614 |
+
result = cq.Workplane("front").box(2, 2, 2).shell(0.1)
|
| 615 |
+
|
| 616 |
+
Use face selectors to select a face to be removed from the resulting hollow shape.
|
| 617 |
+
|
| 618 |
+
.. cadquery::
|
| 619 |
+
|
| 620 |
+
result = cq.Workplane("front").box(2, 2, 2).faces("+Z").shell(0.1)
|
| 621 |
+
|
| 622 |
+
Multiple faces can be removed using more complex selectors.
|
| 623 |
+
|
| 624 |
+
.. cadquery::
|
| 625 |
+
|
| 626 |
+
result = cq.Workplane("front").box(2, 2, 2).faces("+Z or -X or +X").shell(0.1)
|
| 627 |
+
|
| 628 |
+
.. topic:: Api References
|
| 629 |
+
|
| 630 |
+
.. hlist::
|
| 631 |
+
:columns: 2
|
| 632 |
+
|
| 633 |
+
* :py:meth:`Workplane.shell` **!**
|
| 634 |
+
* :ref:`selector_reference`
|
| 635 |
+
* :py:meth:`Workplane.box`
|
| 636 |
+
* :py:meth:`Workplane.faces`
|
| 637 |
+
* :py:meth:`Workplane`
|
| 638 |
+
|
| 639 |
+
Making Lofts
|
| 640 |
+
--------------------------------------------
|
| 641 |
+
|
| 642 |
+
A loft is a solid swept through a set of wires. This example creates lofted section between a rectangle
|
| 643 |
+
and a circular section.
|
| 644 |
+
|
| 645 |
+
.. cadquery::
|
| 646 |
+
|
| 647 |
+
result = (
|
| 648 |
+
cq.Workplane("front")
|
| 649 |
+
.box(4.0, 4.0, 0.25)
|
| 650 |
+
.faces(">Z")
|
| 651 |
+
.circle(1.5)
|
| 652 |
+
.workplane(offset=3.0)
|
| 653 |
+
.rect(0.75, 0.5)
|
| 654 |
+
.loft(combine=True)
|
| 655 |
+
)
|
| 656 |
+
|
| 657 |
+
|
| 658 |
+
.. topic:: Api References
|
| 659 |
+
|
| 660 |
+
.. hlist::
|
| 661 |
+
:columns: 2
|
| 662 |
+
|
| 663 |
+
* :py:meth:`Workplane.loft` **!**
|
| 664 |
+
* :py:meth:`Workplane.box`
|
| 665 |
+
* :py:meth:`Workplane.faces`
|
| 666 |
+
* :py:meth:`Workplane.circle`
|
| 667 |
+
* :py:meth:`Workplane.rect`
|
| 668 |
+
|
| 669 |
+
Extruding until a given face
|
| 670 |
+
--------------------------------------------
|
| 671 |
+
|
| 672 |
+
Sometimes you will want to extrude a wire until a given face that can be not planar or where you
|
| 673 |
+
might not know easily the distance you have to extrude to. In such cases you can use `next`, `last`
|
| 674 |
+
or even give a :class:`~cadquery.Face` object for the `until` argument of
|
| 675 |
+
:meth:`~cadquery.Workplane.extrude`.
|
| 676 |
+
|
| 677 |
+
|
| 678 |
+
.. cadquery::
|
| 679 |
+
|
| 680 |
+
result = (
|
| 681 |
+
cq.Workplane(origin=(20, 0, 0))
|
| 682 |
+
.circle(2)
|
| 683 |
+
.revolve(180, (-20, 0, 0), (-20, -1, 0))
|
| 684 |
+
.center(-20, 0)
|
| 685 |
+
.workplane()
|
| 686 |
+
.rect(20, 4)
|
| 687 |
+
.extrude("next")
|
| 688 |
+
)
|
| 689 |
+
|
| 690 |
+
The same behaviour is available with :meth:`~cadquery.Workplane.cutBlind` and as you can see it is
|
| 691 |
+
also possible to work on several :class:`~cadquery.Wire` objects at a time (the
|
| 692 |
+
same is true for :meth:`~cadquery.Workplane.extrude`).
|
| 693 |
+
|
| 694 |
+
.. cadquery::
|
| 695 |
+
|
| 696 |
+
skyscrapers_locations = [(-16, 1), (-8, 0), (7, 0.2), (17, -1.2)]
|
| 697 |
+
angles = iter([15, 0, -8, 10])
|
| 698 |
+
skyscrapers = (
|
| 699 |
+
cq.Workplane()
|
| 700 |
+
.pushPoints(skyscrapers_locations)
|
| 701 |
+
.eachpoint(
|
| 702 |
+
lambda loc: (
|
| 703 |
+
cq.Workplane()
|
| 704 |
+
.rect(5, 16)
|
| 705 |
+
.workplane(offset=10)
|
| 706 |
+
.ellipse(3, 8)
|
| 707 |
+
.workplane(offset=10)
|
| 708 |
+
.slot2D(20, 5, 90)
|
| 709 |
+
.loft()
|
| 710 |
+
.rotateAboutCenter((0, 0, 1), next(angles))
|
| 711 |
+
.val()
|
| 712 |
+
.located(loc)
|
| 713 |
+
)
|
| 714 |
+
)
|
| 715 |
+
)
|
| 716 |
+
|
| 717 |
+
result = (
|
| 718 |
+
skyscrapers.transformed((0, -90, 0))
|
| 719 |
+
.moveTo(15, 0)
|
| 720 |
+
.rect(3, 3, forConstruction=True)
|
| 721 |
+
.vertices()
|
| 722 |
+
.circle(1)
|
| 723 |
+
.cutBlind("last")
|
| 724 |
+
)
|
| 725 |
+
|
| 726 |
+
Here is a typical situation where extruding and cuting until a given surface is very handy. It allows us to extrude or cut until a curved surface without overlapping issues.
|
| 727 |
+
|
| 728 |
+
.. cadquery::
|
| 729 |
+
|
| 730 |
+
import cadquery as cq
|
| 731 |
+
|
| 732 |
+
sphere = cq.Workplane().sphere(5)
|
| 733 |
+
base = cq.Workplane(origin=(0, 0, -2)).box(12, 12, 10).cut(sphere).edges("|Z").fillet(2)
|
| 734 |
+
sphere_face = base.faces(">>X[2] and (not |Z) and (not |Y)").val()
|
| 735 |
+
base = base.faces("<Z").workplane().circle(2).extrude(10)
|
| 736 |
+
|
| 737 |
+
shaft = cq.Workplane().sphere(4.5).circle(1.5).extrude(20)
|
| 738 |
+
|
| 739 |
+
spherical_joint = (
|
| 740 |
+
base.union(shaft)
|
| 741 |
+
.faces(">X")
|
| 742 |
+
.workplane(centerOption="CenterOfMass")
|
| 743 |
+
.move(0, 4)
|
| 744 |
+
.slot2D(10, 2, 90)
|
| 745 |
+
.cutBlind(sphere_face)
|
| 746 |
+
.workplane(offset=10)
|
| 747 |
+
.move(0, 2)
|
| 748 |
+
.circle(0.9)
|
| 749 |
+
.extrude("next")
|
| 750 |
+
)
|
| 751 |
+
|
| 752 |
+
result = spherical_joint
|
| 753 |
+
|
| 754 |
+
.. warning::
|
| 755 |
+
|
| 756 |
+
If the wire you want to extrude cannot be fully projected on the target surface, the result will
|
| 757 |
+
be unpredictable. Furthermore, the algorithm in charge of finding the candidate faces does its search by counting all the faces intersected
|
| 758 |
+
by a line created from your wire center along your
|
| 759 |
+
extrusion direction. So make sure your wire can be projected on your target face to avoid
|
| 760 |
+
unexpected behaviour.
|
| 761 |
+
|
| 762 |
+
.. topic:: Api References
|
| 763 |
+
|
| 764 |
+
.. hlist::
|
| 765 |
+
:columns: 3
|
| 766 |
+
|
| 767 |
+
* :py:meth:`Workplane.cutBlind` **!**
|
| 768 |
+
* :py:meth:`Workplane.rect`
|
| 769 |
+
* :py:meth:`Workplane.ellipse`
|
| 770 |
+
* :py:meth:`Workplane.workplane`
|
| 771 |
+
* :py:meth:`Workplane.slot2D`
|
| 772 |
+
* :py:meth:`Workplane.loft`
|
| 773 |
+
* :py:meth:`Workplane.rotateAboutCenter`
|
| 774 |
+
* :py:meth:`Workplane.transformed`
|
| 775 |
+
* :py:meth:`Workplane.moveTo`
|
| 776 |
+
* :py:meth:`Workplane.circle`
|
| 777 |
+
|
| 778 |
+
|
| 779 |
+
Making Counter-bored and Counter-sunk Holes
|
| 780 |
+
----------------------------------------------
|
| 781 |
+
|
| 782 |
+
Counterbored and countersunk holes are so common that CadQuery creates macros to create them in a single step.
|
| 783 |
+
|
| 784 |
+
Similar to :py:meth:`Workplane.hole`, these functions operate on a list of points as well as a single point.
|
| 785 |
+
|
| 786 |
+
.. cadquery::
|
| 787 |
+
|
| 788 |
+
result = (
|
| 789 |
+
cq.Workplane(cq.Plane.XY())
|
| 790 |
+
.box(4, 2, 0.5)
|
| 791 |
+
.faces(">Z")
|
| 792 |
+
.workplane()
|
| 793 |
+
.rect(3.5, 1.5, forConstruction=True)
|
| 794 |
+
.vertices()
|
| 795 |
+
.cboreHole(0.125, 0.25, 0.125, depth=None)
|
| 796 |
+
)
|
| 797 |
+
|
| 798 |
+
|
| 799 |
+
.. topic:: Api References
|
| 800 |
+
|
| 801 |
+
.. hlist::
|
| 802 |
+
:columns: 2
|
| 803 |
+
|
| 804 |
+
* :py:meth:`Workplane.cboreHole` **!**
|
| 805 |
+
* :py:meth:`Workplane.cskHole` **!**
|
| 806 |
+
* :py:meth:`Workplane.box`
|
| 807 |
+
* :py:meth:`Workplane.rect`
|
| 808 |
+
* :py:meth:`Workplane.workplane`
|
| 809 |
+
* :py:meth:`Workplane.vertices`
|
| 810 |
+
* :py:meth:`Workplane.faces`
|
| 811 |
+
* :py:meth:`Workplane`
|
| 812 |
+
|
| 813 |
+
Offsetting wires in 2D
|
| 814 |
+
----------------------
|
| 815 |
+
|
| 816 |
+
Two dimensional wires can be transformed with :py:meth:`Workplane.offset2D`. They can be offset
|
| 817 |
+
inwards or outwards, and with different techniques for extending the corners.
|
| 818 |
+
|
| 819 |
+
.. cadquery::
|
| 820 |
+
|
| 821 |
+
original = cq.Workplane().polygon(5, 10).extrude(0.1).translate((0, 0, 2))
|
| 822 |
+
arc = cq.Workplane().polygon(5, 10).offset2D(1, "arc").extrude(0.1).translate((0, 0, 1))
|
| 823 |
+
intersection = cq.Workplane().polygon(5, 10).offset2D(1, "intersection").extrude(0.1)
|
| 824 |
+
result = original.add(arc).add(intersection)
|
| 825 |
+
|
| 826 |
+
|
| 827 |
+
Using the forConstruction argument you can do the common task of offsetting a series of bolt holes
|
| 828 |
+
from the outline of an object. Here is the counterbore example from above but with the bolt holes
|
| 829 |
+
offset from the edges.
|
| 830 |
+
|
| 831 |
+
.. cadquery::
|
| 832 |
+
|
| 833 |
+
result = (
|
| 834 |
+
cq.Workplane()
|
| 835 |
+
.box(4, 2, 0.5)
|
| 836 |
+
.faces(">Z")
|
| 837 |
+
.edges()
|
| 838 |
+
.toPending()
|
| 839 |
+
.offset2D(-0.25, forConstruction=True)
|
| 840 |
+
.vertices()
|
| 841 |
+
.cboreHole(0.125, 0.25, 0.125, depth=None)
|
| 842 |
+
)
|
| 843 |
+
|
| 844 |
+
|
| 845 |
+
Note that :py:meth:`Workplane.edges` is for selecting objects. It does not add the selected edges to
|
| 846 |
+
pending edges in the modelling context, because this would result in your next extrusion including
|
| 847 |
+
everything you had only selected in addition to the lines you had drawn. To specify you want these
|
| 848 |
+
edges to be used in :py:meth:`Workplane.offset2D`, you call :py:meth:`Workplane.toPending` to
|
| 849 |
+
explicitly put them in the list of pending edges.
|
| 850 |
+
|
| 851 |
+
.. topic:: Api References
|
| 852 |
+
|
| 853 |
+
.. hlist::
|
| 854 |
+
:columns: 2
|
| 855 |
+
|
| 856 |
+
* :py:meth:`Workplane.offset2D` **!**
|
| 857 |
+
* :py:meth:`Workplane.cboreHole`
|
| 858 |
+
* :py:meth:`Workplane.cskHole`
|
| 859 |
+
* :py:meth:`Workplane.box`
|
| 860 |
+
* :py:meth:`Workplane.polygon`
|
| 861 |
+
* :py:meth:`Workplane.workplane`
|
| 862 |
+
* :py:meth:`Workplane.vertices`
|
| 863 |
+
* :py:meth:`Workplane.edges`
|
| 864 |
+
* :py:meth:`Workplane.faces`
|
| 865 |
+
* :py:meth:`Workplane`
|
| 866 |
+
|
| 867 |
+
|
| 868 |
+
Rounding Corners with Fillet
|
| 869 |
+
-----------------------------
|
| 870 |
+
|
| 871 |
+
Filleting is done by selecting the edges of a solid, and using the fillet function.
|
| 872 |
+
|
| 873 |
+
Here we fillet all of the edges of a simple plate.
|
| 874 |
+
|
| 875 |
+
.. cadquery::
|
| 876 |
+
|
| 877 |
+
result = cq.Workplane("XY").box(3, 3, 0.5).edges("|Z").fillet(0.125)
|
| 878 |
+
|
| 879 |
+
.. topic:: Api References
|
| 880 |
+
|
| 881 |
+
.. hlist::
|
| 882 |
+
:columns: 2
|
| 883 |
+
|
| 884 |
+
* :py:meth:`Workplane.fillet` **!**
|
| 885 |
+
* :py:meth:`Workplane.box`
|
| 886 |
+
* :py:meth:`Workplane.edges`
|
| 887 |
+
* :py:meth:`Workplane`
|
| 888 |
+
|
| 889 |
+
Tagging objects
|
| 890 |
+
----------------
|
| 891 |
+
|
| 892 |
+
The :py:meth:`Workplane.tag` method can be used to tag a particular object in the chain with a string, so that it can be referred to later in the chain.
|
| 893 |
+
|
| 894 |
+
The :py:meth:`Workplane.workplaneFromTagged` method applies :py:meth:`Workplane.copyWorkplane` to a tagged object. For example, when extruding two different solids from a surface, after the first solid is extruded it can become difficult to reselect the original surface with CadQuery's other selectors.
|
| 895 |
+
|
| 896 |
+
.. cadquery::
|
| 897 |
+
|
| 898 |
+
result = (
|
| 899 |
+
cq.Workplane("XY")
|
| 900 |
+
# create and tag the base workplane
|
| 901 |
+
.box(10, 10, 10)
|
| 902 |
+
.faces(">Z")
|
| 903 |
+
.workplane()
|
| 904 |
+
.tag("baseplane")
|
| 905 |
+
# extrude a cylinder
|
| 906 |
+
.center(-3, 0)
|
| 907 |
+
.circle(1)
|
| 908 |
+
.extrude(3)
|
| 909 |
+
# to reselect the base workplane, simply
|
| 910 |
+
.workplaneFromTagged("baseplane")
|
| 911 |
+
# extrude a second cylinder
|
| 912 |
+
.center(3, 0)
|
| 913 |
+
.circle(1)
|
| 914 |
+
.extrude(2)
|
| 915 |
+
)
|
| 916 |
+
|
| 917 |
+
|
| 918 |
+
Tags can also be used with most selectors, including :py:meth:`Workplane.vertices`, :py:meth:`Workplane.faces`, :py:meth:`Workplane.edges`, :py:meth:`Workplane.wires`, :py:meth:`Workplane.shells`, :py:meth:`Workplane.solids` and :py:meth:`Workplane.compounds`.
|
| 919 |
+
|
| 920 |
+
.. cadquery::
|
| 921 |
+
|
| 922 |
+
result = (
|
| 923 |
+
cq.Workplane("XY")
|
| 924 |
+
# create a triangular prism and tag it
|
| 925 |
+
.polygon(3, 5)
|
| 926 |
+
.extrude(4)
|
| 927 |
+
.tag("prism")
|
| 928 |
+
# create a sphere that obscures the prism
|
| 929 |
+
.sphere(10)
|
| 930 |
+
# create features based on the prism's faces
|
| 931 |
+
.faces("<X", tag="prism")
|
| 932 |
+
.workplane()
|
| 933 |
+
.circle(1)
|
| 934 |
+
.cutThruAll()
|
| 935 |
+
.faces(">X", tag="prism")
|
| 936 |
+
.faces(">Y")
|
| 937 |
+
.workplane()
|
| 938 |
+
.circle(1)
|
| 939 |
+
.cutThruAll()
|
| 940 |
+
)
|
| 941 |
+
|
| 942 |
+
.. topic:: Api References
|
| 943 |
+
|
| 944 |
+
.. hlist::
|
| 945 |
+
:columns: 2
|
| 946 |
+
|
| 947 |
+
* :py:meth:`Workplane.tag` **!**
|
| 948 |
+
* :py:meth:`Workplane.getTagged` **!**
|
| 949 |
+
* :py:meth:`Workplane.workplaneFromTagged` **!**
|
| 950 |
+
* :py:meth:`Workplane.extrude`
|
| 951 |
+
* :py:meth:`Workplane.cutThruAll`
|
| 952 |
+
* :py:meth:`Workplane.circle`
|
| 953 |
+
* :py:meth:`Workplane.faces`
|
| 954 |
+
* :py:meth:`Workplane`
|
| 955 |
+
|
| 956 |
+
A Parametric Bearing Pillow Block
|
| 957 |
+
------------------------------------
|
| 958 |
+
|
| 959 |
+
Combining a few basic functions, its possible to make a very good parametric bearing pillow block,
|
| 960 |
+
with just a few lines of code.
|
| 961 |
+
|
| 962 |
+
.. cadquery::
|
| 963 |
+
|
| 964 |
+
(length, height, bearing_diam, thickness, padding) = (30.0, 40.0, 22.0, 10.0, 8.0)
|
| 965 |
+
|
| 966 |
+
result = (
|
| 967 |
+
cq.Workplane("XY")
|
| 968 |
+
.box(length, height, thickness)
|
| 969 |
+
.faces(">Z")
|
| 970 |
+
.workplane()
|
| 971 |
+
.hole(bearing_diam)
|
| 972 |
+
.faces(">Z")
|
| 973 |
+
.workplane()
|
| 974 |
+
.rect(length - padding, height - padding, forConstruction=True)
|
| 975 |
+
.vertices()
|
| 976 |
+
.cboreHole(2.4, 4.4, 2.1)
|
| 977 |
+
)
|
| 978 |
+
|
| 979 |
+
|
| 980 |
+
Splitting an Object
|
| 981 |
+
---------------------
|
| 982 |
+
|
| 983 |
+
You can split an object using a workplane, and retain either or both halves
|
| 984 |
+
|
| 985 |
+
.. cadquery::
|
| 986 |
+
|
| 987 |
+
c = cq.Workplane("XY").box(1, 1, 1).faces(">Z").workplane().circle(0.25).cutThruAll()
|
| 988 |
+
|
| 989 |
+
# now cut it in half sideways
|
| 990 |
+
result = c.faces(">Y").workplane(-0.5).split(keepTop=True)
|
| 991 |
+
|
| 992 |
+
.. topic:: Api References
|
| 993 |
+
|
| 994 |
+
.. hlist::
|
| 995 |
+
:columns: 2
|
| 996 |
+
|
| 997 |
+
* :py:meth:`Workplane.split` **!**
|
| 998 |
+
* :py:meth:`Workplane.box`
|
| 999 |
+
* :py:meth:`Workplane.circle`
|
| 1000 |
+
* :py:meth:`Workplane.cutThruAll`
|
| 1001 |
+
* :py:meth:`Workplane.workplane`
|
| 1002 |
+
* :py:meth:`Workplane`
|
| 1003 |
+
|
| 1004 |
+
The Classic OCC Bottle
|
| 1005 |
+
----------------------
|
| 1006 |
+
|
| 1007 |
+
CadQuery is based on the OpenCascade.org (OCC) modeling Kernel. Those who are familiar with OCC know about the
|
| 1008 |
+
famous 'bottle' example. `The bottle example in the OCCT online documentation <https://old.opencascade.com/doc/occt-7.5.0/overview/html/occt__tutorial.html>`_.
|
| 1009 |
+
|
| 1010 |
+
A pythonOCC version is listed `here <https://github.com/tpaviot/pythonocc-demos/blob/f3ea9b4f65a9dff482be04b153d4ce5ec2430e13/examples/core_classic_occ_bottle.py>`_.
|
| 1011 |
+
|
| 1012 |
+
Of course one difference between this sample and the OCC version is the length. This sample is one of the longer
|
| 1013 |
+
ones at 13 lines, but that's very short compared to the pythonOCC version, which is 10x longer!
|
| 1014 |
+
|
| 1015 |
+
|
| 1016 |
+
.. cadquery::
|
| 1017 |
+
|
| 1018 |
+
(L, w, t) = (20.0, 6.0, 3.0)
|
| 1019 |
+
s = cq.Workplane("XY")
|
| 1020 |
+
|
| 1021 |
+
# Draw half the profile of the bottle and extrude it
|
| 1022 |
+
p = (
|
| 1023 |
+
s.center(-L / 2.0, 0)
|
| 1024 |
+
.vLine(w / 2.0)
|
| 1025 |
+
.threePointArc((L / 2.0, w / 2.0 + t), (L, w / 2.0))
|
| 1026 |
+
.vLine(-w / 2.0)
|
| 1027 |
+
.mirrorX()
|
| 1028 |
+
.extrude(30.0, True)
|
| 1029 |
+
)
|
| 1030 |
+
|
| 1031 |
+
# Make the neck
|
| 1032 |
+
p = p.faces(">Z").workplane(centerOption="CenterOfMass").circle(3.0).extrude(2.0, True)
|
| 1033 |
+
|
| 1034 |
+
# Make a shell
|
| 1035 |
+
result = p.faces(">Z").shell(0.3)
|
| 1036 |
+
|
| 1037 |
+
.. topic:: Api References
|
| 1038 |
+
|
| 1039 |
+
.. hlist::
|
| 1040 |
+
:columns: 2
|
| 1041 |
+
|
| 1042 |
+
* :py:meth:`Workplane.extrude`
|
| 1043 |
+
* :py:meth:`Workplane.mirrorX`
|
| 1044 |
+
* :py:meth:`Workplane.threePointArc`
|
| 1045 |
+
* :py:meth:`Workplane.workplane`
|
| 1046 |
+
* :py:meth:`Workplane.vertices`
|
| 1047 |
+
* :py:meth:`Workplane.vLine`
|
| 1048 |
+
* :py:meth:`Workplane.faces`
|
| 1049 |
+
* :py:meth:`Workplane`
|
| 1050 |
+
|
| 1051 |
+
A Parametric Enclosure
|
| 1052 |
+
-----------------------
|
| 1053 |
+
|
| 1054 |
+
.. cadquery::
|
| 1055 |
+
:height: 400px
|
| 1056 |
+
|
| 1057 |
+
# parameter definitions
|
| 1058 |
+
p_outerWidth = 100.0 # Outer width of box enclosure
|
| 1059 |
+
p_outerLength = 150.0 # Outer length of box enclosure
|
| 1060 |
+
p_outerHeight = 50.0 # Outer height of box enclosure
|
| 1061 |
+
|
| 1062 |
+
p_thickness = 3.0 # Thickness of the box walls
|
| 1063 |
+
p_sideRadius = 10.0 # Radius for the curves around the sides of the box
|
| 1064 |
+
p_topAndBottomRadius = (
|
| 1065 |
+
2.0 # Radius for the curves on the top and bottom edges of the box
|
| 1066 |
+
)
|
| 1067 |
+
|
| 1068 |
+
p_screwpostInset = 12.0 # How far in from the edges the screw posts should be place.
|
| 1069 |
+
p_screwpostID = 4.0 # Inner Diameter of the screw post holes, should be roughly screw diameter not including threads
|
| 1070 |
+
p_screwpostOD = 10.0 # Outer Diameter of the screw posts.\nDetermines overall thickness of the posts
|
| 1071 |
+
|
| 1072 |
+
p_boreDiameter = 8.0 # Diameter of the counterbore hole, if any
|
| 1073 |
+
p_boreDepth = 1.0 # Depth of the counterbore hole, if
|
| 1074 |
+
p_countersinkDiameter = 0.0 # Outer diameter of countersink. Should roughly match the outer diameter of the screw head
|
| 1075 |
+
p_countersinkAngle = 90.0 # Countersink angle (complete angle between opposite sides, not from center to one side)
|
| 1076 |
+
p_flipLid = True # Whether to place the lid with the top facing down or not.
|
| 1077 |
+
p_lipHeight = 1.0 # Height of lip on the underside of the lid.\nSits inside the box body for a snug fit.
|
| 1078 |
+
|
| 1079 |
+
# outer shell
|
| 1080 |
+
oshell = (
|
| 1081 |
+
cq.Workplane("XY")
|
| 1082 |
+
.rect(p_outerWidth, p_outerLength)
|
| 1083 |
+
.extrude(p_outerHeight + p_lipHeight)
|
| 1084 |
+
)
|
| 1085 |
+
|
| 1086 |
+
# weird geometry happens if we make the fillets in the wrong order
|
| 1087 |
+
if p_sideRadius > p_topAndBottomRadius:
|
| 1088 |
+
oshell = oshell.edges("|Z").fillet(p_sideRadius)
|
| 1089 |
+
oshell = oshell.edges("#Z").fillet(p_topAndBottomRadius)
|
| 1090 |
+
else:
|
| 1091 |
+
oshell = oshell.edges("#Z").fillet(p_topAndBottomRadius)
|
| 1092 |
+
oshell = oshell.edges("|Z").fillet(p_sideRadius)
|
| 1093 |
+
|
| 1094 |
+
# inner shell
|
| 1095 |
+
ishell = (
|
| 1096 |
+
oshell.faces("<Z")
|
| 1097 |
+
.workplane(p_thickness, True)
|
| 1098 |
+
.rect((p_outerWidth - 2.0 * p_thickness), (p_outerLength - 2.0 * p_thickness))
|
| 1099 |
+
.extrude(
|
| 1100 |
+
(p_outerHeight - 2.0 * p_thickness), False
|
| 1101 |
+
) # set combine false to produce just the new boss
|
| 1102 |
+
)
|
| 1103 |
+
ishell = ishell.edges("|Z").fillet(p_sideRadius - p_thickness)
|
| 1104 |
+
|
| 1105 |
+
# make the box outer box
|
| 1106 |
+
box = oshell.cut(ishell)
|
| 1107 |
+
|
| 1108 |
+
# make the screw posts
|
| 1109 |
+
POSTWIDTH = p_outerWidth - 2.0 * p_screwpostInset
|
| 1110 |
+
POSTLENGTH = p_outerLength - 2.0 * p_screwpostInset
|
| 1111 |
+
|
| 1112 |
+
box = (
|
| 1113 |
+
box.faces(">Z")
|
| 1114 |
+
.workplane(-p_thickness)
|
| 1115 |
+
.rect(POSTWIDTH, POSTLENGTH, forConstruction=True)
|
| 1116 |
+
.vertices()
|
| 1117 |
+
.circle(p_screwpostOD / 2.0)
|
| 1118 |
+
.circle(p_screwpostID / 2.0)
|
| 1119 |
+
.extrude(-1.0 * (p_outerHeight + p_lipHeight - p_thickness), True)
|
| 1120 |
+
)
|
| 1121 |
+
|
| 1122 |
+
# split lid into top and bottom parts
|
| 1123 |
+
(lid, bottom) = (
|
| 1124 |
+
box.faces(">Z")
|
| 1125 |
+
.workplane(-p_thickness - p_lipHeight)
|
| 1126 |
+
.split(keepTop=True, keepBottom=True)
|
| 1127 |
+
.all()
|
| 1128 |
+
) # splits into two solids
|
| 1129 |
+
|
| 1130 |
+
# translate the lid, and subtract the bottom from it to produce the lid inset
|
| 1131 |
+
lowerLid = lid.translate((0, 0, -p_lipHeight))
|
| 1132 |
+
cutlip = lowerLid.cut(bottom).translate(
|
| 1133 |
+
(p_outerWidth + p_thickness, 0, p_thickness - p_outerHeight + p_lipHeight)
|
| 1134 |
+
)
|
| 1135 |
+
|
| 1136 |
+
# compute centers for screw holes
|
| 1137 |
+
topOfLidCenters = (
|
| 1138 |
+
cutlip.faces(">Z")
|
| 1139 |
+
.workplane(centerOption="CenterOfMass")
|
| 1140 |
+
.rect(POSTWIDTH, POSTLENGTH, forConstruction=True)
|
| 1141 |
+
.vertices()
|
| 1142 |
+
)
|
| 1143 |
+
|
| 1144 |
+
# add holes of the desired type
|
| 1145 |
+
if p_boreDiameter > 0 and p_boreDepth > 0:
|
| 1146 |
+
topOfLid = topOfLidCenters.cboreHole(
|
| 1147 |
+
p_screwpostID, p_boreDiameter, p_boreDepth, 2.0 * p_thickness
|
| 1148 |
+
)
|
| 1149 |
+
elif p_countersinkDiameter > 0 and p_countersinkAngle > 0:
|
| 1150 |
+
topOfLid = topOfLidCenters.cskHole(
|
| 1151 |
+
p_screwpostID, p_countersinkDiameter, p_countersinkAngle, 2.0 * p_thickness
|
| 1152 |
+
)
|
| 1153 |
+
else:
|
| 1154 |
+
topOfLid = topOfLidCenters.hole(p_screwpostID, 2.0 * p_thickness)
|
| 1155 |
+
|
| 1156 |
+
# flip lid upside down if desired
|
| 1157 |
+
if p_flipLid:
|
| 1158 |
+
topOfLid = topOfLid.rotateAboutCenter((1, 0, 0), 180)
|
| 1159 |
+
|
| 1160 |
+
# return the combined result
|
| 1161 |
+
result = topOfLid.union(bottom)
|
| 1162 |
+
|
| 1163 |
+
|
| 1164 |
+
.. topic:: Api References
|
| 1165 |
+
|
| 1166 |
+
.. hlist::
|
| 1167 |
+
:columns: 3
|
| 1168 |
+
|
| 1169 |
+
* :py:meth:`Workplane.circle`
|
| 1170 |
+
* :py:meth:`Workplane.rect`
|
| 1171 |
+
* :py:meth:`Workplane.extrude`
|
| 1172 |
+
* :py:meth:`Workplane.box`
|
| 1173 |
+
* :py:meth:`Workplane.all`
|
| 1174 |
+
* :py:meth:`Workplane.faces`
|
| 1175 |
+
* :py:meth:`Workplane.vertices`
|
| 1176 |
+
* :py:meth:`Workplane.edges`
|
| 1177 |
+
* :py:meth:`Workplane.workplane`
|
| 1178 |
+
* :py:meth:`Workplane.fillet`
|
| 1179 |
+
* :py:meth:`Workplane.cut`
|
| 1180 |
+
* :py:meth:`Workplane.union`
|
| 1181 |
+
* :py:meth:`Workplane.rotateAboutCenter`
|
| 1182 |
+
* :py:meth:`Workplane.cboreHole`
|
| 1183 |
+
* :py:meth:`Workplane.cskHole`
|
| 1184 |
+
* :py:meth:`Workplane.hole`
|
| 1185 |
+
|
| 1186 |
+
Lego Brick
|
| 1187 |
+
-------------------
|
| 1188 |
+
|
| 1189 |
+
This script will produce any size regular rectangular Lego(TM) brick. Its only tricky because of the logic
|
| 1190 |
+
regarding the underside of the brick.
|
| 1191 |
+
|
| 1192 |
+
.. cadquery::
|
| 1193 |
+
:select: tmp
|
| 1194 |
+
:height: 400px
|
| 1195 |
+
|
| 1196 |
+
#####
|
| 1197 |
+
# Inputs
|
| 1198 |
+
######
|
| 1199 |
+
lbumps = 6 # number of bumps long
|
| 1200 |
+
wbumps = 2 # number of bumps wide
|
| 1201 |
+
thin = True # True for thin, False for thick
|
| 1202 |
+
|
| 1203 |
+
#
|
| 1204 |
+
# Lego Brick Constants-- these make a Lego brick a Lego :)
|
| 1205 |
+
#
|
| 1206 |
+
pitch = 8.0
|
| 1207 |
+
clearance = 0.1
|
| 1208 |
+
bumpDiam = 4.8
|
| 1209 |
+
bumpHeight = 1.8
|
| 1210 |
+
if thin:
|
| 1211 |
+
height = 3.2
|
| 1212 |
+
else:
|
| 1213 |
+
height = 9.6
|
| 1214 |
+
|
| 1215 |
+
t = (pitch - (2 * clearance) - bumpDiam) / 2.0
|
| 1216 |
+
postDiam = pitch - t # works out to 6.5
|
| 1217 |
+
total_length = lbumps * pitch - 2.0 * clearance
|
| 1218 |
+
total_width = wbumps * pitch - 2.0 * clearance
|
| 1219 |
+
|
| 1220 |
+
# make the base
|
| 1221 |
+
s = cq.Workplane("XY").box(total_length, total_width, height)
|
| 1222 |
+
|
| 1223 |
+
# shell inwards not outwards
|
| 1224 |
+
s = s.faces("<Z").shell(-1.0 * t)
|
| 1225 |
+
|
| 1226 |
+
# make the bumps on the top
|
| 1227 |
+
s = (
|
| 1228 |
+
s.faces(">Z")
|
| 1229 |
+
.workplane()
|
| 1230 |
+
.rarray(pitch, pitch, lbumps, wbumps, True)
|
| 1231 |
+
.circle(bumpDiam / 2.0)
|
| 1232 |
+
.extrude(bumpHeight)
|
| 1233 |
+
)
|
| 1234 |
+
|
| 1235 |
+
# add posts on the bottom. posts are different diameter depending on geometry
|
| 1236 |
+
# solid studs for 1 bump, tubes for multiple, none for 1x1
|
| 1237 |
+
tmp = s.faces("<Z").workplane(invert=True)
|
| 1238 |
+
|
| 1239 |
+
if lbumps > 1 and wbumps > 1:
|
| 1240 |
+
tmp = (
|
| 1241 |
+
tmp.rarray(pitch, pitch, lbumps - 1, wbumps - 1, center=True)
|
| 1242 |
+
.circle(postDiam / 2.0)
|
| 1243 |
+
.circle(bumpDiam / 2.0)
|
| 1244 |
+
.extrude(height - t)
|
| 1245 |
+
)
|
| 1246 |
+
elif lbumps > 1:
|
| 1247 |
+
tmp = (
|
| 1248 |
+
tmp.rarray(pitch, pitch, lbumps - 1, 1, center=True)
|
| 1249 |
+
.circle(t)
|
| 1250 |
+
.extrude(height - t)
|
| 1251 |
+
)
|
| 1252 |
+
elif wbumps > 1:
|
| 1253 |
+
tmp = (
|
| 1254 |
+
tmp.rarray(pitch, pitch, 1, wbumps - 1, center=True)
|
| 1255 |
+
.circle(t)
|
| 1256 |
+
.extrude(height - t)
|
| 1257 |
+
)
|
| 1258 |
+
else:
|
| 1259 |
+
tmp = s
|
| 1260 |
+
|
| 1261 |
+
|
| 1262 |
+
Braille Example
|
| 1263 |
+
---------------------
|
| 1264 |
+
|
| 1265 |
+
.. cadquery::
|
| 1266 |
+
:height: 400px
|
| 1267 |
+
|
| 1268 |
+
from collections import namedtuple
|
| 1269 |
+
|
| 1270 |
+
|
| 1271 |
+
# text_lines is a list of text lines.
|
| 1272 |
+
# Braille (converted with braille-converter:
|
| 1273 |
+
# https://github.com/jpaugh/braille-converter.git).
|
| 1274 |
+
text_lines = ["⠠ ⠋ ⠗ ⠑ ⠑ ⠠ ⠉ ⠠ ⠁ ⠠ ⠙"]
|
| 1275 |
+
# See http://www.tiresias.org/research/reports/braille_cell.htm for examples
|
| 1276 |
+
# of braille cell geometry.
|
| 1277 |
+
horizontal_interdot = 2.5
|
| 1278 |
+
vertical_interdot = 2.5
|
| 1279 |
+
horizontal_intercell = 6
|
| 1280 |
+
vertical_interline = 10
|
| 1281 |
+
dot_height = 0.5
|
| 1282 |
+
dot_diameter = 1.3
|
| 1283 |
+
|
| 1284 |
+
base_thickness = 1.5
|
| 1285 |
+
|
| 1286 |
+
# End of configuration.
|
| 1287 |
+
BrailleCellGeometry = namedtuple(
|
| 1288 |
+
"BrailleCellGeometry",
|
| 1289 |
+
(
|
| 1290 |
+
"horizontal_interdot",
|
| 1291 |
+
"vertical_interdot",
|
| 1292 |
+
"intercell",
|
| 1293 |
+
"interline",
|
| 1294 |
+
"dot_height",
|
| 1295 |
+
"dot_diameter",
|
| 1296 |
+
),
|
| 1297 |
+
)
|
| 1298 |
+
|
| 1299 |
+
|
| 1300 |
+
class Point(object):
|
| 1301 |
+
def __init__(self, x, y):
|
| 1302 |
+
self.x = x
|
| 1303 |
+
self.y = y
|
| 1304 |
+
|
| 1305 |
+
def __add__(self, other):
|
| 1306 |
+
return Point(self.x + other.x, self.y + other.y)
|
| 1307 |
+
|
| 1308 |
+
def __len__(self):
|
| 1309 |
+
return 2
|
| 1310 |
+
|
| 1311 |
+
def __getitem__(self, index):
|
| 1312 |
+
return (self.x, self.y)[index]
|
| 1313 |
+
|
| 1314 |
+
def __str__(self):
|
| 1315 |
+
return "({}, {})".format(self.x, self.y)
|
| 1316 |
+
|
| 1317 |
+
|
| 1318 |
+
def brailleToPoints(text, cell_geometry):
|
| 1319 |
+
# Unicode bit pattern (cf. https://en.wikipedia.org/wiki/Braille_Patterns).
|
| 1320 |
+
mask1 = 0b00000001
|
| 1321 |
+
mask2 = 0b00000010
|
| 1322 |
+
mask3 = 0b00000100
|
| 1323 |
+
mask4 = 0b00001000
|
| 1324 |
+
mask5 = 0b00010000
|
| 1325 |
+
mask6 = 0b00100000
|
| 1326 |
+
mask7 = 0b01000000
|
| 1327 |
+
mask8 = 0b10000000
|
| 1328 |
+
masks = (mask1, mask2, mask3, mask4, mask5, mask6, mask7, mask8)
|
| 1329 |
+
|
| 1330 |
+
# Corresponding dot position
|
| 1331 |
+
w = cell_geometry.horizontal_interdot
|
| 1332 |
+
h = cell_geometry.vertical_interdot
|
| 1333 |
+
pos1 = Point(0, 2 * h)
|
| 1334 |
+
pos2 = Point(0, h)
|
| 1335 |
+
pos3 = Point(0, 0)
|
| 1336 |
+
pos4 = Point(w, 2 * h)
|
| 1337 |
+
pos5 = Point(w, h)
|
| 1338 |
+
pos6 = Point(w, 0)
|
| 1339 |
+
pos7 = Point(0, -h)
|
| 1340 |
+
pos8 = Point(w, -h)
|
| 1341 |
+
pos = (pos1, pos2, pos3, pos4, pos5, pos6, pos7, pos8)
|
| 1342 |
+
|
| 1343 |
+
# Braille blank pattern (u'\u2800').
|
| 1344 |
+
blank = "⠀"
|
| 1345 |
+
points = []
|
| 1346 |
+
# Position of dot1 along the x-axis (horizontal).
|
| 1347 |
+
character_origin = 0
|
| 1348 |
+
for c in text:
|
| 1349 |
+
for m, p in zip(masks, pos):
|
| 1350 |
+
delta_to_blank = ord(c) - ord(blank)
|
| 1351 |
+
if m & delta_to_blank:
|
| 1352 |
+
points.append(p + Point(character_origin, 0))
|
| 1353 |
+
character_origin += cell_geometry.intercell
|
| 1354 |
+
return points
|
| 1355 |
+
|
| 1356 |
+
|
| 1357 |
+
def get_plate_height(text_lines, cell_geometry):
|
| 1358 |
+
# cell_geometry.vertical_interdot is also used as space between base
|
| 1359 |
+
# borders and characters.
|
| 1360 |
+
return (
|
| 1361 |
+
2 * cell_geometry.vertical_interdot
|
| 1362 |
+
+ 2 * cell_geometry.vertical_interdot
|
| 1363 |
+
+ (len(text_lines) - 1) * cell_geometry.interline
|
| 1364 |
+
)
|
| 1365 |
+
|
| 1366 |
+
|
| 1367 |
+
def get_plate_width(text_lines, cell_geometry):
|
| 1368 |
+
# cell_geometry.horizontal_interdot is also used as space between base
|
| 1369 |
+
# borders and characters.
|
| 1370 |
+
max_len = max([len(t) for t in text_lines])
|
| 1371 |
+
return (
|
| 1372 |
+
2 * cell_geometry.horizontal_interdot
|
| 1373 |
+
+ cell_geometry.horizontal_interdot
|
| 1374 |
+
+ (max_len - 1) * cell_geometry.intercell
|
| 1375 |
+
)
|
| 1376 |
+
|
| 1377 |
+
|
| 1378 |
+
def get_cylinder_radius(cell_geometry):
|
| 1379 |
+
"""Return the radius the cylinder should have
|
| 1380 |
+
The cylinder have the same radius as the half-sphere make the dots (the
|
| 1381 |
+
hidden and the shown part of the dots).
|
| 1382 |
+
The radius is such that the spherical cap with diameter
|
| 1383 |
+
cell_geometry.dot_diameter has a height of cell_geometry.dot_height.
|
| 1384 |
+
"""
|
| 1385 |
+
h = cell_geometry.dot_height
|
| 1386 |
+
r = cell_geometry.dot_diameter / 2
|
| 1387 |
+
return (r**2 + h**2) / 2 / h
|
| 1388 |
+
|
| 1389 |
+
|
| 1390 |
+
def get_base_plate_thickness(plate_thickness, cell_geometry):
|
| 1391 |
+
"""Return the height on which the half spheres will sit"""
|
| 1392 |
+
return (
|
| 1393 |
+
plate_thickness + get_cylinder_radius(cell_geometry) - cell_geometry.dot_height
|
| 1394 |
+
)
|
| 1395 |
+
|
| 1396 |
+
|
| 1397 |
+
def make_base(text_lines, cell_geometry, plate_thickness):
|
| 1398 |
+
base_width = get_plate_width(text_lines, cell_geometry)
|
| 1399 |
+
base_height = get_plate_height(text_lines, cell_geometry)
|
| 1400 |
+
base_thickness = get_base_plate_thickness(plate_thickness, cell_geometry)
|
| 1401 |
+
base = cq.Workplane("XY").box(
|
| 1402 |
+
base_width, base_height, base_thickness, centered=False
|
| 1403 |
+
)
|
| 1404 |
+
return base
|
| 1405 |
+
|
| 1406 |
+
|
| 1407 |
+
def make_embossed_plate(text_lines, cell_geometry):
|
| 1408 |
+
"""Make an embossed plate with dots as spherical caps
|
| 1409 |
+
Method:
|
| 1410 |
+
- make a thin plate on which sit cylinders
|
| 1411 |
+
- fillet the upper edge of the cylinders so to get pseudo half-spheres
|
| 1412 |
+
- make the union with a thicker plate so that only the sphere caps stay
|
| 1413 |
+
"visible".
|
| 1414 |
+
"""
|
| 1415 |
+
base = make_base(text_lines, cell_geometry, base_thickness)
|
| 1416 |
+
|
| 1417 |
+
dot_pos = []
|
| 1418 |
+
base_width = get_plate_width(text_lines, cell_geometry)
|
| 1419 |
+
base_height = get_plate_height(text_lines, cell_geometry)
|
| 1420 |
+
y = base_height - 3 * cell_geometry.vertical_interdot
|
| 1421 |
+
line_start_pos = Point(cell_geometry.horizontal_interdot, y)
|
| 1422 |
+
for text in text_lines:
|
| 1423 |
+
dots = brailleToPoints(text, cell_geometry)
|
| 1424 |
+
dots = [p + line_start_pos for p in dots]
|
| 1425 |
+
dot_pos += dots
|
| 1426 |
+
line_start_pos += Point(0, -cell_geometry.interline)
|
| 1427 |
+
|
| 1428 |
+
r = get_cylinder_radius(cell_geometry)
|
| 1429 |
+
base = (
|
| 1430 |
+
base.faces(">Z")
|
| 1431 |
+
.vertices("<XY")
|
| 1432 |
+
.workplane()
|
| 1433 |
+
.pushPoints(dot_pos)
|
| 1434 |
+
.circle(r)
|
| 1435 |
+
.extrude(r)
|
| 1436 |
+
)
|
| 1437 |
+
# Make a fillet almost the same radius to get a pseudo spherical cap.
|
| 1438 |
+
base = base.faces(">Z").edges().fillet(r - 0.001)
|
| 1439 |
+
hidding_box = cq.Workplane("XY").box(
|
| 1440 |
+
base_width, base_height, base_thickness, centered=False
|
| 1441 |
+
)
|
| 1442 |
+
result = hidding_box.union(base)
|
| 1443 |
+
return result
|
| 1444 |
+
|
| 1445 |
+
|
| 1446 |
+
_cell_geometry = BrailleCellGeometry(
|
| 1447 |
+
horizontal_interdot,
|
| 1448 |
+
vertical_interdot,
|
| 1449 |
+
horizontal_intercell,
|
| 1450 |
+
vertical_interline,
|
| 1451 |
+
dot_height,
|
| 1452 |
+
dot_diameter,
|
| 1453 |
+
)
|
| 1454 |
+
|
| 1455 |
+
if base_thickness < get_cylinder_radius(_cell_geometry):
|
| 1456 |
+
raise ValueError("Base thickness should be at least {}".format(dot_height))
|
| 1457 |
+
|
| 1458 |
+
result = make_embossed_plate(text_lines, _cell_geometry)
|
| 1459 |
+
|
| 1460 |
+
Panel With Various Connector Holes
|
| 1461 |
+
-----------------------------------
|
| 1462 |
+
|
| 1463 |
+
.. cadquery::
|
| 1464 |
+
:height: 400px
|
| 1465 |
+
|
| 1466 |
+
# The dimensions of the model. These can be modified rather than changing the
|
| 1467 |
+
# object's code directly.
|
| 1468 |
+
width = 400
|
| 1469 |
+
height = 500
|
| 1470 |
+
thickness = 2
|
| 1471 |
+
|
| 1472 |
+
# Create a plate with two polygons cut through it
|
| 1473 |
+
result = cq.Workplane("front").box(width, height, thickness)
|
| 1474 |
+
|
| 1475 |
+
h_sep = 60
|
| 1476 |
+
for idx in range(4):
|
| 1477 |
+
result = (
|
| 1478 |
+
result.workplane(offset=1, centerOption="CenterOfBoundBox")
|
| 1479 |
+
.center(157, 210 - idx * h_sep)
|
| 1480 |
+
.moveTo(-23.5, 0)
|
| 1481 |
+
.circle(1.6)
|
| 1482 |
+
.moveTo(23.5, 0)
|
| 1483 |
+
.circle(1.6)
|
| 1484 |
+
.moveTo(-17.038896, -5.7)
|
| 1485 |
+
.threePointArc((-19.44306, -4.70416), (-20.438896, -2.3))
|
| 1486 |
+
.lineTo(-21.25, 2.3)
|
| 1487 |
+
.threePointArc((-20.25416, 4.70416), (-17.85, 5.7))
|
| 1488 |
+
.lineTo(17.85, 5.7)
|
| 1489 |
+
.threePointArc((20.25416, 4.70416), (21.25, 2.3))
|
| 1490 |
+
.lineTo(20.438896, -2.3)
|
| 1491 |
+
.threePointArc((19.44306, -4.70416), (17.038896, -5.7))
|
| 1492 |
+
.close()
|
| 1493 |
+
.cutThruAll()
|
| 1494 |
+
)
|
| 1495 |
+
|
| 1496 |
+
for idx in range(4):
|
| 1497 |
+
result = (
|
| 1498 |
+
result.workplane(offset=1, centerOption="CenterOfBoundBox")
|
| 1499 |
+
.center(157, -30 - idx * h_sep)
|
| 1500 |
+
.moveTo(-16.65, 0)
|
| 1501 |
+
.circle(1.6)
|
| 1502 |
+
.moveTo(16.65, 0)
|
| 1503 |
+
.circle(1.6)
|
| 1504 |
+
.moveTo(-10.1889, -5.7)
|
| 1505 |
+
.threePointArc((-12.59306, -4.70416), (-13.5889, -2.3))
|
| 1506 |
+
.lineTo(-14.4, 2.3)
|
| 1507 |
+
.threePointArc((-13.40416, 4.70416), (-11, 5.7))
|
| 1508 |
+
.lineTo(11, 5.7)
|
| 1509 |
+
.threePointArc((13.40416, 4.70416), (14.4, 2.3))
|
| 1510 |
+
.lineTo(13.5889, -2.3)
|
| 1511 |
+
.threePointArc((12.59306, -4.70416), (10.1889, -5.7))
|
| 1512 |
+
.close()
|
| 1513 |
+
.cutThruAll()
|
| 1514 |
+
)
|
| 1515 |
+
|
| 1516 |
+
h_sep4DB9 = 30
|
| 1517 |
+
for idx in range(8):
|
| 1518 |
+
result = (
|
| 1519 |
+
result.workplane(offset=1, centerOption="CenterOfBoundBox")
|
| 1520 |
+
.center(91, 225 - idx * h_sep4DB9)
|
| 1521 |
+
.moveTo(-12.5, 0)
|
| 1522 |
+
.circle(1.6)
|
| 1523 |
+
.moveTo(12.5, 0)
|
| 1524 |
+
.circle(1.6)
|
| 1525 |
+
.moveTo(-6.038896, -5.7)
|
| 1526 |
+
.threePointArc((-8.44306, -4.70416), (-9.438896, -2.3))
|
| 1527 |
+
.lineTo(-10.25, 2.3)
|
| 1528 |
+
.threePointArc((-9.25416, 4.70416), (-6.85, 5.7))
|
| 1529 |
+
.lineTo(6.85, 5.7)
|
| 1530 |
+
.threePointArc((9.25416, 4.70416), (10.25, 2.3))
|
| 1531 |
+
.lineTo(9.438896, -2.3)
|
| 1532 |
+
.threePointArc((8.44306, -4.70416), (6.038896, -5.7))
|
| 1533 |
+
.close()
|
| 1534 |
+
.cutThruAll()
|
| 1535 |
+
)
|
| 1536 |
+
|
| 1537 |
+
for idx in range(4):
|
| 1538 |
+
result = (
|
| 1539 |
+
result.workplane(offset=1, centerOption="CenterOfBoundBox")
|
| 1540 |
+
.center(25, 210 - idx * h_sep)
|
| 1541 |
+
.moveTo(-23.5, 0)
|
| 1542 |
+
.circle(1.6)
|
| 1543 |
+
.moveTo(23.5, 0)
|
| 1544 |
+
.circle(1.6)
|
| 1545 |
+
.moveTo(-17.038896, -5.7)
|
| 1546 |
+
.threePointArc((-19.44306, -4.70416), (-20.438896, -2.3))
|
| 1547 |
+
.lineTo(-21.25, 2.3)
|
| 1548 |
+
.threePointArc((-20.25416, 4.70416), (-17.85, 5.7))
|
| 1549 |
+
.lineTo(17.85, 5.7)
|
| 1550 |
+
.threePointArc((20.25416, 4.70416), (21.25, 2.3))
|
| 1551 |
+
.lineTo(20.438896, -2.3)
|
| 1552 |
+
.threePointArc((19.44306, -4.70416), (17.038896, -5.7))
|
| 1553 |
+
.close()
|
| 1554 |
+
.cutThruAll()
|
| 1555 |
+
)
|
| 1556 |
+
|
| 1557 |
+
for idx in range(4):
|
| 1558 |
+
result = (
|
| 1559 |
+
result.workplane(offset=1, centerOption="CenterOfBoundBox")
|
| 1560 |
+
.center(25, -30 - idx * h_sep)
|
| 1561 |
+
.moveTo(-16.65, 0)
|
| 1562 |
+
.circle(1.6)
|
| 1563 |
+
.moveTo(16.65, 0)
|
| 1564 |
+
.circle(1.6)
|
| 1565 |
+
.moveTo(-10.1889, -5.7)
|
| 1566 |
+
.threePointArc((-12.59306, -4.70416), (-13.5889, -2.3))
|
| 1567 |
+
.lineTo(-14.4, 2.3)
|
| 1568 |
+
.threePointArc((-13.40416, 4.70416), (-11, 5.7))
|
| 1569 |
+
.lineTo(11, 5.7)
|
| 1570 |
+
.threePointArc((13.40416, 4.70416), (14.4, 2.3))
|
| 1571 |
+
.lineTo(13.5889, -2.3)
|
| 1572 |
+
.threePointArc((12.59306, -4.70416), (10.1889, -5.7))
|
| 1573 |
+
.close()
|
| 1574 |
+
.cutThruAll()
|
| 1575 |
+
)
|
| 1576 |
+
|
| 1577 |
+
for idx in range(8):
|
| 1578 |
+
result = (
|
| 1579 |
+
result.workplane(offset=1, centerOption="CenterOfBoundBox")
|
| 1580 |
+
.center(-41, 225 - idx * h_sep4DB9)
|
| 1581 |
+
.moveTo(-12.5, 0)
|
| 1582 |
+
.circle(1.6)
|
| 1583 |
+
.moveTo(12.5, 0)
|
| 1584 |
+
.circle(1.6)
|
| 1585 |
+
.moveTo(-6.038896, -5.7)
|
| 1586 |
+
.threePointArc((-8.44306, -4.70416), (-9.438896, -2.3))
|
| 1587 |
+
.lineTo(-10.25, 2.3)
|
| 1588 |
+
.threePointArc((-9.25416, 4.70416), (-6.85, 5.7))
|
| 1589 |
+
.lineTo(6.85, 5.7)
|
| 1590 |
+
.threePointArc((9.25416, 4.70416), (10.25, 2.3))
|
| 1591 |
+
.lineTo(9.438896, -2.3)
|
| 1592 |
+
.threePointArc((8.44306, -4.70416), (6.038896, -5.7))
|
| 1593 |
+
.close()
|
| 1594 |
+
.cutThruAll()
|
| 1595 |
+
)
|
| 1596 |
+
|
| 1597 |
+
for idx in range(4):
|
| 1598 |
+
result = (
|
| 1599 |
+
result.workplane(offset=1, centerOption="CenterOfBoundBox")
|
| 1600 |
+
.center(-107, 210 - idx * h_sep)
|
| 1601 |
+
.moveTo(-23.5, 0)
|
| 1602 |
+
.circle(1.6)
|
| 1603 |
+
.moveTo(23.5, 0)
|
| 1604 |
+
.circle(1.6)
|
| 1605 |
+
.moveTo(-17.038896, -5.7)
|
| 1606 |
+
.threePointArc((-19.44306, -4.70416), (-20.438896, -2.3))
|
| 1607 |
+
.lineTo(-21.25, 2.3)
|
| 1608 |
+
.threePointArc((-20.25416, 4.70416), (-17.85, 5.7))
|
| 1609 |
+
.lineTo(17.85, 5.7)
|
| 1610 |
+
.threePointArc((20.25416, 4.70416), (21.25, 2.3))
|
| 1611 |
+
.lineTo(20.438896, -2.3)
|
| 1612 |
+
.threePointArc((19.44306, -4.70416), (17.038896, -5.7))
|
| 1613 |
+
.close()
|
| 1614 |
+
.cutThruAll()
|
| 1615 |
+
)
|
| 1616 |
+
|
| 1617 |
+
for idx in range(4):
|
| 1618 |
+
result = (
|
| 1619 |
+
result.workplane(offset=1, centerOption="CenterOfBoundBox")
|
| 1620 |
+
.center(-107, -30 - idx * h_sep)
|
| 1621 |
+
.circle(14)
|
| 1622 |
+
.rect(24.7487, 24.7487, forConstruction=True)
|
| 1623 |
+
.vertices()
|
| 1624 |
+
.hole(3.2)
|
| 1625 |
+
.cutThruAll()
|
| 1626 |
+
)
|
| 1627 |
+
|
| 1628 |
+
for idx in range(8):
|
| 1629 |
+
result = (
|
| 1630 |
+
result.workplane(offset=1, centerOption="CenterOfBoundBox")
|
| 1631 |
+
.center(-173, 225 - idx * h_sep4DB9)
|
| 1632 |
+
.moveTo(-12.5, 0)
|
| 1633 |
+
.circle(1.6)
|
| 1634 |
+
.moveTo(12.5, 0)
|
| 1635 |
+
.circle(1.6)
|
| 1636 |
+
.moveTo(-6.038896, -5.7)
|
| 1637 |
+
.threePointArc((-8.44306, -4.70416), (-9.438896, -2.3))
|
| 1638 |
+
.lineTo(-10.25, 2.3)
|
| 1639 |
+
.threePointArc((-9.25416, 4.70416), (-6.85, 5.7))
|
| 1640 |
+
.lineTo(6.85, 5.7)
|
| 1641 |
+
.threePointArc((9.25416, 4.70416), (10.25, 2.3))
|
| 1642 |
+
.lineTo(9.438896, -2.3)
|
| 1643 |
+
.threePointArc((8.44306, -4.70416), (6.038896, -5.7))
|
| 1644 |
+
.close()
|
| 1645 |
+
.cutThruAll()
|
| 1646 |
+
)
|
| 1647 |
+
|
| 1648 |
+
for idx in range(4):
|
| 1649 |
+
result = (
|
| 1650 |
+
result.workplane(offset=1, centerOption="CenterOfBoundBox")
|
| 1651 |
+
.center(-173, -30 - idx * h_sep)
|
| 1652 |
+
.moveTo(-2.9176, -5.3)
|
| 1653 |
+
.threePointArc((-6.05, 0), (-2.9176, 5.3))
|
| 1654 |
+
.lineTo(2.9176, 5.3)
|
| 1655 |
+
.threePointArc((6.05, 0), (2.9176, -5.3))
|
| 1656 |
+
.close()
|
| 1657 |
+
.cutThruAll()
|
| 1658 |
+
)
|
| 1659 |
+
|
| 1660 |
+
|
| 1661 |
+
Cycloidal gear
|
| 1662 |
+
--------------
|
| 1663 |
+
|
| 1664 |
+
You can define complex geometries using the parametricCurve functionality.
|
| 1665 |
+
This specific examples generates a helical cycloidal gear.
|
| 1666 |
+
|
| 1667 |
+
.. cadquery::
|
| 1668 |
+
:height: 400px
|
| 1669 |
+
|
| 1670 |
+
import cadquery as cq
|
| 1671 |
+
from math import sin, cos, pi, floor
|
| 1672 |
+
|
| 1673 |
+
|
| 1674 |
+
# define the generating function
|
| 1675 |
+
def hypocycloid(t, r1, r2):
|
| 1676 |
+
return (
|
| 1677 |
+
(r1 - r2) * cos(t) + r2 * cos(r1 / r2 * t - t),
|
| 1678 |
+
(r1 - r2) * sin(t) + r2 * sin(-(r1 / r2 * t - t)),
|
| 1679 |
+
)
|
| 1680 |
+
|
| 1681 |
+
|
| 1682 |
+
def epicycloid(t, r1, r2):
|
| 1683 |
+
return (
|
| 1684 |
+
(r1 + r2) * cos(t) - r2 * cos(r1 / r2 * t + t),
|
| 1685 |
+
(r1 + r2) * sin(t) - r2 * sin(r1 / r2 * t + t),
|
| 1686 |
+
)
|
| 1687 |
+
|
| 1688 |
+
|
| 1689 |
+
def gear(t, r1=4, r2=1):
|
| 1690 |
+
if (-1) ** (1 + floor(t / 2 / pi * (r1 / r2))) < 0:
|
| 1691 |
+
return epicycloid(t, r1, r2)
|
| 1692 |
+
else:
|
| 1693 |
+
return hypocycloid(t, r1, r2)
|
| 1694 |
+
|
| 1695 |
+
|
| 1696 |
+
# create the gear profile and extrude it
|
| 1697 |
+
result = (
|
| 1698 |
+
cq.Workplane("XY")
|
| 1699 |
+
.parametricCurve(lambda t: gear(t * 2 * pi, 6, 1))
|
| 1700 |
+
.twistExtrude(15, 90)
|
| 1701 |
+
.faces(">Z")
|
| 1702 |
+
.workplane()
|
| 1703 |
+
.circle(2)
|
| 1704 |
+
.cutThruAll()
|
| 1705 |
+
)
|
server/docs/reference/extending.rst
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.. _extending:
|
| 2 |
+
|
| 3 |
+
Extending CadQuery
|
| 4 |
+
======================
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
If you find that CadQuery does not suit your needs, you can easily extend it. CadQuery provides several extension
|
| 8 |
+
methods:
|
| 9 |
+
|
| 10 |
+
* You can load plugins others have developed. This is by far the easiest way to access other code
|
| 11 |
+
* You can define your own plugins.
|
| 12 |
+
* You can use OCP scripting directly
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
Using OpenCascade methods
|
| 16 |
+
-------------------------
|
| 17 |
+
|
| 18 |
+
The easiest way to extend CadQuery is to simply use OpenCascade/OCP scripting inside of your build method. Just about
|
| 19 |
+
any valid OCP script will execute just fine. For example, this simple CadQuery script::
|
| 20 |
+
|
| 21 |
+
return cq.Workplane("XY").box(1.0, 2.0, 3.0).val()
|
| 22 |
+
|
| 23 |
+
is actually equivalent to::
|
| 24 |
+
|
| 25 |
+
from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox
|
| 26 |
+
from OCP.gp import gp_Ax2, gp_Dir, gp_Pnt
|
| 27 |
+
|
| 28 |
+
return cq.Shape.cast(
|
| 29 |
+
BRepPrimAPI_MakeBox(
|
| 30 |
+
gp_Ax2(gp_Pnt(-0.5, -1.0, -1.5), gp_Dir(0, 0, 1)), 1.0, 2.0, 3.0
|
| 31 |
+
).Shape()
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
As long as you return a valid OCP Shape, you can use any OCP methods you like. You can even mix and match the
|
| 35 |
+
two. For example, consider this script, which creates a OCP box, but then uses CadQuery to select its faces::
|
| 36 |
+
|
| 37 |
+
box1 = cq.Shape.cast(
|
| 38 |
+
BRepPrimAPI_MakeBox(
|
| 39 |
+
gp_Ax2(gp_Pnt(-0.5, -1.0, -1.5), gp_Dir(0, 0, 1)), 1.0, 2.0, 3.0
|
| 40 |
+
).Shape()
|
| 41 |
+
)
|
| 42 |
+
return box1.faces(">X").Area() # return 6.0
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
Extending CadQuery: Plugins
|
| 46 |
+
----------------------------
|
| 47 |
+
|
| 48 |
+
Though you can get a lot done with OpenCascade, the code gets pretty nasty in a hurry. CadQuery shields you from
|
| 49 |
+
a lot of the complexity of the OpenCascade API.
|
| 50 |
+
|
| 51 |
+
You can get the best of both worlds by wrapping your OCP script into a CadQuery plugin.
|
| 52 |
+
|
| 53 |
+
A CadQuery plugin is simply a function that is attached to the CadQuery :py:meth:`cadquery.CQ` or :py:meth:`cadquery.Workplane` class.
|
| 54 |
+
When connected, your plugin can be used in the chain just like the built-in functions.
|
| 55 |
+
|
| 56 |
+
There are a few key concepts important to understand when building a plugin
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
The Stack
|
| 60 |
+
-------------------
|
| 61 |
+
|
| 62 |
+
Every CadQuery object has a local stack, which contains a list of items. The items on the stack will be
|
| 63 |
+
one of these types:
|
| 64 |
+
|
| 65 |
+
* **A CadQuery SolidReference object**, which holds a reference to a OCP solid
|
| 66 |
+
* **A OCP object**, a Vertex, Edge, Wire, Face, Shell, Solid, or Compound
|
| 67 |
+
|
| 68 |
+
The stack is available by using self.objects, and will always contain at least one object.
|
| 69 |
+
|
| 70 |
+
.. note::
|
| 71 |
+
|
| 72 |
+
Objects and points on the stack are **always** in global coordinates. Similarly, any objects you
|
| 73 |
+
create must be created in terms of global coordinates as well!
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
Preserving the Chain
|
| 77 |
+
-----------------------
|
| 78 |
+
|
| 79 |
+
CadQuery's fluent API relies on the ability to chain calls together one after another. For this to work,
|
| 80 |
+
you must return a valid CadQuery object as a return value. If you choose not to return a CadQuery object,
|
| 81 |
+
then your plugin will end the chain. Sometimes this is desired for example :py:meth:`cadquery.Workplane.size`
|
| 82 |
+
|
| 83 |
+
There are two ways you can safely continue the chain:
|
| 84 |
+
|
| 85 |
+
1. **return self** If you simply wish to modify the stack contents, you can simply return a reference to
|
| 86 |
+
self. This approach is destructive, because the contents of the stack are modified, but it is also the
|
| 87 |
+
simplest.
|
| 88 |
+
2. :py:meth:`cadquery.Workplane.newObject` Most of the time, you will want to return a new object. Using newObject will
|
| 89 |
+
return a new CQ or Workplane object having the stack you specify, and will link this object to the
|
| 90 |
+
previous one. This preserves the original object and its stack.
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
Helper Methods
|
| 94 |
+
-----------------------
|
| 95 |
+
|
| 96 |
+
When you implement a CadQuery plugin, you are extending CadQuery's base objects. As a result, you can call any
|
| 97 |
+
CadQuery or Workplane methods from inside of your extension. You can also call a number of internal methods that
|
| 98 |
+
are designed to aid in plugin creation:
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
* :py:meth:`cadquery.Workplane._makeWireAtPoints` will invoke a factory function you supply for all points on the stack,
|
| 102 |
+
and return a properly constructed cadquery object. This function takes care of registering wires for you
|
| 103 |
+
and everything like that
|
| 104 |
+
|
| 105 |
+
* :py:meth:`cadquery.Workplane.newObject` returns a new Workplane object with the provided stack, and with its parent set
|
| 106 |
+
to the current object. The preferred way to continue the chain
|
| 107 |
+
|
| 108 |
+
* :py:meth:`cadquery.Workplane.findSolid` returns the first Solid found in the chain, working from the current object upwards
|
| 109 |
+
in the chain. commonly used when your plugin will modify an existing solid, or needs to create objects and
|
| 110 |
+
then combine them onto the 'main' part that is in progress
|
| 111 |
+
|
| 112 |
+
* :py:meth:`cadquery.Workplane._addPendingWire` must be called if you add a wire. This allows the base class to track all the wires
|
| 113 |
+
that are created, so that they can be managed when extrusion occurs.
|
| 114 |
+
|
| 115 |
+
* :py:meth:`cadquery.Workplane.wire` gathers up all of the edges that have been drawn ( eg, by line, vline, etc ), and
|
| 116 |
+
attempts to combine them into a single wire, which is returned. This should be used when your plugin creates
|
| 117 |
+
2D edges, and you know it is time to collect them into a single wire.
|
| 118 |
+
|
| 119 |
+
* :py:meth:`cadquery.Workplane.plane` provides a reference to the workplane, which allows you to convert between workplane
|
| 120 |
+
coordinates and global coordinates:
|
| 121 |
+
* :py:meth:`cadquery.occ_impl.geom.Plane.toWorldCoords` will convert local coordinates to global ones
|
| 122 |
+
* :py:meth:`cadquery.occ_impl.geom.Plane.toLocalCoords` will convert from global coordinates to local coordinates
|
| 123 |
+
|
| 124 |
+
Coordinate Systems
|
| 125 |
+
-----------------------
|
| 126 |
+
|
| 127 |
+
Keep in mind that the user may be using a work plane that has created a local coordinate system. Consequently,
|
| 128 |
+
the orientation of shapes that you create are often implicitly defined by the user's workplane.
|
| 129 |
+
|
| 130 |
+
Any objects that you create must be fully defined in *global coordinates*, even though some or all of the users'
|
| 131 |
+
inputs may be defined in terms of local coordinates.
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
Linking in your plugin
|
| 135 |
+
-----------------------
|
| 136 |
+
|
| 137 |
+
Your plugin is a single method, which is attached to the main Workplane or CadQuery object.
|
| 138 |
+
|
| 139 |
+
Your plugin method's first parameter should be 'self', which will provide a reference to base class functionality.
|
| 140 |
+
You can also accept other arguments.
|
| 141 |
+
|
| 142 |
+
To install it, simply attach it to the CadQuery or Workplane object, like this::
|
| 143 |
+
|
| 144 |
+
def _yourFunction(self, arg1, arg):
|
| 145 |
+
# do stuff
|
| 146 |
+
return whatever_you_want
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
cq.Workplane.yourPlugin = _yourFunction
|
| 150 |
+
|
| 151 |
+
That's it!
|
| 152 |
+
|
| 153 |
+
CadQueryExample Plugins
|
| 154 |
+
-----------------------
|
| 155 |
+
Some core cadquery code is intentionally written exactly like a plugin.
|
| 156 |
+
If you are writing your own plugins, have a look at these methods for inspiration:
|
| 157 |
+
|
| 158 |
+
* :py:meth:`cadquery.Workplane.polygon`
|
| 159 |
+
* :py:meth:`cadquery.Workplane.cboreHole`
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
Plugin Example
|
| 163 |
+
-----------------------
|
| 164 |
+
|
| 165 |
+
This ultra simple plugin makes cubes of the specified size for each stack point.
|
| 166 |
+
|
| 167 |
+
.. cadquery::
|
| 168 |
+
|
| 169 |
+
import cadquery as cq
|
| 170 |
+
from cadquery.func import box
|
| 171 |
+
|
| 172 |
+
def makeCubes(self, length):
|
| 173 |
+
# self refers to the Workplane object
|
| 174 |
+
|
| 175 |
+
# inner method that creates a cube
|
| 176 |
+
def _singleCube(loc):
|
| 177 |
+
# loc is a location in local coordinates
|
| 178 |
+
# since we're using eachpoint with useLocalCoordinates=True
|
| 179 |
+
return box(length, length, length).locate(loc)
|
| 180 |
+
|
| 181 |
+
# use CQ utility method to iterate over the stack, call our
|
| 182 |
+
# method, and convert to/from local coordinates.
|
| 183 |
+
return self.eachpoint(_singleCube, True)
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
# link the plugin into CadQuery
|
| 187 |
+
cq.Workplane.makeCubes = makeCubes
|
| 188 |
+
|
| 189 |
+
# use the plugin
|
| 190 |
+
result = (
|
| 191 |
+
cq.Workplane("XY")
|
| 192 |
+
.box(6.0, 8.0, 0.5)
|
| 193 |
+
.faces(">Z")
|
| 194 |
+
.rect(4.0, 4.0, forConstruction=True)
|
| 195 |
+
.vertices()
|
| 196 |
+
.makeCubes(1.0)
|
| 197 |
+
.combine()
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
Extending CadQuery: Special Methods
|
| 202 |
+
-----------------------------------
|
| 203 |
+
|
| 204 |
+
The above-mentioned approach has one drawback, it requires monkey-patching or subclassing. To avoid this
|
| 205 |
+
one can also use the following special methods of :py:class:`cadquery.Workplane` and :py:class:`cadquery.Sketch`
|
| 206 |
+
and write plugins in a more functional style.
|
| 207 |
+
|
| 208 |
+
* :py:meth:`cadquery.Workplane.map`
|
| 209 |
+
* :py:meth:`cadquery.Workplane.apply`
|
| 210 |
+
* :py:meth:`cadquery.Workplane.invoke`
|
| 211 |
+
* :py:meth:`cadquery.Sketch.map`
|
| 212 |
+
* :py:meth:`cadquery.Sketch.apply`
|
| 213 |
+
* :py:meth:`cadquery.Sketch.invoke`
|
| 214 |
+
|
| 215 |
+
Here is the same plugin rewritten using one of those methods.
|
| 216 |
+
|
| 217 |
+
.. cadquery::
|
| 218 |
+
|
| 219 |
+
import cadquery as cq
|
| 220 |
+
from cadquery.func import box
|
| 221 |
+
|
| 222 |
+
def makeCubes(length):
|
| 223 |
+
|
| 224 |
+
# inner method that creates the cubes
|
| 225 |
+
def callback(wp):
|
| 226 |
+
|
| 227 |
+
return wp.eachpoint(box(length, length, length), True)
|
| 228 |
+
|
| 229 |
+
return callback
|
| 230 |
+
|
| 231 |
+
# use the plugin
|
| 232 |
+
result = (
|
| 233 |
+
cq.Workplane("XY")
|
| 234 |
+
.box(6.0, 8.0, 0.5)
|
| 235 |
+
.faces(">Z")
|
| 236 |
+
.rect(4.0, 4.0, forConstruction=True)
|
| 237 |
+
.vertices()
|
| 238 |
+
.invoke(makeCubes(1.0))
|
| 239 |
+
.combine()
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
Such an approach is more friendly for auto-completion and static analysis tools.
|
server/docs/reference/free-func.rst
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
.. _freefuncapi:
|
| 3 |
+
|
| 4 |
+
*****************
|
| 5 |
+
Free function API
|
| 6 |
+
*****************
|
| 7 |
+
|
| 8 |
+
.. warning:: The free function API is experimental and may change.
|
| 9 |
+
|
| 10 |
+
For situations when more freedom in crafting individual objects is required, a free function API is provided.
|
| 11 |
+
This API has no hidden state, but may result in more verbose code. One can still use selectors as methods, but all other operations are implemented as free functions.
|
| 12 |
+
Placement of objects and creation of patterns can be achieved using the various overloads of the moved method.
|
| 13 |
+
|
| 14 |
+
Currently this documentation is incomplete, more examples can be found in the tests.
|
| 15 |
+
|
| 16 |
+
Tutorial
|
| 17 |
+
--------
|
| 18 |
+
|
| 19 |
+
The purpose of this section is to demonstrate how to construct Shape objects using the free function API.
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
.. cadquery::
|
| 23 |
+
:height: 600px
|
| 24 |
+
|
| 25 |
+
from cadquery.func import *
|
| 26 |
+
|
| 27 |
+
dh = 2
|
| 28 |
+
r = 1
|
| 29 |
+
|
| 30 |
+
# construct edges
|
| 31 |
+
edge1 = circle(r)
|
| 32 |
+
edge2 = circle(1.5*r).moved(z=dh)
|
| 33 |
+
edge3 = circle(r).moved(z=1.5*dh)
|
| 34 |
+
|
| 35 |
+
# loft the side face
|
| 36 |
+
side = loft(edge1, edge2, edge3)
|
| 37 |
+
|
| 38 |
+
# bottom face
|
| 39 |
+
bottom = fill(side.edges('<Z'))
|
| 40 |
+
|
| 41 |
+
# top face with continuous curvature
|
| 42 |
+
top = cap(side.edges('>Z'), side, [(0,0,1.6*dh)])
|
| 43 |
+
|
| 44 |
+
# assemble into a solid
|
| 45 |
+
s = solid(side, bottom, top)
|
| 46 |
+
|
| 47 |
+
# construct the final result
|
| 48 |
+
result = s.moved((-3*r, 0, 0), (3*r, 0, 0))
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
The code above builds a non-trivial object by sequentially constructing individual faces, assembling them into a solid and finally generating a pattern.
|
| 52 |
+
|
| 53 |
+
It begins with defining few edges.
|
| 54 |
+
|
| 55 |
+
.. code-block:: python
|
| 56 |
+
|
| 57 |
+
edge1 = circle(r)
|
| 58 |
+
edge2 = circle(2*r).moved(z=dh)
|
| 59 |
+
edge3 = circle(r).moved(z=1.5*dh)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
Those edges are used to create the side faces of the final solid using :meth:`~cadquery.occ_impl.shapes.loft`.
|
| 63 |
+
|
| 64 |
+
.. code-block:: python
|
| 65 |
+
|
| 66 |
+
side = loft(edge1, edge2, edge3)
|
| 67 |
+
|
| 68 |
+
Once the side is there, :meth:`~cadquery.occ_impl.shapes.cap` and :meth:`~cadquery.occ_impl.shapes.fill` are used to define the top and bottom faces.
|
| 69 |
+
Note that :meth:`~cadquery.occ_impl.shapes.cap` tries to maintain curvature continuity with respect to the context shape. This is not the case for :meth:`~cadquery.occ_impl.shapes.fill`.
|
| 70 |
+
|
| 71 |
+
.. code-block:: python
|
| 72 |
+
|
| 73 |
+
# bottom face
|
| 74 |
+
bottom = fill(side.edges('<Z'))
|
| 75 |
+
|
| 76 |
+
# top face with continuous curvature
|
| 77 |
+
top = cap(side.edges('>Z'), side, [(0,0,1.75*dh)])
|
| 78 |
+
|
| 79 |
+
Next, all the faces are assembled into a solid.
|
| 80 |
+
|
| 81 |
+
.. code-block:: python
|
| 82 |
+
|
| 83 |
+
s = solid(side, bottom, top)
|
| 84 |
+
|
| 85 |
+
Finally, the solid is duplicated and placed in the desired locations creating the final compound object. Note various usages of :meth:`~cadquery.Shape.moved`.
|
| 86 |
+
|
| 87 |
+
.. code-block:: python
|
| 88 |
+
|
| 89 |
+
result = s.moved((-3*r, 0, 0), (3*r, 0, 0))
|
| 90 |
+
|
| 91 |
+
In general all the operations are implemented as free functions, with the exception of placement and selection which are strictly related to a specific shape.
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
Primitives
|
| 95 |
+
----------
|
| 96 |
+
|
| 97 |
+
Various 1D, 2D and 3D primitives are supported.
|
| 98 |
+
|
| 99 |
+
.. cadquery::
|
| 100 |
+
|
| 101 |
+
from cadquery.func import *
|
| 102 |
+
|
| 103 |
+
e = segment((0,0), (0,1))
|
| 104 |
+
|
| 105 |
+
c = circle(1)
|
| 106 |
+
|
| 107 |
+
f = plane(1, 1.5)
|
| 108 |
+
|
| 109 |
+
b = box(1, 1, 1)
|
| 110 |
+
|
| 111 |
+
result = compound(e, c.move(2), f.move(4), b.move(6))
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
Boolean operations
|
| 115 |
+
------------------
|
| 116 |
+
|
| 117 |
+
Boolean operations are supported and implemented as operators and free functions.
|
| 118 |
+
In general boolean operations are slow and it is advised to avoid them and not to perform the in a loop.
|
| 119 |
+
One can for example union multiple solids at once by first combining them into a compound.
|
| 120 |
+
|
| 121 |
+
.. cadquery::
|
| 122 |
+
|
| 123 |
+
from cadquery.func import *
|
| 124 |
+
|
| 125 |
+
c1 = cylinder(1, 2)
|
| 126 |
+
c2 = cylinder(0.5, 3)
|
| 127 |
+
|
| 128 |
+
f1 = plane(2, 2).move(z=1)
|
| 129 |
+
f2 = plane(1, 1).move(z=1)
|
| 130 |
+
|
| 131 |
+
e1 = segment((0,-2.5, 1), (0,2.5,1))
|
| 132 |
+
|
| 133 |
+
# union
|
| 134 |
+
r1 = c2 + c1
|
| 135 |
+
r2 = fuse(f1, f2)
|
| 136 |
+
|
| 137 |
+
# difference
|
| 138 |
+
r3 = c1 - c2
|
| 139 |
+
r4 = cut(f1, f2)
|
| 140 |
+
|
| 141 |
+
# intersection
|
| 142 |
+
r5 = c1*c2
|
| 143 |
+
r6 = intersect(f1, f2)
|
| 144 |
+
|
| 145 |
+
# splitting
|
| 146 |
+
r7 = (c1 / f1).solids('<Z')
|
| 147 |
+
r8 = split(f2, e1).faces('<X')
|
| 148 |
+
|
| 149 |
+
results = (r1, r2, r3, r4, r5, r6, r7, r8)
|
| 150 |
+
result = compound([el.moved(2*i) for i,el in enumerate(results)])
|
| 151 |
+
|
| 152 |
+
Note that bool operations work on 2D shapes as well.
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
Shape construction
|
| 156 |
+
------------------
|
| 157 |
+
|
| 158 |
+
Constructing complex shapes from simple shapes is possible in various contexts.
|
| 159 |
+
|
| 160 |
+
.. cadquery::
|
| 161 |
+
|
| 162 |
+
from cadquery.func import *
|
| 163 |
+
|
| 164 |
+
e1 = segment((0,0), (1,0))
|
| 165 |
+
e2 = segment((1,0), (1,1))
|
| 166 |
+
|
| 167 |
+
# wire from edges
|
| 168 |
+
r1 = wire(e1, e2)
|
| 169 |
+
|
| 170 |
+
c1 = circle(1)
|
| 171 |
+
|
| 172 |
+
# face from a planar wire
|
| 173 |
+
r2 = face(c1)
|
| 174 |
+
|
| 175 |
+
# solid from faces
|
| 176 |
+
f1 = plane(1,1)
|
| 177 |
+
f2 = f1.moved(z=1)
|
| 178 |
+
f3 = extrude(f1.wires(), (0,0,1))
|
| 179 |
+
|
| 180 |
+
r3 = solid(f1,f2,*f3)
|
| 181 |
+
|
| 182 |
+
# compound from shapes
|
| 183 |
+
s1 = circle(1).moved(ry=90)
|
| 184 |
+
s2 = plane(1,1).move(rx=90).move(y=2)
|
| 185 |
+
s3 = cone(1,1.5).move(y=4)
|
| 186 |
+
|
| 187 |
+
r4 = compound(s1, s2, s3)
|
| 188 |
+
|
| 189 |
+
results = (r1, r2, r3, r4,)
|
| 190 |
+
result = compound([el.moved(2*i) for i,el in enumerate(results)])
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
Operations
|
| 194 |
+
----------
|
| 195 |
+
|
| 196 |
+
Free function API currently supports :meth:`~cadquery.occ_impl.shapes.extrude`, :meth:`~cadquery.occ_impl.shapes.loft`, :meth:`~cadquery.occ_impl.shapes.revolve` and :meth:`~cadquery.occ_impl.shapes.sweep` operations.
|
| 197 |
+
|
| 198 |
+
.. cadquery::
|
| 199 |
+
|
| 200 |
+
from cadquery.func import *
|
| 201 |
+
|
| 202 |
+
r = rect(1,0.5)
|
| 203 |
+
f = face(r, circle(0.2).moved(0.2), rect(0.2, 0.4).moved(-0.2))
|
| 204 |
+
c = circle(0.2)
|
| 205 |
+
p = spline([(0,0,0), (0,-1,2)], [(0,0,1), (0,-1,1)])
|
| 206 |
+
|
| 207 |
+
# extrude
|
| 208 |
+
s1 = extrude(r, (0,0,2))
|
| 209 |
+
s2 = extrude(fill(r), (0,0,1))
|
| 210 |
+
|
| 211 |
+
# sweep
|
| 212 |
+
s3 = sweep(r, p)
|
| 213 |
+
s4 = sweep(f, p)
|
| 214 |
+
|
| 215 |
+
# loft
|
| 216 |
+
s5 = loft(r, c.moved(z=2))
|
| 217 |
+
s6 = loft(r, c.moved(z=1), cap=True)\
|
| 218 |
+
|
| 219 |
+
# revolve
|
| 220 |
+
s7 = revolve(fill(r), (0.5, 0, 0), (0, 1, 0), 90)
|
| 221 |
+
|
| 222 |
+
results = (s1, s2, s3, s4, s5, s6, s7)
|
| 223 |
+
result = compound([el.moved(2*i) for i,el in enumerate(results)])
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
Placement
|
| 227 |
+
---------
|
| 228 |
+
|
| 229 |
+
Placement and creation of arrays is possible using :meth:`~cadquery.Shape.move` and :meth:`~cadquery.Shape.moved`.
|
| 230 |
+
|
| 231 |
+
.. cadquery::
|
| 232 |
+
|
| 233 |
+
from cadquery.func import *
|
| 234 |
+
|
| 235 |
+
locs = [(0,-1,0), (0,1,0)]
|
| 236 |
+
|
| 237 |
+
s = sphere(1).moved(locs)
|
| 238 |
+
c = cylinder(1,2).move(rx=15).moved(*locs)
|
| 239 |
+
|
| 240 |
+
result = compound(s, c.moved(2))
|
| 241 |
+
|
| 242 |
+
Text
|
| 243 |
+
----
|
| 244 |
+
|
| 245 |
+
The free function API has extensive text creation capabilities including text on
|
| 246 |
+
planar curves and text on surfaces.
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
.. cadquery::
|
| 250 |
+
|
| 251 |
+
from cadquery.func import *
|
| 252 |
+
|
| 253 |
+
from math import pi
|
| 254 |
+
|
| 255 |
+
# parameters
|
| 256 |
+
D = 5
|
| 257 |
+
H = 2*D
|
| 258 |
+
S = H/10
|
| 259 |
+
TH = S/10
|
| 260 |
+
TXT = "CadQuery"
|
| 261 |
+
|
| 262 |
+
# base and spine
|
| 263 |
+
c = cylinder(D, H).moved(rz=-135)
|
| 264 |
+
cf = c.faces("%CYLINDER")
|
| 265 |
+
spine = (c*plane().moved(z=D)).edges().trim(pi/2, pi)
|
| 266 |
+
|
| 267 |
+
# planar
|
| 268 |
+
r1 = text(TXT, 1, spine, planar=True).moved(z=-S)
|
| 269 |
+
|
| 270 |
+
# normal
|
| 271 |
+
r2 = text(TXT, 1, spine)
|
| 272 |
+
|
| 273 |
+
# projected
|
| 274 |
+
r3 = text(TXT, 1, spine, cf).moved(z=S)
|
| 275 |
+
|
| 276 |
+
# projected and thickened
|
| 277 |
+
r4 = offset(r3, TH).moved(z=S)
|
| 278 |
+
|
| 279 |
+
result = compound(r1, r2, r3, r4)
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
Adding features manually
|
| 283 |
+
------------------------
|
| 284 |
+
|
| 285 |
+
In certain cases it is desirable to add features such as holes or protrusions manually.
|
| 286 |
+
E.g., for complicated shapes it might be beneficial performance-wise because it
|
| 287 |
+
avoids boolean operations. One can add or remove faces, add holes to existing faces
|
| 288 |
+
and last but not least reconstruct existing solids.
|
| 289 |
+
|
| 290 |
+
.. cadquery::
|
| 291 |
+
|
| 292 |
+
from cadquery.func import *
|
| 293 |
+
|
| 294 |
+
w = 1
|
| 295 |
+
r = 0.9*w/2
|
| 296 |
+
|
| 297 |
+
# box
|
| 298 |
+
b = box(w, w, w)
|
| 299 |
+
# bottom face
|
| 300 |
+
b_bot = b.faces('<Z')
|
| 301 |
+
# top faces
|
| 302 |
+
b_top = b.faces('>Z')
|
| 303 |
+
|
| 304 |
+
# inner face
|
| 305 |
+
inner = extrude(circle(r), (0,0,w))
|
| 306 |
+
|
| 307 |
+
# add holes to the bottom and top face
|
| 308 |
+
b_bot_hole = b_bot.addHole(inner.edges('<Z'))
|
| 309 |
+
b_top_hole = b_top.addHole(inner.edges('>Z'))
|
| 310 |
+
|
| 311 |
+
# construct the final solid
|
| 312 |
+
result = solid(
|
| 313 |
+
b.remove(b_top, b_bot).faces(), #side faces
|
| 314 |
+
b_bot_hole, # bottom with a hole
|
| 315 |
+
inner, # inner cylinder face
|
| 316 |
+
b_top_hole, # top with a hole
|
| 317 |
+
)
|
| 318 |
+
|
| 319 |
+
If the base shape is more complicated, it is possible to use local sewing that
|
| 320 |
+
takes into account on indicated elements of the context shape. This, however,
|
| 321 |
+
necessitates a two step approach - first a shell needs to be explicitly sewn
|
| 322 |
+
and only then the final solid can be constructed.
|
| 323 |
+
|
| 324 |
+
.. cadquery::
|
| 325 |
+
|
| 326 |
+
from cadquery.func import *
|
| 327 |
+
|
| 328 |
+
w = 1
|
| 329 |
+
h = 0.1
|
| 330 |
+
r = 0.9*w/2
|
| 331 |
+
|
| 332 |
+
# box
|
| 333 |
+
b = box(w, w, w)
|
| 334 |
+
# top face
|
| 335 |
+
b_top = b.faces('>Z')
|
| 336 |
+
|
| 337 |
+
# protrusion
|
| 338 |
+
feat_side = extrude(circle(r).moved(b_top.Center()), (0,0,h))
|
| 339 |
+
feat_top = face(feat_side.edges('>Z'))
|
| 340 |
+
feat = shell(feat_side, feat_top) # sew into a shell
|
| 341 |
+
|
| 342 |
+
# add hole to the box
|
| 343 |
+
b_top_hole = b_top.addHole(feat.edges('<Z'))
|
| 344 |
+
b = b.replace(b_top, b_top_hole)
|
| 345 |
+
|
| 346 |
+
# local sewing - only two faces are taken into account
|
| 347 |
+
sh = shell(b_top_hole, feat.faces('<Z'), ctx=(b, feat))
|
| 348 |
+
# construct the final solid
|
| 349 |
+
result = solid(sh)
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
Mapping onto parametric space
|
| 353 |
+
-----------------------------
|
| 354 |
+
|
| 355 |
+
To complement functionalities described, it is possible to trim edges and faces explicitly using simple rectangular
|
| 356 |
+
trims, polygons, splines or arbitrary wires.
|
| 357 |
+
|
| 358 |
+
.. cadquery::
|
| 359 |
+
|
| 360 |
+
from math import pi
|
| 361 |
+
from cadquery.func import cylinder, edgeOn, compound, wire
|
| 362 |
+
|
| 363 |
+
# parameters
|
| 364 |
+
d = 1.5
|
| 365 |
+
h = 3
|
| 366 |
+
du = pi
|
| 367 |
+
Nturns = 2
|
| 368 |
+
|
| 369 |
+
# construct the base surface
|
| 370 |
+
base = cylinder(d, h).faces("%CYLINDER")
|
| 371 |
+
|
| 372 |
+
# rectangular trim
|
| 373 |
+
r1 = base.trim(-pi/2, 0, 0, h/3)
|
| 374 |
+
|
| 375 |
+
# polyline trim
|
| 376 |
+
r2 = base.trim((0,0), (pi,0), (pi/2, h/2))
|
| 377 |
+
|
| 378 |
+
# construct a pcurve
|
| 379 |
+
pcurve = edgeOn(base, [(pi/2, h/4), (pi, h/4), (pi, h/2), (pi/2, h/2)], periodic=True)
|
| 380 |
+
|
| 381 |
+
# pcurve trim
|
| 382 |
+
r3 = base.trim(wire(pcurve))
|
| 383 |
+
|
| 384 |
+
result = compound(r1, r2.moved(x=2), r3.moved(x=4))
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
This in principle allows to model arbitrary shapes in the parametric domain, but often it is more desirable
|
| 388 |
+
to work with higher level objects like wires.
|
| 389 |
+
|
| 390 |
+
|
| 391 |
+
.. cadquery::
|
| 392 |
+
|
| 393 |
+
from cadquery.func import cylinder, loft, wireOn, segment
|
| 394 |
+
from math import pi
|
| 395 |
+
|
| 396 |
+
# parameters
|
| 397 |
+
d = 1.5
|
| 398 |
+
h = 3
|
| 399 |
+
du = pi
|
| 400 |
+
Nturns = 2
|
| 401 |
+
|
| 402 |
+
# construct the base surface
|
| 403 |
+
base = cylinder(d, h).faces("%CYLINDER")
|
| 404 |
+
|
| 405 |
+
# construct a planar 2D patch for u,v trimming
|
| 406 |
+
uv_patch = loft(
|
| 407 |
+
segment((0, 0), (du, 0)), segment((Nturns * 2 * pi, h), (Nturns * 2 * pi + du, h))
|
| 408 |
+
)
|
| 409 |
+
|
| 410 |
+
# map it onto the cylinder
|
| 411 |
+
w = wireOn(base, uv_patch)
|
| 412 |
+
|
| 413 |
+
# check that the pcurves were created
|
| 414 |
+
for e in w:
|
| 415 |
+
assert e.hasPCurve(base), "No p-curve on base present"
|
| 416 |
+
|
| 417 |
+
# trim the base surface
|
| 418 |
+
result = base.trim(w)
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
Note that trimming of periodic faces requires manual seam construction and an additional sewing
|
| 422 |
+
step to ensure correctness.
|
| 423 |
+
|
| 424 |
+
|
| 425 |
+
.. cadquery::
|
| 426 |
+
|
| 427 |
+
from cadquery.func import circle, extrude, spline, edgeOn, segment, wire, shell
|
| 428 |
+
from math import pi
|
| 429 |
+
|
| 430 |
+
# base
|
| 431 |
+
r = 5
|
| 432 |
+
h = 5
|
| 433 |
+
|
| 434 |
+
f = extrude(circle(r), (0, 0, -h))
|
| 435 |
+
|
| 436 |
+
# trimming edges
|
| 437 |
+
spl = spline([(0, h), (pi, h / 2.5), (2 * pi, h)], tgts=[(0.1, 0), (0.1, 0)])
|
| 438 |
+
top = edgeOn(f, spl)
|
| 439 |
+
bot = edgeOn(f, segment((2 * pi, 0), (0, 0)))
|
| 440 |
+
side1 = edgeOn(f, segment((0, 0), (0, h)))
|
| 441 |
+
side2 = edgeOn(f, segment((2 * pi, h), (2 * pi, 0)))
|
| 442 |
+
|
| 443 |
+
# trimming wire
|
| 444 |
+
trim_wire = wire(top, side1, bot, side2)
|
| 445 |
+
|
| 446 |
+
# trim and sew
|
| 447 |
+
result = shell(f.trim(trim_wire))
|
| 448 |
+
|
| 449 |
+
|
| 450 |
+
Finally, it is also possible to map complete faces.
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
.. cadquery::
|
| 454 |
+
|
| 455 |
+
from cadquery.func import sphere, text, faceOn
|
| 456 |
+
|
| 457 |
+
base = sphere(5).faces()
|
| 458 |
+
|
| 459 |
+
result = faceOn(base, text("CadQuery", 1))
|
| 460 |
+
|
| 461 |
+
|
server/docs/reference/primer.rst
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.. _3d_cad_primer:
|
| 2 |
+
|
| 3 |
+
.. _cadquery_concepts:
|
| 4 |
+
|
| 5 |
+
Concepts
|
| 6 |
+
===================================
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
3D BREP Topology Concepts
|
| 10 |
+
---------------------------
|
| 11 |
+
Before talking about CadQuery, it makes sense to talk a little about 3D CAD topology. CadQuery is based upon the
|
| 12 |
+
OpenCascade kernel, which uses Boundary Representations (BREP) for objects. This just means that objects
|
| 13 |
+
are defined by their enclosing surfaces.
|
| 14 |
+
|
| 15 |
+
When working in a BREP system, these fundamental constructs exist to define a shape (working up the food chain):
|
| 16 |
+
|
| 17 |
+
:vertex: a single point in space
|
| 18 |
+
:edge: a connection between two or more vertices along a particular path (called a curve)
|
| 19 |
+
:wire: a collection of edges that are connected together.
|
| 20 |
+
:face: a set of edges or wires that enclose a surface
|
| 21 |
+
:shell: a collection of faces that are connected together along some of their edges
|
| 22 |
+
:solid: a shell that has a closed interior
|
| 23 |
+
:compound: a collection of solids
|
| 24 |
+
|
| 25 |
+
When using CadQuery, all of these objects are created, hopefully with the least possible work. In the actual CAD
|
| 26 |
+
kernel, there is another set of Geometrical constructs involved as well. For example, an arc-shaped edge will
|
| 27 |
+
hold a reference to an underlying curve that is a full circle, and each linear edge holds underneath it the equation
|
| 28 |
+
for a line. CadQuery shields you from these constructs.
|
| 29 |
+
|
| 30 |
+
CadQuery API layers
|
| 31 |
+
---------------------------
|
| 32 |
+
|
| 33 |
+
Once you start to dive a bit more into CadQuery, you may find yourself a bit confused juggling between different types of objects the CadQuery APIs can return.
|
| 34 |
+
This chapter aims to give an explanation on this topic and to provide background on the underlying implementation and kernel layers so you can leverage more of CadQuery functionality.
|
| 35 |
+
|
| 36 |
+
CadQuery is composed of 4 different API, which are implemented on top of each other.
|
| 37 |
+
|
| 38 |
+
1. The Fluent API
|
| 39 |
+
#. :class:`~cadquery.Workplane`
|
| 40 |
+
#. :class:`~cadquery.Sketch`
|
| 41 |
+
#. :class:`~cadquery.Assembly`
|
| 42 |
+
2. The Direct API
|
| 43 |
+
#. :class:`~cadquery.Shape`
|
| 44 |
+
3. The Geometry API
|
| 45 |
+
#. :class:`~cadquery.Vector`
|
| 46 |
+
#. :class:`~cadquery.Plane`
|
| 47 |
+
#. :class:`~cadquery.Location`
|
| 48 |
+
4. The OCCT API
|
| 49 |
+
|
| 50 |
+
The Fluent API
|
| 51 |
+
~~~~~~~~~~~~~~~~~~~~~~
|
| 52 |
+
|
| 53 |
+
What we call the fluent API is what you work with when you first start using CadQuery, the :class:`~cadquery.Workplane` class and all its methods defines the Fluent API.
|
| 54 |
+
This is the API you will use and see most of the time, it's fairly easy to use and it simplifies a lot of things for you. A classic example could be : ::
|
| 55 |
+
|
| 56 |
+
part = Workplane("XY").box(1, 2, 3).faces(">Z").vertices().circle(0.5).cutThruAll()
|
| 57 |
+
|
| 58 |
+
Here we create a :class:`~cadquery.Workplane` object on which we subsequently call several methods to create our part. A general way of thinking about the Fluent API is to
|
| 59 |
+
consider the :class:`~cadquery.Workplane` as your part object and all it's methods as operations that will affect your part.
|
| 60 |
+
Often you will start with an empty :class:`~cadquery.Workplane`, then add more features by calling :class:`~cadquery.Workplane` methods.
|
| 61 |
+
|
| 62 |
+
This hierarchical structure of operations modifying a part is well seen with the traditional code style used in CadQuery code.
|
| 63 |
+
Code written with the CadQuery fluent API will often look like this : ::
|
| 64 |
+
|
| 65 |
+
part = Workplane("XY").box(1, 2, 3).faces(">Z").vertices().circle(0.5).cutThruAll()
|
| 66 |
+
|
| 67 |
+
Or like this : ::
|
| 68 |
+
|
| 69 |
+
part = Workplane("XY")
|
| 70 |
+
part = part.box(1, 2, 3)
|
| 71 |
+
part = part.faces(">Z")
|
| 72 |
+
part = part.vertices()
|
| 73 |
+
part = part.circle(0.5)
|
| 74 |
+
part = part.cutThruAll()
|
| 75 |
+
|
| 76 |
+
.. note::
|
| 77 |
+
While the first code style is what people default to, it's important to note that when you write your code like this it's equivalent as writting it on a single line.
|
| 78 |
+
It's then more difficult to debug as you cannot visualize each operation step by step, which is a functionality that is provided by the CQ-Editor debugger for example.
|
| 79 |
+
|
| 80 |
+
The Direct API
|
| 81 |
+
~~~~~~~~~~~~~~
|
| 82 |
+
|
| 83 |
+
While the fluent API exposes much functionality, you may find scenarios that require extra flexibility or require working with lower level objects.
|
| 84 |
+
|
| 85 |
+
The direct API is the API that is called by the fluent API under the hood. The 9 topological classes and their methods compose the direct API.
|
| 86 |
+
These classes actually wrap the equivalent Open CASCADE Technology (OCCT) classes.
|
| 87 |
+
The 9 topological classes are :
|
| 88 |
+
|
| 89 |
+
1. :class:`~cadquery.Shape`
|
| 90 |
+
2. :class:`~cadquery.Compound`
|
| 91 |
+
3. :class:`~cadquery.CompSolid`
|
| 92 |
+
4. :class:`~cadquery.Solid`
|
| 93 |
+
5. :class:`~cadquery.Shell`
|
| 94 |
+
6. :class:`~cadquery.Face`
|
| 95 |
+
7. :class:`~cadquery.Wire`
|
| 96 |
+
8. :class:`~cadquery.Edge`
|
| 97 |
+
9. :class:`~cadquery.Vertex`
|
| 98 |
+
|
| 99 |
+
Each class has its own methods to create and/or edit shapes of their respective type. One can also use the :ref:`freefuncapi` to create and modify shapes. As already explained in :ref:`cadquery_concepts` there is also some kind of hierarchy in the
|
| 100 |
+
topological classes. A Wire is made of several edges which are themselves made of several vertices. This means you can create geometry from the bottom up and have a lot of control over it.
|
| 101 |
+
|
| 102 |
+
For example we can create a circular face like so ::
|
| 103 |
+
|
| 104 |
+
circle_wire = Wire.makeCircle(10, Vector(0, 0, 0), Vector(0, 0, 1))
|
| 105 |
+
circular_face = Face.makeFromWires(circle_wire, [])
|
| 106 |
+
|
| 107 |
+
.. note::
|
| 108 |
+
In CadQuery (and OCCT) all the topological classes are shapes, the :class:`~cadquery.Shape` class is the most abstract topological class.
|
| 109 |
+
The topological class inherits :class:`~cadquery.Mixin3D` or :class:`~cadquery.Mixin1D` which provide aditional methods that are shared between the classes that inherits them.
|
| 110 |
+
|
| 111 |
+
The direct API as its name suggests doesn't provide a parent/children data structure, instead each method call directly returns an object of the specified topological type.
|
| 112 |
+
It is more verbose than the fluent API and more tedious to work with, but as it offers more flexibility (you can work with faces, which is something you can't do in the fluent API)
|
| 113 |
+
it is sometimes more convenient than the fluent API.
|
| 114 |
+
|
| 115 |
+
The OCCT API
|
| 116 |
+
~~~~~~~~~~~~~
|
| 117 |
+
|
| 118 |
+
Finally we are discussing about the OCCT API. The OCCT API is the lowest level of CadQuery. The direct API is built upon the OCCT API, where the OCCT API in CadQuery is available through OCP.
|
| 119 |
+
OCP are the Python bindings of the OCCT C++ libraries CadQuery uses. This means you have access to (almost) all the OCCT C++ libraries in Python and in CadQuery.
|
| 120 |
+
Working with the OCCT API will give you the maximum flexibility and control over you designs, it is however very verbose and difficult to use. You will need to have a strong
|
| 121 |
+
knowledge of the different C++ libraries to be able to achieve what you want. To obtain this knowledge the most obvious ways are :
|
| 122 |
+
|
| 123 |
+
1. Read the direct API source code, since it is build upon the OCCT API it is full of example usage.
|
| 124 |
+
2. Go through the `C++ documentation <https://dev.opencascade.org/doc/overview/html/>`_
|
| 125 |
+
|
| 126 |
+
.. note::
|
| 127 |
+
The general way of importing a specific class of the OCCT API is ::
|
| 128 |
+
|
| 129 |
+
from OCP.thePackageName import theClassName
|
| 130 |
+
|
| 131 |
+
For example if you want to use the class `BRepPrimAPI_MakeBox <https://dev.opencascade.org/doc/refman/html/class_b_rep_prim_a_p_i___make_box.html>`_.
|
| 132 |
+
You will go by the following ::
|
| 133 |
+
|
| 134 |
+
from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox
|
| 135 |
+
|
| 136 |
+
The package name of any class is written at the top of the documentation page. Often it's written in the class name itself as a prefix.
|
| 137 |
+
|
| 138 |
+
Going back and forth between the APIs
|
| 139 |
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
| 140 |
+
|
| 141 |
+
While the 3 APIs provide 3 different layer of complexity and functionality you can mix the 3 layers as you wish.
|
| 142 |
+
Below is presented the different ways you can interact with the different API layers.
|
| 143 |
+
|
| 144 |
+
-------------------------
|
| 145 |
+
Fluent API <=> Direct API
|
| 146 |
+
-------------------------
|
| 147 |
+
|
| 148 |
+
.. currentmodule:: cadquery
|
| 149 |
+
|
| 150 |
+
Here are all the possibilities you have to get an object from the Direct API (i.e a topological object).
|
| 151 |
+
|
| 152 |
+
You can end the Fluent API call chain and get the last object on the stack with :py:meth:`Workplane.val` alternatively you can get all
|
| 153 |
+
the objects with :py:meth:`Workplane.vals`
|
| 154 |
+
|
| 155 |
+
.. code-block:: pycon
|
| 156 |
+
|
| 157 |
+
>>> box = Workplane().box(10, 5, 5)
|
| 158 |
+
>>> print(type(box))
|
| 159 |
+
<class cadquery.cq.Workplane>
|
| 160 |
+
|
| 161 |
+
>>> box = Workplane().box(10, 5, 5).val()
|
| 162 |
+
>>> print(type(box))
|
| 163 |
+
<class cadquery.occ_impl.shapes.Solid>
|
| 164 |
+
|
| 165 |
+
If you are only interested in getting the context solid of your Workplane, you can use :py:meth:`Workplane.findSolid`:
|
| 166 |
+
|
| 167 |
+
.. code-block::
|
| 168 |
+
|
| 169 |
+
>>> part = Workplane().box(10,5,5).circle(3).val()
|
| 170 |
+
>>> print(type(part))
|
| 171 |
+
<class cadquery.cq.Wire>
|
| 172 |
+
|
| 173 |
+
>>> part = Workplane().box(10,5,5).circle(3).findSolid()
|
| 174 |
+
>>> print(type(part))
|
| 175 |
+
<class cadquery.occ_impl.shapes.Compound>
|
| 176 |
+
# The return type of findSolid is either a Solid or a Compound object
|
| 177 |
+
|
| 178 |
+
If you want to go the other way around i.e using objects from the topological API in the Fluent API here are your options :
|
| 179 |
+
|
| 180 |
+
You can pass a topological object as a base object to the :class:`~cadquery.Workplane` object. ::
|
| 181 |
+
|
| 182 |
+
solid_box = Solid.makeBox(10, 10, 10)
|
| 183 |
+
part = Workplane(obj=solid_box)
|
| 184 |
+
# And you can continue your modelling in the fluent API
|
| 185 |
+
part = part.faces(">Z").circle(1).extrude(10)
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
You can add a topological object as a new operation/step in the Fluent API call chain with :py:meth:`Workplane.newObject` ::
|
| 189 |
+
|
| 190 |
+
circle_wire = Wire.makeCircle(1, Vector(0, 0, 0), Vector(0, 0, 1))
|
| 191 |
+
box = Workplane().box(10, 10, 10).newObject([circle_wire])
|
| 192 |
+
# And you can continue modelling
|
| 193 |
+
box = (
|
| 194 |
+
box.toPending().cutThruAll()
|
| 195 |
+
) # notice the call to `toPending` that is needed if you want to use it in a subsequent operation
|
| 196 |
+
|
| 197 |
+
-------------------------
|
| 198 |
+
Direct API <=> OCCT API
|
| 199 |
+
-------------------------
|
| 200 |
+
|
| 201 |
+
Every object of the Direct API stores its OCCT equivalent object in its :attr:`wrapped` attribute.:
|
| 202 |
+
|
| 203 |
+
.. code-block::
|
| 204 |
+
|
| 205 |
+
>>> box = Solid.makeBox(10,5,5)
|
| 206 |
+
>>> print(type(box))
|
| 207 |
+
<class cadquery.occ_impl.shapes.Solid>
|
| 208 |
+
|
| 209 |
+
>>> box = Solid.makeBox(10,5,5).wrapped
|
| 210 |
+
>>> print(type(box))
|
| 211 |
+
<class OCP.TopoDS.TopoDS_Solid>
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
If you want to cast an OCCT object into a Direct API one you can just pass it as a parameter of the intended class:
|
| 215 |
+
|
| 216 |
+
.. code-block::
|
| 217 |
+
|
| 218 |
+
>>> occt_box = BRepPrimAPI_MakeBox(5,5,5).Solid()
|
| 219 |
+
>>> print(type(occt_box))
|
| 220 |
+
<class OCP.TopoDS.TopoDS_Solid>
|
| 221 |
+
|
| 222 |
+
>>> direct_api_box = Solid(occt_box)
|
| 223 |
+
>>> print(type(direct_api_box))
|
| 224 |
+
<class cadquery.occ_impl.shapes.Solid>
|
| 225 |
+
|
| 226 |
+
.. note::
|
| 227 |
+
You can cast into the direct API the types found `here <https://dev.opencascade.org/doc/refman/html/class_topo_d_s___shape.html>`_
|
| 228 |
+
|
| 229 |
+
Multimethods
|
| 230 |
+
------------
|
| 231 |
+
|
| 232 |
+
CadQuery uses `Multimethod <https://coady.github.io/multimethod/>`_ to allow a call to a method to
|
| 233 |
+
be dispatched depending on the types of the arguments. An example is :meth:`~cadquery.Sketch.arc`,
|
| 234 |
+
where ``a_sketch.arc((1, 2), (2, 3))`` would be dispatched to one method but ``a_sketch.arc((1, 2),
|
| 235 |
+
(2, 3), (3, 4))`` would be dispatched to a different method. For multimethods to work, you should
|
| 236 |
+
not use keyword arguments to specify positional parameters. For example, you **should not** write
|
| 237 |
+
``a_sketch.arc(p1=(1, 2), p2=(2, 3), p3=(3, 4))``, instead you should use the previous example.
|
| 238 |
+
Note CadQuery makes an attempt to fall back on the first registered multimethod in the event of a
|
| 239 |
+
dispatch error, but it is still best practice to not use keyword arguments to specify positional
|
| 240 |
+
arguments in CadQuery.
|
| 241 |
+
|
| 242 |
+
Selectors
|
| 243 |
+
---------------------------
|
| 244 |
+
|
| 245 |
+
Selectors allow you to select one or more features, in order to define new features. As an example, you might
|
| 246 |
+
extrude a box, and then select the top face as the location for a new feature. Or, you might extrude a box, and
|
| 247 |
+
then select all of the vertical edges so that you can apply a fillet to them.
|
| 248 |
+
|
| 249 |
+
You can select Vertices, Edges, Faces, Solids, and Wires using selectors.
|
| 250 |
+
|
| 251 |
+
Think of selectors as the equivalent of your hand and mouse, if you were to build an object using a conventional CAD system.
|
| 252 |
+
|
| 253 |
+
See :ref:`selectors` to learn more.
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
Workplane class
|
| 257 |
+
---------------------------
|
| 258 |
+
|
| 259 |
+
The Workplane class contains the currently selected objects (a list of Shapes, Vectors or Locations
|
| 260 |
+
in the :attr:`~cadquery.Workplane.objects` attribute), the modelling context (in the
|
| 261 |
+
:attr:`~cadquery.Workplane.ctx` attribute), and CadQuery's fluent api methods. It is the main class
|
| 262 |
+
that users will instantiate.
|
| 263 |
+
|
| 264 |
+
See :ref:`apireference` to learn more.
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
Assemblies
|
| 268 |
+
----------
|
| 269 |
+
|
| 270 |
+
Simple models can be combined into complex, possibly nested, assemblies.
|
| 271 |
+
|
| 272 |
+
.. image:: _static/assy.png
|
| 273 |
+
|
| 274 |
+
A simple example could look as follows::
|
| 275 |
+
|
| 276 |
+
from cadquery import *
|
| 277 |
+
|
| 278 |
+
w = 10
|
| 279 |
+
d = 10
|
| 280 |
+
h = 10
|
| 281 |
+
|
| 282 |
+
part1 = Workplane().box(2 * w, 2 * d, h)
|
| 283 |
+
part2 = Workplane().box(w, d, 2 * h)
|
| 284 |
+
part3 = Workplane().box(w, d, 3 * h)
|
| 285 |
+
|
| 286 |
+
assy = (
|
| 287 |
+
Assembly(part1, loc=Location(Vector(-w, 0, h / 2)))
|
| 288 |
+
.add(
|
| 289 |
+
part2, loc=Location(Vector(1.5 * w, -0.5 * d, h / 2)), color=Color(0, 0, 1, 0.5)
|
| 290 |
+
)
|
| 291 |
+
.add(part3, loc=Location(Vector(-0.5 * w, -0.5 * d, 2 * h)), color=Color("red"))
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
Resulting in:
|
| 295 |
+
|
| 296 |
+
.. image:: _static/simple_assy.png
|
| 297 |
+
|
| 298 |
+
Note that the locations of the children parts are defined with respect to their parents - in the above example ``part3`` will be located at (-5,-5,20) in the global coordinate system. Assemblies with different colors can be created this way and exported to STEP or the native OCCT xml format.
|
| 299 |
+
|
| 300 |
+
You can browse assembly related methods here: :ref:`assembly`.
|
| 301 |
+
|
| 302 |
+
Assemblies with constraints
|
| 303 |
+
---------------------------
|
| 304 |
+
|
| 305 |
+
Sometimes it is not desirable to define the component positions explicitly but rather use
|
| 306 |
+
constraints to obtain a fully parametric assembly. This can be achieved in the following way::
|
| 307 |
+
|
| 308 |
+
from cadquery import *
|
| 309 |
+
|
| 310 |
+
w = 10
|
| 311 |
+
d = 10
|
| 312 |
+
h = 10
|
| 313 |
+
|
| 314 |
+
part1 = Workplane().box(2 * w, 2 * d, h)
|
| 315 |
+
part2 = Workplane().box(w, d, 2 * h)
|
| 316 |
+
part3 = Workplane().box(w, d, 3 * h)
|
| 317 |
+
|
| 318 |
+
assy = (
|
| 319 |
+
Assembly(part1, name="part1", loc=Location(Vector(-w, 0, h / 2)))
|
| 320 |
+
.add(part2, name="part2", color=Color(0, 0, 1, 0.5))
|
| 321 |
+
.add(part3, name="part3", color=Color("red"))
|
| 322 |
+
.constrain("part1@faces@>Z", "part3@faces@<Z", "Axis")
|
| 323 |
+
.constrain("part1@faces@>Z", "part2@faces@<Z", "Axis")
|
| 324 |
+
.constrain("part1@faces@>Y", "part3@faces@<Y", "Axis")
|
| 325 |
+
.constrain("part1@faces@>Y", "part2@faces@<Y", "Axis")
|
| 326 |
+
.constrain("part1@vertices@>(-1,-1,1)", "part3@vertices@>(-1,-1,-1)", "Point")
|
| 327 |
+
.constrain("part1@vertices@>(1,-1,-1)", "part2@vertices@>(-1,-1,-1)", "Point")
|
| 328 |
+
.solve()
|
| 329 |
+
)
|
| 330 |
+
|
| 331 |
+
This code results in identical object as one from the previous section. The added
|
| 332 |
+
benefit is that with changing parameters ``w``, ``d``, ``h`` the final locations
|
| 333 |
+
will be calculated automatically. It is admittedly dense and can be made clearer
|
| 334 |
+
using tags. Tags can be directly referenced when constructing the constraints::
|
| 335 |
+
|
| 336 |
+
from cadquery import *
|
| 337 |
+
|
| 338 |
+
w = 10
|
| 339 |
+
d = 10
|
| 340 |
+
h = 10
|
| 341 |
+
|
| 342 |
+
part1 = Workplane().box(2 * w, 2 * d, h)
|
| 343 |
+
part2 = Workplane().box(w, d, 2 * h)
|
| 344 |
+
part3 = Workplane().box(w, d, 3 * h)
|
| 345 |
+
|
| 346 |
+
part1.faces(">Z").edges("<X").vertices("<Y").tag("pt1")
|
| 347 |
+
part1.faces(">X").edges("<Z").vertices("<Y").tag("pt2")
|
| 348 |
+
part3.faces("<Z").edges("<X").vertices("<Y").tag("pt1")
|
| 349 |
+
part2.faces("<X").edges("<Z").vertices("<Y").tag("pt2")
|
| 350 |
+
|
| 351 |
+
assy1 = (
|
| 352 |
+
Assembly(part1, name="part1", loc=Location(Vector(-w, 0, h / 2)))
|
| 353 |
+
.add(part2, name="part2", color=Color(0, 0, 1, 0.5))
|
| 354 |
+
.add(part3, name="part3", color=Color("red"))
|
| 355 |
+
.constrain("part1@faces@>Z", "part3@faces@<Z", "Axis")
|
| 356 |
+
.constrain("part1@faces@>Z", "part2@faces@<Z", "Axis")
|
| 357 |
+
.constrain("part1@faces@>Y", "part3@faces@<Y", "Axis")
|
| 358 |
+
.constrain("part1@faces@>Y", "part2@faces@<Y", "Axis")
|
| 359 |
+
.constrain("part1?pt1", "part3?pt1", "Point")
|
| 360 |
+
.constrain("part1?pt2", "part2?pt2", "Point")
|
| 361 |
+
.solve()
|
| 362 |
+
)
|
| 363 |
+
|
| 364 |
+
The following constraints are currently implemented:
|
| 365 |
+
|
| 366 |
+
:Axis: two normal vectors are anti-coincident or the angle (in radians) between them is equal to the specified value. Can be defined for all entities with consistent normal vector - planar faces, wires and edges.
|
| 367 |
+
:Point: two points are coincident or separated by a specified distance. Can be defined for all entities, center of mass is used for lines, faces, solids and the vertex position for vertices.
|
| 368 |
+
:Plane: combination of :Axis: and :Point: constraints.
|
| 369 |
+
|
| 370 |
+
For a more elaborate assembly example see :ref:`assytutorial`.
|
server/docs/reference/quickstart.rst
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.. _quickstart:
|
| 2 |
+
|
| 3 |
+
***********************
|
| 4 |
+
QuickStart
|
| 5 |
+
***********************
|
| 6 |
+
|
| 7 |
+
.. currentmodule:: cadquery
|
| 8 |
+
|
| 9 |
+
Want a quick glimpse of what CadQuery can do? This quickstart will demonstrate the basics of CadQuery using a simple example
|
| 10 |
+
|
| 11 |
+
Prerequisites: CadQuery and CQ-editor installation
|
| 12 |
+
==================================================
|
| 13 |
+
|
| 14 |
+
If you have not already done so, follow the :ref:`installation`, to install CadQuery and CQ-editor.
|
| 15 |
+
|
| 16 |
+
After installation, run CQ-editor:
|
| 17 |
+
|
| 18 |
+
.. image:: _static/quickstart/001.png
|
| 19 |
+
|
| 20 |
+
Find the CadQuery code editor, on the left side. You'll see that we start out with the script for a simple block.
|
| 21 |
+
|
| 22 |
+
What we'll accomplish
|
| 23 |
+
========================
|
| 24 |
+
|
| 25 |
+
We will build a fully parametric bearing pillow block in this quickstart. Our finished object will look like this:
|
| 26 |
+
|
| 27 |
+
.. image:: _static/quickstart/000.png
|
| 28 |
+
|
| 29 |
+
**We would like our block to have these features:**
|
| 30 |
+
|
| 31 |
+
1. It should be sized to hold a single 608 ( 'skate' ) bearing, in the center of the block.
|
| 32 |
+
2. It should have counter-bored holes for M2 socket head cap screws at the corners.
|
| 33 |
+
3. The length and width of the block should be configurable by the user to any reasonable size.
|
| 34 |
+
|
| 35 |
+
A human would describe this as:
|
| 36 |
+
|
| 37 |
+
"A rectangular block 80mm x 60mm x 10mm , with counter-bored holes for M2 socket head cap screws
|
| 38 |
+
at the corners, and a circular pocket 22mm in diameter in the middle for a bearing."
|
| 39 |
+
|
| 40 |
+
Human descriptions are very elegant, right?
|
| 41 |
+
Hopefully our finished script will not be too much more complex than this human-oriented description.
|
| 42 |
+
|
| 43 |
+
Let's see how we do.
|
| 44 |
+
|
| 45 |
+
Start With A single, simple Plate
|
| 46 |
+
======================================
|
| 47 |
+
|
| 48 |
+
Let's start with a simple model that makes nothing but a rectangular block, but
|
| 49 |
+
with place-holders for the dimensions. Paste this into the code editor:
|
| 50 |
+
|
| 51 |
+
.. code-block:: python
|
| 52 |
+
:linenos:
|
| 53 |
+
|
| 54 |
+
height = 60.0
|
| 55 |
+
width = 80.0
|
| 56 |
+
thickness = 10.0
|
| 57 |
+
|
| 58 |
+
# make the base
|
| 59 |
+
result = cq.Workplane("XY").box(height, width, thickness)
|
| 60 |
+
|
| 61 |
+
# Render the solid
|
| 62 |
+
show_object(result)
|
| 63 |
+
|
| 64 |
+
Press the green Render button in the toolbar to run the script. You should see our base object.
|
| 65 |
+
|
| 66 |
+
.. image:: _static/quickstart/002.png
|
| 67 |
+
|
| 68 |
+
Nothing special, but its a start!
|
| 69 |
+
|
| 70 |
+
Add the Holes
|
| 71 |
+
================
|
| 72 |
+
|
| 73 |
+
Our pillow block needs to have a 22mm diameter hole in the center to hold the bearing.
|
| 74 |
+
|
| 75 |
+
This modification will do the trick:
|
| 76 |
+
|
| 77 |
+
.. code-block:: python
|
| 78 |
+
:linenos:
|
| 79 |
+
:emphasize-lines: 4,10-12
|
| 80 |
+
|
| 81 |
+
height = 60.0
|
| 82 |
+
width = 80.0
|
| 83 |
+
thickness = 10.0
|
| 84 |
+
diameter = 22.0
|
| 85 |
+
|
| 86 |
+
# make the base
|
| 87 |
+
result = (
|
| 88 |
+
cq.Workplane("XY")
|
| 89 |
+
.box(height, width, thickness)
|
| 90 |
+
.faces(">Z")
|
| 91 |
+
.workplane()
|
| 92 |
+
.hole(diameter)
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
# Render the solid
|
| 96 |
+
show_object(result)
|
| 97 |
+
|
| 98 |
+
Rebuild your model by clicking the Render button. Your block should look like this:
|
| 99 |
+
|
| 100 |
+
.. image:: _static/quickstart/003.png
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
The code is pretty compact, let's step through it.
|
| 104 |
+
|
| 105 |
+
**Line 4** adds a new parameter, diameter, for the diameter of the hole
|
| 106 |
+
|
| 107 |
+
**Lines 10-12**, we're adding the hole.
|
| 108 |
+
:py:meth:`cadquery.Workplane.faces` selects the top-most face in the Z direction, and then
|
| 109 |
+
:py:meth:`cadquery.Workplane.workplane` begins a new workplane located on this face. The center of this workplane
|
| 110 |
+
is located at the center of mass of the shape, which in this case is the center of the plate.
|
| 111 |
+
Finally, :py:meth:`cadquery.Workplane.hole` drills a hole through the part, 22mm in diameter.
|
| 112 |
+
|
| 113 |
+
.. note::
|
| 114 |
+
|
| 115 |
+
Don't worry about the CadQuery syntax now.. you can learn all about it in the :ref:`apireference` later.
|
| 116 |
+
|
| 117 |
+
More Holes
|
| 118 |
+
============
|
| 119 |
+
|
| 120 |
+
Ok, that hole was not too hard, but what about the counter-bored holes in the corners?
|
| 121 |
+
|
| 122 |
+
An M2 Socket head cap screw has these dimensions:
|
| 123 |
+
|
| 124 |
+
* **Head Diameter** : 3.8 mm
|
| 125 |
+
* **Head height** : 2.0 mm
|
| 126 |
+
* **Clearance Hole** : 2.4 mm
|
| 127 |
+
* **CounterBore diameter** : 4.4 mm
|
| 128 |
+
|
| 129 |
+
The centers of these holes should be 6mm from the edges of the block. And,
|
| 130 |
+
we want the block to work correctly even when the block is re-sized by the user.
|
| 131 |
+
|
| 132 |
+
**Don't tell me** we'll have to repeat the steps above 8 times to get counter-bored holes?
|
| 133 |
+
Good news!-- we can get the job done with just a few lines of code. Here's the code we need:
|
| 134 |
+
|
| 135 |
+
.. code-block:: python
|
| 136 |
+
:linenos:
|
| 137 |
+
:emphasize-lines: 5,14-18
|
| 138 |
+
|
| 139 |
+
height = 60.0
|
| 140 |
+
width = 80.0
|
| 141 |
+
thickness = 10.0
|
| 142 |
+
diameter = 22.0
|
| 143 |
+
padding = 12.0
|
| 144 |
+
|
| 145 |
+
# make the base
|
| 146 |
+
result = (
|
| 147 |
+
cq.Workplane("XY")
|
| 148 |
+
.box(height, width, thickness)
|
| 149 |
+
.faces(">Z")
|
| 150 |
+
.workplane()
|
| 151 |
+
.hole(diameter)
|
| 152 |
+
.faces(">Z")
|
| 153 |
+
.workplane()
|
| 154 |
+
.rect(height - padding, width - padding, forConstruction=True)
|
| 155 |
+
.vertices()
|
| 156 |
+
.cboreHole(2.4, 4.4, 2.1)
|
| 157 |
+
)
|
| 158 |
+
# Render the solid
|
| 159 |
+
show_object(result)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
After clicking the Render button to re-execute the model, you should see something like this:
|
| 163 |
+
|
| 164 |
+
.. image:: _static/quickstart/004.png
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
There is quite a bit going on here, so let's break it down a bit.
|
| 168 |
+
|
| 169 |
+
**Line 5** creates a new padding parameter that decides how far the holes are from the edges of the plate.
|
| 170 |
+
|
| 171 |
+
**Lines 14-15** selects the top-most face of the block, and creates a workplane on the top of that face, which we'll use to
|
| 172 |
+
define the centers of the holes in the corners.
|
| 173 |
+
|
| 174 |
+
**Line 16** draws a rectangle 12mm smaller than the overall length and width of the block, which we will use to
|
| 175 |
+
locate the corner holes. We'll use the vertices ( corners ) of this rectangle to locate the holes. The rectangle's
|
| 176 |
+
center is at the center of the workplane, which in this case coincides with the center of the bearing hole.
|
| 177 |
+
|
| 178 |
+
There are a couple of things to note about this line:
|
| 179 |
+
|
| 180 |
+
1. The :py:meth:`cadquery.Workplane.rect` function draws a rectangle. **forConstruction=True**
|
| 181 |
+
tells CadQuery that this rectangle will not form a part of the solid,
|
| 182 |
+
but we are just using it to help define some other geometry.
|
| 183 |
+
2. Unless you specify otherwise, a rectangle is drawn with its center on the current workplane center-- in
|
| 184 |
+
this case, the center of the top face of the block. So this rectangle will be centered on the face.
|
| 185 |
+
|
| 186 |
+
**Line 17** selects the vertices of the rectangle, which we will use for the centers of the holes.
|
| 187 |
+
The :py:meth:`cadquery.Workplane.vertices` function selects the corners of the rectangle.
|
| 188 |
+
|
| 189 |
+
**Line 18** uses the cboreHole function to draw the holes.
|
| 190 |
+
The :py:meth:`cadquery.Workplane.cboreHole` function is a handy CadQuery function that makes a counterbored hole.
|
| 191 |
+
Like most other CadQuery functions, it operates on the values on the stack. In this case, since we
|
| 192 |
+
selected the four vertices before calling the function, the function operates on each of the four points--
|
| 193 |
+
which results in a counterbore hole at each of the rectangle corners.
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
Filleting
|
| 197 |
+
===========
|
| 198 |
+
|
| 199 |
+
Almost done. Let's just round the corners of the block a bit. That's easy, we just need to select the edges
|
| 200 |
+
and then fillet them:
|
| 201 |
+
|
| 202 |
+
We can do that using the preset dictionaries in the parameter definition:
|
| 203 |
+
|
| 204 |
+
.. code-block:: python
|
| 205 |
+
:linenos:
|
| 206 |
+
:emphasize-lines: 19-20
|
| 207 |
+
|
| 208 |
+
height = 60.0
|
| 209 |
+
width = 80.0
|
| 210 |
+
thickness = 10.0
|
| 211 |
+
diameter = 22.0
|
| 212 |
+
padding = 12.0
|
| 213 |
+
|
| 214 |
+
# make the base
|
| 215 |
+
result = (
|
| 216 |
+
cq.Workplane("XY")
|
| 217 |
+
.box(height, width, thickness)
|
| 218 |
+
.faces(">Z")
|
| 219 |
+
.workplane()
|
| 220 |
+
.hole(diameter)
|
| 221 |
+
.faces(">Z")
|
| 222 |
+
.workplane()
|
| 223 |
+
.rect(height - padding, width - padding, forConstruction=True)
|
| 224 |
+
.vertices()
|
| 225 |
+
.cboreHole(2.4, 4.4, 2.1)
|
| 226 |
+
.edges("|Z")
|
| 227 |
+
.fillet(2.0)
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
# Render the solid
|
| 231 |
+
show_object(result)
|
| 232 |
+
|
| 233 |
+
**Line 19** To grab the right edges, the :py:meth:`cadquery.Workplane.edges` selects all of the
|
| 234 |
+
edges that are parallel to the Z axis ("\|Z").
|
| 235 |
+
|
| 236 |
+
**Line 20** fillets the edges using the :py:meth:`cadquery.Workplane.fillet` method.
|
| 237 |
+
|
| 238 |
+
The finished product looks like this:
|
| 239 |
+
|
| 240 |
+
.. image:: _static/quickstart/005.png
|
| 241 |
+
|
| 242 |
+
Exporting
|
| 243 |
+
=========
|
| 244 |
+
|
| 245 |
+
If you want to fabricate a physical object you need to export the result to STL or DXF. Additionally, exporting as STEP for post-processing in another CAD tool is also possible.
|
| 246 |
+
|
| 247 |
+
This can be easily accomplished using the :py:meth:`cadquery.exporters.export` function:
|
| 248 |
+
|
| 249 |
+
.. code-block:: python
|
| 250 |
+
:linenos:
|
| 251 |
+
:emphasize-lines: 27-29
|
| 252 |
+
|
| 253 |
+
height = 60.0
|
| 254 |
+
width = 80.0
|
| 255 |
+
thickness = 10.0
|
| 256 |
+
diameter = 22.0
|
| 257 |
+
padding = 12.0
|
| 258 |
+
|
| 259 |
+
# make the base
|
| 260 |
+
result = (
|
| 261 |
+
cq.Workplane("XY")
|
| 262 |
+
.box(height, width, thickness)
|
| 263 |
+
.faces(">Z")
|
| 264 |
+
.workplane()
|
| 265 |
+
.hole(diameter)
|
| 266 |
+
.faces(">Z")
|
| 267 |
+
.workplane()
|
| 268 |
+
.rect(height - padding, width - padding, forConstruction=True)
|
| 269 |
+
.vertices()
|
| 270 |
+
.cboreHole(2.4, 4.4, 2.1)
|
| 271 |
+
.edges("|Z")
|
| 272 |
+
.fillet(2.0)
|
| 273 |
+
)
|
| 274 |
+
|
| 275 |
+
# Render the solid
|
| 276 |
+
show_object(result)
|
| 277 |
+
|
| 278 |
+
# Export
|
| 279 |
+
cq.exporters.export(result, "result.stl")
|
| 280 |
+
cq.exporters.export(result.section(), "result.dxf")
|
| 281 |
+
cq.exporters.export(result, "result.step")
|
| 282 |
+
|
| 283 |
+
Done!
|
| 284 |
+
============
|
| 285 |
+
|
| 286 |
+
You just made a parametric, model that can generate pretty much any bearing pillow block
|
| 287 |
+
with <30 lines of code.
|
| 288 |
+
|
| 289 |
+
Want to learn more?
|
| 290 |
+
====================
|
| 291 |
+
|
| 292 |
+
* The :ref:`examples` contains lots of examples demonstrating cadquery features
|
| 293 |
+
* The :ref:`apireference` is a good overview of language features grouped by function
|
| 294 |
+
* The :ref:`classreference` is the hard-core listing of all functions available.
|
server/docs/reference/selectors.rst
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.. _selector_reference:
|
| 2 |
+
|
| 3 |
+
Selectors Reference
|
| 4 |
+
===================
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
CadQuery selector strings allow filtering various types of object lists. Most commonly, Edges, Faces, and Vertices are
|
| 8 |
+
used, but all objects types can be filtered.
|
| 9 |
+
|
| 10 |
+
Object lists are created by using the following methods, which each collect a type of shape:
|
| 11 |
+
|
| 12 |
+
* :py:meth:`cadquery.Workplane.vertices`
|
| 13 |
+
* :py:meth:`cadquery.Workplane.edges`
|
| 14 |
+
* :py:meth:`cadquery.Workplane.faces`
|
| 15 |
+
* :py:meth:`cadquery.Workplane.shells`
|
| 16 |
+
* :py:meth:`cadquery.Workplane.solids`
|
| 17 |
+
|
| 18 |
+
Each of these methods accepts either a Selector object or a string. String selectors are simply
|
| 19 |
+
shortcuts for using the full object equivalents. If you pass one of the string patterns in,
|
| 20 |
+
CadQuery will automatically use the associated selector object.
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
.. note::
|
| 24 |
+
|
| 25 |
+
String selectors are simply shortcuts to concrete selector classes, which you can use or
|
| 26 |
+
extend. For a full description of how each selector class works, see :ref:`classreference`.
|
| 27 |
+
|
| 28 |
+
If you find that the built-in selectors are not sufficient, you can easily plug in your own.
|
| 29 |
+
See :ref:`extending` to see how.
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
Combining Selectors
|
| 33 |
+
--------------------------
|
| 34 |
+
|
| 35 |
+
Selectors can be combined logically, currently defined operators include **and**, **or**, **not** and **exc[ept]** (set difference). For example:
|
| 36 |
+
|
| 37 |
+
.. cadquery::
|
| 38 |
+
|
| 39 |
+
result = cq.Workplane("XY").box(2, 2, 2).edges("|Z and >Y").chamfer(0.2)
|
| 40 |
+
|
| 41 |
+
Much more complex expressions are possible as well:
|
| 42 |
+
|
| 43 |
+
.. cadquery::
|
| 44 |
+
|
| 45 |
+
result = (
|
| 46 |
+
cq.Workplane("XY")
|
| 47 |
+
.box(2, 2, 2)
|
| 48 |
+
.faces(">Z")
|
| 49 |
+
.shell(-0.2)
|
| 50 |
+
.faces(">Z")
|
| 51 |
+
.edges("not(<X or >X or <Y or >Y)")
|
| 52 |
+
.chamfer(0.1)
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
.. _filteringfaces:
|
| 56 |
+
|
| 57 |
+
Filtering Faces
|
| 58 |
+
----------------
|
| 59 |
+
|
| 60 |
+
All types of string selectors work on faces. In most cases, the selector refers to the direction
|
| 61 |
+
of the **normal vector** of the face.
|
| 62 |
+
|
| 63 |
+
.. warning::
|
| 64 |
+
|
| 65 |
+
If a face is not planar, selectors are evaluated at the center of mass of the face. This can lead
|
| 66 |
+
to results that are quite unexpected.
|
| 67 |
+
|
| 68 |
+
The axis used in the listing below are for illustration: any axis would work similarly in each case.
|
| 69 |
+
|
| 70 |
+
========= ========================================= =======================================================
|
| 71 |
+
Selector Selects Selector Class
|
| 72 |
+
========= ========================================= =======================================================
|
| 73 |
+
+Z Faces with normal in +z direction :py:class:`cadquery.DirectionSelector`
|
| 74 |
+
\|Z Faces with normal parallel to z dir :py:class:`cadquery.ParallelDirSelector`
|
| 75 |
+
-X Faces with normal in neg x direction :py:class:`cadquery.DirectionSelector`
|
| 76 |
+
#Z Faces with normal orthogonal to z dir :py:class:`cadquery.PerpendicularDirSelector`
|
| 77 |
+
%Plane Faces of type plane :py:class:`cadquery.TypeSelector`
|
| 78 |
+
>Y Face farthest in the positive y dir :py:class:`cadquery.DirectionMinMaxSelector`
|
| 79 |
+
<Y Face farthest in the negative y dir :py:class:`cadquery.DirectionMinMaxSelector`
|
| 80 |
+
>Y[-2] 2nd farthest Face **normal** to the y dir :py:class:`cadquery.DirectionNthSelector`
|
| 81 |
+
<Y[0] 1st closest Face **normal** to the y dir :py:class:`cadquery.DirectionNthSelector`
|
| 82 |
+
>>Y[-2] 2nd farthest Face in the y dir :py:class:`cadquery.CenterNthSelector`
|
| 83 |
+
<<Y[0] 1st closest Face in the y dir :py:class:`cadquery.CenterNthSelector`
|
| 84 |
+
========= ========================================= =======================================================
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
.. _filteringedges:
|
| 88 |
+
|
| 89 |
+
Filtering Edges
|
| 90 |
+
----------------
|
| 91 |
+
|
| 92 |
+
The selector usually refers to the **direction** of the edge.
|
| 93 |
+
|
| 94 |
+
.. warning::
|
| 95 |
+
|
| 96 |
+
Non-linear edges are not selected for any string selectors except type (%) and center (>>).
|
| 97 |
+
Non-linear edges are never returned when these filters are applied.
|
| 98 |
+
|
| 99 |
+
The axis used in the listing below are for illustration: any axis would work similarly in each case.
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
======== ==================================================== =============================================
|
| 103 |
+
Selector Selects Selector Class
|
| 104 |
+
======== ==================================================== =============================================
|
| 105 |
+
+Z Edges aligned in the Z direction :py:class:`cadquery.DirectionSelector`
|
| 106 |
+
\|Z Edges parallel to z direction :py:class:`cadquery.ParallelDirSelector`
|
| 107 |
+
-X Edges aligned in neg x direction :py:class:`cadquery.DirectionSelector`
|
| 108 |
+
#Z Edges perpendicular to z direction :py:class:`cadquery.PerpendicularDirSelector`
|
| 109 |
+
%Line Edges of type line :py:class:`cadquery.TypeSelector`
|
| 110 |
+
>Y Edges farthest in the positive y dir :py:class:`cadquery.DirectionMinMaxSelector`
|
| 111 |
+
<Y Edges farthest in the negative y dir :py:class:`cadquery.DirectionMinMaxSelector`
|
| 112 |
+
>Y[1] 2nd closest **parallel** edge in the positive y dir :py:class:`cadquery.DirectionNthSelector`
|
| 113 |
+
<Y[-2] 2nd farthest **parallel** edge in the negative y dir :py:class:`cadquery.DirectionNthSelector`
|
| 114 |
+
>>Y[-2] 2nd farthest edge in the y dir :py:class:`cadquery.CenterNthSelector`
|
| 115 |
+
<<Y[0] 1st closest edge in the y dir :py:class:`cadquery.CenterNthSelector`
|
| 116 |
+
======== ==================================================== =============================================
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
.. _filteringvertices:
|
| 120 |
+
|
| 121 |
+
Filtering Vertices
|
| 122 |
+
-------------------
|
| 123 |
+
|
| 124 |
+
Only a few of the filter types apply to vertices. The location of the vertex is the subject of the filter.
|
| 125 |
+
|
| 126 |
+
========= ======================================= =======================================================
|
| 127 |
+
Selector Selects Selector Class
|
| 128 |
+
========= ======================================= =======================================================
|
| 129 |
+
>Y Vertices farthest in the positive y dir :py:class:`cadquery.DirectionMinMaxSelector`
|
| 130 |
+
<Y Vertices farthest in the negative y dir :py:class:`cadquery.DirectionMinMaxSelector`
|
| 131 |
+
>>Y[-2] 2nd farthest vertex in the y dir :py:class:`cadquery.CenterNthSelector`
|
| 132 |
+
<<Y[0] 1st closest vertex in the y dir :py:class:`cadquery.CenterNthSelector`
|
| 133 |
+
========= ======================================= =======================================================
|
| 134 |
+
|
| 135 |
+
User-defined Directions
|
| 136 |
+
-----------------------
|
| 137 |
+
|
| 138 |
+
It is possible to use user defined vectors as a basis for the selectors. For example:
|
| 139 |
+
|
| 140 |
+
.. cadquery::
|
| 141 |
+
|
| 142 |
+
result = cq.Workplane("XY").box(10, 10, 10)
|
| 143 |
+
|
| 144 |
+
# chamfer only one edge
|
| 145 |
+
result = result.edges(">(-1, 1, 0)").chamfer(1)
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
Topological Selectors
|
| 149 |
+
---------------------
|
| 150 |
+
|
| 151 |
+
Is is also possible to use topological relations to select objects. Currently
|
| 152 |
+
the following methods are supported:
|
| 153 |
+
|
| 154 |
+
* :py:meth:`cadquery.Workplane.ancestors`
|
| 155 |
+
* :py:meth:`cadquery.Workplane.siblings`
|
| 156 |
+
|
| 157 |
+
Ancestors allows to select all objects containing currently selected object.
|
| 158 |
+
|
| 159 |
+
.. cadquery::
|
| 160 |
+
|
| 161 |
+
result = cq.Workplane("XY").box(10, 10, 10).faces(">Z").edges("<Y")
|
| 162 |
+
|
| 163 |
+
result = result.ancestors("Face")
|
| 164 |
+
|
| 165 |
+
Siblings allows to select all objects of the same type as selection that are connected
|
| 166 |
+
via the specfied kind of elements.
|
| 167 |
+
|
| 168 |
+
.. cadquery::
|
| 169 |
+
|
| 170 |
+
result = cq.Workplane("XY").box(10, 10, 10).faces(">Z")
|
| 171 |
+
|
| 172 |
+
result = result.siblings("Edge")
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
Using selectors with Shape and Sketch objects
|
| 176 |
+
---------------------------------------------
|
| 177 |
+
|
| 178 |
+
It is possible to use selectors with :py:class:`cadquery.Shape` and :py:class:`cadquery.Sketch`
|
| 179 |
+
objects. This includes chaining and combining.
|
| 180 |
+
|
| 181 |
+
.. cadquery::
|
| 182 |
+
|
| 183 |
+
box = cq.Solid.makeBox(1,2,3)
|
| 184 |
+
|
| 185 |
+
# select top and bottom wires
|
| 186 |
+
result = box.faces(">Z or <Z").wires()
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
Additional special methods
|
| 192 |
+
--------------------------
|
| 193 |
+
|
| 194 |
+
:py:class:`cadquery.Workplane` and :py:class:`cadquery.Sketch` provide the following special methods that can be used
|
| 195 |
+
for quick prototyping of selectors when implementing a complete selector via subclassing of
|
| 196 |
+
:py:class:`cadquery.Selector` is not desirable.
|
| 197 |
+
|
| 198 |
+
* :py:meth:`cadquery.Workplane.filter`
|
| 199 |
+
* :py:meth:`cadquery.Workplane.sort`
|
| 200 |
+
* :py:meth:`cadquery.Workplane.__getitem__`
|
| 201 |
+
* :py:meth:`cadquery.Sketch.filter`
|
| 202 |
+
* :py:meth:`cadquery.Sketch.sort`
|
| 203 |
+
* :py:meth:`cadquery.Sketch.__getitem__`
|
| 204 |
+
|
| 205 |
+
For example, one could use those methods for selecting objects within a certain range of volumes.
|
| 206 |
+
|
| 207 |
+
.. cadquery::
|
| 208 |
+
|
| 209 |
+
from cadquery.occ_impl.shapes import box
|
| 210 |
+
|
| 211 |
+
result = (
|
| 212 |
+
cq.Workplane()
|
| 213 |
+
.add([box(1,1,i+1).moved(x=2*i) for i in range(5)])
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
# select boxes with volume <= 3
|
| 217 |
+
result = result.filter(lambda s: s.Volume() <= 3)
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
The same can be achieved using sorting.
|
| 221 |
+
|
| 222 |
+
.. cadquery::
|
| 223 |
+
|
| 224 |
+
from cadquery.occ_impl.shapes import box
|
| 225 |
+
|
| 226 |
+
result = (
|
| 227 |
+
cq.Workplane()
|
| 228 |
+
.add([box(1,1,i+1).moved(x=2*i) for i in range(5)])
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
# select boxes with volume <= 3
|
| 232 |
+
result = result.sort(lambda s: s.Volume())[:3]
|
server/docs/reference/sketch.rst
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.. _sketchtutorial:
|
| 2 |
+
|
| 3 |
+
******
|
| 4 |
+
Sketch
|
| 5 |
+
******
|
| 6 |
+
|
| 7 |
+
Sketch tutorial
|
| 8 |
+
---------------
|
| 9 |
+
|
| 10 |
+
The purpose of this section is to demonstrate how to construct sketches using different
|
| 11 |
+
approaches.
|
| 12 |
+
|
| 13 |
+
Face-based API
|
| 14 |
+
==============
|
| 15 |
+
|
| 16 |
+
The main approach for constructing sketches is based on constructing faces and
|
| 17 |
+
combining them using boolean operations.
|
| 18 |
+
|
| 19 |
+
.. cadquery::
|
| 20 |
+
:height: 600px
|
| 21 |
+
|
| 22 |
+
import cadquery as cq
|
| 23 |
+
|
| 24 |
+
result = (
|
| 25 |
+
cq.Sketch()
|
| 26 |
+
.trapezoid(4, 3, 90)
|
| 27 |
+
.vertices()
|
| 28 |
+
.circle(0.5, mode="s")
|
| 29 |
+
.reset()
|
| 30 |
+
.vertices()
|
| 31 |
+
.fillet(0.25)
|
| 32 |
+
.reset()
|
| 33 |
+
.rarray(0.6, 1, 5, 1)
|
| 34 |
+
.slot(1.5, 0.4, mode="s", angle=90)
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
Note that selectors are implemented, but selection has to be explicitly reset. Sketch
|
| 38 |
+
class does not implement history and all modifications happen in-place.
|
| 39 |
+
|
| 40 |
+
Modes
|
| 41 |
+
^^^^^
|
| 42 |
+
|
| 43 |
+
Every operation from the face API accepts a mode parameter to define
|
| 44 |
+
how to combine the created object with existing ones. It can be fused (``mode='a'``),
|
| 45 |
+
cut (``mode='s'``), intersected (``mode='i'``), replaced (``mode='r'``)
|
| 46 |
+
or just stored for construction (``mode='c'``).
|
| 47 |
+
In the last case, it is mandatory to specify a ``tag`` in order to be able to
|
| 48 |
+
refer to the object later on. By default faces are fused together.
|
| 49 |
+
Note the usage of the subtractive and additive modes in the example above.
|
| 50 |
+
The additional two are demonstrated below.
|
| 51 |
+
|
| 52 |
+
.. cadquery::
|
| 53 |
+
:height: 600px
|
| 54 |
+
|
| 55 |
+
result = (
|
| 56 |
+
cq.Sketch()
|
| 57 |
+
.rect(1, 2, mode="c", tag="base")
|
| 58 |
+
.vertices(tag="base")
|
| 59 |
+
.circle(0.7)
|
| 60 |
+
.reset()
|
| 61 |
+
.edges("|Y", tag="base")
|
| 62 |
+
.ellipse(1.2, 1, mode="i")
|
| 63 |
+
.reset()
|
| 64 |
+
.rect(2, 2, mode="i")
|
| 65 |
+
.clean()
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
Edge-based API
|
| 70 |
+
==============
|
| 71 |
+
|
| 72 |
+
If needed, one can construct sketches by placing individual edges.
|
| 73 |
+
|
| 74 |
+
.. cadquery::
|
| 75 |
+
:height: 600px
|
| 76 |
+
|
| 77 |
+
import cadquery as cq
|
| 78 |
+
|
| 79 |
+
result = (
|
| 80 |
+
cq.Sketch()
|
| 81 |
+
.segment((0.0, 0), (0.0, 2.0))
|
| 82 |
+
.segment((2.0, 0))
|
| 83 |
+
.close()
|
| 84 |
+
.arc((0.6, 0.6), 0.4, 0.0, 360.0)
|
| 85 |
+
.assemble(tag="face")
|
| 86 |
+
.edges("%LINE", tag="face")
|
| 87 |
+
.vertices()
|
| 88 |
+
.chamfer(0.2)
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
Once the construction is finished it has to be converted to the face-based representation
|
| 92 |
+
using :meth:`~cadquery.Sketch.assemble`. Afterwards, face based operations can be applied.
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
Convex hull
|
| 96 |
+
===========
|
| 97 |
+
|
| 98 |
+
.. warning:: The Convex Hull feature is currently experimental.
|
| 99 |
+
|
| 100 |
+
For certain special use-cases convex hull can be constructed from straight segments
|
| 101 |
+
and circles.
|
| 102 |
+
|
| 103 |
+
.. cadquery::
|
| 104 |
+
:height: 600px
|
| 105 |
+
|
| 106 |
+
result = (
|
| 107 |
+
cq.Sketch()
|
| 108 |
+
.arc((0, 0), 1.0, 0.0, 360.0)
|
| 109 |
+
.arc((1, 1.5), 0.5, 0.0, 360.0)
|
| 110 |
+
.segment((0.0, 2), (-1, 3.0))
|
| 111 |
+
.hull()
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
Constraint-based sketches
|
| 115 |
+
=========================
|
| 116 |
+
|
| 117 |
+
.. warning:: The 2D Sketch constraints and solver is currently experimental.
|
| 118 |
+
|
| 119 |
+
Finally, if desired, geometric constraints can be used to construct sketches. So
|
| 120 |
+
far only line segments and arcs can be used in such a use case.
|
| 121 |
+
|
| 122 |
+
.. cadquery::
|
| 123 |
+
:height: 600px
|
| 124 |
+
|
| 125 |
+
import cadquery as cq
|
| 126 |
+
|
| 127 |
+
result = (
|
| 128 |
+
cq.Sketch()
|
| 129 |
+
.segment((0, 0), (0, 3.0), "s1")
|
| 130 |
+
.arc((0.0, 3.0), (1.5, 1.5), (0.0, 0.0), "a1")
|
| 131 |
+
.constrain("s1", "Fixed", None)
|
| 132 |
+
.constrain("s1", "a1", "Coincident", None)
|
| 133 |
+
.constrain("a1", "s1", "Coincident", None)
|
| 134 |
+
.constrain("s1", "a1", "Angle", 45)
|
| 135 |
+
.solve()
|
| 136 |
+
.assemble()
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
Following constraints are implemented. Arguments are passed in as one tuple in :meth:`~cadquery.Sketch.constrain`. In this table, `0..1` refers to a float between 0 and 1 where 0 would create a constraint relative to the start of the element, and 1 the end.
|
| 140 |
+
|
| 141 |
+
.. list-table::
|
| 142 |
+
:widths: 15 10 15 30 30
|
| 143 |
+
:header-rows: 1
|
| 144 |
+
|
| 145 |
+
* - Name
|
| 146 |
+
- Arity
|
| 147 |
+
- Entities
|
| 148 |
+
- Arguments
|
| 149 |
+
- Description
|
| 150 |
+
* - FixedPoint
|
| 151 |
+
- 1
|
| 152 |
+
- All
|
| 153 |
+
- `None` for arc center or `0..1` for point on segment/arc
|
| 154 |
+
- Specified point is fixed
|
| 155 |
+
* - Coincident
|
| 156 |
+
- 2
|
| 157 |
+
- All
|
| 158 |
+
- None
|
| 159 |
+
- Specified points coincide
|
| 160 |
+
* - Angle
|
| 161 |
+
- 2
|
| 162 |
+
- All
|
| 163 |
+
- `angle`
|
| 164 |
+
- Angle between the tangents of the two entities is fixed
|
| 165 |
+
* - Length
|
| 166 |
+
- 1
|
| 167 |
+
- All
|
| 168 |
+
- `length`
|
| 169 |
+
- Specified entity has fixed length
|
| 170 |
+
* - Distance
|
| 171 |
+
- 2
|
| 172 |
+
- All
|
| 173 |
+
- `None or 0..1, None or 0..1, distance`
|
| 174 |
+
- Distance between two points is fixed
|
| 175 |
+
* - Radius
|
| 176 |
+
- 1
|
| 177 |
+
- Arc
|
| 178 |
+
- `radius`
|
| 179 |
+
- Specified entity has a fixed radius
|
| 180 |
+
* - Orientation
|
| 181 |
+
- 1
|
| 182 |
+
- Segment
|
| 183 |
+
- `x,y`
|
| 184 |
+
- Specified entity is parallel to `(x,y)`
|
| 185 |
+
* - ArcAngle
|
| 186 |
+
- 1
|
| 187 |
+
- Arc
|
| 188 |
+
- `angle`
|
| 189 |
+
- Specified entity is fixed angular span
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
Workplane integration
|
| 193 |
+
---------------------
|
| 194 |
+
|
| 195 |
+
Once created, a sketch can be used to construct various features on a workplane.
|
| 196 |
+
Supported operations include :meth:`~cadquery.Workplane.extrude`,
|
| 197 |
+
:meth:`~cadquery.Workplane.twistExtrude`, :meth:`~cadquery.Workplane.revolve`,
|
| 198 |
+
:meth:`~cadquery.Workplane.sweep`, :meth:`~cadquery.Workplane.cutBlind`, :meth:`~cadquery.Workplane.cutThruAll` and :meth:`~cadquery.Workplane.loft`.
|
| 199 |
+
|
| 200 |
+
Sketches can be created as separate entities and reused, but also created ad-hoc
|
| 201 |
+
in one fluent chain of calls as shown below.
|
| 202 |
+
|
| 203 |
+
Sketches in-place
|
| 204 |
+
=================
|
| 205 |
+
|
| 206 |
+
Constructing sketches in-place can be accomplished as follows.
|
| 207 |
+
|
| 208 |
+
.. cadquery::
|
| 209 |
+
:height: 600px
|
| 210 |
+
|
| 211 |
+
import cadquery as cq
|
| 212 |
+
|
| 213 |
+
result = (
|
| 214 |
+
cq.Workplane()
|
| 215 |
+
.box(5, 5, 1)
|
| 216 |
+
.faces(">Z")
|
| 217 |
+
.sketch()
|
| 218 |
+
.regularPolygon(2, 3, tag="outer")
|
| 219 |
+
.regularPolygon(1.5, 3, mode="s")
|
| 220 |
+
.vertices(tag="outer")
|
| 221 |
+
.fillet(0.2)
|
| 222 |
+
.finalize()
|
| 223 |
+
.extrude(0.5)
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
Sketch API is available after the :meth:`~cadquery.Workplane.sketch` call and original `workplane`.
|
| 227 |
+
|
| 228 |
+
Placing an existing sketch on a workplane
|
| 229 |
+
=========================================
|
| 230 |
+
|
| 231 |
+
Sometimes it is desired to place an existing sketches as-is on a workplane. This can be done with :meth:`~cadquery.Workplane.placeSketch`
|
| 232 |
+
|
| 233 |
+
.. cadquery::
|
| 234 |
+
:height: 600px
|
| 235 |
+
|
| 236 |
+
import cadquery as cq
|
| 237 |
+
|
| 238 |
+
s = cq.Sketch().trapezoid(3, 1, 110).vertices().fillet(0.2)
|
| 239 |
+
|
| 240 |
+
result = (
|
| 241 |
+
cq.Workplane()
|
| 242 |
+
.box(5, 5, 5)
|
| 243 |
+
.faces(">X")
|
| 244 |
+
.workplane()
|
| 245 |
+
.transformed((0, 0, -90))
|
| 246 |
+
.placeSketch(s)
|
| 247 |
+
.cutThruAll()
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
Sketches spanning multiple elements
|
| 251 |
+
===================================
|
| 252 |
+
|
| 253 |
+
When multiple elements are selected before constructing the sketch, multiple sketches will be created.
|
| 254 |
+
|
| 255 |
+
Note that the sketch is placed on all locations that are on the top of the stack.
|
| 256 |
+
|
| 257 |
+
.. cadquery::
|
| 258 |
+
:height: 600px
|
| 259 |
+
|
| 260 |
+
import cadquery as cq
|
| 261 |
+
|
| 262 |
+
result = (
|
| 263 |
+
cq.Workplane()
|
| 264 |
+
.box(5, 5, 1)
|
| 265 |
+
.faces(">Z")
|
| 266 |
+
.workplane()
|
| 267 |
+
.rarray(2, 2, 2, 2)
|
| 268 |
+
.rect(1.5, 1.5)
|
| 269 |
+
.extrude(0.5)
|
| 270 |
+
.faces(">Z")
|
| 271 |
+
.sketch()
|
| 272 |
+
.circle(0.4)
|
| 273 |
+
.wires()
|
| 274 |
+
.distribute(6)
|
| 275 |
+
.circle(0.1, mode="a")
|
| 276 |
+
.clean()
|
| 277 |
+
.finalize()
|
| 278 |
+
.cutBlind(-0.5, taper=10)
|
| 279 |
+
)
|
| 280 |
+
|
| 281 |
+
Lofting between two sketches
|
| 282 |
+
============================
|
| 283 |
+
|
| 284 |
+
Two sketches on different workplanes are needed when using :meth:`~cadquery.Workplane.loft`.
|
| 285 |
+
|
| 286 |
+
.. cadquery::
|
| 287 |
+
:height: 600px
|
| 288 |
+
|
| 289 |
+
from cadquery import Workplane, Sketch, Vector, Location
|
| 290 |
+
|
| 291 |
+
s1 = Sketch().trapezoid(3, 1, 110).vertices().fillet(0.2)
|
| 292 |
+
|
| 293 |
+
s2 = Sketch().rect(2, 1).vertices().fillet(0.2)
|
| 294 |
+
|
| 295 |
+
result = Workplane().placeSketch(s1, s2.moved(z=3)).loft()
|
| 296 |
+
|
| 297 |
+
When lofting only outer wires are taken into account and inner wires are silently ignored. Note that only sketches on the top of stack are considered for the current operation (i.e. there are no pending sketches), so when lofting or sweeping all relevant sketches have to be added in one `placeSketch` call.
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
Combining sketches
|
| 301 |
+
==================
|
| 302 |
+
|
| 303 |
+
Sketches can be combined using :meth:`~cadquery.Sketch.face`.
|
| 304 |
+
|
| 305 |
+
.. cadquery::
|
| 306 |
+
:height: 600px
|
| 307 |
+
|
| 308 |
+
import cadquery as cq
|
| 309 |
+
|
| 310 |
+
s1 = cq.Sketch().rect(2, 2)
|
| 311 |
+
s2 = cq.Sketch().circle(0.5)
|
| 312 |
+
|
| 313 |
+
result = s1.face(s2, mode='s')
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
It is also possible to use boolean operations to achieve the same effect.
|
| 317 |
+
|
| 318 |
+
.. cadquery::
|
| 319 |
+
:height: 600px
|
| 320 |
+
|
| 321 |
+
import cadquery as cq
|
| 322 |
+
|
| 323 |
+
s1 = cq.Sketch().rect(2, 2).vertices().fillet(0.25).reset()
|
| 324 |
+
s2 = cq.Sketch().rect(1, 1, angle=45).vertices().chamfer(0.1).reset()
|
| 325 |
+
|
| 326 |
+
result = s1 - s2
|
| 327 |
+
|
| 328 |
+
Boolean operations are selection sensitive, so in this example
|
| 329 |
+
:meth:`~cadquery.Sketch.reset` call is needed.
|
| 330 |
+
|
| 331 |
+
Offsets made easy
|
| 332 |
+
=================
|
| 333 |
+
|
| 334 |
+
Conveniently, it is possible to reuse a sketch to create an :meth:`~cadquery.Sketch.offset` shape.
|
| 335 |
+
|
| 336 |
+
.. cadquery::
|
| 337 |
+
:height: 600px
|
| 338 |
+
|
| 339 |
+
import cadquery as cq
|
| 340 |
+
|
| 341 |
+
sketch = (cq.Sketch()
|
| 342 |
+
.rect(1.0, 4.0)
|
| 343 |
+
.circle(1.0)
|
| 344 |
+
.clean()
|
| 345 |
+
)
|
| 346 |
+
|
| 347 |
+
sketch_offset = sketch.copy().wires().offset(0.25)
|
| 348 |
+
|
| 349 |
+
result = cq.Workplane("front").placeSketch(sketch_offset).extrude(1.0)
|
| 350 |
+
result = result.faces(">Z").workplane().placeSketch(sketch).cutBlind(-0.50)
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
It is obviously possible to use negative offsets, but it requires being more careful with the mode
|
| 354 |
+
of the offset operation. Usually one wants to replace the original face, hence ``mode='r'``.
|
| 355 |
+
|
| 356 |
+
.. cadquery::
|
| 357 |
+
:height: 600px
|
| 358 |
+
|
| 359 |
+
import cadquery as cq
|
| 360 |
+
|
| 361 |
+
sketch = (cq.Sketch()
|
| 362 |
+
.rect(1.0, 4.0)
|
| 363 |
+
.circle(1.0)
|
| 364 |
+
.clean()
|
| 365 |
+
)
|
| 366 |
+
|
| 367 |
+
sketch_offset = sketch.copy().wires().offset(-0.25, mode='r')
|
| 368 |
+
|
| 369 |
+
result = cq.Workplane("front").placeSketch(sketch).extrude(1.0)
|
| 370 |
+
result = result.faces(">Z").workplane().placeSketch(sketch_offset).cutBlind(-0.50)
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
Exporting and importing
|
| 374 |
+
=======================
|
| 375 |
+
|
| 376 |
+
It is possible to export sketches using :meth:`~cadquery.Sketch.export`.
|
| 377 |
+
See :ref:`importexport` for more details.
|
| 378 |
+
Importing of DXF files is supported as well using :meth:`~cadquery.Sketch.importDXF`.
|
server/docs/reference/workplane.rst
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
Workplane
|
| 3 |
+
=========
|
| 4 |
+
|
| 5 |
+
Most CAD programs use the concept of Workplanes. If you have experience with other CAD programs you will probably
|
| 6 |
+
feel comfortable with CadQuery's Workplanes, but if you don't have experience then they are an essential concept to
|
| 7 |
+
understand.
|
| 8 |
+
|
| 9 |
+
Workplanes represent a plane in space, from which other features can be located. They have a center point and a local
|
| 10 |
+
coordinate system. Most methods that create an object do so relative to the current workplane.
|
| 11 |
+
|
| 12 |
+
Usually the first workplane created is the "XY" plane, also known as the "front" plane. Once a solid is defined the most
|
| 13 |
+
common way to create a workplane is to select a face on the solid that you intend to modify and create a new workplane
|
| 14 |
+
relative to it. You can also create new workplanes anywhere in the world coordinate system, or relative to other planes
|
| 15 |
+
using offsets or rotations.
|
| 16 |
+
|
| 17 |
+
The most powerful feature of workplanes is that they allow you to work in 2D space in the coordinate system of the
|
| 18 |
+
workplane, and then CadQuery will transform these points from the workplane coordinate system to the world coordinate
|
| 19 |
+
system so your 3D features are located where you intended. This makes scripts much easier to create and maintain.
|
| 20 |
+
|
| 21 |
+
See :py:class:`cadquery.Workplane` to learn more.
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
2D Construction
|
| 25 |
+
---------------------------
|
| 26 |
+
|
| 27 |
+
Once you create a workplane, you can work in 2D, and then later use the features you create to make 3D objects.
|
| 28 |
+
You'll find all of the 2D constructs you expect -- circles, lines, arcs, mirroring, points, etc.
|
| 29 |
+
|
| 30 |
+
See :ref:`2dOperations` to learn more.
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
3D Construction
|
| 34 |
+
---------------------------
|
| 35 |
+
|
| 36 |
+
You can construct 3D primitives such as boxes, wedges, cylinders and spheres directly. You can also sweep, extrude,
|
| 37 |
+
and loft 2D geometry to form 3D features. Of course the basic primitive operations are also available.
|
| 38 |
+
|
| 39 |
+
See :ref:`3doperations` to learn more.
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
Selectors
|
| 44 |
+
---------------------------
|
| 45 |
+
|
| 46 |
+
Selectors allow you to select one or more features, in order to define new features. As an example, you might
|
| 47 |
+
extrude a box, and then select the top face as the location for a new feature. Or, you might extrude a box, and
|
| 48 |
+
then select all of the vertical edges so that you can apply a fillet to them.
|
| 49 |
+
|
| 50 |
+
You can select Vertices, Edges, Faces, Solids, and Wires using selectors.
|
| 51 |
+
|
| 52 |
+
Think of selectors as the equivalent of your hand and mouse, if you were to build an object using a conventional CAD system.
|
| 53 |
+
|
| 54 |
+
See :ref:`selectors` to learn more.
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
Construction Geometry
|
| 58 |
+
---------------------------
|
| 59 |
+
Construction geometry are features that are not part of the object, but are only defined to aid in building the object.
|
| 60 |
+
A common example might be to define a rectangle, and then use the corners to define the location of a set of holes.
|
| 61 |
+
|
| 62 |
+
Most CadQuery construction methods provide a ``forConstruction`` keyword, which creates a feature that will only be used
|
| 63 |
+
to locate other features.
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
The Stack
|
| 67 |
+
---------------------------
|
| 68 |
+
|
| 69 |
+
As you work in CadQuery, each operation returns a new Workplane object with the result of that
|
| 70 |
+
operation. Each Workplane object has a list of objects, and a reference to its parent.
|
| 71 |
+
|
| 72 |
+
You can always go backwards to older operations by removing the current object from the stack. For example::
|
| 73 |
+
|
| 74 |
+
Workplane(someObject).faces(">Z").first().vertices()
|
| 75 |
+
|
| 76 |
+
returns a CadQuery object that contains all of the vertices on the highest face of someObject. But you can always move
|
| 77 |
+
backwards in the stack to get the face as well::
|
| 78 |
+
|
| 79 |
+
Workplane(someObject).faces(">Z").first().vertices().end()
|
| 80 |
+
|
| 81 |
+
You can browse stack access methods here: :ref:`stackMethods`.
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
.. _chaining:
|
| 85 |
+
|
| 86 |
+
Chaining
|
| 87 |
+
---------------------------
|
| 88 |
+
|
| 89 |
+
All Workplane methods return another Workplane object, so that you can chain the methods together
|
| 90 |
+
fluently. Use the core Workplane methods to get at the objects that were created.
|
| 91 |
+
|
| 92 |
+
Each time a new Workplane object is produced during these chained calls, it has a
|
| 93 |
+
:attr:`~cadquery.Workplane.parent` attribute that points to the Workplane object that created it.
|
| 94 |
+
Several CadQuery methods search this parent chain, for example when searching for the context solid.
|
| 95 |
+
You can also give a Workplane object a tag, and further down your chain of calls you can refer back
|
| 96 |
+
to this particular object using its tag.
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
The Context Solid
|
| 100 |
+
---------------------------
|
| 101 |
+
|
| 102 |
+
Most of the time, you are building a single object, and adding features to that single object. CadQuery watches
|
| 103 |
+
your operations, and defines the first solid object created as the 'context solid'. After that, any features
|
| 104 |
+
you create are automatically combined (unless you specify otherwise) with that solid. This happens even if the
|
| 105 |
+
solid was created a long way up in the stack. For example::
|
| 106 |
+
|
| 107 |
+
Workplane("XY").box(1, 2, 3).faces(">Z").circle(0.25).extrude(1)
|
| 108 |
+
|
| 109 |
+
Will create a 1x2x3 box, with a cylindrical boss extending from the top face. It was not necessary to manually
|
| 110 |
+
combine the cylinder created by extruding the circle with the box, because the default behavior for extrude is
|
| 111 |
+
to combine the result with the context solid. The :meth:`~cadquery.Workplane.hole` method works similarly -- CadQuery presumes that you want
|
| 112 |
+
to subtract the hole from the context solid.
|
| 113 |
+
|
| 114 |
+
If you want to avoid this, you can specify ``combine=False``, and CadQuery will create the solid separately.
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
Iteration
|
| 118 |
+
---------------------------
|
| 119 |
+
|
| 120 |
+
CAD models often have repeated geometry, and it's really annoying to resort to for loops to construct features.
|
| 121 |
+
Many CadQuery methods operate automatically on each element on the stack, so that you don't have to write loops.
|
| 122 |
+
For example, this::
|
| 123 |
+
|
| 124 |
+
Workplane("XY").box(1, 2, 3).faces(">Z").vertices().circle(0.5)
|
| 125 |
+
|
| 126 |
+
Will actually create 4 circles, because ``vertices()`` selects 4 vertices of a rectangular face, and the ``circle()`` method
|
| 127 |
+
iterates on each member of the stack.
|
| 128 |
+
|
| 129 |
+
This is really useful to remember when you author your own plugins. :py:meth:`cadquery.Workplane.each` is useful for this purpose.
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
An Introspective Example
|
| 133 |
+
------------------------
|
| 134 |
+
|
| 135 |
+
.. note::
|
| 136 |
+
If you are just beginning with CadQuery then you can leave this example for later. If you have
|
| 137 |
+
some experience with creating CadQuery models and now you want to read the CadQuery source to
|
| 138 |
+
better understand what your code does, then it is recommended you read this example first.
|
| 139 |
+
|
| 140 |
+
To demonstrate the above concepts, we can define a more detailed string representations for the
|
| 141 |
+
:class:`~cadquery.Workplane`, :class:`~cadquery.Plane` and :class:`~cadquery.CQContext` classes and
|
| 142 |
+
patch them in::
|
| 143 |
+
|
| 144 |
+
import cadquery as cq
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def tidy_repr(obj):
|
| 148 |
+
"""Shortens a default repr string"""
|
| 149 |
+
return repr(obj).split(".")[-1].rstrip(">")
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def _ctx_str(self):
|
| 153 |
+
return (
|
| 154 |
+
tidy_repr(self)
|
| 155 |
+
+ ":\n"
|
| 156 |
+
+ f" pendingWires: {self.pendingWires}\n"
|
| 157 |
+
+ f" pendingEdges: {self.pendingEdges}\n"
|
| 158 |
+
+ f" tags: {self.tags}"
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
cq.cq.CQContext.__str__ = _ctx_str
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def _plane_str(self):
|
| 166 |
+
return (
|
| 167 |
+
tidy_repr(self)
|
| 168 |
+
+ ":\n"
|
| 169 |
+
+ f" origin: {self.origin.toTuple()}\n"
|
| 170 |
+
+ f" z direction: {self.zDir.toTuple()}"
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
cq.occ_impl.geom.Plane.__str__ = _plane_str
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def _wp_str(self):
|
| 178 |
+
out = tidy_repr(self) + ":\n"
|
| 179 |
+
out += f" parent: {tidy_repr(self.parent)}\n" if self.parent else " no parent\n"
|
| 180 |
+
out += f" plane: {self.plane}\n"
|
| 181 |
+
out += f" objects: {self.objects}\n"
|
| 182 |
+
out += f" modelling context: {self.ctx}"
|
| 183 |
+
return out
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
cq.Workplane.__str__ = _wp_str
|
| 187 |
+
|
| 188 |
+
Now we can make a simple part and examine the :class:`~cadquery.Workplane` and
|
| 189 |
+
:class:`~cadquery.cq.CQContext` objects at each step. The final part looks like:
|
| 190 |
+
|
| 191 |
+
.. cadquery::
|
| 192 |
+
:select: part
|
| 193 |
+
|
| 194 |
+
part = (
|
| 195 |
+
cq.Workplane()
|
| 196 |
+
.box(1, 1, 1)
|
| 197 |
+
.tag("base")
|
| 198 |
+
.wires(">Z")
|
| 199 |
+
.toPending()
|
| 200 |
+
.translate((0.1, 0.1, 1.0))
|
| 201 |
+
.toPending()
|
| 202 |
+
.loft()
|
| 203 |
+
.faces(">>X", tag="base")
|
| 204 |
+
.workplane(centerOption="CenterOfMass")
|
| 205 |
+
.circle(0.2)
|
| 206 |
+
.extrude(1)
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
.. note::
|
| 210 |
+
Some of the modelling process for this part is a bit contrived and not a great example of fluent
|
| 211 |
+
CadQuery techniques.
|
| 212 |
+
|
| 213 |
+
The start of our chain of calls is::
|
| 214 |
+
|
| 215 |
+
part = cq.Workplane()
|
| 216 |
+
print(part)
|
| 217 |
+
|
| 218 |
+
Which produces the output:
|
| 219 |
+
|
| 220 |
+
.. code-block:: none
|
| 221 |
+
|
| 222 |
+
Workplane object at 0x2760:
|
| 223 |
+
no parent
|
| 224 |
+
plane: Plane object at 0x2850:
|
| 225 |
+
origin: (0.0, 0.0, 0.0)
|
| 226 |
+
z direction: (0.0, 0.0, 1.0)
|
| 227 |
+
objects: []
|
| 228 |
+
modelling context: CQContext object at 0x2730:
|
| 229 |
+
pendingWires: []
|
| 230 |
+
pendingEdges: []
|
| 231 |
+
tags: {}
|
| 232 |
+
|
| 233 |
+
This is simply an empty :class:`~cadquery.Workplane`. Being the first :class:`~cadquery.Workplane`
|
| 234 |
+
in the chain, it does not have a parent. The :attr:`~cadquery.Workplane.plane` attribute contains a
|
| 235 |
+
:class:`~cadquery.Plane` object that describes the XY plane.
|
| 236 |
+
|
| 237 |
+
Now we create a simple box. To keep things short, the ``print(part)`` line will not be shown for the
|
| 238 |
+
rest of these code blocks::
|
| 239 |
+
|
| 240 |
+
part = part.box(1, 1, 1)
|
| 241 |
+
|
| 242 |
+
Which produces the output:
|
| 243 |
+
|
| 244 |
+
.. code-block:: none
|
| 245 |
+
|
| 246 |
+
Workplane object at 0xaa90:
|
| 247 |
+
parent: Workplane object at 0x2760
|
| 248 |
+
plane: Plane object at 0x3850:
|
| 249 |
+
origin: (0.0, 0.0, 0.0)
|
| 250 |
+
z direction: (0.0, 0.0, 1.0)
|
| 251 |
+
objects: [<cadquery.occ_impl.shapes.Solid object at 0xbbe0>]
|
| 252 |
+
modelling context: CQContext object at 0x2730:
|
| 253 |
+
pendingWires: []
|
| 254 |
+
pendingEdges: []
|
| 255 |
+
tags: {}
|
| 256 |
+
|
| 257 |
+
The first thing to note is that this is a different :class:`~cadquery.Workplane` object to the
|
| 258 |
+
previous one, and in the :attr:`~cadquery.Workplane.parent` attribute of this
|
| 259 |
+
:class:`~cadquery.Workplane` is our previous :class:`~cadquery.Workplane`. Returning a new instance
|
| 260 |
+
of :class:`~cadquery.Workplane` is the normal behaviour of most :class:`~cadquery.Workplane` methods
|
| 261 |
+
(with some exceptions, as will be shown below) and this is how the `chaining`_ concept is
|
| 262 |
+
implemented.
|
| 263 |
+
|
| 264 |
+
Secondly, the modelling context object is the same as the one in the previous
|
| 265 |
+
:class:`~cadquery.Workplane`, and this one modelling context at ``0x2730`` will be shared between
|
| 266 |
+
every :class:`Workplane` object in this chain. If we instantiate a new :class:`~cadquery.Workplane`
|
| 267 |
+
with ``part2 = cq.Workplane()``, then this ``part2`` would have a different instance of the
|
| 268 |
+
:class:`~cadquery.cq.CQContext` attached to it.
|
| 269 |
+
|
| 270 |
+
Thirdly, in our objects list is a single :class:`~cadquery.Solid` object, which is the box we just
|
| 271 |
+
created.
|
| 272 |
+
|
| 273 |
+
Often when creating models you will find yourself wanting to refer back to a specific
|
| 274 |
+
:class:`~cadquery.Workplane` object, perhaps because it is easier to select the feature you want in this
|
| 275 |
+
earlier state, or because you want to reuse a plane. Tags offer a way to refer back to a previous
|
| 276 |
+
:class:`~cadquery.Workplane`. We can tag the :class:`~cadquery.Workplane` that contains this basic box now::
|
| 277 |
+
|
| 278 |
+
part = part.tag("base")
|
| 279 |
+
|
| 280 |
+
The string representation of ``part`` is now:
|
| 281 |
+
|
| 282 |
+
.. code-block:: none
|
| 283 |
+
|
| 284 |
+
Workplane object at 0xaa90:
|
| 285 |
+
parent: Workplane object at 0x2760
|
| 286 |
+
plane: Plane object at 0x3850:
|
| 287 |
+
origin: (0.0, 0.0, 0.0)
|
| 288 |
+
z direction: (0.0, 0.0, 1.0)
|
| 289 |
+
objects: [<cadquery.occ_impl.shapes.Solid object at 0xbbe0>]
|
| 290 |
+
modelling context: CQContext object at 0x2730:
|
| 291 |
+
pendingWires: []
|
| 292 |
+
pendingEdges: []
|
| 293 |
+
tags: {'base': <cadquery.cq.Workplane object at 0xaa90>}
|
| 294 |
+
|
| 295 |
+
The :attr:`~cadquery.cq.CQContext.tags` attribute of the modelling context is simply a dict
|
| 296 |
+
associating the string name given by the :meth:`~cadquery.Workplane.tag` method to the
|
| 297 |
+
:class:`~cadquery.Workplane`. Methods such as :meth:`~cadquery.Workplane.workplaneFromTagged` and
|
| 298 |
+
selection methods like :meth:`~cadquery.Workplane.edges` can operate on a tagged
|
| 299 |
+
:class:`~cadquery.Workplane`. Note that unlike the ``part = part.box(1, 1, 1)`` step where we went
|
| 300 |
+
from ``Workplane object at 0x2760`` to ``Workplane object at 0xaa90``, the
|
| 301 |
+
:meth:`~cadquery.Workplane.tag` method has returned the same object at ``0xaa90``. This is unusual
|
| 302 |
+
for a :class:`~cadquery.Workplane` method.
|
| 303 |
+
|
| 304 |
+
The next step is::
|
| 305 |
+
|
| 306 |
+
part = part.faces(">>Z")
|
| 307 |
+
|
| 308 |
+
The output is:
|
| 309 |
+
|
| 310 |
+
.. code-block:: none
|
| 311 |
+
|
| 312 |
+
Workplane object at 0x8c40:
|
| 313 |
+
parent: Workplane object at 0xaa90
|
| 314 |
+
plane: Plane object at 0xac40:
|
| 315 |
+
origin: (0.0, 0.0, 0.0)
|
| 316 |
+
z direction: (0.0, 0.0, 1.0)
|
| 317 |
+
objects: [<cadquery.occ_impl.shapes.Face object at 0x3c10>]
|
| 318 |
+
modelling context: CQContext object at 0x2730:
|
| 319 |
+
pendingWires: []
|
| 320 |
+
pendingEdges: []
|
| 321 |
+
tags: {'base': <cadquery.cq.Workplane object at 0xaa90>}
|
| 322 |
+
|
| 323 |
+
Our selection method has taken the :class:`~cadquery.Solid` from the
|
| 324 |
+
:attr:`~cadquery.Workplane.objects` list of the previous :class:`~cadquery.Workplane`, found the
|
| 325 |
+
face with its center furthest in the Z direction, and placed that face into the
|
| 326 |
+
:attr:`~cadquery.Workplane.objects` attribute. The :class:`~cadquery.Solid` representing the box we
|
| 327 |
+
are modelling is gone, and when a :class:`~cadquery.Workplane` method needs to access that solid it
|
| 328 |
+
searches through the parent chain for the nearest solid. This action can also be done by a user
|
| 329 |
+
through the :meth:`~cadquery.Workplane.findSolid` method.
|
| 330 |
+
|
| 331 |
+
Now we want to select the boundary of this :class:`~cadquery.Face` (a :class:`~cadquery.Wire`), so
|
| 332 |
+
we use::
|
| 333 |
+
|
| 334 |
+
part = part.wires()
|
| 335 |
+
|
| 336 |
+
The output is now:
|
| 337 |
+
|
| 338 |
+
.. code-block:: none
|
| 339 |
+
|
| 340 |
+
Workplane object at 0x6880:
|
| 341 |
+
parent: Workplane object at 0x8c40
|
| 342 |
+
plane: Plane object at 0x38b0:
|
| 343 |
+
origin: (0.0, 0.0, 0.0)
|
| 344 |
+
z direction: (0.0, 0.0, 1.0)
|
| 345 |
+
objects: [<cadquery.occ_impl.shapes.Wire object at 0xaca0>]
|
| 346 |
+
modelling context: CQContext object at 0x2730:
|
| 347 |
+
pendingWires: []
|
| 348 |
+
pendingEdges: []
|
| 349 |
+
tags: {'base': <cadquery.cq.Workplane object at 0xaa90>}
|
| 350 |
+
|
| 351 |
+
Modelling operations take their wires and edges from the modelling context's pending lists. In order
|
| 352 |
+
to use the :meth:`~cadquery.Workplane.loft` command further down the chain, we need to push this wire
|
| 353 |
+
to the modelling context with::
|
| 354 |
+
|
| 355 |
+
part = part.toPending()
|
| 356 |
+
|
| 357 |
+
Now we have:
|
| 358 |
+
|
| 359 |
+
.. code-block:: none
|
| 360 |
+
|
| 361 |
+
Workplane object at 0x6880:
|
| 362 |
+
parent: Workplane object at 0x8c40
|
| 363 |
+
plane: Plane object at 0x38b0:
|
| 364 |
+
origin: (0.0, 0.0, 0.0)
|
| 365 |
+
z direction: (0.0, 0.0, 1.0)
|
| 366 |
+
objects: [<cadquery.occ_impl.shapes.Wire object at 0xaca0>]
|
| 367 |
+
modelling context: CQContext object at 0x2730:
|
| 368 |
+
pendingWires: [<cadquery.occ_impl.shapes.Wire object at 0xaca0>]
|
| 369 |
+
pendingEdges: []
|
| 370 |
+
tags: {'base': <cadquery.cq.Workplane object at 0xaa90>}
|
| 371 |
+
|
| 372 |
+
The :class:`~cadquery.Wire` object that was only in the :attr:`~cadquery.Workplane.objects`
|
| 373 |
+
attribute before is now also in the modelling context's :attr:`~cadquery.cq.CQContext.pendingWires`.
|
| 374 |
+
The :meth:`~cadquery.Workplane.toPending` method is also another of the unusual methods that return
|
| 375 |
+
the same :class:`~cadquery.Workplane` object instead of a new one.
|
| 376 |
+
|
| 377 |
+
To set up the other side of the :meth:`~cadquery.Workplane.loft` command further down the chain, we
|
| 378 |
+
translate the wire in :attr:`~cadquery.Workplane.objects` by calling::
|
| 379 |
+
|
| 380 |
+
part = part.translate((0.1, 0.1, 1.0))
|
| 381 |
+
|
| 382 |
+
Now the string representation of ``part`` looks like:
|
| 383 |
+
|
| 384 |
+
.. code-block:: none
|
| 385 |
+
|
| 386 |
+
Workplane object at 0x3a00:
|
| 387 |
+
parent: Workplane object at 0x6880
|
| 388 |
+
plane: Plane object at 0xac70:
|
| 389 |
+
origin: (0.0, 0.0, 0.0)
|
| 390 |
+
z direction: (0.0, 0.0, 1.0)
|
| 391 |
+
objects: [<cadquery.occ_impl.shapes.Wire object at 0x35e0>]
|
| 392 |
+
modelling context: CQContext object at 0x2730:
|
| 393 |
+
pendingWires: [<cadquery.occ_impl.shapes.Wire object at 0xaca0>]
|
| 394 |
+
pendingEdges: []
|
| 395 |
+
tags: {'base': <cadquery.cq.Workplane object at 0xaa90>}
|
| 396 |
+
|
| 397 |
+
It may look similar to the previous step, but the :class:`~cadquery.Wire` object in
|
| 398 |
+
:attr:`~cadquery.Workplane.objects` is different. To get this wire into the pending wires list,
|
| 399 |
+
again we use::
|
| 400 |
+
|
| 401 |
+
part = part.toPending()
|
| 402 |
+
|
| 403 |
+
The result:
|
| 404 |
+
|
| 405 |
+
.. code-block:: none
|
| 406 |
+
|
| 407 |
+
Workplane object at 0x3a00:
|
| 408 |
+
parent: Workplane object at 0x6880
|
| 409 |
+
plane: Plane object at 0xac70:
|
| 410 |
+
origin: (0.0, 0.0, 0.0)
|
| 411 |
+
z direction: (0.0, 0.0, 1.0)
|
| 412 |
+
objects: [<cadquery.occ_impl.shapes.Wire object at 0x35e0>]
|
| 413 |
+
modelling context: CQContext object at 0x2730:
|
| 414 |
+
pendingWires: [<cadquery.occ_impl.shapes.Wire object at 0xaca0>, <cadquery.occ_impl.shapes.Wire object at 0x7f5c7f5c35e0>]
|
| 415 |
+
pendingEdges: []
|
| 416 |
+
tags: {'base': <cadquery.cq.Workplane object at 0xaa90>}
|
| 417 |
+
|
| 418 |
+
The modelling context's :attr:`~cadquery.cq.CQContext.pendingWires` attribute now contains the two
|
| 419 |
+
wires we want to loft between, and we simply call::
|
| 420 |
+
|
| 421 |
+
part = part.loft()
|
| 422 |
+
|
| 423 |
+
After the loft operation, our Workplane looks quite different:
|
| 424 |
+
|
| 425 |
+
.. code-block:: none
|
| 426 |
+
|
| 427 |
+
Workplane object at 0x32b0:
|
| 428 |
+
parent: Workplane object at 0x3a00
|
| 429 |
+
plane: Plane object at 0x3d60:
|
| 430 |
+
origin: (0.0, 0.0, 0.0)
|
| 431 |
+
z direction: (0.0, 0.0, 1.0)
|
| 432 |
+
objects: [<cadquery.occ_impl.shapes.Compound object at 0xad30>]
|
| 433 |
+
modelling context: CQContext object at 0x2730:
|
| 434 |
+
pendingWires: []
|
| 435 |
+
pendingEdges: []
|
| 436 |
+
tags: {'base': <cadquery.cq.Workplane object at 0xaa90>}
|
| 437 |
+
|
| 438 |
+
In the :attr:`cq.Workplane.objects` attribute we now have one :class:`~cadquery.Compound` object and the modelling
|
| 439 |
+
context's :attr:`~cadquery.cq.CQContext.pendingWires` has been cleared by
|
| 440 |
+
:meth:`~cadquery.Workplane.loft`.
|
| 441 |
+
|
| 442 |
+
.. note::
|
| 443 |
+
To inspect the :class:`~cadquery.Compound` object further you can use
|
| 444 |
+
:meth:`~cadquery.Workplane.val` or :meth:`~cadquery.Workplane.findSolid` to get at the
|
| 445 |
+
:class:`~cadquery.Compound` object, then use :meth:`cadquery.Shape.Solids` to return a list
|
| 446 |
+
of the :class:`~cadquery.Solid` objects contained in the :class:`~cadquery.Compound`, which in
|
| 447 |
+
this example will be a single :class:`~cadquery.Solid` object. For example:
|
| 448 |
+
|
| 449 |
+
.. code-block:: pycon
|
| 450 |
+
|
| 451 |
+
>>> a_compound = part.findSolid()
|
| 452 |
+
>>> a_list_of_solids = a_compound.Solids()
|
| 453 |
+
>>> len(a_list_of_solids)
|
| 454 |
+
1
|
| 455 |
+
|
| 456 |
+
Now we will create a small cylinder protruding from a face on the original box. We need to set up a
|
| 457 |
+
workplane to draw a circle on, so firstly we will select the correct face::
|
| 458 |
+
|
| 459 |
+
part = part.faces(">>X", tag="base")
|
| 460 |
+
|
| 461 |
+
Which results in:
|
| 462 |
+
|
| 463 |
+
.. code-block:: none
|
| 464 |
+
|
| 465 |
+
Workplane object at 0x3f10:
|
| 466 |
+
parent: Workplane object at 0x32b0
|
| 467 |
+
plane: Plane object at 0xefa0:
|
| 468 |
+
origin: (0.0, 0.0, 0.0)
|
| 469 |
+
z direction: (0.0, 0.0, 1.0)
|
| 470 |
+
objects: [<cadquery.occ_impl.shapes.Face object at 0x3af0>]
|
| 471 |
+
modelling context: CQContext object at 0x2730:
|
| 472 |
+
pendingWires: []
|
| 473 |
+
pendingEdges: []
|
| 474 |
+
tags: {'base': <cadquery.cq.Workplane object at 0xaa90>}
|
| 475 |
+
|
| 476 |
+
We have the desired :class:`~cadquery.Face` in the :attr:`~cadquery.Workplane.objects` attribute,
|
| 477 |
+
but the :attr:`~cadquery.Workplane.plane` has not changed yet. To create the new plane we use the
|
| 478 |
+
:meth:`Workplane.workplane` method::
|
| 479 |
+
|
| 480 |
+
part = part.workplane()
|
| 481 |
+
|
| 482 |
+
Now:
|
| 483 |
+
|
| 484 |
+
.. code-block:: none
|
| 485 |
+
|
| 486 |
+
Workplane object at 0xe700:
|
| 487 |
+
parent: Workplane object at 0x3f10
|
| 488 |
+
plane: Plane object at 0xe730:
|
| 489 |
+
origin: (0.5, 0.0, 0.0)
|
| 490 |
+
z direction: (1.0, 0.0, 0.0)
|
| 491 |
+
objects: []
|
| 492 |
+
modelling context: CQContext object at 0x2730:
|
| 493 |
+
pendingWires: []
|
| 494 |
+
pendingEdges: []
|
| 495 |
+
tags: {'base': <cadquery.cq.Workplane object at 0xaa90>}
|
| 496 |
+
|
| 497 |
+
The :attr:`~cadquery.Workplane.objects` list has been cleared and the :class:`~cadquery.Plane`
|
| 498 |
+
object has a local Z direction in the global X direction. Since the base of the plane is the side of
|
| 499 |
+
the box, the origin is offset in the X direction.
|
| 500 |
+
|
| 501 |
+
Onto this plane we can draw a circle::
|
| 502 |
+
|
| 503 |
+
part = part.circle(0.2)
|
| 504 |
+
|
| 505 |
+
Now:
|
| 506 |
+
|
| 507 |
+
.. code-block:: none
|
| 508 |
+
|
| 509 |
+
Workplane object at 0xe790:
|
| 510 |
+
parent: Workplane object at 0xe700
|
| 511 |
+
plane: Plane object at 0xaf40:
|
| 512 |
+
origin: (0.5, 0.0, 0.0)
|
| 513 |
+
z direction: (1.0, 0.0, 0.0)
|
| 514 |
+
objects: [<cadquery.occ_impl.shapes.Wire object at 0xe610>]
|
| 515 |
+
modelling context: CQContext object at 0x2730:
|
| 516 |
+
pendingWires: [<cadquery.occ_impl.shapes.Wire object at 0xe610>]
|
| 517 |
+
pendingEdges: []
|
| 518 |
+
tags: {'base': <cadquery.cq.Workplane object at 0xaa90>}
|
| 519 |
+
|
| 520 |
+
The :meth:`~cadquery.Workplane.circle` method - like all 2D drawing methods - has placed the circle
|
| 521 |
+
into both the :attr:`~cadquery.Workplane.objects` attribute (where it will be cleared during the
|
| 522 |
+
next modelling step), and the modelling context's pending wires (where it will persist until used by
|
| 523 |
+
another :class:`~cadquery.Workplane` method).
|
| 524 |
+
|
| 525 |
+
The next step is to extrude this circle and create a cylindrical protrusion::
|
| 526 |
+
|
| 527 |
+
part = part.extrude(1, clean=False)
|
| 528 |
+
|
| 529 |
+
Now:
|
| 530 |
+
|
| 531 |
+
.. code-block:: none
|
| 532 |
+
|
| 533 |
+
Workplane object at 0xafd0:
|
| 534 |
+
parent: Workplane object at 0xe790
|
| 535 |
+
plane: Plane object at 0x3e80:
|
| 536 |
+
origin: (0.5, 0.0, 0.0)
|
| 537 |
+
z direction: (1.0, 0.0, 0.0)
|
| 538 |
+
objects: [<cadquery.occ_impl.shapes.Compound object at 0xaaf0>]
|
| 539 |
+
modelling context: CQContext object at 0x2730:
|
| 540 |
+
pendingWires: []
|
| 541 |
+
pendingEdges: []
|
| 542 |
+
tags: {'base': <cadquery.cq.Workplane object at 0xaa90>}
|
| 543 |
+
|
| 544 |
+
The :meth:`~cadquery.Workplane.extrude` method has cleared all the pending wires and edges. The
|
| 545 |
+
:attr:`~cadquery.Workplane.objects` attribute contains the final :class:`~cadquery.Compound` object
|
| 546 |
+
that is shown in the 3D view above.
|
| 547 |
+
|
| 548 |
+
|
| 549 |
+
.. note::
|
| 550 |
+
The :meth:`~cadquery.Workplane.extrude` has an argument for ``clean`` which defaults to ``True``.
|
| 551 |
+
This extrudes the pending wires (creating a new :class:`~cadquery.Workplane` object), then runs
|
| 552 |
+
the :meth:`~cadquery.Workplane.clean` method to refine the result, creating another
|
| 553 |
+
:class:`~cadquery.Workplane`. If you were to run the example with the default
|
| 554 |
+
``clean=True`` then you would see an intermediate
|
| 555 |
+
:class:`~cadquery.Workplane` object in :attr:`~cadquery.Workplane.parent`
|
| 556 |
+
rather than the object from the previous step.
|
| 557 |
+
|
server/docs/skill.md
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CadQuery LLM Skill
|
| 2 |
+
|
| 3 |
+
This skill helps LLMs write correct, idiomatic CadQuery code.
|
| 4 |
+
Load this file before any CadQuery task. For deeper reference, read the files listed at the bottom.
|
| 5 |
+
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
## Two APIs — Know Which One You're Using
|
| 9 |
+
|
| 10 |
+
CadQuery provides two distinct APIs. Always identify which is appropriate before writing code.
|
| 11 |
+
|
| 12 |
+
### Fluent (Workplane) API
|
| 13 |
+
```python
|
| 14 |
+
import cadquery as cq
|
| 15 |
+
result = cq.Workplane("XY").box(10, 10, 10).faces(">Z").hole(3)
|
| 16 |
+
```
|
| 17 |
+
- Chains operations on a `Workplane` object with hidden state (current plane, stack)
|
| 18 |
+
- Best for: parts built up from sketches and feature operations (extrude, hole, fillet, shell)
|
| 19 |
+
- Most tutorials and examples use this style
|
| 20 |
+
|
| 21 |
+
### Free Function API
|
| 22 |
+
```python
|
| 23 |
+
from cadquery.func import *
|
| 24 |
+
b = box(10, 10, 10)
|
| 25 |
+
result = b - cylinder(1.5, 10)
|
| 26 |
+
```
|
| 27 |
+
- No hidden state — all operations are explicit free functions
|
| 28 |
+
- Selectors still work as methods on Shape objects
|
| 29 |
+
- Boolean ops available as operators: `+` (union), `-` (cut), `*` (intersect), `/` (split)
|
| 30 |
+
- Best for: complex face-by-face construction, lofted/swept surfaces, avoiding booleans via `addHole()`/`replace()`, parametric surface mapping
|
| 31 |
+
- More verbose but more explicit and composable
|
| 32 |
+
|
| 33 |
+
**Do not mix the two styles in the same script** unless you explicitly convert between them.
|
| 34 |
+
When the task involves building shapes face-by-face, lofting between non-planar edges, or
|
| 35 |
+
trimming surfaces in parametric space — prefer the Free Function API.
|
| 36 |
+
|
| 37 |
+
---
|
| 38 |
+
|
| 39 |
+
## The Core Mindset: BRep, Not CSG
|
| 40 |
+
|
| 41 |
+
CadQuery uses **Boundary Representation (BRep)**: a model is defined by its surfaces, edges, and vertices — not by Boolean operations on primitives.
|
| 42 |
+
|
| 43 |
+
The CSG reflex (union/cut everything) produces brittle, unidiomatic code. The BRep reflex asks:
|
| 44 |
+
- What faces, edges, or vertices already exist on this shape?
|
| 45 |
+
- Can I select them and build from there?
|
| 46 |
+
- Does a workplane operation (extrude, shell, fillet, chamfer) get me there without a Boolean?
|
| 47 |
+
|
| 48 |
+
**Wrong reflex (CSG):**
|
| 49 |
+
```python
|
| 50 |
+
result = base.union(cq.Workplane().box(10, 10, 5).translate((0, 0, 10)))
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
**Right reflex (BRep):**
|
| 54 |
+
```python
|
| 55 |
+
result = base.faces(">Z").workplane().rect(10, 10).extrude(5)
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
Only reach for `.union()`, `.cut()`, or `.intersect()` when you genuinely need to combine separate solids.
|
| 59 |
+
|
| 60 |
+
---
|
| 61 |
+
|
| 62 |
+
## Workplanes
|
| 63 |
+
|
| 64 |
+
A workplane is a local 2D coordinate system. Most geometry is created relative to it.
|
| 65 |
+
|
| 66 |
+
```python
|
| 67 |
+
import cadquery as cq
|
| 68 |
+
|
| 69 |
+
# Start on XY plane at origin
|
| 70 |
+
wp = cq.Workplane("XY")
|
| 71 |
+
|
| 72 |
+
# Build a solid to work from
|
| 73 |
+
cube = cq.Workplane("XY").box(10, 10, 10)
|
| 74 |
+
|
| 75 |
+
# Move to a face
|
| 76 |
+
wp2 = cube.faces(">Z").workplane()
|
| 77 |
+
|
| 78 |
+
# Offset from a face
|
| 79 |
+
wp3 = cube.faces(">Z").workplane(offset=5)
|
| 80 |
+
|
| 81 |
+
# Arbitrary origin and normal — must use cq.Plane, not keyword args on Workplane
|
| 82 |
+
wp4 = cq.Workplane(cq.Plane(origin=(0, 0, 10), normal=(0, 1, 0)))
|
| 83 |
+
|
| 84 |
+
# Move with transformation (rotate is degrees around local X, Y, Z)
|
| 85 |
+
wp5 = cube.faces(">Z").workplane().transformed(offset=(1, 2, 3), rotate=(0, 0, 45))
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
**Rules:**
|
| 89 |
+
- `.workplane()` resets the 2D origin to the center of the selected face/edge by default.
|
| 90 |
+
- `centerOption` controls what "center" means — the default `"ProjectedOrigin"` projects the parent workplane origin onto the new face, which is often not the face center. Always set it explicitly when position matters.
|
| 91 |
+
- After `.workplane()`, coordinates are **local** to that plane, not global.
|
| 92 |
+
- `.transformed()` is cumulative within the current workplane context.
|
| 93 |
+
|
| 94 |
+
---
|
| 95 |
+
|
| 96 |
+
## Selectors — Cheat Sheet
|
| 97 |
+
|
| 98 |
+
Selectors filter faces, edges, wires, or vertices from the current stack.
|
| 99 |
+
|
| 100 |
+
| Selector | Meaning |
|
| 101 |
+
|----------|---------|
|
| 102 |
+
| `">Z"` | Face/edge with highest Z centroid |
|
| 103 |
+
| `"<Z"` | Face/edge with lowest Z centroid |
|
| 104 |
+
| `"\|X"` | Faces whose normal is parallel to X axis |
|
| 105 |
+
| `"#Z"` | Faces/edges whose normal or direction is orthogonal to Z |
|
| 106 |
+
| `"+Z"` | Faces whose normal points in +Z direction |
|
| 107 |
+
| `">>Z[1]"` | Second item when sorted ascending by Z (0-indexed) |
|
| 108 |
+
| `"<<Z[0]"` | First item when sorted descending by Z |
|
| 109 |
+
| `"%Plane"` | Faces of type Plane (vs Cylinder, Cone, etc.) |
|
| 110 |
+
| `"not >Z"` | Inverts the selector |
|
| 111 |
+
| `">Z and \|X"` | Combines selectors (AND) |
|
| 112 |
+
| `">Z or <Z"` | Combines selectors (OR) |
|
| 113 |
+
|
| 114 |
+
**Tag-based selection (preferred for stability):**
|
| 115 |
+
```python
|
| 116 |
+
result = (
|
| 117 |
+
cq.Workplane("XY")
|
| 118 |
+
.box(10, 10, 10)
|
| 119 |
+
.faces(">Z").tag("top")
|
| 120 |
+
.end()
|
| 121 |
+
.faces(tag="top").workplane().hole(3)
|
| 122 |
+
)
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
The code above demonstrates how to use tags, and is not a suggestion of the proper way to use them.
|
| 126 |
+
In practices you would not call `end` and then query the tagged face immediately again.
|
| 127 |
+
Use tags when the geometry might need to be referred to later, and may become hard to access.
|
| 128 |
+
Tagging can be very useful, but do not force it in to the script unless it seems needed.
|
| 129 |
+
|
| 130 |
+
---
|
| 131 |
+
|
| 132 |
+
## Critical Anti-Patterns
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
### 1. Boolean when a feature operation suffices
|
| 136 |
+
| Instead of... | Use... |
|
| 137 |
+
|--------------|--------|
|
| 138 |
+
| `.cut(cylinder)` | `.faces(...).hole(d)` |
|
| 139 |
+
| `.cut(shell_solid)` | `.shell(thickness)` |
|
| 140 |
+
| `.union(chamfered_edge)` | `.edges(...).chamfer(d)` |
|
| 141 |
+
|
| 142 |
+
### 2. workplane `centerOption` behaviors
|
| 143 |
+
By default, a `workplane()` call uses `ProjectedOrigin` as the default, which uses the current origin and projects it onto the plan defined by the selected faces.
|
| 144 |
+
This usually works, but can cause unintended results sometimes.
|
| 145 |
+
If the geometric center of the object is the indended center for the operation, it can sometimes be better to use `CenterOfBoundBox`, which uses the X, Y and Z centers of the object's bounding box.
|
| 146 |
+
If the user mentions that the features added on the workplane are not in the expected position, one cause could be this `centerOption` parameter.
|
| 147 |
+
```python
|
| 148 |
+
# Explicit is always better
|
| 149 |
+
cq.Workplane("XY").box(10, 10, 5).faces(">Z").workplane(centerOption="CenterOfBoundBox")
|
| 150 |
+
```
|
| 151 |
+
|
| 152 |
+
### 3. Selector ordering assumptions
|
| 153 |
+
Selectors like `">>Z[1]"` depend on sort order across all matching entities. Adding fillets or other features changes the entity count and can shift indices. Prefer tags or geometric selectors (`">Z"`) over index-based ones.
|
| 154 |
+
|
| 155 |
+
### 4. Unclosed wires
|
| 156 |
+
When building profiles with `.lineTo()`, `.spline()`, etc., always call `.close()` before `.extrude()` or `.revolve()` unless the wire is intentionally open.
|
| 157 |
+
|
| 158 |
+
### 5. Forgetting `.end()` after tagging or selector context
|
| 159 |
+
After `.tag()` or navigating into a sub-context, use `.end()` to return to the solid before chaining further operations.
|
| 160 |
+
|
| 161 |
+
### 6. `BREP_API command not done`
|
| 162 |
+
This OpenCASCADE kernel error means the requested geometry is invalid. Common causes:
|
| 163 |
+
- Fillet/chamfer radius larger than the shortest adjacent edge, or adjacent fillets overlapping — reduce the radius or fillet edge groups separately
|
| 164 |
+
- Self-intersecting profile (wire crosses itself, revolve profile crosses the axis)
|
| 165 |
+
- Sweep profile too large for the path curvature — the solid folds back on itself
|
| 166 |
+
- Shell thickness larger than the local radius of curvature
|
| 167 |
+
|
| 168 |
+
See `patterns/anti-patterns.md` #14 for detailed fixes.
|
| 169 |
+
|
| 170 |
+
---
|
| 171 |
+
|
| 172 |
+
## Free Function API — Quick Reference
|
| 173 |
+
|
| 174 |
+
Import: `from cadquery.func import *`
|
| 175 |
+
|
| 176 |
+
**Primitives:**
|
| 177 |
+
```python
|
| 178 |
+
box(w, h, d)
|
| 179 |
+
cylinder(r, h)
|
| 180 |
+
sphere(r)
|
| 181 |
+
cone(r1, r2)
|
| 182 |
+
plane(w, h) # flat face
|
| 183 |
+
circle(r) # edge
|
| 184 |
+
rect(w, h) # edge
|
| 185 |
+
segment((x1,y1,z1), (x2,y2,z2))
|
| 186 |
+
```
|
| 187 |
+
|
| 188 |
+
**Shape assembly:**
|
| 189 |
+
```python
|
| 190 |
+
wire(e1, e2, ...) # edges → wire
|
| 191 |
+
face(wire) # closed wire → face
|
| 192 |
+
solid(f1, f2, ...) # faces → solid
|
| 193 |
+
shell(f1, f2, ...) # faces → shell (open solid)
|
| 194 |
+
compound(s1, s2, ...) # shapes → compound
|
| 195 |
+
```
|
| 196 |
+
|
| 197 |
+
**Operations:**
|
| 198 |
+
```python
|
| 199 |
+
extrude(profile, direction_vector)
|
| 200 |
+
loft(e1, e2, e3, cap=False)
|
| 201 |
+
sweep(profile, path)
|
| 202 |
+
revolve(face, axis_point, axis_dir, angle)
|
| 203 |
+
```
|
| 204 |
+
|
| 205 |
+
**Placement (no workplane needed):**
|
| 206 |
+
```python
|
| 207 |
+
shape.moved(x=1, y=2, z=3) # translate
|
| 208 |
+
shape.moved(rx=90, ry=0, rz=45) # rotate (degrees)
|
| 209 |
+
shape.moved((1,0,0), (0,1,0)) # place at multiple locations → compound
|
| 210 |
+
shape.move(z=5) # in-place variant
|
| 211 |
+
```
|
| 212 |
+
|
| 213 |
+
**Boolean operators:**
|
| 214 |
+
```python
|
| 215 |
+
a + b # fuse / union
|
| 216 |
+
a - b # cut / difference
|
| 217 |
+
a * b # intersect
|
| 218 |
+
a / plane # split
|
| 219 |
+
```
|
| 220 |
+
|
| 221 |
+
**Adding features without booleans (preferred for performance):**
|
| 222 |
+
```python
|
| 223 |
+
top = solid.faces(">Z")
|
| 224 |
+
inner = extrude(circle(r), (0, 0, h))
|
| 225 |
+
top_with_hole = top.addHole(inner.edges("<Z"))
|
| 226 |
+
result = solid(solid.remove(top).faces(), inner, top_with_hole)
|
| 227 |
+
```
|
| 228 |
+
|
| 229 |
+
**Selectors work the same way** — they are methods on Shape objects, not tied to either API.
|
| 230 |
+
|
| 231 |
+
---
|
| 232 |
+
|
| 233 |
+
## `.shell()` and Hollow Bodies
|
| 234 |
+
|
| 235 |
+
Shell operates on the existing solid — select the face(s) to open, then shell:
|
| 236 |
+
|
| 237 |
+
```python
|
| 238 |
+
box = cq.Workplane("XY").box(20, 20, 20)
|
| 239 |
+
hollow = box.faces(">Z").shell(-2) # negative = inward
|
| 240 |
+
```
|
| 241 |
+
|
| 242 |
+
Common mistake: calling `.shell()` without first selecting which face to remove, resulting in a fully closed thin shell (usually not what you want).
|
| 243 |
+
Shell can be a little buggy with certain profiles, as can offsetting.
|
| 244 |
+
|
| 245 |
+
---
|
| 246 |
+
|
| 247 |
+
## For Deeper Reference
|
| 248 |
+
|
| 249 |
+
| Topic | File |
|
| 250 |
+
|-------|------|
|
| 251 |
+
| BRep vs CSG concepts | `concepts/brep-mindset.md` |
|
| 252 |
+
| Workplane (fluent) API | `concepts/workplanes.md` |
|
| 253 |
+
| Free Function API | `concepts/free-function-api.md` |
|
| 254 |
+
| Full selector reference | `concepts/selectors.md` |
|
| 255 |
+
| Idiomatic patterns | `patterns/common-patterns.md` |
|
| 256 |
+
| Anti-patterns (extended) | `patterns/anti-patterns.md` |
|
| 257 |
+
| Annotated examples | `examples/` |
|
| 258 |
+
| CadQuery API reference | `docs/` |
|
| 259 |
+
|
| 260 |
+
When working on a CadQuery task:
|
| 261 |
+
1. Read this file first.
|
| 262 |
+
2. If the task involves selectors, read `concepts/selectors.md`.
|
| 263 |
+
3. Grep `examples/` for methods you're unsure about: `grep -rn "\.shell\(" examples/`
|
| 264 |
+
4. Grep `docs/` for API signatures: `grep -n "def shell" docs/`
|
server/docs_search.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import re
|
| 3 |
+
import time
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Dict, List, Optional
|
| 6 |
+
|
| 7 |
+
logger = logging.getLogger(__name__)
|
| 8 |
+
|
| 9 |
+
DOCS_ROOT = Path(__file__).parent / "docs"
|
| 10 |
+
|
| 11 |
+
TOPIC_MAP: Dict[str, List[Path]] = {
|
| 12 |
+
"basics": [
|
| 13 |
+
DOCS_ROOT / "skill.md",
|
| 14 |
+
DOCS_ROOT / "reference" / "quickstart.rst",
|
| 15 |
+
DOCS_ROOT / "reference" / "primer.rst",
|
| 16 |
+
],
|
| 17 |
+
"selectors": [
|
| 18 |
+
DOCS_ROOT / "concepts" / "selectors.md",
|
| 19 |
+
DOCS_ROOT / "reference" / "selectors.rst",
|
| 20 |
+
],
|
| 21 |
+
"booleans": [
|
| 22 |
+
DOCS_ROOT / "concepts" / "brep-mindset.md",
|
| 23 |
+
DOCS_ROOT / "patterns" / "common-patterns.md",
|
| 24 |
+
],
|
| 25 |
+
"transforms": [
|
| 26 |
+
DOCS_ROOT / "concepts" / "workplanes.md",
|
| 27 |
+
DOCS_ROOT / "reference" / "workplane.rst",
|
| 28 |
+
],
|
| 29 |
+
"features": [
|
| 30 |
+
DOCS_ROOT / "patterns" / "common-patterns.md",
|
| 31 |
+
DOCS_ROOT / "patterns" / "anti-patterns.md",
|
| 32 |
+
],
|
| 33 |
+
"sketch": [
|
| 34 |
+
DOCS_ROOT / "reference" / "sketch.rst",
|
| 35 |
+
],
|
| 36 |
+
"advanced": [
|
| 37 |
+
DOCS_ROOT / "concepts" / "free-function-api.md",
|
| 38 |
+
DOCS_ROOT / "reference" / "free-func.rst",
|
| 39 |
+
DOCS_ROOT / "reference" / "extending.rst",
|
| 40 |
+
],
|
| 41 |
+
"examples": [
|
| 42 |
+
DOCS_ROOT / "reference" / "examples.rst",
|
| 43 |
+
],
|
| 44 |
+
"anti-patterns": [
|
| 45 |
+
DOCS_ROOT / "patterns" / "anti-patterns.md",
|
| 46 |
+
],
|
| 47 |
+
"workplanes": [
|
| 48 |
+
DOCS_ROOT / "concepts" / "workplanes.md",
|
| 49 |
+
],
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def search_docs(
|
| 54 |
+
topic: Optional[str] = None,
|
| 55 |
+
query: Optional[str] = None,
|
| 56 |
+
context_lines: int = 5,
|
| 57 |
+
max_results: int = 10,
|
| 58 |
+
max_chars: int = 4000,
|
| 59 |
+
) -> List[str]:
|
| 60 |
+
t0 = time.time()
|
| 61 |
+
|
| 62 |
+
if topic and topic in TOPIC_MAP:
|
| 63 |
+
files = TOPIC_MAP[topic]
|
| 64 |
+
elif topic:
|
| 65 |
+
files = _find_files_by_name(topic)
|
| 66 |
+
else:
|
| 67 |
+
files = []
|
| 68 |
+
for file_list in TOPIC_MAP.values():
|
| 69 |
+
files.extend(file_list)
|
| 70 |
+
files = list(set(files))
|
| 71 |
+
|
| 72 |
+
if not files:
|
| 73 |
+
return [f"No documentation found for topic: {topic}"]
|
| 74 |
+
|
| 75 |
+
if not query:
|
| 76 |
+
results = []
|
| 77 |
+
total_chars = 0
|
| 78 |
+
for fp in files:
|
| 79 |
+
if not fp.exists():
|
| 80 |
+
continue
|
| 81 |
+
content = fp.read_text(encoding="utf-8", errors="replace")
|
| 82 |
+
if total_chars + len(content) > max_chars:
|
| 83 |
+
remaining = max_chars - total_chars
|
| 84 |
+
if remaining > 200:
|
| 85 |
+
results.append(f"=== {fp.name} (truncated) ===\n{content[:remaining]}...")
|
| 86 |
+
break
|
| 87 |
+
results.append(f"=== {fp.name} ===\n{content}")
|
| 88 |
+
total_chars += len(content)
|
| 89 |
+
elapsed = time.time() - t0
|
| 90 |
+
logger.info(f"search_docs(topic={topic}) returned {len(results)} files in {elapsed:.3f}s")
|
| 91 |
+
return results
|
| 92 |
+
|
| 93 |
+
results = _grep_search(files, query, context_lines, max_results)
|
| 94 |
+
|
| 95 |
+
if not results:
|
| 96 |
+
results = _fuzzy_search(files, query, context_lines, max_results)
|
| 97 |
+
|
| 98 |
+
total_chars = 0
|
| 99 |
+
trimmed = []
|
| 100 |
+
for r in results:
|
| 101 |
+
if total_chars + len(r) > max_chars:
|
| 102 |
+
remaining = max_chars - total_chars
|
| 103 |
+
if remaining > 100:
|
| 104 |
+
trimmed.append(r[:remaining] + "...")
|
| 105 |
+
break
|
| 106 |
+
trimmed.append(r)
|
| 107 |
+
total_chars += len(r)
|
| 108 |
+
|
| 109 |
+
elapsed = time.time() - t0
|
| 110 |
+
logger.info(f"search_docs(topic={topic}, query={query}) returned {len(trimmed)} results in {elapsed:.3f}s")
|
| 111 |
+
|
| 112 |
+
if not trimmed:
|
| 113 |
+
return [f"No results found for query: {query}"]
|
| 114 |
+
return trimmed
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def _find_files_by_name(name: str) -> List[Path]:
|
| 118 |
+
results = []
|
| 119 |
+
for fp in DOCS_ROOT.rglob("*"):
|
| 120 |
+
if fp.is_file() and name.lower() in fp.stem.lower():
|
| 121 |
+
results.append(fp)
|
| 122 |
+
return results
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def _grep_search(
|
| 126 |
+
files: List[Path],
|
| 127 |
+
query: str,
|
| 128 |
+
context_lines: int = 5,
|
| 129 |
+
max_results: int = 10,
|
| 130 |
+
) -> List[str]:
|
| 131 |
+
results = []
|
| 132 |
+
keywords = query.lower().split()
|
| 133 |
+
|
| 134 |
+
for fp in files:
|
| 135 |
+
if not fp.exists():
|
| 136 |
+
continue
|
| 137 |
+
try:
|
| 138 |
+
lines = fp.read_text(encoding="utf-8", errors="replace").splitlines()
|
| 139 |
+
except Exception:
|
| 140 |
+
continue
|
| 141 |
+
|
| 142 |
+
for i, line in enumerate(lines):
|
| 143 |
+
line_lower = line.lower()
|
| 144 |
+
if any(kw in line_lower for kw in keywords):
|
| 145 |
+
start = max(0, i - context_lines)
|
| 146 |
+
end = min(len(lines), i + context_lines + 1)
|
| 147 |
+
snippet = "\n".join(lines[start:end])
|
| 148 |
+
results.append(f"--- {fp.name}:{i+1} ---\n{snippet}")
|
| 149 |
+
|
| 150 |
+
if len(results) >= max_results:
|
| 151 |
+
return results
|
| 152 |
+
|
| 153 |
+
return results
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def _fuzzy_search(
|
| 157 |
+
files: List[Path],
|
| 158 |
+
query: str,
|
| 159 |
+
context_lines: int = 5,
|
| 160 |
+
max_results: int = 5,
|
| 161 |
+
) -> List[str]:
|
| 162 |
+
results = []
|
| 163 |
+
keywords = query.lower().split()
|
| 164 |
+
|
| 165 |
+
for fp in files:
|
| 166 |
+
if not fp.exists():
|
| 167 |
+
continue
|
| 168 |
+
try:
|
| 169 |
+
content = fp.read_text(encoding="utf-8", errors="replace")
|
| 170 |
+
except Exception:
|
| 171 |
+
continue
|
| 172 |
+
|
| 173 |
+
paragraphs = re.split(r"\n\s*\n", content)
|
| 174 |
+
|
| 175 |
+
scored = []
|
| 176 |
+
for para in paragraphs:
|
| 177 |
+
para_lower = para.lower()
|
| 178 |
+
score = sum(1 for kw in keywords if kw in para_lower)
|
| 179 |
+
if score > 0:
|
| 180 |
+
scored.append((score, para, fp.name))
|
| 181 |
+
|
| 182 |
+
scored.sort(key=lambda x: x[0], reverse=True)
|
| 183 |
+
|
| 184 |
+
for score, para, fname in scored[:max_results]:
|
| 185 |
+
results.append(f"--- {fname} (relevance: {score}/{len(keywords)}) ---\n{para.strip()}")
|
| 186 |
+
if len(results) >= max_results:
|
| 187 |
+
return results
|
| 188 |
+
|
| 189 |
+
return results
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def get_system_prompt() -> str:
|
| 193 |
+
skill_path = DOCS_ROOT / "skill.md"
|
| 194 |
+
if skill_path.exists():
|
| 195 |
+
return skill_path.read_text(encoding="utf-8", errors="replace")
|
| 196 |
+
return ""
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def list_topics() -> List[str]:
|
| 200 |
+
return list(TOPIC_MAP.keys())
|
server/executor.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import logging
|
| 3 |
+
import subprocess
|
| 4 |
+
import sys
|
| 5 |
+
import tempfile
|
| 6 |
+
import time
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Any, Dict, Optional, Tuple
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
RUNNER_SCRIPT = '''
|
| 13 |
+
import sys
|
| 14 |
+
import json
|
| 15 |
+
import traceback
|
| 16 |
+
|
| 17 |
+
try:
|
| 18 |
+
import cadquery as cq
|
| 19 |
+
code = sys.stdin.read()
|
| 20 |
+
local_ns = {"cq": cq, "cadquery": cq}
|
| 21 |
+
|
| 22 |
+
try:
|
| 23 |
+
import math
|
| 24 |
+
local_ns["math"] = math
|
| 25 |
+
except Exception:
|
| 26 |
+
pass
|
| 27 |
+
|
| 28 |
+
exec(code, local_ns)
|
| 29 |
+
|
| 30 |
+
if "result" not in local_ns:
|
| 31 |
+
print(json.dumps({"success": False, "error": "No variable named 'result' was defined"}))
|
| 32 |
+
sys.exit(0)
|
| 33 |
+
|
| 34 |
+
result = local_ns["result"]
|
| 35 |
+
|
| 36 |
+
if hasattr(result, "val"):
|
| 37 |
+
shape = result.val()
|
| 38 |
+
else:
|
| 39 |
+
shape = result
|
| 40 |
+
|
| 41 |
+
bb = shape.BoundingBox()
|
| 42 |
+
volume = shape.Volume()
|
| 43 |
+
is_valid = shape.isValid()
|
| 44 |
+
|
| 45 |
+
faces = shape.Faces()
|
| 46 |
+
face_types = [f.geomType() for f in faces]
|
| 47 |
+
|
| 48 |
+
from collections import Counter
|
| 49 |
+
type_counts = dict(Counter(face_types))
|
| 50 |
+
total_faces = len(face_types)
|
| 51 |
+
dominant = max(type_counts, key=type_counts.get) if type_counts else "UNKNOWN"
|
| 52 |
+
|
| 53 |
+
edges = shape.Edges()
|
| 54 |
+
vertices = shape.Vertices()
|
| 55 |
+
|
| 56 |
+
dims = [bb.xlen, bb.ylen, bb.zlen]
|
| 57 |
+
sorted_axes = sorted(zip(["X", "Y", "Z"], dims), key=lambda x: x[1], reverse=True)
|
| 58 |
+
longest_axis = sorted_axes[0][0]
|
| 59 |
+
max_dim = max(dims) if max(dims) > 0 else 1.0
|
| 60 |
+
|
| 61 |
+
shells = shape.Shells()
|
| 62 |
+
is_watertight = True
|
| 63 |
+
if not shells:
|
| 64 |
+
is_watertight = False
|
| 65 |
+
else:
|
| 66 |
+
for s in shells:
|
| 67 |
+
if not s.Closed():
|
| 68 |
+
is_watertight = False
|
| 69 |
+
break
|
| 70 |
+
|
| 71 |
+
V = len(vertices)
|
| 72 |
+
E = len(edges)
|
| 73 |
+
F = total_faces
|
| 74 |
+
|
| 75 |
+
output = {
|
| 76 |
+
"success": True,
|
| 77 |
+
"properties": {
|
| 78 |
+
"is_valid": is_valid,
|
| 79 |
+
"is_watertight": is_watertight,
|
| 80 |
+
"volume_mm3": round(volume, 4),
|
| 81 |
+
"surface_area_mm2": round(shape.Area(), 4),
|
| 82 |
+
"bbox_x_mm": round(bb.xlen, 4),
|
| 83 |
+
"bbox_y_mm": round(bb.ylen, 4),
|
| 84 |
+
"bbox_z_mm": round(bb.zlen, 4),
|
| 85 |
+
"bbox_longest_axis": longest_axis,
|
| 86 |
+
"bbox_ratio_yx": round(sorted_axes[1][1] / max_dim, 4),
|
| 87 |
+
"bbox_ratio_zx": round(sorted_axes[2][1] / max_dim, 4),
|
| 88 |
+
"face_count": total_faces,
|
| 89 |
+
"face_type_counts": type_counts,
|
| 90 |
+
"dominant_face_type": dominant,
|
| 91 |
+
"face_type_distribution": {k: round(v / total_faces, 4) for k, v in type_counts.items()} if total_faces > 0 else {},
|
| 92 |
+
"edge_count": E,
|
| 93 |
+
"vertex_count": V,
|
| 94 |
+
"euler_characteristic": V - E + F,
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
print(json.dumps(output))
|
| 98 |
+
|
| 99 |
+
except Exception as e:
|
| 100 |
+
tb = traceback.format_exc()
|
| 101 |
+
print(json.dumps({"success": False, "error": str(e), "traceback": tb}))
|
| 102 |
+
'''
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def execute_cadquery_code(
|
| 106 |
+
code: str,
|
| 107 |
+
timeout: float = 10.0,
|
| 108 |
+
python_path: Optional[str] = None,
|
| 109 |
+
) -> Dict[str, Any]:
|
| 110 |
+
t0 = time.time()
|
| 111 |
+
if python_path is None:
|
| 112 |
+
python_path = sys.executable
|
| 113 |
+
|
| 114 |
+
logger.info(f"Executing CadQuery code ({len(code)} chars, timeout={timeout}s)")
|
| 115 |
+
|
| 116 |
+
try:
|
| 117 |
+
proc = subprocess.run(
|
| 118 |
+
[python_path, "-c", RUNNER_SCRIPT],
|
| 119 |
+
input=code,
|
| 120 |
+
capture_output=True,
|
| 121 |
+
text=True,
|
| 122 |
+
timeout=timeout,
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
elapsed = time.time() - t0
|
| 126 |
+
logger.info(f"Subprocess completed in {elapsed:.3f}s, returncode={proc.returncode}")
|
| 127 |
+
|
| 128 |
+
stdout = proc.stdout.strip()
|
| 129 |
+
stderr = proc.stderr.strip()
|
| 130 |
+
|
| 131 |
+
if proc.returncode != 0 and not stdout:
|
| 132 |
+
return {
|
| 133 |
+
"success": False,
|
| 134 |
+
"error": stderr or f"Process exited with code {proc.returncode}",
|
| 135 |
+
"properties": None,
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
if not stdout:
|
| 139 |
+
return {
|
| 140 |
+
"success": False,
|
| 141 |
+
"error": "No output from subprocess",
|
| 142 |
+
"properties": None,
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
result = json.loads(stdout)
|
| 146 |
+
|
| 147 |
+
if not result.get("success", False):
|
| 148 |
+
return {
|
| 149 |
+
"success": False,
|
| 150 |
+
"error": result.get("error", "Unknown error"),
|
| 151 |
+
"properties": None,
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
return {
|
| 155 |
+
"success": True,
|
| 156 |
+
"error": None,
|
| 157 |
+
"properties": result["properties"],
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
except subprocess.TimeoutExpired:
|
| 161 |
+
elapsed = time.time() - t0
|
| 162 |
+
logger.warning(f"CadQuery execution timed out after {elapsed:.3f}s")
|
| 163 |
+
return {
|
| 164 |
+
"success": False,
|
| 165 |
+
"error": f"Code execution timed out after {timeout} seconds",
|
| 166 |
+
"properties": None,
|
| 167 |
+
}
|
| 168 |
+
except json.JSONDecodeError as e:
|
| 169 |
+
elapsed = time.time() - t0
|
| 170 |
+
logger.error(f"Failed to parse subprocess output after {elapsed:.3f}s: {e}")
|
| 171 |
+
return {
|
| 172 |
+
"success": False,
|
| 173 |
+
"error": f"Failed to parse execution output: {e}",
|
| 174 |
+
"properties": None,
|
| 175 |
+
}
|
| 176 |
+
except Exception as e:
|
| 177 |
+
elapsed = time.time() - t0
|
| 178 |
+
logger.error(f"execute_cadquery_code failed after {elapsed:.3f}s: {e}")
|
| 179 |
+
return {
|
| 180 |
+
"success": False,
|
| 181 |
+
"error": str(e),
|
| 182 |
+
"properties": None,
|
| 183 |
+
}
|
server/geometry.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import time
|
| 3 |
+
from collections import Counter
|
| 4 |
+
from typing import Any, Dict, List, Optional, Tuple
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
|
| 8 |
+
logger = logging.getLogger(__name__)
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def extract_properties(shape) -> Dict[str, Any]:
|
| 12 |
+
t0 = time.time()
|
| 13 |
+
try:
|
| 14 |
+
props = {}
|
| 15 |
+
|
| 16 |
+
bb = shape.BoundingBox()
|
| 17 |
+
dims = [bb.xlen, bb.ylen, bb.zlen]
|
| 18 |
+
sorted_axes = sorted(
|
| 19 |
+
zip(["X", "Y", "Z"], dims), key=lambda x: x[1], reverse=True
|
| 20 |
+
)
|
| 21 |
+
longest_axis = sorted_axes[0][0]
|
| 22 |
+
|
| 23 |
+
props["is_valid"] = shape.isValid()
|
| 24 |
+
|
| 25 |
+
volume = shape.Volume()
|
| 26 |
+
surface_area = shape.Area()
|
| 27 |
+
props["volume_mm3"] = round(volume, 4)
|
| 28 |
+
props["surface_area_mm2"] = round(surface_area, 4)
|
| 29 |
+
|
| 30 |
+
props["bbox_x_mm"] = round(bb.xlen, 4)
|
| 31 |
+
props["bbox_y_mm"] = round(bb.ylen, 4)
|
| 32 |
+
props["bbox_z_mm"] = round(bb.zlen, 4)
|
| 33 |
+
props["bbox_longest_axis"] = longest_axis
|
| 34 |
+
|
| 35 |
+
max_dim = max(dims)
|
| 36 |
+
props["bbox_ratio_yx"] = round(sorted_axes[1][1] / max_dim, 4) if max_dim > 0 else 0.0
|
| 37 |
+
props["bbox_ratio_zx"] = round(sorted_axes[2][1] / max_dim, 4) if max_dim > 0 else 0.0
|
| 38 |
+
|
| 39 |
+
faces = shape.Faces()
|
| 40 |
+
face_types = [f.geomType() for f in faces]
|
| 41 |
+
type_counts = dict(Counter(face_types))
|
| 42 |
+
total_faces = len(face_types)
|
| 43 |
+
|
| 44 |
+
props["face_count"] = total_faces
|
| 45 |
+
props["face_type_counts"] = type_counts
|
| 46 |
+
props["dominant_face_type"] = max(type_counts, key=type_counts.get) if type_counts else "UNKNOWN"
|
| 47 |
+
props["face_type_distribution"] = {
|
| 48 |
+
k: round(v / total_faces, 4) for k, v in type_counts.items()
|
| 49 |
+
} if total_faces > 0 else {}
|
| 50 |
+
|
| 51 |
+
props["edge_count"] = len(shape.Edges())
|
| 52 |
+
props["vertex_count"] = len(shape.Vertices())
|
| 53 |
+
|
| 54 |
+
V = props["vertex_count"]
|
| 55 |
+
E = props["edge_count"]
|
| 56 |
+
F = props["face_count"]
|
| 57 |
+
props["euler_characteristic"] = V - E + F
|
| 58 |
+
|
| 59 |
+
props["is_watertight"] = _check_watertight(shape)
|
| 60 |
+
|
| 61 |
+
xy_sym, xz_sym, yz_sym = _check_symmetry(shape, bb)
|
| 62 |
+
props["has_xy_symmetry"] = xy_sym
|
| 63 |
+
props["has_xz_symmetry"] = xz_sym
|
| 64 |
+
props["has_yz_symmetry"] = yz_sym
|
| 65 |
+
|
| 66 |
+
props["shape_class"] = _classify_shape(props)
|
| 67 |
+
|
| 68 |
+
elapsed = time.time() - t0
|
| 69 |
+
logger.info(f"extract_properties took {elapsed:.3f}s")
|
| 70 |
+
return props
|
| 71 |
+
|
| 72 |
+
except Exception as e:
|
| 73 |
+
elapsed = time.time() - t0
|
| 74 |
+
logger.error(f"extract_properties failed after {elapsed:.3f}s: {e}")
|
| 75 |
+
raise
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def _check_watertight(shape) -> bool:
|
| 79 |
+
try:
|
| 80 |
+
shells = shape.Shells()
|
| 81 |
+
if not shells:
|
| 82 |
+
return False
|
| 83 |
+
for shell in shells:
|
| 84 |
+
if not shell.Closed():
|
| 85 |
+
return False
|
| 86 |
+
return True
|
| 87 |
+
except Exception:
|
| 88 |
+
return False
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def _check_symmetry(shape, bb, n_sample: int = 200, tol_ratio: float = 0.05) -> Tuple[bool, bool, bool]:
|
| 92 |
+
try:
|
| 93 |
+
from OCP.BRepMesh import BRepMesh_IncrementalMesh
|
| 94 |
+
from OCP.BRep import BRep_Tool
|
| 95 |
+
from OCP.TopExp import TopExp_Explorer
|
| 96 |
+
from OCP.TopAbs import TopAbs_FACE
|
| 97 |
+
from OCP.TopLoc import TopLoc_Location
|
| 98 |
+
from OCP.TopoDS import TopoDS
|
| 99 |
+
|
| 100 |
+
mesh = BRepMesh_IncrementalMesh(shape.wrapped, 0.5, False, 0.5, True)
|
| 101 |
+
mesh.Perform()
|
| 102 |
+
|
| 103 |
+
points = []
|
| 104 |
+
explorer = TopExp_Explorer(shape.wrapped, TopAbs_FACE)
|
| 105 |
+
while explorer.More():
|
| 106 |
+
face = TopoDS.Face_s(explorer.Current())
|
| 107 |
+
loc = TopLoc_Location()
|
| 108 |
+
tri = BRep_Tool.Triangulation_s(face, loc)
|
| 109 |
+
if tri is not None:
|
| 110 |
+
for i in range(1, tri.NbNodes() + 1):
|
| 111 |
+
p = tri.Node(i)
|
| 112 |
+
trsf = loc.Transformation()
|
| 113 |
+
p.Transform(trsf)
|
| 114 |
+
points.append([p.X(), p.Y(), p.Z()])
|
| 115 |
+
explorer.Next()
|
| 116 |
+
|
| 117 |
+
if len(points) < 10:
|
| 118 |
+
return False, False, False
|
| 119 |
+
|
| 120 |
+
pts = np.array(points)
|
| 121 |
+
if len(pts) > n_sample:
|
| 122 |
+
idx = np.random.choice(len(pts), n_sample, replace=False)
|
| 123 |
+
pts = pts[idx]
|
| 124 |
+
|
| 125 |
+
diag = np.sqrt(bb.xlen**2 + bb.ylen**2 + bb.zlen**2)
|
| 126 |
+
tol = diag * tol_ratio
|
| 127 |
+
|
| 128 |
+
from scipy.spatial import cKDTree
|
| 129 |
+
|
| 130 |
+
tree = cKDTree(pts)
|
| 131 |
+
|
| 132 |
+
def check_plane_symmetry(pts_arr, axis_idx):
|
| 133 |
+
reflected = pts_arr.copy()
|
| 134 |
+
reflected[:, axis_idx] = -reflected[:, axis_idx]
|
| 135 |
+
dists, _ = tree.query(reflected)
|
| 136 |
+
return float(np.mean(dists)) < tol
|
| 137 |
+
|
| 138 |
+
cx = (bb.xmin + bb.xmax) / 2
|
| 139 |
+
cy = (bb.ymin + bb.ymax) / 2
|
| 140 |
+
cz = (bb.zmin + bb.zmax) / 2
|
| 141 |
+
centered = pts - np.array([cx, cy, cz])
|
| 142 |
+
tree_c = cKDTree(centered)
|
| 143 |
+
|
| 144 |
+
def check_sym(axis_idx):
|
| 145 |
+
reflected = centered.copy()
|
| 146 |
+
reflected[:, axis_idx] = -reflected[:, axis_idx]
|
| 147 |
+
dists, _ = tree_c.query(reflected)
|
| 148 |
+
return float(np.mean(dists)) < tol
|
| 149 |
+
|
| 150 |
+
has_yz = check_sym(0)
|
| 151 |
+
has_xz = check_sym(1)
|
| 152 |
+
has_xy = check_sym(2)
|
| 153 |
+
|
| 154 |
+
return has_xy, has_xz, has_yz
|
| 155 |
+
|
| 156 |
+
except Exception as e:
|
| 157 |
+
logger.warning(f"Symmetry check failed: {e}")
|
| 158 |
+
return False, False, False
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def _classify_shape(props: Dict[str, Any]) -> str:
|
| 162 |
+
ratio_yx = props.get("bbox_ratio_yx", 0)
|
| 163 |
+
ratio_zx = props.get("bbox_ratio_zx", 0)
|
| 164 |
+
dominant = props.get("dominant_face_type", "")
|
| 165 |
+
euler = props.get("euler_characteristic", 2)
|
| 166 |
+
is_valid = props.get("is_valid", False)
|
| 167 |
+
|
| 168 |
+
if not is_valid or props.get("volume_mm3", 0) <= 0:
|
| 169 |
+
return "DEGENERATE"
|
| 170 |
+
|
| 171 |
+
is_flat = ratio_zx < 0.25
|
| 172 |
+
is_cubic = ratio_yx > 0.7 and ratio_zx > 0.7
|
| 173 |
+
is_round = dominant in ("CYLINDER", "CONE", "SPHERE", "TORUS")
|
| 174 |
+
is_tall = ratio_zx > 0.5 and not is_cubic
|
| 175 |
+
|
| 176 |
+
if is_round:
|
| 177 |
+
if is_flat:
|
| 178 |
+
return "FLAT_ROUND_SOLID"
|
| 179 |
+
if euler != 2:
|
| 180 |
+
return "HOLLOW_ROUND_SOLID"
|
| 181 |
+
if is_tall:
|
| 182 |
+
return "TALL_ROUND_SOLID"
|
| 183 |
+
return "FLAT_ROUND_SOLID"
|
| 184 |
+
|
| 185 |
+
if is_flat:
|
| 186 |
+
if euler != 2:
|
| 187 |
+
return "FLAT_SOLID_WITH_HOLES"
|
| 188 |
+
return "FLAT_SOLID"
|
| 189 |
+
|
| 190 |
+
if is_cubic:
|
| 191 |
+
if euler != 2:
|
| 192 |
+
return "CUBIC_SOLID_WITH_HOLES"
|
| 193 |
+
return "CUBIC_SOLID"
|
| 194 |
+
|
| 195 |
+
face_count = props.get("face_count", 0)
|
| 196 |
+
|
| 197 |
+
if face_count > 8 and ratio_yx < 0.5:
|
| 198 |
+
return "L_SHAPED_SOLID"
|
| 199 |
+
|
| 200 |
+
if face_count > 8 and ratio_yx > 0.5:
|
| 201 |
+
return "T_SHAPED_SOLID"
|
| 202 |
+
|
| 203 |
+
if face_count > 10:
|
| 204 |
+
return "STEPPED_SOLID"
|
| 205 |
+
|
| 206 |
+
return "COMPLEX_SOLID"
|
server/preprocessor.py
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import logging
|
| 3 |
+
import time
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Any, Dict, Optional, Tuple
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
|
| 9 |
+
logger = logging.getLogger(__name__)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def normalize_shape(shape) -> Tuple[Any, Dict[str, Any]]:
|
| 13 |
+
t0 = time.time()
|
| 14 |
+
import cadquery as cq
|
| 15 |
+
|
| 16 |
+
bb = shape.BoundingBox()
|
| 17 |
+
cx = (bb.xmin + bb.xmax) / 2
|
| 18 |
+
cy = (bb.ymin + bb.ymax) / 2
|
| 19 |
+
cz = (bb.zmin + bb.zmax) / 2
|
| 20 |
+
shape = shape.translate((-cx, -cy, -cz))
|
| 21 |
+
|
| 22 |
+
bb = shape.BoundingBox()
|
| 23 |
+
dims = {"X": bb.xlen, "Y": bb.ylen, "Z": bb.zlen}
|
| 24 |
+
sorted_dims = sorted(dims.items(), key=lambda x: x[1], reverse=True)
|
| 25 |
+
|
| 26 |
+
longest, second, shortest = sorted_dims[0][0], sorted_dims[1][0], sorted_dims[2][0]
|
| 27 |
+
|
| 28 |
+
target_order = ["X", "Y", "Z"]
|
| 29 |
+
current_order = [longest, second, shortest]
|
| 30 |
+
|
| 31 |
+
rotation = _compute_alignment_rotation(current_order, target_order)
|
| 32 |
+
if rotation is not None:
|
| 33 |
+
rx, ry, rz = rotation
|
| 34 |
+
if rx != 0 or ry != 0 or rz != 0:
|
| 35 |
+
from OCP.gp import gp_Ax1, gp_Pnt, gp_Dir, gp_Trsf
|
| 36 |
+
from OCP.BRepBuilderAPI import BRepBuilderAPI_Transform
|
| 37 |
+
|
| 38 |
+
trsf = gp_Trsf()
|
| 39 |
+
if rz != 0:
|
| 40 |
+
trsf_z = gp_Trsf()
|
| 41 |
+
trsf_z.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1)), np.radians(rz))
|
| 42 |
+
trsf.Multiply(trsf_z)
|
| 43 |
+
if ry != 0:
|
| 44 |
+
trsf_y = gp_Trsf()
|
| 45 |
+
trsf_y.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(0, 1, 0)), np.radians(ry))
|
| 46 |
+
trsf.Multiply(trsf_y)
|
| 47 |
+
if rx != 0:
|
| 48 |
+
trsf_x = gp_Trsf()
|
| 49 |
+
trsf_x.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(1, 0, 0)), np.radians(rx))
|
| 50 |
+
trsf.Multiply(trsf_x)
|
| 51 |
+
|
| 52 |
+
builder = BRepBuilderAPI_Transform(shape.wrapped, trsf, True)
|
| 53 |
+
builder.Build()
|
| 54 |
+
shape = cq.Shape(builder.Shape())
|
| 55 |
+
|
| 56 |
+
bb = shape.BoundingBox()
|
| 57 |
+
cx2 = (bb.xmin + bb.xmax) / 2
|
| 58 |
+
cy2 = (bb.ymin + bb.ymax) / 2
|
| 59 |
+
cz2 = (bb.zmin + bb.zmax) / 2
|
| 60 |
+
if abs(cx2) > 0.01 or abs(cy2) > 0.01 or abs(cz2) > 0.01:
|
| 61 |
+
shape = shape.translate((-cx2, -cy2, -cz2))
|
| 62 |
+
|
| 63 |
+
transform_info = {
|
| 64 |
+
"units": "mm",
|
| 65 |
+
"translation_to_origin": [round(cx, 4), round(cy, 4), round(cz, 4)],
|
| 66 |
+
"axis_alignment": current_order,
|
| 67 |
+
"longest_axis": "X",
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
elapsed = time.time() - t0
|
| 71 |
+
logger.info(f"normalize_shape took {elapsed:.3f}s")
|
| 72 |
+
return shape, transform_info
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _compute_alignment_rotation(current, target):
|
| 76 |
+
if current == target:
|
| 77 |
+
return None
|
| 78 |
+
|
| 79 |
+
c = current
|
| 80 |
+
if c == ["Y", "X", "Z"]:
|
| 81 |
+
return (0, 0, 90)
|
| 82 |
+
elif c == ["Z", "Y", "X"]:
|
| 83 |
+
return (0, 90, 0)
|
| 84 |
+
elif c == ["Z", "X", "Y"]:
|
| 85 |
+
return (90, 0, 0)
|
| 86 |
+
elif c == ["X", "Z", "Y"]:
|
| 87 |
+
return (90, 0, 0)
|
| 88 |
+
elif c == ["Y", "Z", "X"]:
|
| 89 |
+
return (0, 0, 90)
|
| 90 |
+
else:
|
| 91 |
+
return (0, 0, 0)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def sample_surface_points(shape, n_points: int = 2048) -> np.ndarray:
|
| 95 |
+
t0 = time.time()
|
| 96 |
+
from OCP.BRepMesh import BRepMesh_IncrementalMesh
|
| 97 |
+
from OCP.BRep import BRep_Tool
|
| 98 |
+
from OCP.TopExp import TopExp_Explorer
|
| 99 |
+
from OCP.TopAbs import TopAbs_FACE
|
| 100 |
+
from OCP.TopLoc import TopLoc_Location
|
| 101 |
+
from OCP.TopoDS import TopoDS
|
| 102 |
+
|
| 103 |
+
mesh = BRepMesh_IncrementalMesh(shape.wrapped, 0.1, False, 0.1, True)
|
| 104 |
+
mesh.Perform()
|
| 105 |
+
|
| 106 |
+
all_points = []
|
| 107 |
+
all_areas = []
|
| 108 |
+
|
| 109 |
+
explorer = TopExp_Explorer(shape.wrapped, TopAbs_FACE)
|
| 110 |
+
while explorer.More():
|
| 111 |
+
face = TopoDS.Face_s(explorer.Current())
|
| 112 |
+
loc = TopLoc_Location()
|
| 113 |
+
tri = BRep_Tool.Triangulation_s(face, loc)
|
| 114 |
+
if tri is not None:
|
| 115 |
+
trsf = loc.Transformation()
|
| 116 |
+
nodes = []
|
| 117 |
+
for i in range(1, tri.NbNodes() + 1):
|
| 118 |
+
p = tri.Node(i)
|
| 119 |
+
p.Transform(trsf)
|
| 120 |
+
nodes.append([p.X(), p.Y(), p.Z()])
|
| 121 |
+
nodes = np.array(nodes)
|
| 122 |
+
|
| 123 |
+
for i in range(1, tri.NbTriangles() + 1):
|
| 124 |
+
t = tri.Triangle(i)
|
| 125 |
+
n1, n2, n3 = t.Get()
|
| 126 |
+
v0 = nodes[n1 - 1]
|
| 127 |
+
v1 = nodes[n2 - 1]
|
| 128 |
+
v2 = nodes[n3 - 1]
|
| 129 |
+
area = 0.5 * np.linalg.norm(np.cross(v1 - v0, v2 - v0))
|
| 130 |
+
if area > 1e-12:
|
| 131 |
+
all_points.append((v0, v1, v2))
|
| 132 |
+
all_areas.append(area)
|
| 133 |
+
|
| 134 |
+
explorer.Next()
|
| 135 |
+
|
| 136 |
+
if not all_points:
|
| 137 |
+
logger.warning("No triangles found for surface sampling")
|
| 138 |
+
return np.zeros((n_points, 3))
|
| 139 |
+
|
| 140 |
+
areas = np.array(all_areas)
|
| 141 |
+
probs = areas / areas.sum()
|
| 142 |
+
|
| 143 |
+
sampled = []
|
| 144 |
+
chosen = np.random.choice(len(all_points), size=n_points, p=probs)
|
| 145 |
+
for idx in chosen:
|
| 146 |
+
v0, v1, v2 = all_points[idx]
|
| 147 |
+
r1, r2 = np.random.random(), np.random.random()
|
| 148 |
+
if r1 + r2 > 1:
|
| 149 |
+
r1, r2 = 1 - r1, 1 - r2
|
| 150 |
+
pt = v0 * (1 - r1 - r2) + v1 * r1 + v2 * r2
|
| 151 |
+
sampled.append(pt)
|
| 152 |
+
|
| 153 |
+
result = np.array(sampled, dtype=np.float32)
|
| 154 |
+
elapsed = time.time() - t0
|
| 155 |
+
logger.info(f"sample_surface_points ({n_points} pts) took {elapsed:.3f}s")
|
| 156 |
+
return result
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def _occ_to_trimesh(shape):
|
| 160 |
+
import trimesh
|
| 161 |
+
from OCP.BRepMesh import BRepMesh_IncrementalMesh
|
| 162 |
+
from OCP.BRep import BRep_Tool
|
| 163 |
+
from OCP.TopExp import TopExp_Explorer
|
| 164 |
+
from OCP.TopAbs import TopAbs_FACE
|
| 165 |
+
from OCP.TopLoc import TopLoc_Location
|
| 166 |
+
from OCP.TopoDS import TopoDS
|
| 167 |
+
|
| 168 |
+
mesh_occ = BRepMesh_IncrementalMesh(shape.wrapped, 0.1, False, 0.1, True)
|
| 169 |
+
mesh_occ.Perform()
|
| 170 |
+
|
| 171 |
+
verts, faces = [], []
|
| 172 |
+
offset = 0
|
| 173 |
+
explorer = TopExp_Explorer(shape.wrapped, TopAbs_FACE)
|
| 174 |
+
while explorer.More():
|
| 175 |
+
face = TopoDS.Face_s(explorer.Current())
|
| 176 |
+
loc = TopLoc_Location()
|
| 177 |
+
tri = BRep_Tool.Triangulation_s(face, loc)
|
| 178 |
+
if tri is not None:
|
| 179 |
+
trsf = loc.Transformation()
|
| 180 |
+
nodes = []
|
| 181 |
+
for i in range(1, tri.NbNodes() + 1):
|
| 182 |
+
p = tri.Node(i)
|
| 183 |
+
p.Transform(trsf)
|
| 184 |
+
nodes.append([p.X(), p.Y(), p.Z()])
|
| 185 |
+
for i in range(1, tri.NbTriangles() + 1):
|
| 186 |
+
t = tri.Triangle(i)
|
| 187 |
+
n1, n2, n3 = t.Get()
|
| 188 |
+
faces.append([n1 - 1 + offset, n2 - 1 + offset, n3 - 1 + offset])
|
| 189 |
+
verts.extend(nodes)
|
| 190 |
+
offset += len(nodes)
|
| 191 |
+
explorer.Next()
|
| 192 |
+
|
| 193 |
+
if not verts:
|
| 194 |
+
raise ValueError("No mesh triangles extracted from shape")
|
| 195 |
+
|
| 196 |
+
m = trimesh.Trimesh(
|
| 197 |
+
vertices=np.array(verts, dtype=np.float64),
|
| 198 |
+
faces=np.array(faces, dtype=np.int64),
|
| 199 |
+
process=True,
|
| 200 |
+
)
|
| 201 |
+
m.fix_normals()
|
| 202 |
+
return m
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def voxelize(shape, resolution: int = 64) -> np.ndarray:
|
| 206 |
+
t0 = time.time()
|
| 207 |
+
|
| 208 |
+
tri_mesh = _occ_to_trimesh(shape)
|
| 209 |
+
|
| 210 |
+
bb = shape.BoundingBox()
|
| 211 |
+
padding = 0.01
|
| 212 |
+
xs = np.linspace(bb.xmin - padding, bb.xmax + padding, resolution)
|
| 213 |
+
ys = np.linspace(bb.ymin - padding, bb.ymax + padding, resolution)
|
| 214 |
+
zs = np.linspace(bb.zmin - padding, bb.zmax + padding, resolution)
|
| 215 |
+
grid_pts = np.stack(np.meshgrid(xs, ys, zs, indexing='ij'), axis=-1).reshape(-1, 3)
|
| 216 |
+
|
| 217 |
+
inside = tri_mesh.contains(grid_pts)
|
| 218 |
+
grid = inside.reshape(resolution, resolution, resolution)
|
| 219 |
+
|
| 220 |
+
elapsed = time.time() - t0
|
| 221 |
+
logger.info(f"voxelize ({resolution}^3, trimesh+embree) took {elapsed:.3f}s, fill={grid.sum()}")
|
| 222 |
+
return grid
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def voxelize_in_bbox(shape, bbox_min, bbox_max, resolution: int = 64) -> np.ndarray:
|
| 226 |
+
t0 = time.time()
|
| 227 |
+
|
| 228 |
+
tri_mesh = _occ_to_trimesh(shape)
|
| 229 |
+
|
| 230 |
+
padding = 0.01
|
| 231 |
+
xs = np.linspace(bbox_min[0] - padding, bbox_max[0] + padding, resolution)
|
| 232 |
+
ys = np.linspace(bbox_min[1] - padding, bbox_max[1] + padding, resolution)
|
| 233 |
+
zs = np.linspace(bbox_min[2] - padding, bbox_max[2] + padding, resolution)
|
| 234 |
+
grid_pts = np.stack(np.meshgrid(xs, ys, zs, indexing='ij'), axis=-1).reshape(-1, 3)
|
| 235 |
+
|
| 236 |
+
inside = tri_mesh.contains(grid_pts)
|
| 237 |
+
grid = inside.reshape(resolution, resolution, resolution)
|
| 238 |
+
|
| 239 |
+
elapsed = time.time() - t0
|
| 240 |
+
logger.info(f"voxelize_in_bbox ({resolution}^3) took {elapsed:.3f}s, fill={grid.sum()}/{resolution**3}")
|
| 241 |
+
return grid
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def generate_ground_truth(
|
| 245 |
+
shape,
|
| 246 |
+
output_dir: str,
|
| 247 |
+
source_step: Optional[str] = None,
|
| 248 |
+
) -> Dict[str, Any]:
|
| 249 |
+
t0 = time.time()
|
| 250 |
+
output_path = Path(output_dir)
|
| 251 |
+
output_path.mkdir(parents=True, exist_ok=True)
|
| 252 |
+
|
| 253 |
+
from .geometry import extract_properties
|
| 254 |
+
props = extract_properties(shape)
|
| 255 |
+
|
| 256 |
+
points = sample_surface_points(shape, n_points=2048)
|
| 257 |
+
np.save(str(output_path / "surface_points.npy"), points)
|
| 258 |
+
|
| 259 |
+
voxels = voxelize(shape, resolution=64)
|
| 260 |
+
np.save(str(output_path / "voxels_64.npy"), voxels)
|
| 261 |
+
|
| 262 |
+
bb = shape.BoundingBox()
|
| 263 |
+
|
| 264 |
+
ground_truth = {
|
| 265 |
+
"source_step": source_step,
|
| 266 |
+
"volume_mm3": props["volume_mm3"],
|
| 267 |
+
"surface_area_mm2": props["surface_area_mm2"],
|
| 268 |
+
"bbox_mm": [round(bb.xlen, 4), round(bb.ylen, 4), round(bb.zlen, 4)],
|
| 269 |
+
"face_count": props["face_count"],
|
| 270 |
+
"face_types": list(props["face_type_counts"].keys()),
|
| 271 |
+
"dominant_face_type": props["dominant_face_type"],
|
| 272 |
+
"euler_characteristic": props["euler_characteristic"],
|
| 273 |
+
"surface_points_file": "surface_points.npy",
|
| 274 |
+
"voxels_file": "voxels_64.npy",
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
with open(output_path / "ground_truth.json", "w") as f:
|
| 278 |
+
json.dump(ground_truth, f, indent=2)
|
| 279 |
+
|
| 280 |
+
elapsed = time.time() - t0
|
| 281 |
+
logger.info(f"generate_ground_truth took {elapsed:.3f}s")
|
| 282 |
+
return ground_truth
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
def preprocess_from_code(
|
| 286 |
+
code: str,
|
| 287 |
+
output_dir: str,
|
| 288 |
+
task_id: Optional[str] = None,
|
| 289 |
+
) -> Dict[str, Any]:
|
| 290 |
+
t0 = time.time()
|
| 291 |
+
import cadquery as cq
|
| 292 |
+
|
| 293 |
+
local_ns = {"cq": cq, "cadquery": cq}
|
| 294 |
+
try:
|
| 295 |
+
import math
|
| 296 |
+
local_ns["math"] = math
|
| 297 |
+
except Exception:
|
| 298 |
+
pass
|
| 299 |
+
|
| 300 |
+
exec(code, local_ns)
|
| 301 |
+
|
| 302 |
+
if "result" not in local_ns:
|
| 303 |
+
raise ValueError("Code must define a variable named 'result'")
|
| 304 |
+
|
| 305 |
+
result = local_ns["result"]
|
| 306 |
+
if hasattr(result, "val"):
|
| 307 |
+
shape = result.val()
|
| 308 |
+
else:
|
| 309 |
+
shape = result
|
| 310 |
+
|
| 311 |
+
orig_bb = shape.BoundingBox()
|
| 312 |
+
orig_bbox_mm = [round(orig_bb.xlen, 4), round(orig_bb.ylen, 4), round(orig_bb.zlen, 4)]
|
| 313 |
+
|
| 314 |
+
normalized_shape, transform_info = normalize_shape(shape)
|
| 315 |
+
|
| 316 |
+
output_path = Path(output_dir)
|
| 317 |
+
output_path.mkdir(parents=True, exist_ok=True)
|
| 318 |
+
|
| 319 |
+
import cadquery as cq
|
| 320 |
+
original_step_path = str(output_path / "ground_truth.step")
|
| 321 |
+
cq.exporters.export(cq.Workplane().add(shape), original_step_path, exportType="STEP")
|
| 322 |
+
|
| 323 |
+
normalized_step_path = str(output_path / "ground_truth_normalized.step")
|
| 324 |
+
cq.exporters.export(cq.Workplane().add(normalized_shape), normalized_step_path, exportType="STEP")
|
| 325 |
+
|
| 326 |
+
gt = generate_ground_truth(
|
| 327 |
+
normalized_shape,
|
| 328 |
+
output_dir,
|
| 329 |
+
source_step=f"server/tasks/{task_id}/ground_truth.step" if task_id else original_step_path,
|
| 330 |
+
)
|
| 331 |
+
gt["canonical_transform"] = transform_info
|
| 332 |
+
gt["original_bbox_mm"] = orig_bbox_mm
|
| 333 |
+
|
| 334 |
+
with open(output_path / "ground_truth.json", "w") as f:
|
| 335 |
+
json.dump(gt, f, indent=2)
|
| 336 |
+
|
| 337 |
+
elapsed = time.time() - t0
|
| 338 |
+
logger.info(f"preprocess_from_code took {elapsed:.3f}s total")
|
| 339 |
+
return gt
|
server/requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
openenv[core]>=0.2.0
|
| 2 |
+
fastapi>=0.115.0
|
| 3 |
+
uvicorn>=0.24.0
|
| 4 |
+
scipy>=1.11.0
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
|
server/reward.py
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import time
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from typing import Any, Dict
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
from scipy.spatial import cKDTree
|
| 8 |
+
|
| 9 |
+
from openenv.core.rubrics.base import Rubric
|
| 10 |
+
from openenv.core.rubrics.containers import Gate, Sequential, WeightedSum
|
| 11 |
+
|
| 12 |
+
from .preprocessor import normalize_shape, sample_surface_points, voxelize_in_bbox
|
| 13 |
+
|
| 14 |
+
logger = logging.getLogger(__name__)
|
| 15 |
+
|
| 16 |
+
ENABLE_CHAMFER_DISTANCE = True
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class RexecRubric(Rubric):
|
| 20 |
+
def __init__(self):
|
| 21 |
+
super().__init__()
|
| 22 |
+
|
| 23 |
+
def forward(self, action: Any, observation: Any) -> float:
|
| 24 |
+
t0 = time.time()
|
| 25 |
+
|
| 26 |
+
if observation.code_executed is not True:
|
| 27 |
+
logger.info("Rexec=0: code did not execute successfully")
|
| 28 |
+
return 0.0
|
| 29 |
+
|
| 30 |
+
props = observation.object_properties
|
| 31 |
+
if props is None:
|
| 32 |
+
logger.info("Rexec=0: no object properties")
|
| 33 |
+
return 0.0
|
| 34 |
+
|
| 35 |
+
if not props.get("is_valid", False):
|
| 36 |
+
logger.info("Rexec=0: shape is not valid")
|
| 37 |
+
return 0.0
|
| 38 |
+
|
| 39 |
+
if props.get("volume_mm3", 0) <= 0:
|
| 40 |
+
logger.info("Rexec=0: volume is zero or negative")
|
| 41 |
+
return 0.0
|
| 42 |
+
|
| 43 |
+
elapsed = time.time() - t0
|
| 44 |
+
logger.info(f"Rexec=1.0 (took {elapsed:.3f}s)")
|
| 45 |
+
return 1.0
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class RgeomRubric(Rubric):
|
| 49 |
+
def __init__(self, ground_truth_dir: str, enable_cd: bool = True):
|
| 50 |
+
super().__init__()
|
| 51 |
+
self._gt_dir = Path(ground_truth_dir)
|
| 52 |
+
self._gt_shape = None
|
| 53 |
+
self._gt_points = None
|
| 54 |
+
self._gt_data = None
|
| 55 |
+
self._gt_bbox = None
|
| 56 |
+
self._enable_cd = enable_cd and ENABLE_CHAMFER_DISTANCE
|
| 57 |
+
|
| 58 |
+
def _load_ground_truth(self):
|
| 59 |
+
if self._gt_data is None:
|
| 60 |
+
import json
|
| 61 |
+
gt_path = self._gt_dir / "ground_truth.json"
|
| 62 |
+
with open(gt_path) as f:
|
| 63 |
+
self._gt_data = json.load(f)
|
| 64 |
+
|
| 65 |
+
if self._gt_shape is None:
|
| 66 |
+
import cadquery as cq
|
| 67 |
+
t0 = time.time()
|
| 68 |
+
gt_step_path = self._gt_dir / "ground_truth_normalized.step"
|
| 69 |
+
if not gt_step_path.exists():
|
| 70 |
+
gt_step_path = self._gt_dir / "ground_truth.step"
|
| 71 |
+
wp = cq.importers.importStep(str(gt_step_path))
|
| 72 |
+
self._gt_shape = wp.val()
|
| 73 |
+
bb = self._gt_shape.BoundingBox()
|
| 74 |
+
self._gt_bbox = {
|
| 75 |
+
"xmin": bb.xmin, "xmax": bb.xmax,
|
| 76 |
+
"ymin": bb.ymin, "ymax": bb.ymax,
|
| 77 |
+
"zmin": bb.zmin, "zmax": bb.zmax,
|
| 78 |
+
}
|
| 79 |
+
elapsed = time.time() - t0
|
| 80 |
+
logger.info(f"Loaded GT STEP in {elapsed:.3f}s, bbox=[{bb.xlen:.2f}, {bb.ylen:.2f}, {bb.zlen:.2f}]")
|
| 81 |
+
|
| 82 |
+
if self._gt_points is None:
|
| 83 |
+
pts_path = self._gt_dir / "surface_points.npy"
|
| 84 |
+
if pts_path.exists():
|
| 85 |
+
self._gt_points = np.load(str(pts_path))
|
| 86 |
+
logger.info(f"Loaded ground truth points: {self._gt_points.shape}")
|
| 87 |
+
else:
|
| 88 |
+
t0 = time.time()
|
| 89 |
+
self._gt_points = sample_surface_points(self._gt_shape, n_points=2048)
|
| 90 |
+
elapsed = time.time() - t0
|
| 91 |
+
logger.info(f"Sampled GT surface points in {elapsed:.3f}s")
|
| 92 |
+
|
| 93 |
+
def _load_agent_shape(self, step_path: str):
|
| 94 |
+
import cadquery as cq
|
| 95 |
+
t0 = time.time()
|
| 96 |
+
wp = cq.importers.importStep(step_path)
|
| 97 |
+
raw_shape = wp.val()
|
| 98 |
+
normalized_shape, _ = normalize_shape(raw_shape)
|
| 99 |
+
elapsed = time.time() - t0
|
| 100 |
+
logger.info(f"Loaded + normalized agent STEP in {elapsed:.3f}s")
|
| 101 |
+
return normalized_shape
|
| 102 |
+
|
| 103 |
+
def forward(self, action: Any, observation: Any) -> float:
|
| 104 |
+
t0 = time.time()
|
| 105 |
+
|
| 106 |
+
props = observation.object_properties
|
| 107 |
+
if props is None:
|
| 108 |
+
return 0.0
|
| 109 |
+
|
| 110 |
+
raw_data = getattr(observation, "_raw_data", None)
|
| 111 |
+
if raw_data is None or raw_data.get("step_path") is None:
|
| 112 |
+
logger.warning("Rgeom: no agent STEP path in _raw_data, returning 0")
|
| 113 |
+
return 0.0
|
| 114 |
+
|
| 115 |
+
agent_step_path = raw_data["step_path"]
|
| 116 |
+
if not Path(agent_step_path).exists():
|
| 117 |
+
logger.warning(f"Rgeom: agent STEP file not found: {agent_step_path}")
|
| 118 |
+
return 0.0
|
| 119 |
+
|
| 120 |
+
self._load_ground_truth()
|
| 121 |
+
|
| 122 |
+
try:
|
| 123 |
+
agent_shape = self._load_agent_shape(agent_step_path)
|
| 124 |
+
except Exception as e:
|
| 125 |
+
logger.error(f"Rgeom: failed to load agent STEP: {e}")
|
| 126 |
+
return 0.0
|
| 127 |
+
|
| 128 |
+
gt_bb = self._gt_bbox
|
| 129 |
+
bbox_min = [gt_bb["xmin"], gt_bb["ymin"], gt_bb["zmin"]]
|
| 130 |
+
bbox_max = [gt_bb["xmax"], gt_bb["ymax"], gt_bb["zmax"]]
|
| 131 |
+
|
| 132 |
+
try:
|
| 133 |
+
t_vox = time.time()
|
| 134 |
+
agent_voxels = voxelize_in_bbox(agent_shape, bbox_min, bbox_max, resolution=64)
|
| 135 |
+
gt_voxels = voxelize_in_bbox(self._gt_shape, bbox_min, bbox_max, resolution=64)
|
| 136 |
+
logger.info(f"Voxelization (both shapes) took {time.time() - t_vox:.3f}s")
|
| 137 |
+
except Exception as e:
|
| 138 |
+
logger.error(f"Rgeom: voxelization failed: {e}")
|
| 139 |
+
return 0.0
|
| 140 |
+
|
| 141 |
+
iou_score = best_of_6_iou(agent_voxels, gt_voxels)
|
| 142 |
+
|
| 143 |
+
mean_cd_reward = 0.0
|
| 144 |
+
median_cd_reward = 0.0
|
| 145 |
+
|
| 146 |
+
if self._enable_cd:
|
| 147 |
+
try:
|
| 148 |
+
t_cd = time.time()
|
| 149 |
+
agent_points = sample_surface_points(agent_shape, n_points=2048)
|
| 150 |
+
|
| 151 |
+
bbox_mm = self._gt_data.get("bbox_mm", [1, 1, 1])
|
| 152 |
+
bbox_diag = np.sqrt(sum(d**2 for d in bbox_mm))
|
| 153 |
+
threshold = bbox_diag * 0.1
|
| 154 |
+
|
| 155 |
+
mean_cd = compute_mean_chamfer(agent_points, self._gt_points)
|
| 156 |
+
mean_cd_reward = max(0.0, 1.0 - (mean_cd / threshold)) if threshold > 0 else 0.0
|
| 157 |
+
|
| 158 |
+
median_cd = compute_median_chamfer(agent_points, self._gt_points)
|
| 159 |
+
median_cd_reward = max(0.0, 1.0 - (median_cd / threshold)) if threshold > 0 else 0.0
|
| 160 |
+
logger.info(f"Chamfer distance took {time.time() - t_cd:.3f}s")
|
| 161 |
+
except Exception as e:
|
| 162 |
+
logger.error(f"Rgeom: chamfer distance failed: {e}")
|
| 163 |
+
|
| 164 |
+
if self._enable_cd:
|
| 165 |
+
rgeom = 0.60 * iou_score + 0.20 * mean_cd_reward + 0.20 * median_cd_reward
|
| 166 |
+
else:
|
| 167 |
+
rgeom = iou_score
|
| 168 |
+
|
| 169 |
+
elapsed = time.time() - t0
|
| 170 |
+
logger.info(
|
| 171 |
+
f"Rgeom={rgeom:.4f} (IoU={iou_score:.4f}, MeanCD={mean_cd_reward:.4f}, "
|
| 172 |
+
f"MedianCD={median_cd_reward:.4f}, cd_enabled={self._enable_cd}, took {elapsed:.3f}s)"
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
return rgeom
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
class RevalRubric(Rubric):
|
| 179 |
+
W_VOLUME = 0.35
|
| 180 |
+
W_BBOX = 0.30
|
| 181 |
+
W_FACE_TYPE = 0.15
|
| 182 |
+
W_EULER = 0.20
|
| 183 |
+
|
| 184 |
+
def __init__(self, ground_truth_dir: str):
|
| 185 |
+
super().__init__()
|
| 186 |
+
self._gt_dir = Path(ground_truth_dir)
|
| 187 |
+
self._gt_data = None
|
| 188 |
+
|
| 189 |
+
def _load_ground_truth(self):
|
| 190 |
+
if self._gt_data is None:
|
| 191 |
+
import json
|
| 192 |
+
gt_path = self._gt_dir / "ground_truth.json"
|
| 193 |
+
with open(gt_path) as f:
|
| 194 |
+
self._gt_data = json.load(f)
|
| 195 |
+
|
| 196 |
+
def forward(self, action: Any, observation: Any) -> float:
|
| 197 |
+
t0 = time.time()
|
| 198 |
+
|
| 199 |
+
props = observation.object_properties
|
| 200 |
+
if props is None:
|
| 201 |
+
return 0.0
|
| 202 |
+
|
| 203 |
+
self._load_ground_truth()
|
| 204 |
+
|
| 205 |
+
gt_vol = self._gt_data.get("volume_mm3", 0)
|
| 206 |
+
agent_vol = props.get("volume_mm3", 0)
|
| 207 |
+
if gt_vol > 0:
|
| 208 |
+
vol_ratio = min(agent_vol, gt_vol) / max(agent_vol, gt_vol)
|
| 209 |
+
volume_score = max(0.0, vol_ratio)
|
| 210 |
+
else:
|
| 211 |
+
volume_score = 1.0 if agent_vol == 0 else 0.0
|
| 212 |
+
|
| 213 |
+
gt_bbox = sorted(self._gt_data.get("bbox_mm", [0, 0, 0]), reverse=True)
|
| 214 |
+
agent_bbox = sorted([
|
| 215 |
+
props.get("bbox_x_mm", 0),
|
| 216 |
+
props.get("bbox_y_mm", 0),
|
| 217 |
+
props.get("bbox_z_mm", 0),
|
| 218 |
+
], reverse=True)
|
| 219 |
+
|
| 220 |
+
bbox_scores = []
|
| 221 |
+
for a, g in zip(agent_bbox, gt_bbox):
|
| 222 |
+
if g > 0:
|
| 223 |
+
bbox_scores.append(max(0.0, 1.0 - abs(a - g) / g))
|
| 224 |
+
else:
|
| 225 |
+
bbox_scores.append(1.0 if a == 0 else 0.0)
|
| 226 |
+
bbox_score = sum(bbox_scores) / 3.0 if bbox_scores else 0.0
|
| 227 |
+
|
| 228 |
+
target_dominant = self._gt_data.get("dominant_face_type", "")
|
| 229 |
+
agent_dominant = props.get("dominant_face_type", "")
|
| 230 |
+
face_type_score = 1.0 if agent_dominant == target_dominant else 0.0
|
| 231 |
+
|
| 232 |
+
gt_euler = self._gt_data.get("euler_characteristic", 2)
|
| 233 |
+
agent_euler = props.get("euler_characteristic", 2)
|
| 234 |
+
euler_score = 1.0 if agent_euler == gt_euler else 0.0
|
| 235 |
+
|
| 236 |
+
reval = (
|
| 237 |
+
self.W_VOLUME * volume_score
|
| 238 |
+
+ self.W_BBOX * bbox_score
|
| 239 |
+
+ self.W_FACE_TYPE * face_type_score
|
| 240 |
+
+ self.W_EULER * euler_score
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
elapsed = time.time() - t0
|
| 244 |
+
logger.info(
|
| 245 |
+
f"Reval={reval:.4f} (vol={volume_score:.2f}, bbox={bbox_score:.2f}, "
|
| 246 |
+
f"face_type={face_type_score:.2f}, euler={euler_score:.2f}, took {elapsed:.3f}s)"
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
return reval
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
def build_cadforge_rubric(ground_truth_dir: str, enable_cd: bool = True) -> Rubric:
|
| 253 |
+
rexec = RexecRubric()
|
| 254 |
+
rgeom = RgeomRubric(ground_truth_dir, enable_cd=enable_cd)
|
| 255 |
+
reval = RevalRubric(ground_truth_dir)
|
| 256 |
+
|
| 257 |
+
quality = WeightedSum([rgeom, reval], weights=[0.8, 0.2])
|
| 258 |
+
rubric = Sequential(Gate(rexec), quality)
|
| 259 |
+
|
| 260 |
+
return rubric
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
def compute_iou(voxels_a: np.ndarray, voxels_b: np.ndarray) -> float:
|
| 264 |
+
a = voxels_a.astype(bool)
|
| 265 |
+
b = voxels_b.astype(bool)
|
| 266 |
+
|
| 267 |
+
if a.shape != b.shape:
|
| 268 |
+
from scipy.ndimage import zoom
|
| 269 |
+
target_shape = b.shape
|
| 270 |
+
zoom_factors = tuple(t / s for t, s in zip(target_shape, a.shape))
|
| 271 |
+
a = zoom(a.astype(float), zoom_factors, order=0) > 0.5
|
| 272 |
+
|
| 273 |
+
intersection = np.logical_and(a, b).sum()
|
| 274 |
+
union = np.logical_or(a, b).sum()
|
| 275 |
+
|
| 276 |
+
if union == 0:
|
| 277 |
+
return 0.0
|
| 278 |
+
return float(intersection / union)
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
def best_of_6_iou(agent_voxels: np.ndarray, gt_voxels: np.ndarray) -> float:
|
| 282 |
+
orientations = [
|
| 283 |
+
lambda v: v,
|
| 284 |
+
lambda v: np.rot90(v, 1, (0, 1)),
|
| 285 |
+
lambda v: np.rot90(v, 1, (0, 2)),
|
| 286 |
+
lambda v: np.rot90(v, 1, (1, 2)),
|
| 287 |
+
lambda v: np.rot90(v, 2, (0, 1)),
|
| 288 |
+
lambda v: np.rot90(v, 1, (0, 1)).T,
|
| 289 |
+
]
|
| 290 |
+
|
| 291 |
+
best = 0.0
|
| 292 |
+
for orient in orientations:
|
| 293 |
+
rotated = orient(agent_voxels.copy())
|
| 294 |
+
iou = compute_iou(rotated, gt_voxels)
|
| 295 |
+
if iou > best:
|
| 296 |
+
best = iou
|
| 297 |
+
return best
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
def compute_mean_chamfer(points_a: np.ndarray, points_b: np.ndarray) -> float:
|
| 301 |
+
if len(points_a) == 0 or len(points_b) == 0:
|
| 302 |
+
return float("inf")
|
| 303 |
+
|
| 304 |
+
tree_a = cKDTree(points_a)
|
| 305 |
+
tree_b = cKDTree(points_b)
|
| 306 |
+
|
| 307 |
+
dists_a_to_b, _ = tree_b.query(points_a)
|
| 308 |
+
dists_b_to_a, _ = tree_a.query(points_b)
|
| 309 |
+
|
| 310 |
+
mean_cd = (np.mean(dists_a_to_b) + np.mean(dists_b_to_a)) / 2
|
| 311 |
+
return float(mean_cd)
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
def compute_median_chamfer(points_a: np.ndarray, points_b: np.ndarray) -> float:
|
| 315 |
+
if len(points_a) == 0 or len(points_b) == 0:
|
| 316 |
+
return float("inf")
|
| 317 |
+
|
| 318 |
+
tree_a = cKDTree(points_a)
|
| 319 |
+
tree_b = cKDTree(points_b)
|
| 320 |
+
|
| 321 |
+
dists_a_to_b, _ = tree_b.query(points_a)
|
| 322 |
+
dists_b_to_a, _ = tree_a.query(points_b)
|
| 323 |
+
|
| 324 |
+
median_cd = (np.median(dists_a_to_b) + np.median(dists_b_to_a)) / 2
|
| 325 |
+
return float(median_cd)
|
server/tasks/task_001_flat_plate/ground_truth.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"source_step": "tasks/task_001_flat_plate/ground_truth.step",
|
| 3 |
+
"volume_mm3": 16000.0,
|
| 4 |
+
"surface_area_mm2": 7600.0,
|
| 5 |
+
"bbox_mm": [
|
| 6 |
+
80.0,
|
| 7 |
+
40.0,
|
| 8 |
+
5.0
|
| 9 |
+
],
|
| 10 |
+
"face_count": 6,
|
| 11 |
+
"face_types": [
|
| 12 |
+
"PLANE"
|
| 13 |
+
],
|
| 14 |
+
"dominant_face_type": "PLANE",
|
| 15 |
+
"hole_count": 0,
|
| 16 |
+
"hole_diameters_mm": [],
|
| 17 |
+
"euler_characteristic": 2,
|
| 18 |
+
"surface_points_file": "surface_points.npy",
|
| 19 |
+
"voxels_file": "voxels_64.npy",
|
| 20 |
+
"canonical_transform": {
|
| 21 |
+
"units": "mm",
|
| 22 |
+
"translation_to_origin": [
|
| 23 |
+
0.0,
|
| 24 |
+
0.0,
|
| 25 |
+
0.0
|
| 26 |
+
],
|
| 27 |
+
"axis_alignment": [
|
| 28 |
+
"X",
|
| 29 |
+
"Y",
|
| 30 |
+
"Z"
|
| 31 |
+
],
|
| 32 |
+
"longest_axis": "X"
|
| 33 |
+
},
|
| 34 |
+
"original_bbox_mm": [
|
| 35 |
+
80.0,
|
| 36 |
+
40.0,
|
| 37 |
+
5.0
|
| 38 |
+
]
|
| 39 |
+
}
|
server/tasks/task_001_flat_plate/ground_truth.step
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
ISO-10303-21;
|
| 2 |
+
HEADER;
|
| 3 |
+
FILE_DESCRIPTION(('Open CASCADE Model'),'2;1');
|
| 4 |
+
FILE_NAME('Open CASCADE Shape Model','2026-04-25T17:44:28',('Author'),(
|
| 5 |
+
'Open CASCADE'),'Open CASCADE STEP processor 7.8','Open CASCADE 7.8'
|
| 6 |
+
,'Unknown');
|
| 7 |
+
FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));
|
| 8 |
+
ENDSEC;
|
| 9 |
+
DATA;
|
| 10 |
+
#1 = APPLICATION_PROTOCOL_DEFINITION('international standard',
|
| 11 |
+
'automotive_design',2000,#2);
|
| 12 |
+
#2 = APPLICATION_CONTEXT(
|
| 13 |
+
'core data for automotive mechanical design processes');
|
| 14 |
+
#3 = SHAPE_DEFINITION_REPRESENTATION(#4,#10);
|
| 15 |
+
#4 = PRODUCT_DEFINITION_SHAPE('','',#5);
|
| 16 |
+
#5 = PRODUCT_DEFINITION('design','',#6,#9);
|
| 17 |
+
#6 = PRODUCT_DEFINITION_FORMATION('','',#7);
|
| 18 |
+
#7 = PRODUCT('Open CASCADE STEP translator 7.8 1',
|
| 19 |
+
'Open CASCADE STEP translator 7.8 1','',(#8));
|
| 20 |
+
#8 = PRODUCT_CONTEXT('',#2,'mechanical');
|
| 21 |
+
#9 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design');
|
| 22 |
+
#10 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#15),#345);
|
| 23 |
+
#11 = AXIS2_PLACEMENT_3D('',#12,#13,#14);
|
| 24 |
+
#12 = CARTESIAN_POINT('',(0.,0.,0.));
|
| 25 |
+
#13 = DIRECTION('',(0.,0.,1.));
|
| 26 |
+
#14 = DIRECTION('',(1.,0.,-0.));
|
| 27 |
+
#15 = MANIFOLD_SOLID_BREP('',#16);
|
| 28 |
+
#16 = CLOSED_SHELL('',(#17,#137,#237,#284,#331,#338));
|
| 29 |
+
#17 = ADVANCED_FACE('',(#18),#32,.F.);
|
| 30 |
+
#18 = FACE_BOUND('',#19,.F.);
|
| 31 |
+
#19 = EDGE_LOOP('',(#20,#55,#83,#111));
|
| 32 |
+
#20 = ORIENTED_EDGE('',*,*,#21,.F.);
|
| 33 |
+
#21 = EDGE_CURVE('',#22,#24,#26,.T.);
|
| 34 |
+
#22 = VERTEX_POINT('',#23);
|
| 35 |
+
#23 = CARTESIAN_POINT('',(-40.,-20.,-2.5));
|
| 36 |
+
#24 = VERTEX_POINT('',#25);
|
| 37 |
+
#25 = CARTESIAN_POINT('',(-40.,-20.,2.5));
|
| 38 |
+
#26 = SURFACE_CURVE('',#27,(#31,#43),.PCURVE_S1.);
|
| 39 |
+
#27 = LINE('',#28,#29);
|
| 40 |
+
#28 = CARTESIAN_POINT('',(-40.,-20.,-2.5));
|
| 41 |
+
#29 = VECTOR('',#30,1.);
|
| 42 |
+
#30 = DIRECTION('',(0.,0.,1.));
|
| 43 |
+
#31 = PCURVE('',#32,#37);
|
| 44 |
+
#32 = PLANE('',#33);
|
| 45 |
+
#33 = AXIS2_PLACEMENT_3D('',#34,#35,#36);
|
| 46 |
+
#34 = CARTESIAN_POINT('',(-40.,-20.,-2.5));
|
| 47 |
+
#35 = DIRECTION('',(1.,0.,0.));
|
| 48 |
+
#36 = DIRECTION('',(0.,0.,1.));
|
| 49 |
+
#37 = DEFINITIONAL_REPRESENTATION('',(#38),#42);
|
| 50 |
+
#38 = LINE('',#39,#40);
|
| 51 |
+
#39 = CARTESIAN_POINT('',(0.,0.));
|
| 52 |
+
#40 = VECTOR('',#41,1.);
|
| 53 |
+
#41 = DIRECTION('',(1.,0.));
|
| 54 |
+
#42 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 55 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 56 |
+
) );
|
| 57 |
+
#43 = PCURVE('',#44,#49);
|
| 58 |
+
#44 = PLANE('',#45);
|
| 59 |
+
#45 = AXIS2_PLACEMENT_3D('',#46,#47,#48);
|
| 60 |
+
#46 = CARTESIAN_POINT('',(-40.,-20.,-2.5));
|
| 61 |
+
#47 = DIRECTION('',(0.,1.,0.));
|
| 62 |
+
#48 = DIRECTION('',(0.,0.,1.));
|
| 63 |
+
#49 = DEFINITIONAL_REPRESENTATION('',(#50),#54);
|
| 64 |
+
#50 = LINE('',#51,#52);
|
| 65 |
+
#51 = CARTESIAN_POINT('',(0.,0.));
|
| 66 |
+
#52 = VECTOR('',#53,1.);
|
| 67 |
+
#53 = DIRECTION('',(1.,0.));
|
| 68 |
+
#54 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 69 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 70 |
+
) );
|
| 71 |
+
#55 = ORIENTED_EDGE('',*,*,#56,.T.);
|
| 72 |
+
#56 = EDGE_CURVE('',#22,#57,#59,.T.);
|
| 73 |
+
#57 = VERTEX_POINT('',#58);
|
| 74 |
+
#58 = CARTESIAN_POINT('',(-40.,20.,-2.5));
|
| 75 |
+
#59 = SURFACE_CURVE('',#60,(#64,#71),.PCURVE_S1.);
|
| 76 |
+
#60 = LINE('',#61,#62);
|
| 77 |
+
#61 = CARTESIAN_POINT('',(-40.,-20.,-2.5));
|
| 78 |
+
#62 = VECTOR('',#63,1.);
|
| 79 |
+
#63 = DIRECTION('',(0.,1.,0.));
|
| 80 |
+
#64 = PCURVE('',#32,#65);
|
| 81 |
+
#65 = DEFINITIONAL_REPRESENTATION('',(#66),#70);
|
| 82 |
+
#66 = LINE('',#67,#68);
|
| 83 |
+
#67 = CARTESIAN_POINT('',(0.,0.));
|
| 84 |
+
#68 = VECTOR('',#69,1.);
|
| 85 |
+
#69 = DIRECTION('',(0.,-1.));
|
| 86 |
+
#70 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 87 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 88 |
+
) );
|
| 89 |
+
#71 = PCURVE('',#72,#77);
|
| 90 |
+
#72 = PLANE('',#73);
|
| 91 |
+
#73 = AXIS2_PLACEMENT_3D('',#74,#75,#76);
|
| 92 |
+
#74 = CARTESIAN_POINT('',(-40.,-20.,-2.5));
|
| 93 |
+
#75 = DIRECTION('',(0.,0.,1.));
|
| 94 |
+
#76 = DIRECTION('',(1.,0.,0.));
|
| 95 |
+
#77 = DEFINITIONAL_REPRESENTATION('',(#78),#82);
|
| 96 |
+
#78 = LINE('',#79,#80);
|
| 97 |
+
#79 = CARTESIAN_POINT('',(0.,0.));
|
| 98 |
+
#80 = VECTOR('',#81,1.);
|
| 99 |
+
#81 = DIRECTION('',(0.,1.));
|
| 100 |
+
#82 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 101 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 102 |
+
) );
|
| 103 |
+
#83 = ORIENTED_EDGE('',*,*,#84,.T.);
|
| 104 |
+
#84 = EDGE_CURVE('',#57,#85,#87,.T.);
|
| 105 |
+
#85 = VERTEX_POINT('',#86);
|
| 106 |
+
#86 = CARTESIAN_POINT('',(-40.,20.,2.5));
|
| 107 |
+
#87 = SURFACE_CURVE('',#88,(#92,#99),.PCURVE_S1.);
|
| 108 |
+
#88 = LINE('',#89,#90);
|
| 109 |
+
#89 = CARTESIAN_POINT('',(-40.,20.,-2.5));
|
| 110 |
+
#90 = VECTOR('',#91,1.);
|
| 111 |
+
#91 = DIRECTION('',(0.,0.,1.));
|
| 112 |
+
#92 = PCURVE('',#32,#93);
|
| 113 |
+
#93 = DEFINITIONAL_REPRESENTATION('',(#94),#98);
|
| 114 |
+
#94 = LINE('',#95,#96);
|
| 115 |
+
#95 = CARTESIAN_POINT('',(0.,-40.));
|
| 116 |
+
#96 = VECTOR('',#97,1.);
|
| 117 |
+
#97 = DIRECTION('',(1.,0.));
|
| 118 |
+
#98 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 119 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 120 |
+
) );
|
| 121 |
+
#99 = PCURVE('',#100,#105);
|
| 122 |
+
#100 = PLANE('',#101);
|
| 123 |
+
#101 = AXIS2_PLACEMENT_3D('',#102,#103,#104);
|
| 124 |
+
#102 = CARTESIAN_POINT('',(-40.,20.,-2.5));
|
| 125 |
+
#103 = DIRECTION('',(0.,1.,0.));
|
| 126 |
+
#104 = DIRECTION('',(0.,0.,1.));
|
| 127 |
+
#105 = DEFINITIONAL_REPRESENTATION('',(#106),#110);
|
| 128 |
+
#106 = LINE('',#107,#108);
|
| 129 |
+
#107 = CARTESIAN_POINT('',(0.,0.));
|
| 130 |
+
#108 = VECTOR('',#109,1.);
|
| 131 |
+
#109 = DIRECTION('',(1.,0.));
|
| 132 |
+
#110 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 133 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 134 |
+
) );
|
| 135 |
+
#111 = ORIENTED_EDGE('',*,*,#112,.F.);
|
| 136 |
+
#112 = EDGE_CURVE('',#24,#85,#113,.T.);
|
| 137 |
+
#113 = SURFACE_CURVE('',#114,(#118,#125),.PCURVE_S1.);
|
| 138 |
+
#114 = LINE('',#115,#116);
|
| 139 |
+
#115 = CARTESIAN_POINT('',(-40.,-20.,2.5));
|
| 140 |
+
#116 = VECTOR('',#117,1.);
|
| 141 |
+
#117 = DIRECTION('',(0.,1.,0.));
|
| 142 |
+
#118 = PCURVE('',#32,#119);
|
| 143 |
+
#119 = DEFINITIONAL_REPRESENTATION('',(#120),#124);
|
| 144 |
+
#120 = LINE('',#121,#122);
|
| 145 |
+
#121 = CARTESIAN_POINT('',(5.,0.));
|
| 146 |
+
#122 = VECTOR('',#123,1.);
|
| 147 |
+
#123 = DIRECTION('',(0.,-1.));
|
| 148 |
+
#124 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 149 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 150 |
+
) );
|
| 151 |
+
#125 = PCURVE('',#126,#131);
|
| 152 |
+
#126 = PLANE('',#127);
|
| 153 |
+
#127 = AXIS2_PLACEMENT_3D('',#128,#129,#130);
|
| 154 |
+
#128 = CARTESIAN_POINT('',(-40.,-20.,2.5));
|
| 155 |
+
#129 = DIRECTION('',(0.,0.,1.));
|
| 156 |
+
#130 = DIRECTION('',(1.,0.,0.));
|
| 157 |
+
#131 = DEFINITIONAL_REPRESENTATION('',(#132),#136);
|
| 158 |
+
#132 = LINE('',#133,#134);
|
| 159 |
+
#133 = CARTESIAN_POINT('',(0.,0.));
|
| 160 |
+
#134 = VECTOR('',#135,1.);
|
| 161 |
+
#135 = DIRECTION('',(0.,1.));
|
| 162 |
+
#136 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 163 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 164 |
+
) );
|
| 165 |
+
#137 = ADVANCED_FACE('',(#138),#152,.T.);
|
| 166 |
+
#138 = FACE_BOUND('',#139,.T.);
|
| 167 |
+
#139 = EDGE_LOOP('',(#140,#170,#193,#216));
|
| 168 |
+
#140 = ORIENTED_EDGE('',*,*,#141,.F.);
|
| 169 |
+
#141 = EDGE_CURVE('',#142,#144,#146,.T.);
|
| 170 |
+
#142 = VERTEX_POINT('',#143);
|
| 171 |
+
#143 = CARTESIAN_POINT('',(40.,-20.,-2.5));
|
| 172 |
+
#144 = VERTEX_POINT('',#145);
|
| 173 |
+
#145 = CARTESIAN_POINT('',(40.,-20.,2.5));
|
| 174 |
+
#146 = SURFACE_CURVE('',#147,(#151,#163),.PCURVE_S1.);
|
| 175 |
+
#147 = LINE('',#148,#149);
|
| 176 |
+
#148 = CARTESIAN_POINT('',(40.,-20.,-2.5));
|
| 177 |
+
#149 = VECTOR('',#150,1.);
|
| 178 |
+
#150 = DIRECTION('',(0.,0.,1.));
|
| 179 |
+
#151 = PCURVE('',#152,#157);
|
| 180 |
+
#152 = PLANE('',#153);
|
| 181 |
+
#153 = AXIS2_PLACEMENT_3D('',#154,#155,#156);
|
| 182 |
+
#154 = CARTESIAN_POINT('',(40.,-20.,-2.5));
|
| 183 |
+
#155 = DIRECTION('',(1.,0.,0.));
|
| 184 |
+
#156 = DIRECTION('',(0.,0.,1.));
|
| 185 |
+
#157 = DEFINITIONAL_REPRESENTATION('',(#158),#162);
|
| 186 |
+
#158 = LINE('',#159,#160);
|
| 187 |
+
#159 = CARTESIAN_POINT('',(0.,0.));
|
| 188 |
+
#160 = VECTOR('',#161,1.);
|
| 189 |
+
#161 = DIRECTION('',(1.,0.));
|
| 190 |
+
#162 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 191 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 192 |
+
) );
|
| 193 |
+
#163 = PCURVE('',#44,#164);
|
| 194 |
+
#164 = DEFINITIONAL_REPRESENTATION('',(#165),#169);
|
| 195 |
+
#165 = LINE('',#166,#167);
|
| 196 |
+
#166 = CARTESIAN_POINT('',(0.,80.));
|
| 197 |
+
#167 = VECTOR('',#168,1.);
|
| 198 |
+
#168 = DIRECTION('',(1.,0.));
|
| 199 |
+
#169 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 200 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 201 |
+
) );
|
| 202 |
+
#170 = ORIENTED_EDGE('',*,*,#171,.T.);
|
| 203 |
+
#171 = EDGE_CURVE('',#142,#172,#174,.T.);
|
| 204 |
+
#172 = VERTEX_POINT('',#173);
|
| 205 |
+
#173 = CARTESIAN_POINT('',(40.,20.,-2.5));
|
| 206 |
+
#174 = SURFACE_CURVE('',#175,(#179,#186),.PCURVE_S1.);
|
| 207 |
+
#175 = LINE('',#176,#177);
|
| 208 |
+
#176 = CARTESIAN_POINT('',(40.,-20.,-2.5));
|
| 209 |
+
#177 = VECTOR('',#178,1.);
|
| 210 |
+
#178 = DIRECTION('',(0.,1.,0.));
|
| 211 |
+
#179 = PCURVE('',#152,#180);
|
| 212 |
+
#180 = DEFINITIONAL_REPRESENTATION('',(#181),#185);
|
| 213 |
+
#181 = LINE('',#182,#183);
|
| 214 |
+
#182 = CARTESIAN_POINT('',(0.,0.));
|
| 215 |
+
#183 = VECTOR('',#184,1.);
|
| 216 |
+
#184 = DIRECTION('',(0.,-1.));
|
| 217 |
+
#185 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 218 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 219 |
+
) );
|
| 220 |
+
#186 = PCURVE('',#72,#187);
|
| 221 |
+
#187 = DEFINITIONAL_REPRESENTATION('',(#188),#192);
|
| 222 |
+
#188 = LINE('',#189,#190);
|
| 223 |
+
#189 = CARTESIAN_POINT('',(80.,0.));
|
| 224 |
+
#190 = VECTOR('',#191,1.);
|
| 225 |
+
#191 = DIRECTION('',(0.,1.));
|
| 226 |
+
#192 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 227 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 228 |
+
) );
|
| 229 |
+
#193 = ORIENTED_EDGE('',*,*,#194,.T.);
|
| 230 |
+
#194 = EDGE_CURVE('',#172,#195,#197,.T.);
|
| 231 |
+
#195 = VERTEX_POINT('',#196);
|
| 232 |
+
#196 = CARTESIAN_POINT('',(40.,20.,2.5));
|
| 233 |
+
#197 = SURFACE_CURVE('',#198,(#202,#209),.PCURVE_S1.);
|
| 234 |
+
#198 = LINE('',#199,#200);
|
| 235 |
+
#199 = CARTESIAN_POINT('',(40.,20.,-2.5));
|
| 236 |
+
#200 = VECTOR('',#201,1.);
|
| 237 |
+
#201 = DIRECTION('',(0.,0.,1.));
|
| 238 |
+
#202 = PCURVE('',#152,#203);
|
| 239 |
+
#203 = DEFINITIONAL_REPRESENTATION('',(#204),#208);
|
| 240 |
+
#204 = LINE('',#205,#206);
|
| 241 |
+
#205 = CARTESIAN_POINT('',(0.,-40.));
|
| 242 |
+
#206 = VECTOR('',#207,1.);
|
| 243 |
+
#207 = DIRECTION('',(1.,0.));
|
| 244 |
+
#208 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 245 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 246 |
+
) );
|
| 247 |
+
#209 = PCURVE('',#100,#210);
|
| 248 |
+
#210 = DEFINITIONAL_REPRESENTATION('',(#211),#215);
|
| 249 |
+
#211 = LINE('',#212,#213);
|
| 250 |
+
#212 = CARTESIAN_POINT('',(0.,80.));
|
| 251 |
+
#213 = VECTOR('',#214,1.);
|
| 252 |
+
#214 = DIRECTION('',(1.,0.));
|
| 253 |
+
#215 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 254 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 255 |
+
) );
|
| 256 |
+
#216 = ORIENTED_EDGE('',*,*,#217,.F.);
|
| 257 |
+
#217 = EDGE_CURVE('',#144,#195,#218,.T.);
|
| 258 |
+
#218 = SURFACE_CURVE('',#219,(#223,#230),.PCURVE_S1.);
|
| 259 |
+
#219 = LINE('',#220,#221);
|
| 260 |
+
#220 = CARTESIAN_POINT('',(40.,-20.,2.5));
|
| 261 |
+
#221 = VECTOR('',#222,1.);
|
| 262 |
+
#222 = DIRECTION('',(0.,1.,0.));
|
| 263 |
+
#223 = PCURVE('',#152,#224);
|
| 264 |
+
#224 = DEFINITIONAL_REPRESENTATION('',(#225),#229);
|
| 265 |
+
#225 = LINE('',#226,#227);
|
| 266 |
+
#226 = CARTESIAN_POINT('',(5.,0.));
|
| 267 |
+
#227 = VECTOR('',#228,1.);
|
| 268 |
+
#228 = DIRECTION('',(0.,-1.));
|
| 269 |
+
#229 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 270 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 271 |
+
) );
|
| 272 |
+
#230 = PCURVE('',#126,#231);
|
| 273 |
+
#231 = DEFINITIONAL_REPRESENTATION('',(#232),#236);
|
| 274 |
+
#232 = LINE('',#233,#234);
|
| 275 |
+
#233 = CARTESIAN_POINT('',(80.,0.));
|
| 276 |
+
#234 = VECTOR('',#235,1.);
|
| 277 |
+
#235 = DIRECTION('',(0.,1.));
|
| 278 |
+
#236 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 279 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 280 |
+
) );
|
| 281 |
+
#237 = ADVANCED_FACE('',(#238),#44,.F.);
|
| 282 |
+
#238 = FACE_BOUND('',#239,.F.);
|
| 283 |
+
#239 = EDGE_LOOP('',(#240,#261,#262,#283));
|
| 284 |
+
#240 = ORIENTED_EDGE('',*,*,#241,.F.);
|
| 285 |
+
#241 = EDGE_CURVE('',#22,#142,#242,.T.);
|
| 286 |
+
#242 = SURFACE_CURVE('',#243,(#247,#254),.PCURVE_S1.);
|
| 287 |
+
#243 = LINE('',#244,#245);
|
| 288 |
+
#244 = CARTESIAN_POINT('',(-40.,-20.,-2.5));
|
| 289 |
+
#245 = VECTOR('',#246,1.);
|
| 290 |
+
#246 = DIRECTION('',(1.,0.,0.));
|
| 291 |
+
#247 = PCURVE('',#44,#248);
|
| 292 |
+
#248 = DEFINITIONAL_REPRESENTATION('',(#249),#253);
|
| 293 |
+
#249 = LINE('',#250,#251);
|
| 294 |
+
#250 = CARTESIAN_POINT('',(0.,0.));
|
| 295 |
+
#251 = VECTOR('',#252,1.);
|
| 296 |
+
#252 = DIRECTION('',(0.,1.));
|
| 297 |
+
#253 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 298 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 299 |
+
) );
|
| 300 |
+
#254 = PCURVE('',#72,#255);
|
| 301 |
+
#255 = DEFINITIONAL_REPRESENTATION('',(#256),#260);
|
| 302 |
+
#256 = LINE('',#257,#258);
|
| 303 |
+
#257 = CARTESIAN_POINT('',(0.,0.));
|
| 304 |
+
#258 = VECTOR('',#259,1.);
|
| 305 |
+
#259 = DIRECTION('',(1.,0.));
|
| 306 |
+
#260 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 307 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 308 |
+
) );
|
| 309 |
+
#261 = ORIENTED_EDGE('',*,*,#21,.T.);
|
| 310 |
+
#262 = ORIENTED_EDGE('',*,*,#263,.T.);
|
| 311 |
+
#263 = EDGE_CURVE('',#24,#144,#264,.T.);
|
| 312 |
+
#264 = SURFACE_CURVE('',#265,(#269,#276),.PCURVE_S1.);
|
| 313 |
+
#265 = LINE('',#266,#267);
|
| 314 |
+
#266 = CARTESIAN_POINT('',(-40.,-20.,2.5));
|
| 315 |
+
#267 = VECTOR('',#268,1.);
|
| 316 |
+
#268 = DIRECTION('',(1.,0.,0.));
|
| 317 |
+
#269 = PCURVE('',#44,#270);
|
| 318 |
+
#270 = DEFINITIONAL_REPRESENTATION('',(#271),#275);
|
| 319 |
+
#271 = LINE('',#272,#273);
|
| 320 |
+
#272 = CARTESIAN_POINT('',(5.,0.));
|
| 321 |
+
#273 = VECTOR('',#274,1.);
|
| 322 |
+
#274 = DIRECTION('',(0.,1.));
|
| 323 |
+
#275 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 324 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 325 |
+
) );
|
| 326 |
+
#276 = PCURVE('',#126,#277);
|
| 327 |
+
#277 = DEFINITIONAL_REPRESENTATION('',(#278),#282);
|
| 328 |
+
#278 = LINE('',#279,#280);
|
| 329 |
+
#279 = CARTESIAN_POINT('',(0.,0.));
|
| 330 |
+
#280 = VECTOR('',#281,1.);
|
| 331 |
+
#281 = DIRECTION('',(1.,0.));
|
| 332 |
+
#282 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 333 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 334 |
+
) );
|
| 335 |
+
#283 = ORIENTED_EDGE('',*,*,#141,.F.);
|
| 336 |
+
#284 = ADVANCED_FACE('',(#285),#100,.T.);
|
| 337 |
+
#285 = FACE_BOUND('',#286,.T.);
|
| 338 |
+
#286 = EDGE_LOOP('',(#287,#308,#309,#330));
|
| 339 |
+
#287 = ORIENTED_EDGE('',*,*,#288,.F.);
|
| 340 |
+
#288 = EDGE_CURVE('',#57,#172,#289,.T.);
|
| 341 |
+
#289 = SURFACE_CURVE('',#290,(#294,#301),.PCURVE_S1.);
|
| 342 |
+
#290 = LINE('',#291,#292);
|
| 343 |
+
#291 = CARTESIAN_POINT('',(-40.,20.,-2.5));
|
| 344 |
+
#292 = VECTOR('',#293,1.);
|
| 345 |
+
#293 = DIRECTION('',(1.,0.,0.));
|
| 346 |
+
#294 = PCURVE('',#100,#295);
|
| 347 |
+
#295 = DEFINITIONAL_REPRESENTATION('',(#296),#300);
|
| 348 |
+
#296 = LINE('',#297,#298);
|
| 349 |
+
#297 = CARTESIAN_POINT('',(0.,0.));
|
| 350 |
+
#298 = VECTOR('',#299,1.);
|
| 351 |
+
#299 = DIRECTION('',(0.,1.));
|
| 352 |
+
#300 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 353 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 354 |
+
) );
|
| 355 |
+
#301 = PCURVE('',#72,#302);
|
| 356 |
+
#302 = DEFINITIONAL_REPRESENTATION('',(#303),#307);
|
| 357 |
+
#303 = LINE('',#304,#305);
|
| 358 |
+
#304 = CARTESIAN_POINT('',(0.,40.));
|
| 359 |
+
#305 = VECTOR('',#306,1.);
|
| 360 |
+
#306 = DIRECTION('',(1.,0.));
|
| 361 |
+
#307 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 362 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 363 |
+
) );
|
| 364 |
+
#308 = ORIENTED_EDGE('',*,*,#84,.T.);
|
| 365 |
+
#309 = ORIENTED_EDGE('',*,*,#310,.T.);
|
| 366 |
+
#310 = EDGE_CURVE('',#85,#195,#311,.T.);
|
| 367 |
+
#311 = SURFACE_CURVE('',#312,(#316,#323),.PCURVE_S1.);
|
| 368 |
+
#312 = LINE('',#313,#314);
|
| 369 |
+
#313 = CARTESIAN_POINT('',(-40.,20.,2.5));
|
| 370 |
+
#314 = VECTOR('',#315,1.);
|
| 371 |
+
#315 = DIRECTION('',(1.,0.,0.));
|
| 372 |
+
#316 = PCURVE('',#100,#317);
|
| 373 |
+
#317 = DEFINITIONAL_REPRESENTATION('',(#318),#322);
|
| 374 |
+
#318 = LINE('',#319,#320);
|
| 375 |
+
#319 = CARTESIAN_POINT('',(5.,0.));
|
| 376 |
+
#320 = VECTOR('',#321,1.);
|
| 377 |
+
#321 = DIRECTION('',(0.,1.));
|
| 378 |
+
#322 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 379 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 380 |
+
) );
|
| 381 |
+
#323 = PCURVE('',#126,#324);
|
| 382 |
+
#324 = DEFINITIONAL_REPRESENTATION('',(#325),#329);
|
| 383 |
+
#325 = LINE('',#326,#327);
|
| 384 |
+
#326 = CARTESIAN_POINT('',(0.,40.));
|
| 385 |
+
#327 = VECTOR('',#328,1.);
|
| 386 |
+
#328 = DIRECTION('',(1.,0.));
|
| 387 |
+
#329 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 388 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 389 |
+
) );
|
| 390 |
+
#330 = ORIENTED_EDGE('',*,*,#194,.F.);
|
| 391 |
+
#331 = ADVANCED_FACE('',(#332),#72,.F.);
|
| 392 |
+
#332 = FACE_BOUND('',#333,.F.);
|
| 393 |
+
#333 = EDGE_LOOP('',(#334,#335,#336,#337));
|
| 394 |
+
#334 = ORIENTED_EDGE('',*,*,#56,.F.);
|
| 395 |
+
#335 = ORIENTED_EDGE('',*,*,#241,.T.);
|
| 396 |
+
#336 = ORIENTED_EDGE('',*,*,#171,.T.);
|
| 397 |
+
#337 = ORIENTED_EDGE('',*,*,#288,.F.);
|
| 398 |
+
#338 = ADVANCED_FACE('',(#339),#126,.T.);
|
| 399 |
+
#339 = FACE_BOUND('',#340,.T.);
|
| 400 |
+
#340 = EDGE_LOOP('',(#341,#342,#343,#344));
|
| 401 |
+
#341 = ORIENTED_EDGE('',*,*,#112,.F.);
|
| 402 |
+
#342 = ORIENTED_EDGE('',*,*,#263,.T.);
|
| 403 |
+
#343 = ORIENTED_EDGE('',*,*,#217,.T.);
|
| 404 |
+
#344 = ORIENTED_EDGE('',*,*,#310,.F.);
|
| 405 |
+
#345 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3)
|
| 406 |
+
GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#349)) GLOBAL_UNIT_ASSIGNED_CONTEXT
|
| 407 |
+
((#346,#347,#348)) REPRESENTATION_CONTEXT('Context #1',
|
| 408 |
+
'3D Context with UNIT and UNCERTAINTY') );
|
| 409 |
+
#346 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
|
| 410 |
+
#347 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
|
| 411 |
+
#348 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
|
| 412 |
+
#349 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#346,
|
| 413 |
+
'distance_accuracy_value','confusion accuracy');
|
| 414 |
+
#350 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#7));
|
| 415 |
+
ENDSEC;
|
| 416 |
+
END-ISO-10303-21;
|
server/tasks/task_001_flat_plate/ground_truth_normalized.step
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
ISO-10303-21;
|
| 2 |
+
HEADER;
|
| 3 |
+
FILE_DESCRIPTION(('Open CASCADE Model'),'2;1');
|
| 4 |
+
FILE_NAME('Open CASCADE Shape Model','2026-04-25T17:44:28',('Author'),(
|
| 5 |
+
'Open CASCADE'),'Open CASCADE STEP processor 7.8','Open CASCADE 7.8'
|
| 6 |
+
,'Unknown');
|
| 7 |
+
FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));
|
| 8 |
+
ENDSEC;
|
| 9 |
+
DATA;
|
| 10 |
+
#1 = APPLICATION_PROTOCOL_DEFINITION('international standard',
|
| 11 |
+
'automotive_design',2000,#2);
|
| 12 |
+
#2 = APPLICATION_CONTEXT(
|
| 13 |
+
'core data for automotive mechanical design processes');
|
| 14 |
+
#3 = SHAPE_DEFINITION_REPRESENTATION(#4,#10);
|
| 15 |
+
#4 = PRODUCT_DEFINITION_SHAPE('','',#5);
|
| 16 |
+
#5 = PRODUCT_DEFINITION('design','',#6,#9);
|
| 17 |
+
#6 = PRODUCT_DEFINITION_FORMATION('','',#7);
|
| 18 |
+
#7 = PRODUCT('Open CASCADE STEP translator 7.8 2',
|
| 19 |
+
'Open CASCADE STEP translator 7.8 2','',(#8));
|
| 20 |
+
#8 = PRODUCT_CONTEXT('',#2,'mechanical');
|
| 21 |
+
#9 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design');
|
| 22 |
+
#10 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#15),#345);
|
| 23 |
+
#11 = AXIS2_PLACEMENT_3D('',#12,#13,#14);
|
| 24 |
+
#12 = CARTESIAN_POINT('',(0.,0.,0.));
|
| 25 |
+
#13 = DIRECTION('',(0.,0.,1.));
|
| 26 |
+
#14 = DIRECTION('',(1.,0.,-0.));
|
| 27 |
+
#15 = MANIFOLD_SOLID_BREP('',#16);
|
| 28 |
+
#16 = CLOSED_SHELL('',(#17,#137,#237,#284,#331,#338));
|
| 29 |
+
#17 = ADVANCED_FACE('',(#18),#32,.F.);
|
| 30 |
+
#18 = FACE_BOUND('',#19,.F.);
|
| 31 |
+
#19 = EDGE_LOOP('',(#20,#55,#83,#111));
|
| 32 |
+
#20 = ORIENTED_EDGE('',*,*,#21,.F.);
|
| 33 |
+
#21 = EDGE_CURVE('',#22,#24,#26,.T.);
|
| 34 |
+
#22 = VERTEX_POINT('',#23);
|
| 35 |
+
#23 = CARTESIAN_POINT('',(-40.,-20.,-2.5));
|
| 36 |
+
#24 = VERTEX_POINT('',#25);
|
| 37 |
+
#25 = CARTESIAN_POINT('',(-40.,-20.,2.5));
|
| 38 |
+
#26 = SURFACE_CURVE('',#27,(#31,#43),.PCURVE_S1.);
|
| 39 |
+
#27 = LINE('',#28,#29);
|
| 40 |
+
#28 = CARTESIAN_POINT('',(-40.,-20.,-2.5));
|
| 41 |
+
#29 = VECTOR('',#30,1.);
|
| 42 |
+
#30 = DIRECTION('',(0.,0.,1.));
|
| 43 |
+
#31 = PCURVE('',#32,#37);
|
| 44 |
+
#32 = PLANE('',#33);
|
| 45 |
+
#33 = AXIS2_PLACEMENT_3D('',#34,#35,#36);
|
| 46 |
+
#34 = CARTESIAN_POINT('',(-40.,-20.,-2.5));
|
| 47 |
+
#35 = DIRECTION('',(1.,0.,0.));
|
| 48 |
+
#36 = DIRECTION('',(0.,0.,1.));
|
| 49 |
+
#37 = DEFINITIONAL_REPRESENTATION('',(#38),#42);
|
| 50 |
+
#38 = LINE('',#39,#40);
|
| 51 |
+
#39 = CARTESIAN_POINT('',(0.,0.));
|
| 52 |
+
#40 = VECTOR('',#41,1.);
|
| 53 |
+
#41 = DIRECTION('',(1.,0.));
|
| 54 |
+
#42 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 55 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 56 |
+
) );
|
| 57 |
+
#43 = PCURVE('',#44,#49);
|
| 58 |
+
#44 = PLANE('',#45);
|
| 59 |
+
#45 = AXIS2_PLACEMENT_3D('',#46,#47,#48);
|
| 60 |
+
#46 = CARTESIAN_POINT('',(-40.,-20.,-2.5));
|
| 61 |
+
#47 = DIRECTION('',(0.,1.,0.));
|
| 62 |
+
#48 = DIRECTION('',(0.,0.,1.));
|
| 63 |
+
#49 = DEFINITIONAL_REPRESENTATION('',(#50),#54);
|
| 64 |
+
#50 = LINE('',#51,#52);
|
| 65 |
+
#51 = CARTESIAN_POINT('',(0.,0.));
|
| 66 |
+
#52 = VECTOR('',#53,1.);
|
| 67 |
+
#53 = DIRECTION('',(1.,0.));
|
| 68 |
+
#54 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 69 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 70 |
+
) );
|
| 71 |
+
#55 = ORIENTED_EDGE('',*,*,#56,.T.);
|
| 72 |
+
#56 = EDGE_CURVE('',#22,#57,#59,.T.);
|
| 73 |
+
#57 = VERTEX_POINT('',#58);
|
| 74 |
+
#58 = CARTESIAN_POINT('',(-40.,20.,-2.5));
|
| 75 |
+
#59 = SURFACE_CURVE('',#60,(#64,#71),.PCURVE_S1.);
|
| 76 |
+
#60 = LINE('',#61,#62);
|
| 77 |
+
#61 = CARTESIAN_POINT('',(-40.,-20.,-2.5));
|
| 78 |
+
#62 = VECTOR('',#63,1.);
|
| 79 |
+
#63 = DIRECTION('',(0.,1.,0.));
|
| 80 |
+
#64 = PCURVE('',#32,#65);
|
| 81 |
+
#65 = DEFINITIONAL_REPRESENTATION('',(#66),#70);
|
| 82 |
+
#66 = LINE('',#67,#68);
|
| 83 |
+
#67 = CARTESIAN_POINT('',(0.,0.));
|
| 84 |
+
#68 = VECTOR('',#69,1.);
|
| 85 |
+
#69 = DIRECTION('',(0.,-1.));
|
| 86 |
+
#70 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 87 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 88 |
+
) );
|
| 89 |
+
#71 = PCURVE('',#72,#77);
|
| 90 |
+
#72 = PLANE('',#73);
|
| 91 |
+
#73 = AXIS2_PLACEMENT_3D('',#74,#75,#76);
|
| 92 |
+
#74 = CARTESIAN_POINT('',(-40.,-20.,-2.5));
|
| 93 |
+
#75 = DIRECTION('',(0.,0.,1.));
|
| 94 |
+
#76 = DIRECTION('',(1.,0.,0.));
|
| 95 |
+
#77 = DEFINITIONAL_REPRESENTATION('',(#78),#82);
|
| 96 |
+
#78 = LINE('',#79,#80);
|
| 97 |
+
#79 = CARTESIAN_POINT('',(0.,0.));
|
| 98 |
+
#80 = VECTOR('',#81,1.);
|
| 99 |
+
#81 = DIRECTION('',(0.,1.));
|
| 100 |
+
#82 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 101 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 102 |
+
) );
|
| 103 |
+
#83 = ORIENTED_EDGE('',*,*,#84,.T.);
|
| 104 |
+
#84 = EDGE_CURVE('',#57,#85,#87,.T.);
|
| 105 |
+
#85 = VERTEX_POINT('',#86);
|
| 106 |
+
#86 = CARTESIAN_POINT('',(-40.,20.,2.5));
|
| 107 |
+
#87 = SURFACE_CURVE('',#88,(#92,#99),.PCURVE_S1.);
|
| 108 |
+
#88 = LINE('',#89,#90);
|
| 109 |
+
#89 = CARTESIAN_POINT('',(-40.,20.,-2.5));
|
| 110 |
+
#90 = VECTOR('',#91,1.);
|
| 111 |
+
#91 = DIRECTION('',(0.,0.,1.));
|
| 112 |
+
#92 = PCURVE('',#32,#93);
|
| 113 |
+
#93 = DEFINITIONAL_REPRESENTATION('',(#94),#98);
|
| 114 |
+
#94 = LINE('',#95,#96);
|
| 115 |
+
#95 = CARTESIAN_POINT('',(0.,-40.));
|
| 116 |
+
#96 = VECTOR('',#97,1.);
|
| 117 |
+
#97 = DIRECTION('',(1.,0.));
|
| 118 |
+
#98 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 119 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 120 |
+
) );
|
| 121 |
+
#99 = PCURVE('',#100,#105);
|
| 122 |
+
#100 = PLANE('',#101);
|
| 123 |
+
#101 = AXIS2_PLACEMENT_3D('',#102,#103,#104);
|
| 124 |
+
#102 = CARTESIAN_POINT('',(-40.,20.,-2.5));
|
| 125 |
+
#103 = DIRECTION('',(0.,1.,0.));
|
| 126 |
+
#104 = DIRECTION('',(0.,0.,1.));
|
| 127 |
+
#105 = DEFINITIONAL_REPRESENTATION('',(#106),#110);
|
| 128 |
+
#106 = LINE('',#107,#108);
|
| 129 |
+
#107 = CARTESIAN_POINT('',(0.,0.));
|
| 130 |
+
#108 = VECTOR('',#109,1.);
|
| 131 |
+
#109 = DIRECTION('',(1.,0.));
|
| 132 |
+
#110 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 133 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 134 |
+
) );
|
| 135 |
+
#111 = ORIENTED_EDGE('',*,*,#112,.F.);
|
| 136 |
+
#112 = EDGE_CURVE('',#24,#85,#113,.T.);
|
| 137 |
+
#113 = SURFACE_CURVE('',#114,(#118,#125),.PCURVE_S1.);
|
| 138 |
+
#114 = LINE('',#115,#116);
|
| 139 |
+
#115 = CARTESIAN_POINT('',(-40.,-20.,2.5));
|
| 140 |
+
#116 = VECTOR('',#117,1.);
|
| 141 |
+
#117 = DIRECTION('',(0.,1.,0.));
|
| 142 |
+
#118 = PCURVE('',#32,#119);
|
| 143 |
+
#119 = DEFINITIONAL_REPRESENTATION('',(#120),#124);
|
| 144 |
+
#120 = LINE('',#121,#122);
|
| 145 |
+
#121 = CARTESIAN_POINT('',(5.,0.));
|
| 146 |
+
#122 = VECTOR('',#123,1.);
|
| 147 |
+
#123 = DIRECTION('',(0.,-1.));
|
| 148 |
+
#124 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 149 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 150 |
+
) );
|
| 151 |
+
#125 = PCURVE('',#126,#131);
|
| 152 |
+
#126 = PLANE('',#127);
|
| 153 |
+
#127 = AXIS2_PLACEMENT_3D('',#128,#129,#130);
|
| 154 |
+
#128 = CARTESIAN_POINT('',(-40.,-20.,2.5));
|
| 155 |
+
#129 = DIRECTION('',(0.,0.,1.));
|
| 156 |
+
#130 = DIRECTION('',(1.,0.,0.));
|
| 157 |
+
#131 = DEFINITIONAL_REPRESENTATION('',(#132),#136);
|
| 158 |
+
#132 = LINE('',#133,#134);
|
| 159 |
+
#133 = CARTESIAN_POINT('',(0.,0.));
|
| 160 |
+
#134 = VECTOR('',#135,1.);
|
| 161 |
+
#135 = DIRECTION('',(0.,1.));
|
| 162 |
+
#136 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 163 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 164 |
+
) );
|
| 165 |
+
#137 = ADVANCED_FACE('',(#138),#152,.T.);
|
| 166 |
+
#138 = FACE_BOUND('',#139,.T.);
|
| 167 |
+
#139 = EDGE_LOOP('',(#140,#170,#193,#216));
|
| 168 |
+
#140 = ORIENTED_EDGE('',*,*,#141,.F.);
|
| 169 |
+
#141 = EDGE_CURVE('',#142,#144,#146,.T.);
|
| 170 |
+
#142 = VERTEX_POINT('',#143);
|
| 171 |
+
#143 = CARTESIAN_POINT('',(40.,-20.,-2.5));
|
| 172 |
+
#144 = VERTEX_POINT('',#145);
|
| 173 |
+
#145 = CARTESIAN_POINT('',(40.,-20.,2.5));
|
| 174 |
+
#146 = SURFACE_CURVE('',#147,(#151,#163),.PCURVE_S1.);
|
| 175 |
+
#147 = LINE('',#148,#149);
|
| 176 |
+
#148 = CARTESIAN_POINT('',(40.,-20.,-2.5));
|
| 177 |
+
#149 = VECTOR('',#150,1.);
|
| 178 |
+
#150 = DIRECTION('',(0.,0.,1.));
|
| 179 |
+
#151 = PCURVE('',#152,#157);
|
| 180 |
+
#152 = PLANE('',#153);
|
| 181 |
+
#153 = AXIS2_PLACEMENT_3D('',#154,#155,#156);
|
| 182 |
+
#154 = CARTESIAN_POINT('',(40.,-20.,-2.5));
|
| 183 |
+
#155 = DIRECTION('',(1.,0.,0.));
|
| 184 |
+
#156 = DIRECTION('',(0.,0.,1.));
|
| 185 |
+
#157 = DEFINITIONAL_REPRESENTATION('',(#158),#162);
|
| 186 |
+
#158 = LINE('',#159,#160);
|
| 187 |
+
#159 = CARTESIAN_POINT('',(0.,0.));
|
| 188 |
+
#160 = VECTOR('',#161,1.);
|
| 189 |
+
#161 = DIRECTION('',(1.,0.));
|
| 190 |
+
#162 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 191 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 192 |
+
) );
|
| 193 |
+
#163 = PCURVE('',#44,#164);
|
| 194 |
+
#164 = DEFINITIONAL_REPRESENTATION('',(#165),#169);
|
| 195 |
+
#165 = LINE('',#166,#167);
|
| 196 |
+
#166 = CARTESIAN_POINT('',(0.,80.));
|
| 197 |
+
#167 = VECTOR('',#168,1.);
|
| 198 |
+
#168 = DIRECTION('',(1.,0.));
|
| 199 |
+
#169 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 200 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 201 |
+
) );
|
| 202 |
+
#170 = ORIENTED_EDGE('',*,*,#171,.T.);
|
| 203 |
+
#171 = EDGE_CURVE('',#142,#172,#174,.T.);
|
| 204 |
+
#172 = VERTEX_POINT('',#173);
|
| 205 |
+
#173 = CARTESIAN_POINT('',(40.,20.,-2.5));
|
| 206 |
+
#174 = SURFACE_CURVE('',#175,(#179,#186),.PCURVE_S1.);
|
| 207 |
+
#175 = LINE('',#176,#177);
|
| 208 |
+
#176 = CARTESIAN_POINT('',(40.,-20.,-2.5));
|
| 209 |
+
#177 = VECTOR('',#178,1.);
|
| 210 |
+
#178 = DIRECTION('',(0.,1.,0.));
|
| 211 |
+
#179 = PCURVE('',#152,#180);
|
| 212 |
+
#180 = DEFINITIONAL_REPRESENTATION('',(#181),#185);
|
| 213 |
+
#181 = LINE('',#182,#183);
|
| 214 |
+
#182 = CARTESIAN_POINT('',(0.,0.));
|
| 215 |
+
#183 = VECTOR('',#184,1.);
|
| 216 |
+
#184 = DIRECTION('',(0.,-1.));
|
| 217 |
+
#185 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 218 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 219 |
+
) );
|
| 220 |
+
#186 = PCURVE('',#72,#187);
|
| 221 |
+
#187 = DEFINITIONAL_REPRESENTATION('',(#188),#192);
|
| 222 |
+
#188 = LINE('',#189,#190);
|
| 223 |
+
#189 = CARTESIAN_POINT('',(80.,0.));
|
| 224 |
+
#190 = VECTOR('',#191,1.);
|
| 225 |
+
#191 = DIRECTION('',(0.,1.));
|
| 226 |
+
#192 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 227 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 228 |
+
) );
|
| 229 |
+
#193 = ORIENTED_EDGE('',*,*,#194,.T.);
|
| 230 |
+
#194 = EDGE_CURVE('',#172,#195,#197,.T.);
|
| 231 |
+
#195 = VERTEX_POINT('',#196);
|
| 232 |
+
#196 = CARTESIAN_POINT('',(40.,20.,2.5));
|
| 233 |
+
#197 = SURFACE_CURVE('',#198,(#202,#209),.PCURVE_S1.);
|
| 234 |
+
#198 = LINE('',#199,#200);
|
| 235 |
+
#199 = CARTESIAN_POINT('',(40.,20.,-2.5));
|
| 236 |
+
#200 = VECTOR('',#201,1.);
|
| 237 |
+
#201 = DIRECTION('',(0.,0.,1.));
|
| 238 |
+
#202 = PCURVE('',#152,#203);
|
| 239 |
+
#203 = DEFINITIONAL_REPRESENTATION('',(#204),#208);
|
| 240 |
+
#204 = LINE('',#205,#206);
|
| 241 |
+
#205 = CARTESIAN_POINT('',(0.,-40.));
|
| 242 |
+
#206 = VECTOR('',#207,1.);
|
| 243 |
+
#207 = DIRECTION('',(1.,0.));
|
| 244 |
+
#208 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 245 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 246 |
+
) );
|
| 247 |
+
#209 = PCURVE('',#100,#210);
|
| 248 |
+
#210 = DEFINITIONAL_REPRESENTATION('',(#211),#215);
|
| 249 |
+
#211 = LINE('',#212,#213);
|
| 250 |
+
#212 = CARTESIAN_POINT('',(0.,80.));
|
| 251 |
+
#213 = VECTOR('',#214,1.);
|
| 252 |
+
#214 = DIRECTION('',(1.,0.));
|
| 253 |
+
#215 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 254 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 255 |
+
) );
|
| 256 |
+
#216 = ORIENTED_EDGE('',*,*,#217,.F.);
|
| 257 |
+
#217 = EDGE_CURVE('',#144,#195,#218,.T.);
|
| 258 |
+
#218 = SURFACE_CURVE('',#219,(#223,#230),.PCURVE_S1.);
|
| 259 |
+
#219 = LINE('',#220,#221);
|
| 260 |
+
#220 = CARTESIAN_POINT('',(40.,-20.,2.5));
|
| 261 |
+
#221 = VECTOR('',#222,1.);
|
| 262 |
+
#222 = DIRECTION('',(0.,1.,0.));
|
| 263 |
+
#223 = PCURVE('',#152,#224);
|
| 264 |
+
#224 = DEFINITIONAL_REPRESENTATION('',(#225),#229);
|
| 265 |
+
#225 = LINE('',#226,#227);
|
| 266 |
+
#226 = CARTESIAN_POINT('',(5.,0.));
|
| 267 |
+
#227 = VECTOR('',#228,1.);
|
| 268 |
+
#228 = DIRECTION('',(0.,-1.));
|
| 269 |
+
#229 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 270 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 271 |
+
) );
|
| 272 |
+
#230 = PCURVE('',#126,#231);
|
| 273 |
+
#231 = DEFINITIONAL_REPRESENTATION('',(#232),#236);
|
| 274 |
+
#232 = LINE('',#233,#234);
|
| 275 |
+
#233 = CARTESIAN_POINT('',(80.,0.));
|
| 276 |
+
#234 = VECTOR('',#235,1.);
|
| 277 |
+
#235 = DIRECTION('',(0.,1.));
|
| 278 |
+
#236 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 279 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 280 |
+
) );
|
| 281 |
+
#237 = ADVANCED_FACE('',(#238),#44,.F.);
|
| 282 |
+
#238 = FACE_BOUND('',#239,.F.);
|
| 283 |
+
#239 = EDGE_LOOP('',(#240,#261,#262,#283));
|
| 284 |
+
#240 = ORIENTED_EDGE('',*,*,#241,.F.);
|
| 285 |
+
#241 = EDGE_CURVE('',#22,#142,#242,.T.);
|
| 286 |
+
#242 = SURFACE_CURVE('',#243,(#247,#254),.PCURVE_S1.);
|
| 287 |
+
#243 = LINE('',#244,#245);
|
| 288 |
+
#244 = CARTESIAN_POINT('',(-40.,-20.,-2.5));
|
| 289 |
+
#245 = VECTOR('',#246,1.);
|
| 290 |
+
#246 = DIRECTION('',(1.,0.,0.));
|
| 291 |
+
#247 = PCURVE('',#44,#248);
|
| 292 |
+
#248 = DEFINITIONAL_REPRESENTATION('',(#249),#253);
|
| 293 |
+
#249 = LINE('',#250,#251);
|
| 294 |
+
#250 = CARTESIAN_POINT('',(0.,0.));
|
| 295 |
+
#251 = VECTOR('',#252,1.);
|
| 296 |
+
#252 = DIRECTION('',(0.,1.));
|
| 297 |
+
#253 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 298 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 299 |
+
) );
|
| 300 |
+
#254 = PCURVE('',#72,#255);
|
| 301 |
+
#255 = DEFINITIONAL_REPRESENTATION('',(#256),#260);
|
| 302 |
+
#256 = LINE('',#257,#258);
|
| 303 |
+
#257 = CARTESIAN_POINT('',(0.,0.));
|
| 304 |
+
#258 = VECTOR('',#259,1.);
|
| 305 |
+
#259 = DIRECTION('',(1.,0.));
|
| 306 |
+
#260 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 307 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 308 |
+
) );
|
| 309 |
+
#261 = ORIENTED_EDGE('',*,*,#21,.T.);
|
| 310 |
+
#262 = ORIENTED_EDGE('',*,*,#263,.T.);
|
| 311 |
+
#263 = EDGE_CURVE('',#24,#144,#264,.T.);
|
| 312 |
+
#264 = SURFACE_CURVE('',#265,(#269,#276),.PCURVE_S1.);
|
| 313 |
+
#265 = LINE('',#266,#267);
|
| 314 |
+
#266 = CARTESIAN_POINT('',(-40.,-20.,2.5));
|
| 315 |
+
#267 = VECTOR('',#268,1.);
|
| 316 |
+
#268 = DIRECTION('',(1.,0.,0.));
|
| 317 |
+
#269 = PCURVE('',#44,#270);
|
| 318 |
+
#270 = DEFINITIONAL_REPRESENTATION('',(#271),#275);
|
| 319 |
+
#271 = LINE('',#272,#273);
|
| 320 |
+
#272 = CARTESIAN_POINT('',(5.,0.));
|
| 321 |
+
#273 = VECTOR('',#274,1.);
|
| 322 |
+
#274 = DIRECTION('',(0.,1.));
|
| 323 |
+
#275 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 324 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 325 |
+
) );
|
| 326 |
+
#276 = PCURVE('',#126,#277);
|
| 327 |
+
#277 = DEFINITIONAL_REPRESENTATION('',(#278),#282);
|
| 328 |
+
#278 = LINE('',#279,#280);
|
| 329 |
+
#279 = CARTESIAN_POINT('',(0.,0.));
|
| 330 |
+
#280 = VECTOR('',#281,1.);
|
| 331 |
+
#281 = DIRECTION('',(1.,0.));
|
| 332 |
+
#282 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 333 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 334 |
+
) );
|
| 335 |
+
#283 = ORIENTED_EDGE('',*,*,#141,.F.);
|
| 336 |
+
#284 = ADVANCED_FACE('',(#285),#100,.T.);
|
| 337 |
+
#285 = FACE_BOUND('',#286,.T.);
|
| 338 |
+
#286 = EDGE_LOOP('',(#287,#308,#309,#330));
|
| 339 |
+
#287 = ORIENTED_EDGE('',*,*,#288,.F.);
|
| 340 |
+
#288 = EDGE_CURVE('',#57,#172,#289,.T.);
|
| 341 |
+
#289 = SURFACE_CURVE('',#290,(#294,#301),.PCURVE_S1.);
|
| 342 |
+
#290 = LINE('',#291,#292);
|
| 343 |
+
#291 = CARTESIAN_POINT('',(-40.,20.,-2.5));
|
| 344 |
+
#292 = VECTOR('',#293,1.);
|
| 345 |
+
#293 = DIRECTION('',(1.,0.,0.));
|
| 346 |
+
#294 = PCURVE('',#100,#295);
|
| 347 |
+
#295 = DEFINITIONAL_REPRESENTATION('',(#296),#300);
|
| 348 |
+
#296 = LINE('',#297,#298);
|
| 349 |
+
#297 = CARTESIAN_POINT('',(0.,0.));
|
| 350 |
+
#298 = VECTOR('',#299,1.);
|
| 351 |
+
#299 = DIRECTION('',(0.,1.));
|
| 352 |
+
#300 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 353 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 354 |
+
) );
|
| 355 |
+
#301 = PCURVE('',#72,#302);
|
| 356 |
+
#302 = DEFINITIONAL_REPRESENTATION('',(#303),#307);
|
| 357 |
+
#303 = LINE('',#304,#305);
|
| 358 |
+
#304 = CARTESIAN_POINT('',(0.,40.));
|
| 359 |
+
#305 = VECTOR('',#306,1.);
|
| 360 |
+
#306 = DIRECTION('',(1.,0.));
|
| 361 |
+
#307 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 362 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 363 |
+
) );
|
| 364 |
+
#308 = ORIENTED_EDGE('',*,*,#84,.T.);
|
| 365 |
+
#309 = ORIENTED_EDGE('',*,*,#310,.T.);
|
| 366 |
+
#310 = EDGE_CURVE('',#85,#195,#311,.T.);
|
| 367 |
+
#311 = SURFACE_CURVE('',#312,(#316,#323),.PCURVE_S1.);
|
| 368 |
+
#312 = LINE('',#313,#314);
|
| 369 |
+
#313 = CARTESIAN_POINT('',(-40.,20.,2.5));
|
| 370 |
+
#314 = VECTOR('',#315,1.);
|
| 371 |
+
#315 = DIRECTION('',(1.,0.,0.));
|
| 372 |
+
#316 = PCURVE('',#100,#317);
|
| 373 |
+
#317 = DEFINITIONAL_REPRESENTATION('',(#318),#322);
|
| 374 |
+
#318 = LINE('',#319,#320);
|
| 375 |
+
#319 = CARTESIAN_POINT('',(5.,0.));
|
| 376 |
+
#320 = VECTOR('',#321,1.);
|
| 377 |
+
#321 = DIRECTION('',(0.,1.));
|
| 378 |
+
#322 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 379 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 380 |
+
) );
|
| 381 |
+
#323 = PCURVE('',#126,#324);
|
| 382 |
+
#324 = DEFINITIONAL_REPRESENTATION('',(#325),#329);
|
| 383 |
+
#325 = LINE('',#326,#327);
|
| 384 |
+
#326 = CARTESIAN_POINT('',(0.,40.));
|
| 385 |
+
#327 = VECTOR('',#328,1.);
|
| 386 |
+
#328 = DIRECTION('',(1.,0.));
|
| 387 |
+
#329 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 388 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 389 |
+
) );
|
| 390 |
+
#330 = ORIENTED_EDGE('',*,*,#194,.F.);
|
| 391 |
+
#331 = ADVANCED_FACE('',(#332),#72,.F.);
|
| 392 |
+
#332 = FACE_BOUND('',#333,.F.);
|
| 393 |
+
#333 = EDGE_LOOP('',(#334,#335,#336,#337));
|
| 394 |
+
#334 = ORIENTED_EDGE('',*,*,#56,.F.);
|
| 395 |
+
#335 = ORIENTED_EDGE('',*,*,#241,.T.);
|
| 396 |
+
#336 = ORIENTED_EDGE('',*,*,#171,.T.);
|
| 397 |
+
#337 = ORIENTED_EDGE('',*,*,#288,.F.);
|
| 398 |
+
#338 = ADVANCED_FACE('',(#339),#126,.T.);
|
| 399 |
+
#339 = FACE_BOUND('',#340,.T.);
|
| 400 |
+
#340 = EDGE_LOOP('',(#341,#342,#343,#344));
|
| 401 |
+
#341 = ORIENTED_EDGE('',*,*,#112,.F.);
|
| 402 |
+
#342 = ORIENTED_EDGE('',*,*,#263,.T.);
|
| 403 |
+
#343 = ORIENTED_EDGE('',*,*,#217,.T.);
|
| 404 |
+
#344 = ORIENTED_EDGE('',*,*,#310,.F.);
|
| 405 |
+
#345 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3)
|
| 406 |
+
GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#349)) GLOBAL_UNIT_ASSIGNED_CONTEXT
|
| 407 |
+
((#346,#347,#348)) REPRESENTATION_CONTEXT('Context #1',
|
| 408 |
+
'3D Context with UNIT and UNCERTAINTY') );
|
| 409 |
+
#346 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
|
| 410 |
+
#347 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
|
| 411 |
+
#348 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
|
| 412 |
+
#349 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#346,
|
| 413 |
+
'distance_accuracy_value','confusion accuracy');
|
| 414 |
+
#350 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#7));
|
| 415 |
+
ENDSEC;
|
| 416 |
+
END-ISO-10303-21;
|
server/tasks/task_001_flat_plate/reference_code.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cadquery as cq
|
| 2 |
+
result = cq.Workplane("XY").box(80, 40, 5)
|
server/tasks/task_001_flat_plate/surface_points.npy
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2d18b2112728d40eb99ff7ee970f562d4f3fbfa29c6f16bc85e6e01cfddc1a4f
|
| 3 |
+
size 24704
|
server/tasks/task_001_flat_plate/task.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "task_001_flat_plate",
|
| 3 |
+
"part_class": "flat plate",
|
| 4 |
+
"difficulty_bin": 1,
|
| 5 |
+
"max_steps": 10,
|
| 6 |
+
"prompt": "Create a solid flat rectangular plate made of a single block of material. The plate measures exactly 80 millimeters along the X axis, 40 millimeters along the Y axis, and 5 millimeters along the Z axis. The bottom face of the plate sits flush on the XY plane. The plate has no holes, no chamfers, no fillets, and no other features. It is a plain rectangular solid. Orient the longest axis along X.",
|
| 7 |
+
"ground_truth_step": "tasks/task_001_flat_plate/ground_truth.step",
|
| 8 |
+
"ground_truth_json": "tasks/task_001_flat_plate/ground_truth.json",
|
| 9 |
+
"reference_code": "tasks/task_001_flat_plate/reference_code.py"
|
| 10 |
+
}
|
server/tasks/task_001_flat_plate/voxels_64.npy
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5d029cfdec59acafbd813c82ddea228d541ec16cc6bee655108120bd95682c32
|
| 3 |
+
size 262272
|
server/tasks/task_002_box_with_hole/ground_truth.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"source_step": "tasks/task_002_box_with_hole/ground_truth.step",
|
| 3 |
+
"volume_mm3": 28429.2037,
|
| 4 |
+
"surface_area_mm2": 6671.2389,
|
| 5 |
+
"bbox_mm": [
|
| 6 |
+
50.0,
|
| 7 |
+
30.0,
|
| 8 |
+
20.0031
|
| 9 |
+
],
|
| 10 |
+
"face_count": 7,
|
| 11 |
+
"face_types": [
|
| 12 |
+
"PLANE",
|
| 13 |
+
"CYLINDER"
|
| 14 |
+
],
|
| 15 |
+
"dominant_face_type": "PLANE",
|
| 16 |
+
"hole_count": 1,
|
| 17 |
+
"hole_diameters_mm": [
|
| 18 |
+
10.0
|
| 19 |
+
],
|
| 20 |
+
"euler_characteristic": 2,
|
| 21 |
+
"surface_points_file": "surface_points.npy",
|
| 22 |
+
"voxels_file": "voxels_64.npy",
|
| 23 |
+
"canonical_transform": {
|
| 24 |
+
"units": "mm",
|
| 25 |
+
"translation_to_origin": [
|
| 26 |
+
0.0,
|
| 27 |
+
0.0,
|
| 28 |
+
0.0
|
| 29 |
+
],
|
| 30 |
+
"axis_alignment": [
|
| 31 |
+
"X",
|
| 32 |
+
"Y",
|
| 33 |
+
"Z"
|
| 34 |
+
],
|
| 35 |
+
"longest_axis": "X"
|
| 36 |
+
},
|
| 37 |
+
"original_bbox_mm": [
|
| 38 |
+
50.0,
|
| 39 |
+
30.0,
|
| 40 |
+
20.0
|
| 41 |
+
]
|
| 42 |
+
}
|
server/tasks/task_002_box_with_hole/ground_truth.step
ADDED
|
@@ -0,0 +1,533 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
ISO-10303-21;
|
| 2 |
+
HEADER;
|
| 3 |
+
FILE_DESCRIPTION(('Open CASCADE Model'),'2;1');
|
| 4 |
+
FILE_NAME('Open CASCADE Shape Model','2026-04-25T17:44:29',('Author'),(
|
| 5 |
+
'Open CASCADE'),'Open CASCADE STEP processor 7.8','Open CASCADE 7.8'
|
| 6 |
+
,'Unknown');
|
| 7 |
+
FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));
|
| 8 |
+
ENDSEC;
|
| 9 |
+
DATA;
|
| 10 |
+
#1 = APPLICATION_PROTOCOL_DEFINITION('international standard',
|
| 11 |
+
'automotive_design',2000,#2);
|
| 12 |
+
#2 = APPLICATION_CONTEXT(
|
| 13 |
+
'core data for automotive mechanical design processes');
|
| 14 |
+
#3 = SHAPE_DEFINITION_REPRESENTATION(#4,#10);
|
| 15 |
+
#4 = PRODUCT_DEFINITION_SHAPE('','',#5);
|
| 16 |
+
#5 = PRODUCT_DEFINITION('design','',#6,#9);
|
| 17 |
+
#6 = PRODUCT_DEFINITION_FORMATION('','',#7);
|
| 18 |
+
#7 = PRODUCT('Open CASCADE STEP translator 7.8 3',
|
| 19 |
+
'Open CASCADE STEP translator 7.8 3','',(#8));
|
| 20 |
+
#8 = PRODUCT_CONTEXT('',#2,'mechanical');
|
| 21 |
+
#9 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design');
|
| 22 |
+
#10 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#15),#437);
|
| 23 |
+
#11 = AXIS2_PLACEMENT_3D('',#12,#13,#14);
|
| 24 |
+
#12 = CARTESIAN_POINT('',(0.,0.,0.));
|
| 25 |
+
#13 = DIRECTION('',(0.,0.,1.));
|
| 26 |
+
#14 = DIRECTION('',(1.,0.,-0.));
|
| 27 |
+
#15 = MANIFOLD_SOLID_BREP('',#16);
|
| 28 |
+
#16 = CLOSED_SHELL('',(#17,#137,#213,#297,#346,#403,#410));
|
| 29 |
+
#17 = ADVANCED_FACE('',(#18),#32,.F.);
|
| 30 |
+
#18 = FACE_BOUND('',#19,.F.);
|
| 31 |
+
#19 = EDGE_LOOP('',(#20,#55,#83,#111));
|
| 32 |
+
#20 = ORIENTED_EDGE('',*,*,#21,.F.);
|
| 33 |
+
#21 = EDGE_CURVE('',#22,#24,#26,.T.);
|
| 34 |
+
#22 = VERTEX_POINT('',#23);
|
| 35 |
+
#23 = CARTESIAN_POINT('',(-25.,-15.,-10.));
|
| 36 |
+
#24 = VERTEX_POINT('',#25);
|
| 37 |
+
#25 = CARTESIAN_POINT('',(-25.,-15.,10.));
|
| 38 |
+
#26 = SURFACE_CURVE('',#27,(#31,#43),.PCURVE_S1.);
|
| 39 |
+
#27 = LINE('',#28,#29);
|
| 40 |
+
#28 = CARTESIAN_POINT('',(-25.,-15.,-10.));
|
| 41 |
+
#29 = VECTOR('',#30,1.);
|
| 42 |
+
#30 = DIRECTION('',(0.,0.,1.));
|
| 43 |
+
#31 = PCURVE('',#32,#37);
|
| 44 |
+
#32 = PLANE('',#33);
|
| 45 |
+
#33 = AXIS2_PLACEMENT_3D('',#34,#35,#36);
|
| 46 |
+
#34 = CARTESIAN_POINT('',(-25.,-15.,-10.));
|
| 47 |
+
#35 = DIRECTION('',(1.,0.,0.));
|
| 48 |
+
#36 = DIRECTION('',(0.,0.,1.));
|
| 49 |
+
#37 = DEFINITIONAL_REPRESENTATION('',(#38),#42);
|
| 50 |
+
#38 = LINE('',#39,#40);
|
| 51 |
+
#39 = CARTESIAN_POINT('',(0.,0.));
|
| 52 |
+
#40 = VECTOR('',#41,1.);
|
| 53 |
+
#41 = DIRECTION('',(1.,0.));
|
| 54 |
+
#42 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 55 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 56 |
+
) );
|
| 57 |
+
#43 = PCURVE('',#44,#49);
|
| 58 |
+
#44 = PLANE('',#45);
|
| 59 |
+
#45 = AXIS2_PLACEMENT_3D('',#46,#47,#48);
|
| 60 |
+
#46 = CARTESIAN_POINT('',(-25.,-15.,-10.));
|
| 61 |
+
#47 = DIRECTION('',(0.,1.,0.));
|
| 62 |
+
#48 = DIRECTION('',(0.,0.,1.));
|
| 63 |
+
#49 = DEFINITIONAL_REPRESENTATION('',(#50),#54);
|
| 64 |
+
#50 = LINE('',#51,#52);
|
| 65 |
+
#51 = CARTESIAN_POINT('',(0.,0.));
|
| 66 |
+
#52 = VECTOR('',#53,1.);
|
| 67 |
+
#53 = DIRECTION('',(1.,0.));
|
| 68 |
+
#54 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 69 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 70 |
+
) );
|
| 71 |
+
#55 = ORIENTED_EDGE('',*,*,#56,.T.);
|
| 72 |
+
#56 = EDGE_CURVE('',#22,#57,#59,.T.);
|
| 73 |
+
#57 = VERTEX_POINT('',#58);
|
| 74 |
+
#58 = CARTESIAN_POINT('',(-25.,15.,-10.));
|
| 75 |
+
#59 = SURFACE_CURVE('',#60,(#64,#71),.PCURVE_S1.);
|
| 76 |
+
#60 = LINE('',#61,#62);
|
| 77 |
+
#61 = CARTESIAN_POINT('',(-25.,-15.,-10.));
|
| 78 |
+
#62 = VECTOR('',#63,1.);
|
| 79 |
+
#63 = DIRECTION('',(0.,1.,0.));
|
| 80 |
+
#64 = PCURVE('',#32,#65);
|
| 81 |
+
#65 = DEFINITIONAL_REPRESENTATION('',(#66),#70);
|
| 82 |
+
#66 = LINE('',#67,#68);
|
| 83 |
+
#67 = CARTESIAN_POINT('',(0.,0.));
|
| 84 |
+
#68 = VECTOR('',#69,1.);
|
| 85 |
+
#69 = DIRECTION('',(0.,-1.));
|
| 86 |
+
#70 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 87 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 88 |
+
) );
|
| 89 |
+
#71 = PCURVE('',#72,#77);
|
| 90 |
+
#72 = PLANE('',#73);
|
| 91 |
+
#73 = AXIS2_PLACEMENT_3D('',#74,#75,#76);
|
| 92 |
+
#74 = CARTESIAN_POINT('',(-25.,-15.,-10.));
|
| 93 |
+
#75 = DIRECTION('',(0.,0.,1.));
|
| 94 |
+
#76 = DIRECTION('',(1.,0.,0.));
|
| 95 |
+
#77 = DEFINITIONAL_REPRESENTATION('',(#78),#82);
|
| 96 |
+
#78 = LINE('',#79,#80);
|
| 97 |
+
#79 = CARTESIAN_POINT('',(0.,0.));
|
| 98 |
+
#80 = VECTOR('',#81,1.);
|
| 99 |
+
#81 = DIRECTION('',(0.,1.));
|
| 100 |
+
#82 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 101 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 102 |
+
) );
|
| 103 |
+
#83 = ORIENTED_EDGE('',*,*,#84,.T.);
|
| 104 |
+
#84 = EDGE_CURVE('',#57,#85,#87,.T.);
|
| 105 |
+
#85 = VERTEX_POINT('',#86);
|
| 106 |
+
#86 = CARTESIAN_POINT('',(-25.,15.,10.));
|
| 107 |
+
#87 = SURFACE_CURVE('',#88,(#92,#99),.PCURVE_S1.);
|
| 108 |
+
#88 = LINE('',#89,#90);
|
| 109 |
+
#89 = CARTESIAN_POINT('',(-25.,15.,-10.));
|
| 110 |
+
#90 = VECTOR('',#91,1.);
|
| 111 |
+
#91 = DIRECTION('',(0.,0.,1.));
|
| 112 |
+
#92 = PCURVE('',#32,#93);
|
| 113 |
+
#93 = DEFINITIONAL_REPRESENTATION('',(#94),#98);
|
| 114 |
+
#94 = LINE('',#95,#96);
|
| 115 |
+
#95 = CARTESIAN_POINT('',(0.,-30.));
|
| 116 |
+
#96 = VECTOR('',#97,1.);
|
| 117 |
+
#97 = DIRECTION('',(1.,0.));
|
| 118 |
+
#98 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 119 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 120 |
+
) );
|
| 121 |
+
#99 = PCURVE('',#100,#105);
|
| 122 |
+
#100 = PLANE('',#101);
|
| 123 |
+
#101 = AXIS2_PLACEMENT_3D('',#102,#103,#104);
|
| 124 |
+
#102 = CARTESIAN_POINT('',(-25.,15.,-10.));
|
| 125 |
+
#103 = DIRECTION('',(0.,1.,0.));
|
| 126 |
+
#104 = DIRECTION('',(0.,0.,1.));
|
| 127 |
+
#105 = DEFINITIONAL_REPRESENTATION('',(#106),#110);
|
| 128 |
+
#106 = LINE('',#107,#108);
|
| 129 |
+
#107 = CARTESIAN_POINT('',(0.,0.));
|
| 130 |
+
#108 = VECTOR('',#109,1.);
|
| 131 |
+
#109 = DIRECTION('',(1.,0.));
|
| 132 |
+
#110 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 133 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 134 |
+
) );
|
| 135 |
+
#111 = ORIENTED_EDGE('',*,*,#112,.F.);
|
| 136 |
+
#112 = EDGE_CURVE('',#24,#85,#113,.T.);
|
| 137 |
+
#113 = SURFACE_CURVE('',#114,(#118,#125),.PCURVE_S1.);
|
| 138 |
+
#114 = LINE('',#115,#116);
|
| 139 |
+
#115 = CARTESIAN_POINT('',(-25.,-15.,10.));
|
| 140 |
+
#116 = VECTOR('',#117,1.);
|
| 141 |
+
#117 = DIRECTION('',(0.,1.,0.));
|
| 142 |
+
#118 = PCURVE('',#32,#119);
|
| 143 |
+
#119 = DEFINITIONAL_REPRESENTATION('',(#120),#124);
|
| 144 |
+
#120 = LINE('',#121,#122);
|
| 145 |
+
#121 = CARTESIAN_POINT('',(20.,0.));
|
| 146 |
+
#122 = VECTOR('',#123,1.);
|
| 147 |
+
#123 = DIRECTION('',(0.,-1.));
|
| 148 |
+
#124 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 149 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 150 |
+
) );
|
| 151 |
+
#125 = PCURVE('',#126,#131);
|
| 152 |
+
#126 = PLANE('',#127);
|
| 153 |
+
#127 = AXIS2_PLACEMENT_3D('',#128,#129,#130);
|
| 154 |
+
#128 = CARTESIAN_POINT('',(-25.,-15.,10.));
|
| 155 |
+
#129 = DIRECTION('',(0.,0.,1.));
|
| 156 |
+
#130 = DIRECTION('',(1.,0.,0.));
|
| 157 |
+
#131 = DEFINITIONAL_REPRESENTATION('',(#132),#136);
|
| 158 |
+
#132 = LINE('',#133,#134);
|
| 159 |
+
#133 = CARTESIAN_POINT('',(0.,0.));
|
| 160 |
+
#134 = VECTOR('',#135,1.);
|
| 161 |
+
#135 = DIRECTION('',(0.,1.));
|
| 162 |
+
#136 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 163 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 164 |
+
) );
|
| 165 |
+
#137 = ADVANCED_FACE('',(#138),#44,.F.);
|
| 166 |
+
#138 = FACE_BOUND('',#139,.F.);
|
| 167 |
+
#139 = EDGE_LOOP('',(#140,#163,#164,#187));
|
| 168 |
+
#140 = ORIENTED_EDGE('',*,*,#141,.F.);
|
| 169 |
+
#141 = EDGE_CURVE('',#22,#142,#144,.T.);
|
| 170 |
+
#142 = VERTEX_POINT('',#143);
|
| 171 |
+
#143 = CARTESIAN_POINT('',(25.,-15.,-10.));
|
| 172 |
+
#144 = SURFACE_CURVE('',#145,(#149,#156),.PCURVE_S1.);
|
| 173 |
+
#145 = LINE('',#146,#147);
|
| 174 |
+
#146 = CARTESIAN_POINT('',(-25.,-15.,-10.));
|
| 175 |
+
#147 = VECTOR('',#148,1.);
|
| 176 |
+
#148 = DIRECTION('',(1.,0.,0.));
|
| 177 |
+
#149 = PCURVE('',#44,#150);
|
| 178 |
+
#150 = DEFINITIONAL_REPRESENTATION('',(#151),#155);
|
| 179 |
+
#151 = LINE('',#152,#153);
|
| 180 |
+
#152 = CARTESIAN_POINT('',(0.,0.));
|
| 181 |
+
#153 = VECTOR('',#154,1.);
|
| 182 |
+
#154 = DIRECTION('',(0.,1.));
|
| 183 |
+
#155 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 184 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 185 |
+
) );
|
| 186 |
+
#156 = PCURVE('',#72,#157);
|
| 187 |
+
#157 = DEFINITIONAL_REPRESENTATION('',(#158),#162);
|
| 188 |
+
#158 = LINE('',#159,#160);
|
| 189 |
+
#159 = CARTESIAN_POINT('',(0.,0.));
|
| 190 |
+
#160 = VECTOR('',#161,1.);
|
| 191 |
+
#161 = DIRECTION('',(1.,0.));
|
| 192 |
+
#162 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 193 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 194 |
+
) );
|
| 195 |
+
#163 = ORIENTED_EDGE('',*,*,#21,.T.);
|
| 196 |
+
#164 = ORIENTED_EDGE('',*,*,#165,.T.);
|
| 197 |
+
#165 = EDGE_CURVE('',#24,#166,#168,.T.);
|
| 198 |
+
#166 = VERTEX_POINT('',#167);
|
| 199 |
+
#167 = CARTESIAN_POINT('',(25.,-15.,10.));
|
| 200 |
+
#168 = SURFACE_CURVE('',#169,(#173,#180),.PCURVE_S1.);
|
| 201 |
+
#169 = LINE('',#170,#171);
|
| 202 |
+
#170 = CARTESIAN_POINT('',(-25.,-15.,10.));
|
| 203 |
+
#171 = VECTOR('',#172,1.);
|
| 204 |
+
#172 = DIRECTION('',(1.,0.,0.));
|
| 205 |
+
#173 = PCURVE('',#44,#174);
|
| 206 |
+
#174 = DEFINITIONAL_REPRESENTATION('',(#175),#179);
|
| 207 |
+
#175 = LINE('',#176,#177);
|
| 208 |
+
#176 = CARTESIAN_POINT('',(20.,0.));
|
| 209 |
+
#177 = VECTOR('',#178,1.);
|
| 210 |
+
#178 = DIRECTION('',(0.,1.));
|
| 211 |
+
#179 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 212 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 213 |
+
) );
|
| 214 |
+
#180 = PCURVE('',#126,#181);
|
| 215 |
+
#181 = DEFINITIONAL_REPRESENTATION('',(#182),#186);
|
| 216 |
+
#182 = LINE('',#183,#184);
|
| 217 |
+
#183 = CARTESIAN_POINT('',(0.,0.));
|
| 218 |
+
#184 = VECTOR('',#185,1.);
|
| 219 |
+
#185 = DIRECTION('',(1.,0.));
|
| 220 |
+
#186 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 221 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 222 |
+
) );
|
| 223 |
+
#187 = ORIENTED_EDGE('',*,*,#188,.F.);
|
| 224 |
+
#188 = EDGE_CURVE('',#142,#166,#189,.T.);
|
| 225 |
+
#189 = SURFACE_CURVE('',#190,(#194,#201),.PCURVE_S1.);
|
| 226 |
+
#190 = LINE('',#191,#192);
|
| 227 |
+
#191 = CARTESIAN_POINT('',(25.,-15.,-10.));
|
| 228 |
+
#192 = VECTOR('',#193,1.);
|
| 229 |
+
#193 = DIRECTION('',(0.,0.,1.));
|
| 230 |
+
#194 = PCURVE('',#44,#195);
|
| 231 |
+
#195 = DEFINITIONAL_REPRESENTATION('',(#196),#200);
|
| 232 |
+
#196 = LINE('',#197,#198);
|
| 233 |
+
#197 = CARTESIAN_POINT('',(0.,50.));
|
| 234 |
+
#198 = VECTOR('',#199,1.);
|
| 235 |
+
#199 = DIRECTION('',(1.,0.));
|
| 236 |
+
#200 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 237 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 238 |
+
) );
|
| 239 |
+
#201 = PCURVE('',#202,#207);
|
| 240 |
+
#202 = PLANE('',#203);
|
| 241 |
+
#203 = AXIS2_PLACEMENT_3D('',#204,#205,#206);
|
| 242 |
+
#204 = CARTESIAN_POINT('',(25.,-15.,-10.));
|
| 243 |
+
#205 = DIRECTION('',(1.,0.,0.));
|
| 244 |
+
#206 = DIRECTION('',(0.,0.,1.));
|
| 245 |
+
#207 = DEFINITIONAL_REPRESENTATION('',(#208),#212);
|
| 246 |
+
#208 = LINE('',#209,#210);
|
| 247 |
+
#209 = CARTESIAN_POINT('',(0.,0.));
|
| 248 |
+
#210 = VECTOR('',#211,1.);
|
| 249 |
+
#211 = DIRECTION('',(1.,0.));
|
| 250 |
+
#212 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 251 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 252 |
+
) );
|
| 253 |
+
#213 = ADVANCED_FACE('',(#214,#262),#126,.T.);
|
| 254 |
+
#214 = FACE_BOUND('',#215,.T.);
|
| 255 |
+
#215 = EDGE_LOOP('',(#216,#217,#218,#241));
|
| 256 |
+
#216 = ORIENTED_EDGE('',*,*,#112,.F.);
|
| 257 |
+
#217 = ORIENTED_EDGE('',*,*,#165,.T.);
|
| 258 |
+
#218 = ORIENTED_EDGE('',*,*,#219,.T.);
|
| 259 |
+
#219 = EDGE_CURVE('',#166,#220,#222,.T.);
|
| 260 |
+
#220 = VERTEX_POINT('',#221);
|
| 261 |
+
#221 = CARTESIAN_POINT('',(25.,15.,10.));
|
| 262 |
+
#222 = SURFACE_CURVE('',#223,(#227,#234),.PCURVE_S1.);
|
| 263 |
+
#223 = LINE('',#224,#225);
|
| 264 |
+
#224 = CARTESIAN_POINT('',(25.,-15.,10.));
|
| 265 |
+
#225 = VECTOR('',#226,1.);
|
| 266 |
+
#226 = DIRECTION('',(0.,1.,0.));
|
| 267 |
+
#227 = PCURVE('',#126,#228);
|
| 268 |
+
#228 = DEFINITIONAL_REPRESENTATION('',(#229),#233);
|
| 269 |
+
#229 = LINE('',#230,#231);
|
| 270 |
+
#230 = CARTESIAN_POINT('',(50.,0.));
|
| 271 |
+
#231 = VECTOR('',#232,1.);
|
| 272 |
+
#232 = DIRECTION('',(0.,1.));
|
| 273 |
+
#233 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 274 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 275 |
+
) );
|
| 276 |
+
#234 = PCURVE('',#202,#235);
|
| 277 |
+
#235 = DEFINITIONAL_REPRESENTATION('',(#236),#240);
|
| 278 |
+
#236 = LINE('',#237,#238);
|
| 279 |
+
#237 = CARTESIAN_POINT('',(20.,0.));
|
| 280 |
+
#238 = VECTOR('',#239,1.);
|
| 281 |
+
#239 = DIRECTION('',(0.,-1.));
|
| 282 |
+
#240 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 283 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 284 |
+
) );
|
| 285 |
+
#241 = ORIENTED_EDGE('',*,*,#242,.F.);
|
| 286 |
+
#242 = EDGE_CURVE('',#85,#220,#243,.T.);
|
| 287 |
+
#243 = SURFACE_CURVE('',#244,(#248,#255),.PCURVE_S1.);
|
| 288 |
+
#244 = LINE('',#245,#246);
|
| 289 |
+
#245 = CARTESIAN_POINT('',(-25.,15.,10.));
|
| 290 |
+
#246 = VECTOR('',#247,1.);
|
| 291 |
+
#247 = DIRECTION('',(1.,0.,0.));
|
| 292 |
+
#248 = PCURVE('',#126,#249);
|
| 293 |
+
#249 = DEFINITIONAL_REPRESENTATION('',(#250),#254);
|
| 294 |
+
#250 = LINE('',#251,#252);
|
| 295 |
+
#251 = CARTESIAN_POINT('',(0.,30.));
|
| 296 |
+
#252 = VECTOR('',#253,1.);
|
| 297 |
+
#253 = DIRECTION('',(1.,0.));
|
| 298 |
+
#254 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 299 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 300 |
+
) );
|
| 301 |
+
#255 = PCURVE('',#100,#256);
|
| 302 |
+
#256 = DEFINITIONAL_REPRESENTATION('',(#257),#261);
|
| 303 |
+
#257 = LINE('',#258,#259);
|
| 304 |
+
#258 = CARTESIAN_POINT('',(20.,0.));
|
| 305 |
+
#259 = VECTOR('',#260,1.);
|
| 306 |
+
#260 = DIRECTION('',(0.,1.));
|
| 307 |
+
#261 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 308 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 309 |
+
) );
|
| 310 |
+
#262 = FACE_BOUND('',#263,.T.);
|
| 311 |
+
#263 = EDGE_LOOP('',(#264));
|
| 312 |
+
#264 = ORIENTED_EDGE('',*,*,#265,.T.);
|
| 313 |
+
#265 = EDGE_CURVE('',#266,#266,#268,.T.);
|
| 314 |
+
#266 = VERTEX_POINT('',#267);
|
| 315 |
+
#267 = CARTESIAN_POINT('',(-5.,-1.722026714179E-15,10.));
|
| 316 |
+
#268 = SURFACE_CURVE('',#269,(#274,#285),.PCURVE_S1.);
|
| 317 |
+
#269 = CIRCLE('',#270,5.);
|
| 318 |
+
#270 = AXIS2_PLACEMENT_3D('',#271,#272,#273);
|
| 319 |
+
#271 = CARTESIAN_POINT('',(-4.440892098501E-16,-4.973799150321E-16,10.)
|
| 320 |
+
);
|
| 321 |
+
#272 = DIRECTION('',(0.,0.,-1.));
|
| 322 |
+
#273 = DIRECTION('',(-1.,0.,0.));
|
| 323 |
+
#274 = PCURVE('',#126,#275);
|
| 324 |
+
#275 = DEFINITIONAL_REPRESENTATION('',(#276),#284);
|
| 325 |
+
#276 = ( BOUNDED_CURVE() B_SPLINE_CURVE(2,(#277,#278,#279,#280,#281,#282
|
| 326 |
+
,#283),.UNSPECIFIED.,.T.,.F.) B_SPLINE_CURVE_WITH_KNOTS((1,2,2,2,2,1),(
|
| 327 |
+
-2.094395102393,0.,2.094395102393,4.188790204786,6.28318530718,
|
| 328 |
+
8.377580409573),.UNSPECIFIED.) CURVE() GEOMETRIC_REPRESENTATION_ITEM()
|
| 329 |
+
RATIONAL_B_SPLINE_CURVE((1.,0.5,1.,0.5,1.,0.5,1.)) REPRESENTATION_ITEM(
|
| 330 |
+
'') );
|
| 331 |
+
#277 = CARTESIAN_POINT('',(20.,15.));
|
| 332 |
+
#278 = CARTESIAN_POINT('',(20.,23.660254037844));
|
| 333 |
+
#279 = CARTESIAN_POINT('',(27.5,19.330127018922));
|
| 334 |
+
#280 = CARTESIAN_POINT('',(35.,15.));
|
| 335 |
+
#281 = CARTESIAN_POINT('',(27.5,10.669872981078));
|
| 336 |
+
#282 = CARTESIAN_POINT('',(20.,6.339745962156));
|
| 337 |
+
#283 = CARTESIAN_POINT('',(20.,15.));
|
| 338 |
+
#284 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 339 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 340 |
+
) );
|
| 341 |
+
#285 = PCURVE('',#286,#291);
|
| 342 |
+
#286 = CYLINDRICAL_SURFACE('',#287,5.);
|
| 343 |
+
#287 = AXIS2_PLACEMENT_3D('',#288,#289,#290);
|
| 344 |
+
#288 = CARTESIAN_POINT('',(-4.440892098501E-16,-4.973799150321E-16,10.)
|
| 345 |
+
);
|
| 346 |
+
#289 = DIRECTION('',(0.,0.,-1.));
|
| 347 |
+
#290 = DIRECTION('',(-1.,0.,0.));
|
| 348 |
+
#291 = DEFINITIONAL_REPRESENTATION('',(#292),#296);
|
| 349 |
+
#292 = LINE('',#293,#294);
|
| 350 |
+
#293 = CARTESIAN_POINT('',(0.,0.));
|
| 351 |
+
#294 = VECTOR('',#295,1.);
|
| 352 |
+
#295 = DIRECTION('',(1.,0.));
|
| 353 |
+
#296 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 354 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 355 |
+
) );
|
| 356 |
+
#297 = ADVANCED_FACE('',(#298),#100,.T.);
|
| 357 |
+
#298 = FACE_BOUND('',#299,.T.);
|
| 358 |
+
#299 = EDGE_LOOP('',(#300,#323,#324,#325));
|
| 359 |
+
#300 = ORIENTED_EDGE('',*,*,#301,.F.);
|
| 360 |
+
#301 = EDGE_CURVE('',#57,#302,#304,.T.);
|
| 361 |
+
#302 = VERTEX_POINT('',#303);
|
| 362 |
+
#303 = CARTESIAN_POINT('',(25.,15.,-10.));
|
| 363 |
+
#304 = SURFACE_CURVE('',#305,(#309,#316),.PCURVE_S1.);
|
| 364 |
+
#305 = LINE('',#306,#307);
|
| 365 |
+
#306 = CARTESIAN_POINT('',(-25.,15.,-10.));
|
| 366 |
+
#307 = VECTOR('',#308,1.);
|
| 367 |
+
#308 = DIRECTION('',(1.,0.,0.));
|
| 368 |
+
#309 = PCURVE('',#100,#310);
|
| 369 |
+
#310 = DEFINITIONAL_REPRESENTATION('',(#311),#315);
|
| 370 |
+
#311 = LINE('',#312,#313);
|
| 371 |
+
#312 = CARTESIAN_POINT('',(0.,0.));
|
| 372 |
+
#313 = VECTOR('',#314,1.);
|
| 373 |
+
#314 = DIRECTION('',(0.,1.));
|
| 374 |
+
#315 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 375 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 376 |
+
) );
|
| 377 |
+
#316 = PCURVE('',#72,#317);
|
| 378 |
+
#317 = DEFINITIONAL_REPRESENTATION('',(#318),#322);
|
| 379 |
+
#318 = LINE('',#319,#320);
|
| 380 |
+
#319 = CARTESIAN_POINT('',(0.,30.));
|
| 381 |
+
#320 = VECTOR('',#321,1.);
|
| 382 |
+
#321 = DIRECTION('',(1.,0.));
|
| 383 |
+
#322 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 384 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 385 |
+
) );
|
| 386 |
+
#323 = ORIENTED_EDGE('',*,*,#84,.T.);
|
| 387 |
+
#324 = ORIENTED_EDGE('',*,*,#242,.T.);
|
| 388 |
+
#325 = ORIENTED_EDGE('',*,*,#326,.F.);
|
| 389 |
+
#326 = EDGE_CURVE('',#302,#220,#327,.T.);
|
| 390 |
+
#327 = SURFACE_CURVE('',#328,(#332,#339),.PCURVE_S1.);
|
| 391 |
+
#328 = LINE('',#329,#330);
|
| 392 |
+
#329 = CARTESIAN_POINT('',(25.,15.,-10.));
|
| 393 |
+
#330 = VECTOR('',#331,1.);
|
| 394 |
+
#331 = DIRECTION('',(0.,0.,1.));
|
| 395 |
+
#332 = PCURVE('',#100,#333);
|
| 396 |
+
#333 = DEFINITIONAL_REPRESENTATION('',(#334),#338);
|
| 397 |
+
#334 = LINE('',#335,#336);
|
| 398 |
+
#335 = CARTESIAN_POINT('',(0.,50.));
|
| 399 |
+
#336 = VECTOR('',#337,1.);
|
| 400 |
+
#337 = DIRECTION('',(1.,0.));
|
| 401 |
+
#338 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 402 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 403 |
+
) );
|
| 404 |
+
#339 = PCURVE('',#202,#340);
|
| 405 |
+
#340 = DEFINITIONAL_REPRESENTATION('',(#341),#345);
|
| 406 |
+
#341 = LINE('',#342,#343);
|
| 407 |
+
#342 = CARTESIAN_POINT('',(0.,-30.));
|
| 408 |
+
#343 = VECTOR('',#344,1.);
|
| 409 |
+
#344 = DIRECTION('',(1.,0.));
|
| 410 |
+
#345 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 411 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 412 |
+
) );
|
| 413 |
+
#346 = ADVANCED_FACE('',(#347,#373),#72,.F.);
|
| 414 |
+
#347 = FACE_BOUND('',#348,.F.);
|
| 415 |
+
#348 = EDGE_LOOP('',(#349,#350,#351,#372));
|
| 416 |
+
#349 = ORIENTED_EDGE('',*,*,#56,.F.);
|
| 417 |
+
#350 = ORIENTED_EDGE('',*,*,#141,.T.);
|
| 418 |
+
#351 = ORIENTED_EDGE('',*,*,#352,.T.);
|
| 419 |
+
#352 = EDGE_CURVE('',#142,#302,#353,.T.);
|
| 420 |
+
#353 = SURFACE_CURVE('',#354,(#358,#365),.PCURVE_S1.);
|
| 421 |
+
#354 = LINE('',#355,#356);
|
| 422 |
+
#355 = CARTESIAN_POINT('',(25.,-15.,-10.));
|
| 423 |
+
#356 = VECTOR('',#357,1.);
|
| 424 |
+
#357 = DIRECTION('',(0.,1.,0.));
|
| 425 |
+
#358 = PCURVE('',#72,#359);
|
| 426 |
+
#359 = DEFINITIONAL_REPRESENTATION('',(#360),#364);
|
| 427 |
+
#360 = LINE('',#361,#362);
|
| 428 |
+
#361 = CARTESIAN_POINT('',(50.,0.));
|
| 429 |
+
#362 = VECTOR('',#363,1.);
|
| 430 |
+
#363 = DIRECTION('',(0.,1.));
|
| 431 |
+
#364 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 432 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 433 |
+
) );
|
| 434 |
+
#365 = PCURVE('',#202,#366);
|
| 435 |
+
#366 = DEFINITIONAL_REPRESENTATION('',(#367),#371);
|
| 436 |
+
#367 = LINE('',#368,#369);
|
| 437 |
+
#368 = CARTESIAN_POINT('',(0.,0.));
|
| 438 |
+
#369 = VECTOR('',#370,1.);
|
| 439 |
+
#370 = DIRECTION('',(0.,-1.));
|
| 440 |
+
#371 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 441 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 442 |
+
) );
|
| 443 |
+
#372 = ORIENTED_EDGE('',*,*,#301,.F.);
|
| 444 |
+
#373 = FACE_BOUND('',#374,.F.);
|
| 445 |
+
#374 = EDGE_LOOP('',(#375));
|
| 446 |
+
#375 = ORIENTED_EDGE('',*,*,#376,.T.);
|
| 447 |
+
#376 = EDGE_CURVE('',#377,#377,#379,.T.);
|
| 448 |
+
#377 = VERTEX_POINT('',#378);
|
| 449 |
+
#378 = CARTESIAN_POINT('',(-5.,-1.722026714179E-15,-10.));
|
| 450 |
+
#379 = SURFACE_CURVE('',#380,(#385,#396),.PCURVE_S1.);
|
| 451 |
+
#380 = CIRCLE('',#381,5.);
|
| 452 |
+
#381 = AXIS2_PLACEMENT_3D('',#382,#383,#384);
|
| 453 |
+
#382 = CARTESIAN_POINT('',(-4.440892098501E-16,-4.973799150321E-16,-10.)
|
| 454 |
+
);
|
| 455 |
+
#383 = DIRECTION('',(0.,0.,-1.));
|
| 456 |
+
#384 = DIRECTION('',(-1.,0.,0.));
|
| 457 |
+
#385 = PCURVE('',#72,#386);
|
| 458 |
+
#386 = DEFINITIONAL_REPRESENTATION('',(#387),#395);
|
| 459 |
+
#387 = ( BOUNDED_CURVE() B_SPLINE_CURVE(2,(#388,#389,#390,#391,#392,#393
|
| 460 |
+
,#394),.UNSPECIFIED.,.T.,.F.) B_SPLINE_CURVE_WITH_KNOTS((1,2,2,2,2,1),(
|
| 461 |
+
-2.094395102393,0.,2.094395102393,4.188790204786,6.28318530718,
|
| 462 |
+
8.377580409573),.UNSPECIFIED.) CURVE() GEOMETRIC_REPRESENTATION_ITEM()
|
| 463 |
+
RATIONAL_B_SPLINE_CURVE((1.,0.5,1.,0.5,1.,0.5,1.)) REPRESENTATION_ITEM(
|
| 464 |
+
'') );
|
| 465 |
+
#388 = CARTESIAN_POINT('',(20.,15.));
|
| 466 |
+
#389 = CARTESIAN_POINT('',(20.,23.660254037844));
|
| 467 |
+
#390 = CARTESIAN_POINT('',(27.5,19.330127018922));
|
| 468 |
+
#391 = CARTESIAN_POINT('',(35.,15.));
|
| 469 |
+
#392 = CARTESIAN_POINT('',(27.5,10.669872981078));
|
| 470 |
+
#393 = CARTESIAN_POINT('',(20.,6.339745962156));
|
| 471 |
+
#394 = CARTESIAN_POINT('',(20.,15.));
|
| 472 |
+
#395 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 473 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 474 |
+
) );
|
| 475 |
+
#396 = PCURVE('',#286,#397);
|
| 476 |
+
#397 = DEFINITIONAL_REPRESENTATION('',(#398),#402);
|
| 477 |
+
#398 = LINE('',#399,#400);
|
| 478 |
+
#399 = CARTESIAN_POINT('',(0.,20.));
|
| 479 |
+
#400 = VECTOR('',#401,1.);
|
| 480 |
+
#401 = DIRECTION('',(1.,0.));
|
| 481 |
+
#402 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 482 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 483 |
+
) );
|
| 484 |
+
#403 = ADVANCED_FACE('',(#404),#202,.T.);
|
| 485 |
+
#404 = FACE_BOUND('',#405,.T.);
|
| 486 |
+
#405 = EDGE_LOOP('',(#406,#407,#408,#409));
|
| 487 |
+
#406 = ORIENTED_EDGE('',*,*,#188,.F.);
|
| 488 |
+
#407 = ORIENTED_EDGE('',*,*,#352,.T.);
|
| 489 |
+
#408 = ORIENTED_EDGE('',*,*,#326,.T.);
|
| 490 |
+
#409 = ORIENTED_EDGE('',*,*,#219,.F.);
|
| 491 |
+
#410 = ADVANCED_FACE('',(#411),#286,.F.);
|
| 492 |
+
#411 = FACE_BOUND('',#412,.F.);
|
| 493 |
+
#412 = EDGE_LOOP('',(#413,#414,#435,#436));
|
| 494 |
+
#413 = ORIENTED_EDGE('',*,*,#376,.F.);
|
| 495 |
+
#414 = ORIENTED_EDGE('',*,*,#415,.F.);
|
| 496 |
+
#415 = EDGE_CURVE('',#266,#377,#416,.T.);
|
| 497 |
+
#416 = SEAM_CURVE('',#417,(#421,#428),.PCURVE_S1.);
|
| 498 |
+
#417 = LINE('',#418,#419);
|
| 499 |
+
#418 = CARTESIAN_POINT('',(-5.,-1.722026714179E-15,10.));
|
| 500 |
+
#419 = VECTOR('',#420,1.);
|
| 501 |
+
#420 = DIRECTION('',(0.,0.,-1.));
|
| 502 |
+
#421 = PCURVE('',#286,#422);
|
| 503 |
+
#422 = DEFINITIONAL_REPRESENTATION('',(#423),#427);
|
| 504 |
+
#423 = LINE('',#424,#425);
|
| 505 |
+
#424 = CARTESIAN_POINT('',(6.28318530718,-0.));
|
| 506 |
+
#425 = VECTOR('',#426,1.);
|
| 507 |
+
#426 = DIRECTION('',(0.,1.));
|
| 508 |
+
#427 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 509 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 510 |
+
) );
|
| 511 |
+
#428 = PCURVE('',#286,#429);
|
| 512 |
+
#429 = DEFINITIONAL_REPRESENTATION('',(#430),#434);
|
| 513 |
+
#430 = LINE('',#431,#432);
|
| 514 |
+
#431 = CARTESIAN_POINT('',(0.,-0.));
|
| 515 |
+
#432 = VECTOR('',#433,1.);
|
| 516 |
+
#433 = DIRECTION('',(0.,1.));
|
| 517 |
+
#434 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 518 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 519 |
+
) );
|
| 520 |
+
#435 = ORIENTED_EDGE('',*,*,#265,.T.);
|
| 521 |
+
#436 = ORIENTED_EDGE('',*,*,#415,.T.);
|
| 522 |
+
#437 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3)
|
| 523 |
+
GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#441)) GLOBAL_UNIT_ASSIGNED_CONTEXT
|
| 524 |
+
((#438,#439,#440)) REPRESENTATION_CONTEXT('Context #1',
|
| 525 |
+
'3D Context with UNIT and UNCERTAINTY') );
|
| 526 |
+
#438 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
|
| 527 |
+
#439 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
|
| 528 |
+
#440 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
|
| 529 |
+
#441 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#438,
|
| 530 |
+
'distance_accuracy_value','confusion accuracy');
|
| 531 |
+
#442 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#7));
|
| 532 |
+
ENDSEC;
|
| 533 |
+
END-ISO-10303-21;
|
server/tasks/task_002_box_with_hole/ground_truth_normalized.step
ADDED
|
@@ -0,0 +1,533 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
ISO-10303-21;
|
| 2 |
+
HEADER;
|
| 3 |
+
FILE_DESCRIPTION(('Open CASCADE Model'),'2;1');
|
| 4 |
+
FILE_NAME('Open CASCADE Shape Model','2026-04-25T17:44:29',('Author'),(
|
| 5 |
+
'Open CASCADE'),'Open CASCADE STEP processor 7.8','Open CASCADE 7.8'
|
| 6 |
+
,'Unknown');
|
| 7 |
+
FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));
|
| 8 |
+
ENDSEC;
|
| 9 |
+
DATA;
|
| 10 |
+
#1 = APPLICATION_PROTOCOL_DEFINITION('international standard',
|
| 11 |
+
'automotive_design',2000,#2);
|
| 12 |
+
#2 = APPLICATION_CONTEXT(
|
| 13 |
+
'core data for automotive mechanical design processes');
|
| 14 |
+
#3 = SHAPE_DEFINITION_REPRESENTATION(#4,#10);
|
| 15 |
+
#4 = PRODUCT_DEFINITION_SHAPE('','',#5);
|
| 16 |
+
#5 = PRODUCT_DEFINITION('design','',#6,#9);
|
| 17 |
+
#6 = PRODUCT_DEFINITION_FORMATION('','',#7);
|
| 18 |
+
#7 = PRODUCT('Open CASCADE STEP translator 7.8 4',
|
| 19 |
+
'Open CASCADE STEP translator 7.8 4','',(#8));
|
| 20 |
+
#8 = PRODUCT_CONTEXT('',#2,'mechanical');
|
| 21 |
+
#9 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design');
|
| 22 |
+
#10 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#15),#437);
|
| 23 |
+
#11 = AXIS2_PLACEMENT_3D('',#12,#13,#14);
|
| 24 |
+
#12 = CARTESIAN_POINT('',(0.,0.,0.));
|
| 25 |
+
#13 = DIRECTION('',(0.,0.,1.));
|
| 26 |
+
#14 = DIRECTION('',(1.,0.,-0.));
|
| 27 |
+
#15 = MANIFOLD_SOLID_BREP('',#16);
|
| 28 |
+
#16 = CLOSED_SHELL('',(#17,#137,#213,#297,#346,#403,#410));
|
| 29 |
+
#17 = ADVANCED_FACE('',(#18),#32,.F.);
|
| 30 |
+
#18 = FACE_BOUND('',#19,.F.);
|
| 31 |
+
#19 = EDGE_LOOP('',(#20,#55,#83,#111));
|
| 32 |
+
#20 = ORIENTED_EDGE('',*,*,#21,.F.);
|
| 33 |
+
#21 = EDGE_CURVE('',#22,#24,#26,.T.);
|
| 34 |
+
#22 = VERTEX_POINT('',#23);
|
| 35 |
+
#23 = CARTESIAN_POINT('',(-25.,-15.,-10.));
|
| 36 |
+
#24 = VERTEX_POINT('',#25);
|
| 37 |
+
#25 = CARTESIAN_POINT('',(-25.,-15.,10.));
|
| 38 |
+
#26 = SURFACE_CURVE('',#27,(#31,#43),.PCURVE_S1.);
|
| 39 |
+
#27 = LINE('',#28,#29);
|
| 40 |
+
#28 = CARTESIAN_POINT('',(-25.,-15.,-10.));
|
| 41 |
+
#29 = VECTOR('',#30,1.);
|
| 42 |
+
#30 = DIRECTION('',(0.,0.,1.));
|
| 43 |
+
#31 = PCURVE('',#32,#37);
|
| 44 |
+
#32 = PLANE('',#33);
|
| 45 |
+
#33 = AXIS2_PLACEMENT_3D('',#34,#35,#36);
|
| 46 |
+
#34 = CARTESIAN_POINT('',(-25.,-15.,-10.));
|
| 47 |
+
#35 = DIRECTION('',(1.,0.,0.));
|
| 48 |
+
#36 = DIRECTION('',(0.,0.,1.));
|
| 49 |
+
#37 = DEFINITIONAL_REPRESENTATION('',(#38),#42);
|
| 50 |
+
#38 = LINE('',#39,#40);
|
| 51 |
+
#39 = CARTESIAN_POINT('',(0.,0.));
|
| 52 |
+
#40 = VECTOR('',#41,1.);
|
| 53 |
+
#41 = DIRECTION('',(1.,0.));
|
| 54 |
+
#42 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 55 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 56 |
+
) );
|
| 57 |
+
#43 = PCURVE('',#44,#49);
|
| 58 |
+
#44 = PLANE('',#45);
|
| 59 |
+
#45 = AXIS2_PLACEMENT_3D('',#46,#47,#48);
|
| 60 |
+
#46 = CARTESIAN_POINT('',(-25.,-15.,-10.));
|
| 61 |
+
#47 = DIRECTION('',(0.,1.,0.));
|
| 62 |
+
#48 = DIRECTION('',(0.,0.,1.));
|
| 63 |
+
#49 = DEFINITIONAL_REPRESENTATION('',(#50),#54);
|
| 64 |
+
#50 = LINE('',#51,#52);
|
| 65 |
+
#51 = CARTESIAN_POINT('',(0.,0.));
|
| 66 |
+
#52 = VECTOR('',#53,1.);
|
| 67 |
+
#53 = DIRECTION('',(1.,0.));
|
| 68 |
+
#54 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 69 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 70 |
+
) );
|
| 71 |
+
#55 = ORIENTED_EDGE('',*,*,#56,.T.);
|
| 72 |
+
#56 = EDGE_CURVE('',#22,#57,#59,.T.);
|
| 73 |
+
#57 = VERTEX_POINT('',#58);
|
| 74 |
+
#58 = CARTESIAN_POINT('',(-25.,15.,-10.));
|
| 75 |
+
#59 = SURFACE_CURVE('',#60,(#64,#71),.PCURVE_S1.);
|
| 76 |
+
#60 = LINE('',#61,#62);
|
| 77 |
+
#61 = CARTESIAN_POINT('',(-25.,-15.,-10.));
|
| 78 |
+
#62 = VECTOR('',#63,1.);
|
| 79 |
+
#63 = DIRECTION('',(0.,1.,0.));
|
| 80 |
+
#64 = PCURVE('',#32,#65);
|
| 81 |
+
#65 = DEFINITIONAL_REPRESENTATION('',(#66),#70);
|
| 82 |
+
#66 = LINE('',#67,#68);
|
| 83 |
+
#67 = CARTESIAN_POINT('',(0.,0.));
|
| 84 |
+
#68 = VECTOR('',#69,1.);
|
| 85 |
+
#69 = DIRECTION('',(0.,-1.));
|
| 86 |
+
#70 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 87 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 88 |
+
) );
|
| 89 |
+
#71 = PCURVE('',#72,#77);
|
| 90 |
+
#72 = PLANE('',#73);
|
| 91 |
+
#73 = AXIS2_PLACEMENT_3D('',#74,#75,#76);
|
| 92 |
+
#74 = CARTESIAN_POINT('',(-25.,-15.,-10.));
|
| 93 |
+
#75 = DIRECTION('',(0.,0.,1.));
|
| 94 |
+
#76 = DIRECTION('',(1.,0.,0.));
|
| 95 |
+
#77 = DEFINITIONAL_REPRESENTATION('',(#78),#82);
|
| 96 |
+
#78 = LINE('',#79,#80);
|
| 97 |
+
#79 = CARTESIAN_POINT('',(0.,0.));
|
| 98 |
+
#80 = VECTOR('',#81,1.);
|
| 99 |
+
#81 = DIRECTION('',(0.,1.));
|
| 100 |
+
#82 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 101 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 102 |
+
) );
|
| 103 |
+
#83 = ORIENTED_EDGE('',*,*,#84,.T.);
|
| 104 |
+
#84 = EDGE_CURVE('',#57,#85,#87,.T.);
|
| 105 |
+
#85 = VERTEX_POINT('',#86);
|
| 106 |
+
#86 = CARTESIAN_POINT('',(-25.,15.,10.));
|
| 107 |
+
#87 = SURFACE_CURVE('',#88,(#92,#99),.PCURVE_S1.);
|
| 108 |
+
#88 = LINE('',#89,#90);
|
| 109 |
+
#89 = CARTESIAN_POINT('',(-25.,15.,-10.));
|
| 110 |
+
#90 = VECTOR('',#91,1.);
|
| 111 |
+
#91 = DIRECTION('',(0.,0.,1.));
|
| 112 |
+
#92 = PCURVE('',#32,#93);
|
| 113 |
+
#93 = DEFINITIONAL_REPRESENTATION('',(#94),#98);
|
| 114 |
+
#94 = LINE('',#95,#96);
|
| 115 |
+
#95 = CARTESIAN_POINT('',(0.,-30.));
|
| 116 |
+
#96 = VECTOR('',#97,1.);
|
| 117 |
+
#97 = DIRECTION('',(1.,0.));
|
| 118 |
+
#98 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 119 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 120 |
+
) );
|
| 121 |
+
#99 = PCURVE('',#100,#105);
|
| 122 |
+
#100 = PLANE('',#101);
|
| 123 |
+
#101 = AXIS2_PLACEMENT_3D('',#102,#103,#104);
|
| 124 |
+
#102 = CARTESIAN_POINT('',(-25.,15.,-10.));
|
| 125 |
+
#103 = DIRECTION('',(0.,1.,0.));
|
| 126 |
+
#104 = DIRECTION('',(0.,0.,1.));
|
| 127 |
+
#105 = DEFINITIONAL_REPRESENTATION('',(#106),#110);
|
| 128 |
+
#106 = LINE('',#107,#108);
|
| 129 |
+
#107 = CARTESIAN_POINT('',(0.,0.));
|
| 130 |
+
#108 = VECTOR('',#109,1.);
|
| 131 |
+
#109 = DIRECTION('',(1.,0.));
|
| 132 |
+
#110 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 133 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 134 |
+
) );
|
| 135 |
+
#111 = ORIENTED_EDGE('',*,*,#112,.F.);
|
| 136 |
+
#112 = EDGE_CURVE('',#24,#85,#113,.T.);
|
| 137 |
+
#113 = SURFACE_CURVE('',#114,(#118,#125),.PCURVE_S1.);
|
| 138 |
+
#114 = LINE('',#115,#116);
|
| 139 |
+
#115 = CARTESIAN_POINT('',(-25.,-15.,10.));
|
| 140 |
+
#116 = VECTOR('',#117,1.);
|
| 141 |
+
#117 = DIRECTION('',(0.,1.,0.));
|
| 142 |
+
#118 = PCURVE('',#32,#119);
|
| 143 |
+
#119 = DEFINITIONAL_REPRESENTATION('',(#120),#124);
|
| 144 |
+
#120 = LINE('',#121,#122);
|
| 145 |
+
#121 = CARTESIAN_POINT('',(20.,0.));
|
| 146 |
+
#122 = VECTOR('',#123,1.);
|
| 147 |
+
#123 = DIRECTION('',(0.,-1.));
|
| 148 |
+
#124 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 149 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 150 |
+
) );
|
| 151 |
+
#125 = PCURVE('',#126,#131);
|
| 152 |
+
#126 = PLANE('',#127);
|
| 153 |
+
#127 = AXIS2_PLACEMENT_3D('',#128,#129,#130);
|
| 154 |
+
#128 = CARTESIAN_POINT('',(-25.,-15.,10.));
|
| 155 |
+
#129 = DIRECTION('',(0.,0.,1.));
|
| 156 |
+
#130 = DIRECTION('',(1.,0.,0.));
|
| 157 |
+
#131 = DEFINITIONAL_REPRESENTATION('',(#132),#136);
|
| 158 |
+
#132 = LINE('',#133,#134);
|
| 159 |
+
#133 = CARTESIAN_POINT('',(0.,0.));
|
| 160 |
+
#134 = VECTOR('',#135,1.);
|
| 161 |
+
#135 = DIRECTION('',(0.,1.));
|
| 162 |
+
#136 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 163 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 164 |
+
) );
|
| 165 |
+
#137 = ADVANCED_FACE('',(#138),#44,.F.);
|
| 166 |
+
#138 = FACE_BOUND('',#139,.F.);
|
| 167 |
+
#139 = EDGE_LOOP('',(#140,#163,#164,#187));
|
| 168 |
+
#140 = ORIENTED_EDGE('',*,*,#141,.F.);
|
| 169 |
+
#141 = EDGE_CURVE('',#22,#142,#144,.T.);
|
| 170 |
+
#142 = VERTEX_POINT('',#143);
|
| 171 |
+
#143 = CARTESIAN_POINT('',(25.,-15.,-10.));
|
| 172 |
+
#144 = SURFACE_CURVE('',#145,(#149,#156),.PCURVE_S1.);
|
| 173 |
+
#145 = LINE('',#146,#147);
|
| 174 |
+
#146 = CARTESIAN_POINT('',(-25.,-15.,-10.));
|
| 175 |
+
#147 = VECTOR('',#148,1.);
|
| 176 |
+
#148 = DIRECTION('',(1.,0.,0.));
|
| 177 |
+
#149 = PCURVE('',#44,#150);
|
| 178 |
+
#150 = DEFINITIONAL_REPRESENTATION('',(#151),#155);
|
| 179 |
+
#151 = LINE('',#152,#153);
|
| 180 |
+
#152 = CARTESIAN_POINT('',(0.,0.));
|
| 181 |
+
#153 = VECTOR('',#154,1.);
|
| 182 |
+
#154 = DIRECTION('',(0.,1.));
|
| 183 |
+
#155 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 184 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 185 |
+
) );
|
| 186 |
+
#156 = PCURVE('',#72,#157);
|
| 187 |
+
#157 = DEFINITIONAL_REPRESENTATION('',(#158),#162);
|
| 188 |
+
#158 = LINE('',#159,#160);
|
| 189 |
+
#159 = CARTESIAN_POINT('',(0.,0.));
|
| 190 |
+
#160 = VECTOR('',#161,1.);
|
| 191 |
+
#161 = DIRECTION('',(1.,0.));
|
| 192 |
+
#162 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 193 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 194 |
+
) );
|
| 195 |
+
#163 = ORIENTED_EDGE('',*,*,#21,.T.);
|
| 196 |
+
#164 = ORIENTED_EDGE('',*,*,#165,.T.);
|
| 197 |
+
#165 = EDGE_CURVE('',#24,#166,#168,.T.);
|
| 198 |
+
#166 = VERTEX_POINT('',#167);
|
| 199 |
+
#167 = CARTESIAN_POINT('',(25.,-15.,10.));
|
| 200 |
+
#168 = SURFACE_CURVE('',#169,(#173,#180),.PCURVE_S1.);
|
| 201 |
+
#169 = LINE('',#170,#171);
|
| 202 |
+
#170 = CARTESIAN_POINT('',(-25.,-15.,10.));
|
| 203 |
+
#171 = VECTOR('',#172,1.);
|
| 204 |
+
#172 = DIRECTION('',(1.,0.,0.));
|
| 205 |
+
#173 = PCURVE('',#44,#174);
|
| 206 |
+
#174 = DEFINITIONAL_REPRESENTATION('',(#175),#179);
|
| 207 |
+
#175 = LINE('',#176,#177);
|
| 208 |
+
#176 = CARTESIAN_POINT('',(20.,0.));
|
| 209 |
+
#177 = VECTOR('',#178,1.);
|
| 210 |
+
#178 = DIRECTION('',(0.,1.));
|
| 211 |
+
#179 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 212 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 213 |
+
) );
|
| 214 |
+
#180 = PCURVE('',#126,#181);
|
| 215 |
+
#181 = DEFINITIONAL_REPRESENTATION('',(#182),#186);
|
| 216 |
+
#182 = LINE('',#183,#184);
|
| 217 |
+
#183 = CARTESIAN_POINT('',(0.,0.));
|
| 218 |
+
#184 = VECTOR('',#185,1.);
|
| 219 |
+
#185 = DIRECTION('',(1.,0.));
|
| 220 |
+
#186 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 221 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 222 |
+
) );
|
| 223 |
+
#187 = ORIENTED_EDGE('',*,*,#188,.F.);
|
| 224 |
+
#188 = EDGE_CURVE('',#142,#166,#189,.T.);
|
| 225 |
+
#189 = SURFACE_CURVE('',#190,(#194,#201),.PCURVE_S1.);
|
| 226 |
+
#190 = LINE('',#191,#192);
|
| 227 |
+
#191 = CARTESIAN_POINT('',(25.,-15.,-10.));
|
| 228 |
+
#192 = VECTOR('',#193,1.);
|
| 229 |
+
#193 = DIRECTION('',(0.,0.,1.));
|
| 230 |
+
#194 = PCURVE('',#44,#195);
|
| 231 |
+
#195 = DEFINITIONAL_REPRESENTATION('',(#196),#200);
|
| 232 |
+
#196 = LINE('',#197,#198);
|
| 233 |
+
#197 = CARTESIAN_POINT('',(0.,50.));
|
| 234 |
+
#198 = VECTOR('',#199,1.);
|
| 235 |
+
#199 = DIRECTION('',(1.,0.));
|
| 236 |
+
#200 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 237 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 238 |
+
) );
|
| 239 |
+
#201 = PCURVE('',#202,#207);
|
| 240 |
+
#202 = PLANE('',#203);
|
| 241 |
+
#203 = AXIS2_PLACEMENT_3D('',#204,#205,#206);
|
| 242 |
+
#204 = CARTESIAN_POINT('',(25.,-15.,-10.));
|
| 243 |
+
#205 = DIRECTION('',(1.,0.,0.));
|
| 244 |
+
#206 = DIRECTION('',(0.,0.,1.));
|
| 245 |
+
#207 = DEFINITIONAL_REPRESENTATION('',(#208),#212);
|
| 246 |
+
#208 = LINE('',#209,#210);
|
| 247 |
+
#209 = CARTESIAN_POINT('',(0.,0.));
|
| 248 |
+
#210 = VECTOR('',#211,1.);
|
| 249 |
+
#211 = DIRECTION('',(1.,0.));
|
| 250 |
+
#212 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 251 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 252 |
+
) );
|
| 253 |
+
#213 = ADVANCED_FACE('',(#214,#262),#126,.T.);
|
| 254 |
+
#214 = FACE_BOUND('',#215,.T.);
|
| 255 |
+
#215 = EDGE_LOOP('',(#216,#217,#218,#241));
|
| 256 |
+
#216 = ORIENTED_EDGE('',*,*,#112,.F.);
|
| 257 |
+
#217 = ORIENTED_EDGE('',*,*,#165,.T.);
|
| 258 |
+
#218 = ORIENTED_EDGE('',*,*,#219,.T.);
|
| 259 |
+
#219 = EDGE_CURVE('',#166,#220,#222,.T.);
|
| 260 |
+
#220 = VERTEX_POINT('',#221);
|
| 261 |
+
#221 = CARTESIAN_POINT('',(25.,15.,10.));
|
| 262 |
+
#222 = SURFACE_CURVE('',#223,(#227,#234),.PCURVE_S1.);
|
| 263 |
+
#223 = LINE('',#224,#225);
|
| 264 |
+
#224 = CARTESIAN_POINT('',(25.,-15.,10.));
|
| 265 |
+
#225 = VECTOR('',#226,1.);
|
| 266 |
+
#226 = DIRECTION('',(0.,1.,0.));
|
| 267 |
+
#227 = PCURVE('',#126,#228);
|
| 268 |
+
#228 = DEFINITIONAL_REPRESENTATION('',(#229),#233);
|
| 269 |
+
#229 = LINE('',#230,#231);
|
| 270 |
+
#230 = CARTESIAN_POINT('',(50.,0.));
|
| 271 |
+
#231 = VECTOR('',#232,1.);
|
| 272 |
+
#232 = DIRECTION('',(0.,1.));
|
| 273 |
+
#233 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 274 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 275 |
+
) );
|
| 276 |
+
#234 = PCURVE('',#202,#235);
|
| 277 |
+
#235 = DEFINITIONAL_REPRESENTATION('',(#236),#240);
|
| 278 |
+
#236 = LINE('',#237,#238);
|
| 279 |
+
#237 = CARTESIAN_POINT('',(20.,0.));
|
| 280 |
+
#238 = VECTOR('',#239,1.);
|
| 281 |
+
#239 = DIRECTION('',(0.,-1.));
|
| 282 |
+
#240 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 283 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 284 |
+
) );
|
| 285 |
+
#241 = ORIENTED_EDGE('',*,*,#242,.F.);
|
| 286 |
+
#242 = EDGE_CURVE('',#85,#220,#243,.T.);
|
| 287 |
+
#243 = SURFACE_CURVE('',#244,(#248,#255),.PCURVE_S1.);
|
| 288 |
+
#244 = LINE('',#245,#246);
|
| 289 |
+
#245 = CARTESIAN_POINT('',(-25.,15.,10.));
|
| 290 |
+
#246 = VECTOR('',#247,1.);
|
| 291 |
+
#247 = DIRECTION('',(1.,0.,0.));
|
| 292 |
+
#248 = PCURVE('',#126,#249);
|
| 293 |
+
#249 = DEFINITIONAL_REPRESENTATION('',(#250),#254);
|
| 294 |
+
#250 = LINE('',#251,#252);
|
| 295 |
+
#251 = CARTESIAN_POINT('',(0.,30.));
|
| 296 |
+
#252 = VECTOR('',#253,1.);
|
| 297 |
+
#253 = DIRECTION('',(1.,0.));
|
| 298 |
+
#254 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 299 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 300 |
+
) );
|
| 301 |
+
#255 = PCURVE('',#100,#256);
|
| 302 |
+
#256 = DEFINITIONAL_REPRESENTATION('',(#257),#261);
|
| 303 |
+
#257 = LINE('',#258,#259);
|
| 304 |
+
#258 = CARTESIAN_POINT('',(20.,0.));
|
| 305 |
+
#259 = VECTOR('',#260,1.);
|
| 306 |
+
#260 = DIRECTION('',(0.,1.));
|
| 307 |
+
#261 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 308 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 309 |
+
) );
|
| 310 |
+
#262 = FACE_BOUND('',#263,.T.);
|
| 311 |
+
#263 = EDGE_LOOP('',(#264));
|
| 312 |
+
#264 = ORIENTED_EDGE('',*,*,#265,.T.);
|
| 313 |
+
#265 = EDGE_CURVE('',#266,#266,#268,.T.);
|
| 314 |
+
#266 = VERTEX_POINT('',#267);
|
| 315 |
+
#267 = CARTESIAN_POINT('',(-5.,-1.722026714179E-15,10.));
|
| 316 |
+
#268 = SURFACE_CURVE('',#269,(#274,#285),.PCURVE_S1.);
|
| 317 |
+
#269 = CIRCLE('',#270,5.);
|
| 318 |
+
#270 = AXIS2_PLACEMENT_3D('',#271,#272,#273);
|
| 319 |
+
#271 = CARTESIAN_POINT('',(-4.440892098501E-16,-4.973799150321E-16,10.)
|
| 320 |
+
);
|
| 321 |
+
#272 = DIRECTION('',(0.,0.,-1.));
|
| 322 |
+
#273 = DIRECTION('',(-1.,0.,0.));
|
| 323 |
+
#274 = PCURVE('',#126,#275);
|
| 324 |
+
#275 = DEFINITIONAL_REPRESENTATION('',(#276),#284);
|
| 325 |
+
#276 = ( BOUNDED_CURVE() B_SPLINE_CURVE(2,(#277,#278,#279,#280,#281,#282
|
| 326 |
+
,#283),.UNSPECIFIED.,.T.,.F.) B_SPLINE_CURVE_WITH_KNOTS((1,2,2,2,2,1),(
|
| 327 |
+
-2.094395102393,0.,2.094395102393,4.188790204786,6.28318530718,
|
| 328 |
+
8.377580409573),.UNSPECIFIED.) CURVE() GEOMETRIC_REPRESENTATION_ITEM()
|
| 329 |
+
RATIONAL_B_SPLINE_CURVE((1.,0.5,1.,0.5,1.,0.5,1.)) REPRESENTATION_ITEM(
|
| 330 |
+
'') );
|
| 331 |
+
#277 = CARTESIAN_POINT('',(20.,15.));
|
| 332 |
+
#278 = CARTESIAN_POINT('',(20.,23.660254037844));
|
| 333 |
+
#279 = CARTESIAN_POINT('',(27.5,19.330127018922));
|
| 334 |
+
#280 = CARTESIAN_POINT('',(35.,15.));
|
| 335 |
+
#281 = CARTESIAN_POINT('',(27.5,10.669872981078));
|
| 336 |
+
#282 = CARTESIAN_POINT('',(20.,6.339745962156));
|
| 337 |
+
#283 = CARTESIAN_POINT('',(20.,15.));
|
| 338 |
+
#284 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 339 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 340 |
+
) );
|
| 341 |
+
#285 = PCURVE('',#286,#291);
|
| 342 |
+
#286 = CYLINDRICAL_SURFACE('',#287,5.);
|
| 343 |
+
#287 = AXIS2_PLACEMENT_3D('',#288,#289,#290);
|
| 344 |
+
#288 = CARTESIAN_POINT('',(-4.440892098501E-16,-4.973799150321E-16,10.)
|
| 345 |
+
);
|
| 346 |
+
#289 = DIRECTION('',(0.,0.,-1.));
|
| 347 |
+
#290 = DIRECTION('',(-1.,0.,0.));
|
| 348 |
+
#291 = DEFINITIONAL_REPRESENTATION('',(#292),#296);
|
| 349 |
+
#292 = LINE('',#293,#294);
|
| 350 |
+
#293 = CARTESIAN_POINT('',(0.,0.));
|
| 351 |
+
#294 = VECTOR('',#295,1.);
|
| 352 |
+
#295 = DIRECTION('',(1.,0.));
|
| 353 |
+
#296 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 354 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 355 |
+
) );
|
| 356 |
+
#297 = ADVANCED_FACE('',(#298),#100,.T.);
|
| 357 |
+
#298 = FACE_BOUND('',#299,.T.);
|
| 358 |
+
#299 = EDGE_LOOP('',(#300,#323,#324,#325));
|
| 359 |
+
#300 = ORIENTED_EDGE('',*,*,#301,.F.);
|
| 360 |
+
#301 = EDGE_CURVE('',#57,#302,#304,.T.);
|
| 361 |
+
#302 = VERTEX_POINT('',#303);
|
| 362 |
+
#303 = CARTESIAN_POINT('',(25.,15.,-10.));
|
| 363 |
+
#304 = SURFACE_CURVE('',#305,(#309,#316),.PCURVE_S1.);
|
| 364 |
+
#305 = LINE('',#306,#307);
|
| 365 |
+
#306 = CARTESIAN_POINT('',(-25.,15.,-10.));
|
| 366 |
+
#307 = VECTOR('',#308,1.);
|
| 367 |
+
#308 = DIRECTION('',(1.,0.,0.));
|
| 368 |
+
#309 = PCURVE('',#100,#310);
|
| 369 |
+
#310 = DEFINITIONAL_REPRESENTATION('',(#311),#315);
|
| 370 |
+
#311 = LINE('',#312,#313);
|
| 371 |
+
#312 = CARTESIAN_POINT('',(0.,0.));
|
| 372 |
+
#313 = VECTOR('',#314,1.);
|
| 373 |
+
#314 = DIRECTION('',(0.,1.));
|
| 374 |
+
#315 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 375 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 376 |
+
) );
|
| 377 |
+
#316 = PCURVE('',#72,#317);
|
| 378 |
+
#317 = DEFINITIONAL_REPRESENTATION('',(#318),#322);
|
| 379 |
+
#318 = LINE('',#319,#320);
|
| 380 |
+
#319 = CARTESIAN_POINT('',(0.,30.));
|
| 381 |
+
#320 = VECTOR('',#321,1.);
|
| 382 |
+
#321 = DIRECTION('',(1.,0.));
|
| 383 |
+
#322 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 384 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 385 |
+
) );
|
| 386 |
+
#323 = ORIENTED_EDGE('',*,*,#84,.T.);
|
| 387 |
+
#324 = ORIENTED_EDGE('',*,*,#242,.T.);
|
| 388 |
+
#325 = ORIENTED_EDGE('',*,*,#326,.F.);
|
| 389 |
+
#326 = EDGE_CURVE('',#302,#220,#327,.T.);
|
| 390 |
+
#327 = SURFACE_CURVE('',#328,(#332,#339),.PCURVE_S1.);
|
| 391 |
+
#328 = LINE('',#329,#330);
|
| 392 |
+
#329 = CARTESIAN_POINT('',(25.,15.,-10.));
|
| 393 |
+
#330 = VECTOR('',#331,1.);
|
| 394 |
+
#331 = DIRECTION('',(0.,0.,1.));
|
| 395 |
+
#332 = PCURVE('',#100,#333);
|
| 396 |
+
#333 = DEFINITIONAL_REPRESENTATION('',(#334),#338);
|
| 397 |
+
#334 = LINE('',#335,#336);
|
| 398 |
+
#335 = CARTESIAN_POINT('',(0.,50.));
|
| 399 |
+
#336 = VECTOR('',#337,1.);
|
| 400 |
+
#337 = DIRECTION('',(1.,0.));
|
| 401 |
+
#338 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 402 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 403 |
+
) );
|
| 404 |
+
#339 = PCURVE('',#202,#340);
|
| 405 |
+
#340 = DEFINITIONAL_REPRESENTATION('',(#341),#345);
|
| 406 |
+
#341 = LINE('',#342,#343);
|
| 407 |
+
#342 = CARTESIAN_POINT('',(0.,-30.));
|
| 408 |
+
#343 = VECTOR('',#344,1.);
|
| 409 |
+
#344 = DIRECTION('',(1.,0.));
|
| 410 |
+
#345 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 411 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 412 |
+
) );
|
| 413 |
+
#346 = ADVANCED_FACE('',(#347,#373),#72,.F.);
|
| 414 |
+
#347 = FACE_BOUND('',#348,.F.);
|
| 415 |
+
#348 = EDGE_LOOP('',(#349,#350,#351,#372));
|
| 416 |
+
#349 = ORIENTED_EDGE('',*,*,#56,.F.);
|
| 417 |
+
#350 = ORIENTED_EDGE('',*,*,#141,.T.);
|
| 418 |
+
#351 = ORIENTED_EDGE('',*,*,#352,.T.);
|
| 419 |
+
#352 = EDGE_CURVE('',#142,#302,#353,.T.);
|
| 420 |
+
#353 = SURFACE_CURVE('',#354,(#358,#365),.PCURVE_S1.);
|
| 421 |
+
#354 = LINE('',#355,#356);
|
| 422 |
+
#355 = CARTESIAN_POINT('',(25.,-15.,-10.));
|
| 423 |
+
#356 = VECTOR('',#357,1.);
|
| 424 |
+
#357 = DIRECTION('',(0.,1.,0.));
|
| 425 |
+
#358 = PCURVE('',#72,#359);
|
| 426 |
+
#359 = DEFINITIONAL_REPRESENTATION('',(#360),#364);
|
| 427 |
+
#360 = LINE('',#361,#362);
|
| 428 |
+
#361 = CARTESIAN_POINT('',(50.,0.));
|
| 429 |
+
#362 = VECTOR('',#363,1.);
|
| 430 |
+
#363 = DIRECTION('',(0.,1.));
|
| 431 |
+
#364 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 432 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 433 |
+
) );
|
| 434 |
+
#365 = PCURVE('',#202,#366);
|
| 435 |
+
#366 = DEFINITIONAL_REPRESENTATION('',(#367),#371);
|
| 436 |
+
#367 = LINE('',#368,#369);
|
| 437 |
+
#368 = CARTESIAN_POINT('',(0.,0.));
|
| 438 |
+
#369 = VECTOR('',#370,1.);
|
| 439 |
+
#370 = DIRECTION('',(0.,-1.));
|
| 440 |
+
#371 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 441 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 442 |
+
) );
|
| 443 |
+
#372 = ORIENTED_EDGE('',*,*,#301,.F.);
|
| 444 |
+
#373 = FACE_BOUND('',#374,.F.);
|
| 445 |
+
#374 = EDGE_LOOP('',(#375));
|
| 446 |
+
#375 = ORIENTED_EDGE('',*,*,#376,.T.);
|
| 447 |
+
#376 = EDGE_CURVE('',#377,#377,#379,.T.);
|
| 448 |
+
#377 = VERTEX_POINT('',#378);
|
| 449 |
+
#378 = CARTESIAN_POINT('',(-5.,-1.722026714179E-15,-10.));
|
| 450 |
+
#379 = SURFACE_CURVE('',#380,(#385,#396),.PCURVE_S1.);
|
| 451 |
+
#380 = CIRCLE('',#381,5.);
|
| 452 |
+
#381 = AXIS2_PLACEMENT_3D('',#382,#383,#384);
|
| 453 |
+
#382 = CARTESIAN_POINT('',(-4.440892098501E-16,-4.973799150321E-16,-10.)
|
| 454 |
+
);
|
| 455 |
+
#383 = DIRECTION('',(0.,0.,-1.));
|
| 456 |
+
#384 = DIRECTION('',(-1.,0.,0.));
|
| 457 |
+
#385 = PCURVE('',#72,#386);
|
| 458 |
+
#386 = DEFINITIONAL_REPRESENTATION('',(#387),#395);
|
| 459 |
+
#387 = ( BOUNDED_CURVE() B_SPLINE_CURVE(2,(#388,#389,#390,#391,#392,#393
|
| 460 |
+
,#394),.UNSPECIFIED.,.T.,.F.) B_SPLINE_CURVE_WITH_KNOTS((1,2,2,2,2,1),(
|
| 461 |
+
-2.094395102393,0.,2.094395102393,4.188790204786,6.28318530718,
|
| 462 |
+
8.377580409573),.UNSPECIFIED.) CURVE() GEOMETRIC_REPRESENTATION_ITEM()
|
| 463 |
+
RATIONAL_B_SPLINE_CURVE((1.,0.5,1.,0.5,1.,0.5,1.)) REPRESENTATION_ITEM(
|
| 464 |
+
'') );
|
| 465 |
+
#388 = CARTESIAN_POINT('',(20.,15.));
|
| 466 |
+
#389 = CARTESIAN_POINT('',(20.,23.660254037844));
|
| 467 |
+
#390 = CARTESIAN_POINT('',(27.5,19.330127018922));
|
| 468 |
+
#391 = CARTESIAN_POINT('',(35.,15.));
|
| 469 |
+
#392 = CARTESIAN_POINT('',(27.5,10.669872981078));
|
| 470 |
+
#393 = CARTESIAN_POINT('',(20.,6.339745962156));
|
| 471 |
+
#394 = CARTESIAN_POINT('',(20.,15.));
|
| 472 |
+
#395 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 473 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 474 |
+
) );
|
| 475 |
+
#396 = PCURVE('',#286,#397);
|
| 476 |
+
#397 = DEFINITIONAL_REPRESENTATION('',(#398),#402);
|
| 477 |
+
#398 = LINE('',#399,#400);
|
| 478 |
+
#399 = CARTESIAN_POINT('',(0.,20.));
|
| 479 |
+
#400 = VECTOR('',#401,1.);
|
| 480 |
+
#401 = DIRECTION('',(1.,0.));
|
| 481 |
+
#402 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 482 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 483 |
+
) );
|
| 484 |
+
#403 = ADVANCED_FACE('',(#404),#202,.T.);
|
| 485 |
+
#404 = FACE_BOUND('',#405,.T.);
|
| 486 |
+
#405 = EDGE_LOOP('',(#406,#407,#408,#409));
|
| 487 |
+
#406 = ORIENTED_EDGE('',*,*,#188,.F.);
|
| 488 |
+
#407 = ORIENTED_EDGE('',*,*,#352,.T.);
|
| 489 |
+
#408 = ORIENTED_EDGE('',*,*,#326,.T.);
|
| 490 |
+
#409 = ORIENTED_EDGE('',*,*,#219,.F.);
|
| 491 |
+
#410 = ADVANCED_FACE('',(#411),#286,.F.);
|
| 492 |
+
#411 = FACE_BOUND('',#412,.F.);
|
| 493 |
+
#412 = EDGE_LOOP('',(#413,#414,#435,#436));
|
| 494 |
+
#413 = ORIENTED_EDGE('',*,*,#376,.F.);
|
| 495 |
+
#414 = ORIENTED_EDGE('',*,*,#415,.F.);
|
| 496 |
+
#415 = EDGE_CURVE('',#266,#377,#416,.T.);
|
| 497 |
+
#416 = SEAM_CURVE('',#417,(#421,#428),.PCURVE_S1.);
|
| 498 |
+
#417 = LINE('',#418,#419);
|
| 499 |
+
#418 = CARTESIAN_POINT('',(-5.,-1.722026714179E-15,10.));
|
| 500 |
+
#419 = VECTOR('',#420,1.);
|
| 501 |
+
#420 = DIRECTION('',(0.,0.,-1.));
|
| 502 |
+
#421 = PCURVE('',#286,#422);
|
| 503 |
+
#422 = DEFINITIONAL_REPRESENTATION('',(#423),#427);
|
| 504 |
+
#423 = LINE('',#424,#425);
|
| 505 |
+
#424 = CARTESIAN_POINT('',(6.28318530718,-0.));
|
| 506 |
+
#425 = VECTOR('',#426,1.);
|
| 507 |
+
#426 = DIRECTION('',(0.,1.));
|
| 508 |
+
#427 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 509 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 510 |
+
) );
|
| 511 |
+
#428 = PCURVE('',#286,#429);
|
| 512 |
+
#429 = DEFINITIONAL_REPRESENTATION('',(#430),#434);
|
| 513 |
+
#430 = LINE('',#431,#432);
|
| 514 |
+
#431 = CARTESIAN_POINT('',(0.,-0.));
|
| 515 |
+
#432 = VECTOR('',#433,1.);
|
| 516 |
+
#433 = DIRECTION('',(0.,1.));
|
| 517 |
+
#434 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
|
| 518 |
+
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
|
| 519 |
+
) );
|
| 520 |
+
#435 = ORIENTED_EDGE('',*,*,#265,.T.);
|
| 521 |
+
#436 = ORIENTED_EDGE('',*,*,#415,.T.);
|
| 522 |
+
#437 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3)
|
| 523 |
+
GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#441)) GLOBAL_UNIT_ASSIGNED_CONTEXT
|
| 524 |
+
((#438,#439,#440)) REPRESENTATION_CONTEXT('Context #1',
|
| 525 |
+
'3D Context with UNIT and UNCERTAINTY') );
|
| 526 |
+
#438 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
|
| 527 |
+
#439 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
|
| 528 |
+
#440 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
|
| 529 |
+
#441 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#438,
|
| 530 |
+
'distance_accuracy_value','confusion accuracy');
|
| 531 |
+
#442 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#7));
|
| 532 |
+
ENDSEC;
|
| 533 |
+
END-ISO-10303-21;
|
server/tasks/task_002_box_with_hole/reference_code.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cadquery as cq
|
| 2 |
+
result = cq.Workplane("XY").box(50, 30, 20).faces(">Z").hole(10)
|