eventhorizon28 commited on
Commit
7c72eb2
·
verified ·
1 Parent(s): 4821b80

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. Dockerfile +91 -0
  2. README.md +207 -5
  3. __init__.py +16 -0
  4. client.py +50 -0
  5. inference.py +734 -0
  6. models.py +59 -0
  7. openenv.yaml +7 -0
  8. pyproject.toml +42 -0
  9. scripts/generate_ground_truth.py +92 -0
  10. scripts/generate_tasks_thomasmaker.py +329 -0
  11. scripts/verify_all_tasks.py +261 -0
  12. scripts/verify_reference_codes.py +106 -0
  13. scripts/verify_reward_scenarios.py +370 -0
  14. scripts/verify_rewards.py +205 -0
  15. server/__init__.py +11 -0
  16. server/app.py +36 -0
  17. server/cadforge_environment.py +363 -0
  18. server/docs/concepts/brep-mindset.md +121 -0
  19. server/docs/concepts/free-function-api.md +306 -0
  20. server/docs/concepts/selectors.md +200 -0
  21. server/docs/concepts/workplanes.md +274 -0
  22. server/docs/patterns/anti-patterns.md +353 -0
  23. server/docs/patterns/common-patterns.md +278 -0
  24. server/docs/reference/Untitled +1 -0
  25. server/docs/reference/examples.rst +1705 -0
  26. server/docs/reference/extending.rst +242 -0
  27. server/docs/reference/free-func.rst +461 -0
  28. server/docs/reference/primer.rst +370 -0
  29. server/docs/reference/quickstart.rst +294 -0
  30. server/docs/reference/selectors.rst +232 -0
  31. server/docs/reference/sketch.rst +378 -0
  32. server/docs/reference/workplane.rst +557 -0
  33. server/docs/skill.md +264 -0
  34. server/docs_search.py +200 -0
  35. server/executor.py +183 -0
  36. server/geometry.py +206 -0
  37. server/preprocessor.py +339 -0
  38. server/requirements.txt +7 -0
  39. server/reward.py +325 -0
  40. server/tasks/task_001_flat_plate/ground_truth.json +39 -0
  41. server/tasks/task_001_flat_plate/ground_truth.step +416 -0
  42. server/tasks/task_001_flat_plate/ground_truth_normalized.step +416 -0
  43. server/tasks/task_001_flat_plate/reference_code.py +2 -0
  44. server/tasks/task_001_flat_plate/surface_points.npy +3 -0
  45. server/tasks/task_001_flat_plate/task.json +10 -0
  46. server/tasks/task_001_flat_plate/voxels_64.npy +3 -0
  47. server/tasks/task_002_box_with_hole/ground_truth.json +42 -0
  48. server/tasks/task_002_box_with_hole/ground_truth.step +533 -0
  49. server/tasks/task_002_box_with_hole/ground_truth_normalized.step +533 -0
  50. 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: Cadforge
3
- emoji: 🌍
4
- colorFrom: indigo
5
- colorTo: pink
6
  sdk: docker
7
  pinned: false
 
 
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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)